sealed interface Game

data object NotStartedGame : Game

data class StartedGame(
    val secret: Code,
    val attempts: Int,
    val totalAttempts: Int,
    val availablePegs: Set<Code.Peg>,
    val outcome: Feedback.Outcome
) : Game {
    val secretLength: Int = secret.length

    val secretPegs: List<Code.Peg> = secret.pegs

    fun isWon(): Boolean = outcome == WON

    fun isLost(): Boolean = outcome == LOST

    fun isGuessTooShort(guess: Code): Boolean =
        guess.length < secretLength

    fun isGuessTooLong(guess: Code): Boolean =
        guess.length > secretLength

    fun isGuessValid(guess: Code): Boolean =
        availablePegs.containsAll(guess.pegs)
}

fun applyEvent(
    game: Game,
    event: GameEvent
): Game = when (game) {
    is NotStartedGame -> when (event) {
        is GameStarted -> StartedGame(event.secret, 0, event.totalAttempts, event.availablePegs, IN_PROGRESS)
        else -> game
    }
    is StartedGame -> when (event) {
        is GameStarted -> game
        is GuessMade -> game.copy(attempts = game.attempts + 1)
        is GameWon -> game.copy(outcome = WON)
        is GameLost -> game.copy(outcome = LOST)
    }
}

private fun startedNotFinishedGame(command: MakeGuess, game: Game): Either<GameError, StartedGame> {
    if (game !is StartedGame) {
        return GameNotStarted(command.gameId).left()
    }
    if (game.isWon()) {
        return GameAlreadyWon(command.gameId).left()
    }
    if (game.isLost()) {
        return GameAlreadyLost(command.gameId).left()
    }
    return game.right()
}

private fun validGuess(command: MakeGuess, game: StartedGame): Either<GameError, Code> {
    if (game.isGuessTooShort(command.guess)) {
        return GuessTooShort(command.gameId, command.guess, game.secretLength).left()
    }
    if (game.isGuessTooLong(command.guess)) {
        return GuessTooLong(command.gameId, command.guess, game.secretLength).left()
    }
    if (!game.isGuessValid(command.guess)) {
        return InvalidPegInGuess(command.gameId, command.guess, game.availablePegs).left()
    }
    return command.guess.right()
}

private fun StartedGame.feedbackOn(guess: Code): Feedback =
    feedbackPegsOn(guess)
        .let { (exactHits, colourHits) ->
            Feedback(outcomeFor(exactHits), exactHits + colourHits)
        }

private fun StartedGame.feedbackPegsOn(guess: Code) =
    exactHits(guess).map { BLACK } to colourHits(guess).map { WHITE }

private fun StartedGame.outcomeFor(exactHits: List<Feedback.Peg>) = when {
    exactHits.size == this.secretLength -> WON
    this.attempts + 1 == this.totalAttempts -> LOST
    else -> IN_PROGRESS
}

private fun StartedGame.exactHits(guess: Code): List<Code.Peg> = this.secretPegs
    .zip(guess.pegs)
    .filter { (secretColour, guessColour) -> secretColour == guessColour }
    .unzip()
    .second

private fun StartedGame.colourHits(guess: Code): List<Code.Peg> = this.secretPegs
    .zip(guess.pegs)
    .filter { (secretColour, guessColour) -> secretColour != guessColour }
    .unzip()
    .let { (secret, guess) ->
        guess.fold(secret to emptyList<Code.Peg>()) { (secretPegs, colourHits), guessPeg ->
            secretPegs.remove(guessPeg)?.let { it to colourHits + guessPeg } ?: (secretPegs to colourHits)
        }.second
    }

fun notStartedGame(): Game = NotStartedGame