package de.nowchess.backcore.game

import de.nowchess.api.board.{Color, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.backcore.dto.{CreateGameRequest, ImportFenRequest, PlayerInfoDto}
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import de.nowchess.rules.sets.DefaultRules
import jakarta.enterprise.context.ApplicationScoped

private case class ServiceState(
    drawOfferedBy: Option[Color] = None,
    externalResult: Option[GameResult] = None,
)

@ApplicationScoped
class GameService:
  private val engine = GameEngineHolder.engine
  @SuppressWarnings(Array("DisableSyntax.var"))
  private var state: ServiceState = ServiceState()

  def isKnownId(id: String): Boolean = id == GameEngineHolder.gameId

  def getSnapshot: GameSnapshot = synchronized:
    GameSnapshot(
      gameId = GameEngineHolder.gameId,
      white = GameEngineHolder.white,
      black = GameEngineHolder.black,
      context = engine.context,
      drawOfferedBy = state.drawOfferedBy,
      externalResult = state.externalResult,
      canUndo = engine.canUndo,
      canRedo = engine.canRedo,
    )

  def reset(req: CreateGameRequest): GameSnapshot = synchronized:
    engine.reset()
    state = ServiceState()
    GameEngineHolder.white = toPlayerInfo(req.white, "white", "White")
    GameEngineHolder.black = toPlayerInfo(req.black, "black", "Black")
    getSnapshot

  def applyMove(uci: String): Either[String, GameSnapshot] = synchronized:
    if hasEnded then Left("Game is already over")
    else
      parseUci(uci) match
        case None => Left(s"Invalid UCI notation: $uci")
        case Some((from, to, promotion)) =>
          val candidates = engine.ruleSet.legalMoves(engine.context)(from)
          findMatchingMove(candidates, to, promotion) match
            case None => Left(s"$uci is not a legal move")
            case Some(move) =>
              engine.processUserInput(s"$from$to")
              promotion.foreach(engine.completePromotion)
              Right(getSnapshot)

  def legalMoves(square: Option[Square]): List[Move] = synchronized:
    val ctx = engine.context
    square match
      case Some(sq) => engine.ruleSet.legalMoves(ctx)(sq)
      case None     => engine.ruleSet.allLegalMoves(ctx)

  def undo(): Either[String, GameSnapshot] = synchronized:
    if !engine.canUndo then Left("No moves to undo")
    else
      engine.undo()
      state = state.copy(externalResult = None, drawOfferedBy = None)
      Right(getSnapshot)

  def redo(): Either[String, GameSnapshot] = synchronized:
    if !engine.canRedo then Left("No moves to redo")
    else
      engine.redo()
      Right(getSnapshot)

  def resign(): Either[String, GameSnapshot] = synchronized:
    if hasEnded then Left("Game is already over")
    else
      val winner = engine.context.turn.opposite
      state = state.copy(externalResult = Some(GameResult.Resign(winner)))
      Right(getSnapshot)

  def drawAction(action: String): Either[String, GameSnapshot] = synchronized:
    if hasEnded then Left("Game is already over")
    else
      action match
        case "offer" =>
          state = state.copy(drawOfferedBy = Some(engine.context.turn))
          Right(getSnapshot)
        case "accept" =>
          state.drawOfferedBy match
            case None => Left("No draw offer to accept")
            case Some(offerer) if offerer == engine.context.turn =>
              Left("Cannot accept your own draw offer")
            case Some(_) =>
              state = state.copy(externalResult = Some(GameResult.AgreedDraw), drawOfferedBy = None)
              Right(getSnapshot)
        case "decline" =>
          state.drawOfferedBy match
            case None => Left("No draw offer to decline")
            case Some(_) =>
              state = state.copy(drawOfferedBy = None)
              Right(getSnapshot)
        case "claim" =>
          if DefaultRules.isFiftyMoveRule(engine.context) then
            state = state.copy(externalResult = Some(GameResult.FiftyMoveDraw))
            Right(getSnapshot)
          else Left("Fifty-move rule has not been triggered")
        case other => Left(s"Unknown draw action: $other")

  def importFen(req: ImportFenRequest): Either[String, GameSnapshot] = synchronized:
    FenParser.parseFen(req.fen) match
      case Left(err) => Left(err)
      case Right(ctx) =>
        engine.loadPosition(ctx)
        state = ServiceState()
        GameEngineHolder.white = toPlayerInfo(req.white, "white", "White")
        GameEngineHolder.black = toPlayerInfo(req.black, "black", "Black")
        Right(getSnapshot)

  def importPgn(pgn: String): Either[String, GameSnapshot] = synchronized:
    engine.loadGame(PgnParser, pgn) match
      case Left(err) => Left(err)
      case Right(_) =>
        state = ServiceState()
        Right(getSnapshot)

  def exportFen(): String = FenExporter.exportGameContext(engine.context)

  def exportPgn(): String = PgnExporter.exportGameContext(engine.context)

  private def hasEnded: Boolean =
    state.externalResult.isDefined || engine.context.result.isDefined

  private def toPlayerInfo(dto: Option[PlayerInfoDto], defaultId: String, defaultName: String): PlayerInfo =
    dto.fold(PlayerInfo(PlayerId(defaultId), defaultName))(d => PlayerInfo(PlayerId(d.id), d.displayName))

  private def parseUci(uci: String): Option[(Square, Square, Option[PromotionPiece])] =
    if uci.length < 4 || uci.length > 5 then None
    else
      for
        from <- Square.fromAlgebraic(uci.substring(0, 2))
        to   <- Square.fromAlgebraic(uci.substring(2, 4))
      yield
        val promotion = if uci.length == 5 then parsePromotionChar(uci.charAt(4)) else None
        (from, to, promotion)

  private def parsePromotionChar(c: Char): Option[PromotionPiece] =
    c match
      case 'q' => Some(PromotionPiece.Queen)
      case 'r' => Some(PromotionPiece.Rook)
      case 'b' => Some(PromotionPiece.Bishop)
      case 'n' => Some(PromotionPiece.Knight)
      case _   => None

  private def findMatchingMove(
      candidates: List[Move],
      to: Square,
      promotion: Option[PromotionPiece],
  ): Option[Move] =
    candidates.filter(_.to == to) match
      case Nil => None
      case moves =>
        promotion match
          case Some(pp) => moves.find(_.moveType == MoveType.Promotion(pp))
          case None =>
            moves
              .find(m =>
                m.moveType match
                  case _: MoveType.Promotion => false
                  case _                     => true,
              )
              .orElse(moves.headOption)
