// ---------------------------------------------------------------------------
// AI entry point — strategy-driven, three difficulty levels
// ---------------------------------------------------------------------------

import { Card, GameState, PlayerIndex, Difficulty, AIMove, PRIMIERA_VALUES } from './types';
import { findCaptures, teamOf, RandomSource } from './engine';
import { CardTracker } from './card-tracker';
import { CardInferenceEngine } from './card-inference';
import {
  getCategoryStates,
  getPhase,
  solveEndgame,
  analyzeTableParity,
  rankDumpsBySpariglio,
} from './ai-strategy';
import { pimcSearch } from './ai-pimc';
import type { PIMCOptions } from './ai-pimc';

export type { AIMove };

export interface AIDecisionProgress {
  difficulty: Difficulty;
  progress: number;
  elapsedMs: number;
  budgetMs: number;
  batchesCompleted: number;
  cardsRemaining?: number;
  sampleCount?: number;
  maxDepth?: number;
  completedDepth?: number;
  rootMoveCount?: number;
  timedOut?: boolean;
  aspirationExpansions?: number;
}

interface MasterProgressDetails {
  cardsRemaining: number;
  sampleCount: number;
  maxDepth: number;
  completedDepth: number;
  rootMoveCount: number;
  timedOut: boolean;
  aspirationExpansions: number;
}

interface SearchProfile {
  timeBudgetMs: number;
  sampleCount: number;
  maxDepth: number;
  batchSize: number;
}

export interface AISearchProfileOverride {
  timeBudgetMs?: number;
  sampleCount?: number;
  maxDepth?: number;
  batchSize?: number;
}

export interface AITimingSource {
  now(): number;
  advance?(elapsedMs: number): number;
  isSimulated?: boolean;
}

export interface AIChooseMoveOptions {
  rng?: RandomSource;
  profileOverride?: AISearchProfileOverride;
  timingSource?: AITimingSource;
  inference?: CardInferenceEngine;
}

interface SearchTimingContext {
  now(): number;
  checkpoint(costMs?: number): number;
  yieldToHost(): Promise<void>;
}

const SEARCH_PROFILES: Record<Difficulty, SearchProfile> = {
  beginner: { timeBudgetMs: 120, sampleCount: 0, maxDepth: 0, batchSize: 0 },
  advanced: { timeBudgetMs: 650, sampleCount: 0, maxDepth: 0, batchSize: 0 },
  master: { timeBudgetMs: 4300, sampleCount: 8, maxDepth: 5, batchSize: 2 },
};

const REAL_TIME_SOURCE: AITimingSource = {
  now: () => Date.now(),
};

const SIMULATED_YIELD_COST_MS = 1;

function createSearchTimingContext(timingSource?: AITimingSource): SearchTimingContext {
  const source = timingSource ?? REAL_TIME_SOURCE;

  return {
    now: () => source.now(),
    checkpoint: (costMs = 0) => {
      if (source.advance && costMs > 0) {
        return source.advance(costMs);
      }
      return source.now();
    },
    yieldToHost: () => {
      if (source.advance) {
        source.advance(SIMULATED_YIELD_COST_MS);
        return Promise.resolve();
      }
      return new Promise(resolve => setTimeout(resolve, 0));
    },
  };
}

// ---------------------------------------------------------------------------
// Search profile helpers (preserved)
// ---------------------------------------------------------------------------

function applySearchProfileOverride(
  profile: SearchProfile,
  profileOverride?: AISearchProfileOverride,
): SearchProfile {
  if (!profileOverride) return profile;

  return {
    timeBudgetMs: profileOverride.timeBudgetMs ?? profile.timeBudgetMs,
    sampleCount: profileOverride.sampleCount ?? profile.sampleCount,
    maxDepth: profileOverride.maxDepth ?? profile.maxDepth,
    batchSize: profileOverride.batchSize ?? profile.batchSize,
  };
}

function getSearchProfile(
  state: GameState,
  difficulty: Difficulty,
  profileOverride?: AISearchProfileOverride,
): SearchProfile {
  if (difficulty !== 'master') {
    return applySearchProfileOverride(SEARCH_PROFILES[difficulty], profileOverride);
  }

  const cardsRemaining = state.players.reduce((sum, player) => sum + player.hand.length, 0);
  if (cardsRemaining <= 4) {
    return applySearchProfileOverride(
      { timeBudgetMs: 3200, sampleCount: 4, maxDepth: cardsRemaining, batchSize: 1 },
      profileOverride,
    );
  }
  if (cardsRemaining <= 6) {
    return applySearchProfileOverride(
      { timeBudgetMs: 3600, sampleCount: 6, maxDepth: cardsRemaining, batchSize: 1 },
      profileOverride,
    );
  }
  if (cardsRemaining <= 8) {
    return applySearchProfileOverride(
      { timeBudgetMs: 3900, sampleCount: 8, maxDepth: cardsRemaining, batchSize: 1 },
      profileOverride,
    );
  }
  if (cardsRemaining <= 12) {
    return applySearchProfileOverride(
      { timeBudgetMs: 4200, sampleCount: 8, maxDepth: 8, batchSize: 1 },
      profileOverride,
    );
  }
  if (cardsRemaining <= 20) {
    return applySearchProfileOverride(
      { timeBudgetMs: 4350, sampleCount: 12, maxDepth: 5, batchSize: 2 },
      profileOverride,
    );
  }
  return applySearchProfileOverride(SEARCH_PROFILES.master, profileOverride);
}

function reportDecisionProgress(
  onProgress: ((progress: AIDecisionProgress) => void) | undefined,
  difficulty: Difficulty,
  startedAt: number,
  timing: SearchTimingContext,
  budgetMs: number,
  progress: number,
  batchesCompleted: number,
  masterDetails?: MasterProgressDetails,
): void {
  if (!onProgress) return;

  onProgress({
    difficulty,
    progress: Math.max(0, Math.min(1, progress)),
    elapsedMs: timing.now() - startedAt,
    budgetMs,
    batchesCompleted,
    ...(masterDetails ?? {}),
  });
}

// ---------------------------------------------------------------------------
// Core move helpers
// ---------------------------------------------------------------------------

function nextPlayer(p: PlayerIndex): PlayerIndex {
  return ((p + 1) % 4) as PlayerIndex;
}

function isOpponent(me: PlayerIndex, other: PlayerIndex): boolean {
  return teamOf(me) !== teamOf(other);
}

function getLegalMoves(state: GameState, playerIdx: PlayerIndex): AIMove[] {
  const moves: AIMove[] = [];
  const player = state.players[playerIdx];
  const table = state.table;
  for (const card of player.hand) {
    const captures = findCaptures(card, table);
    if (captures.length > 0) {
      for (const captureSet of captures) moves.push({ card, capture: captureSet });
    } else {
      moves.push({ card, capture: [] });
    }
  }
  return moves;
}

/**
 * Returns a forced move (scopa or settebello capture) if one exists.
 * Priority 1: any capture that empties the table (scopa).
 * Priority 2: any capture that includes the settebello (denara_7).
 */
function checkForcedMove(legalMoves: AIMove[], table: Card[]): AIMove | null {
  // 1. Scopa: capture that clears the table
  for (const move of legalMoves) {
    if (move.capture.length > 0) {
      const tableAfter = table.filter(c => !move.capture.some(cap => cap.id === c.id));
      if (tableAfter.length === 0) return move;
    }
  }
  // 2. Settebello capture
  for (const move of legalMoves) {
    if (move.capture.some(c => c.id === 'denara_7')) return move;
  }
  return null;
}

/**
 * Danger score for dumping a card on an empty table.
 * Lower = safer: fewer capture-combo possibilities + less strategic value.
 *
 * Primary combo-capture proxy: face value (higher → more subsets of future
 * table cards can sum to it, letting opponents capture it indirectly).
 * Category penalties: settebello, non-coin 7 (primiera), other denara, high
 * primiera-value cards (1/5/6 whose primiera contribution ≥ 15 pts).
 */
function emptyTableDumpDanger(card: Card): number {
  let danger = card.value; // 1-10: direct proxy for combo-capture breadth
  if (card.id === 'denara_7')        danger += 50; // settebello — never dump
  else if (card.value === 7)         danger += 20; // non-coin 7: top primiera
  else if (card.suit === 'denara')   danger += 15; // coin card
  // High-primiera cards (1=16, 5=15, 6=18) get a small extra penalty.
  if (card.value !== 7 && (PRIMIERA_VALUES[card.value] ?? 0) >= 15) danger += 5;
  return danger;
}

/**
 * When the table is empty, choose the least-dangerous dump move.
 *
 * Rank 1 (primary):   highest count of same face-value in our hand
 *                     — opponent holds fewer of that value → harder to
 *                       capture it with a direct same-value play.
 * Rank 2 (secondary): lowest emptyTableDumpDanger() score
 *                     — lower face value (fewer capture combos) and
 *                       not a strategically valuable card (7, denara, etc.).
 */
function pickSafestEmptyTableDump(hand: Card[], legalMoves: AIMove[]): AIMove | null {
  const dumps = legalMoves.filter(m => m.capture.length === 0);
  if (dumps.length === 0) return null;

  const countByValue = new Map<number, number>();
  for (const c of hand) countByValue.set(c.value, (countByValue.get(c.value) ?? 0) + 1);

  const sorted = [...dumps].sort((a, b) => {
    const cntA = countByValue.get(a.card.value) ?? 1;
    const cntB = countByValue.get(b.card.value) ?? 1;
    if (cntB !== cntA) return cntB - cntA; // more in hand → safer (desc)
    return emptyTableDumpDanger(a.card) - emptyTableDumpDanger(b.card); // less danger (asc)
  });
  return sorted[0];
}

function buildFallbackInference(tracker: CardTracker | undefined): CardInferenceEngine {
  return new CardInferenceEngine(tracker ?? new CardTracker());
}

// ===========================================================================
// BEGINNER — beatable, static priority + noise
// ===========================================================================

function beginnerMove(
  state: GameState,
  playerIdx: PlayerIndex,
  _tracker: CardTracker | undefined,
  rng: RandomSource,
): AIMove {
  const legalMoves = getLegalMoves(state, playerIdx);

  // 5% pure random
  if (rng() < 0.05) {
    return legalMoves[Math.floor(rng() * legalMoves.length)];
  }

  // Forced moves (scopa / settebello) — unconditional
  const forced = checkForcedMove(legalMoves, state.table);
  if (forced) return forced;

  let bestMove = legalMoves[0];
  let bestScore = -Infinity;

  for (const move of legalMoves) {
    let score = 0;
    const captured = move.capture;

    if (captured.length > 0) {
      // 7s (primiera)
      score += captured.filter(c => c.value === 7).length * 30;
      // Coins
      score += captured.filter(c => c.suit === 'denara').length * 20;
      // Cards
      score += captured.length * 5;
    } else {
      // Dump: penalise if it makes the table easy to scopa
      const tableAfter = [...state.table, move.card];
      const tableSum = tableAfter.reduce((s, c) => s + c.value, 0);
      if (tableSum <= 10 && tableAfter.length <= 3) {
        score -= 80;
      }
    }

    // 20% noise band
    score *= 0.8 + rng() * 0.4;

    if (score > bestScore) {
      bestScore = score;
      bestMove = move;
    }
  }

  return bestMove;
}

// ===========================================================================
// ADVANCED — forced check → category states → phase-aware heuristics + inference
// ===========================================================================

function advancedMove(
  state: GameState,
  playerIdx: PlayerIndex,
  _tracker: CardTracker | undefined,
  inference: CardInferenceEngine | null,
): AIMove {
  const legalMoves = getLegalMoves(state, playerIdx);

  // Forced moves first
  const forced = checkForcedMove(legalMoves, state.table);
  if (forced) return forced;

  // Early-game: empty table → no captures possible, dump the safest card.
  if (state.table.length === 0) {
    const safe = pickSafestEmptyTableDump(state.players[playerIdx].hand, legalMoves);
    if (safe) return safe;
  }

  const myTeam = teamOf(playerIdx);
  const categoryStates = getCategoryStates(state, myTeam);
  const phase = getPhase(state);

  // Endgame: attempt deterministic solve
  if (phase === 'endgame' && inference) {
    const endgameMove = solveEndgame(state, playerIdx, inference, legalMoves);
    if (endgameMove) return endgameMove;
  }

  const next = nextPlayer(playerIdx);
  const nextIsOpp = isOpponent(playerIdx, next);
  const nextHandSize = state.players[next].hand.length;
  const myHand = state.players[playerIdx].hand;

  // Spariglio analysis for dump moves
  const dealerTeam = teamOf(state.dealer);
  const isNonDealerTeam = myTeam !== dealerTeam;
  const spariglioPotentials = rankDumpsBySpariglio(myHand, state.table, isNonDealerTeam);
  const spariglioPotentialMap = new Map(spariglioPotentials.map(sp => [sp.card.id, sp]));

  const denariCloseness = categoryStates.denari.state === 'contested' ? categoryStates.denari.closeness : 0;
  const carteCloseness = categoryStates.carte.state === 'contested' ? categoryStates.carte.closeness : 0;
  const primieraCloseness = categoryStates.primiera.overallCloseness;

  let bestMove = legalMoves[0];
  let bestScore = -Infinity;

  for (const move of legalMoves) {
    let score = 0;
    const captured = move.capture;

    if (captured.length > 0) {
      // Settebello
      if (captured.some(c => c.id === 'denara_7')) score += 50;
      // Coins weighted by closeness
      score += captured.filter(c => c.suit === 'denara').length * Math.round(15 + denariCloseness * 25);
      // 7s weighted by primiera closeness
      score += captured.filter(c => c.value === 7).length * Math.round(20 + primieraCloseness * 40);
      // Cards: use floor of 6 so captures stay attractive even when carte is secured
      score += captured.length * Math.round(6 + carteCloseness * 8);
    } else {
      // Dump: spariglio-aware scoring
      const sp = spariglioPotentialMap.get(move.card.id);
      if (sp) {
        score += sp.spariglioDelta * (isNonDealerTeam ? 15 : -15);
        if (sp.isSpariglio3Card) score += isNonDealerTeam ? 20 : -20;
      }

      // Damage minimization: penalise dumping contested-category cards.
      // We cannot capture with them, so we're leaving them on the table for
      // the opponent to take — the higher the contest, the bigger the risk.
      const dumpCard = move.card;
      if (dumpCard.suit === 'denara') {
        // Giving away a coin card when the denari race is live
        score -= Math.round(8 + denariCloseness * 18);
        if (dumpCard.value === 7) score -= 18; // settebello is uniquely dangerous
      }
      if (dumpCard.value === 7) {
        // Giving away a 7 hurts primiera regardless of suit
        score -= Math.round(10 + primieraCloseness * 24);
      }
      // High primiera-value cards in contested suits (7=21, 6=18, 1=16, 5=15)
      const dumpSuitEntry = categoryStates.primiera.perSuit[dumpCard.suit];
      if (dumpSuitEntry && dumpSuitEntry.state === 'contested') {
        const primVal = PRIMIERA_VALUES[dumpCard.value] ?? 0;
        if (primVal >= 15) {
          score -= Math.round(primVal * 0.25 + dumpSuitEntry.closeness * 14);
        }
      }
      // If the opponent likely holds the same value they can directly capture
      // our dump card on the very next turn — compound the damage
      if (inference && nextIsOpp && nextHandSize > 0) {
        const projectedTable = [...state.table, dumpCard];
        const probCapture = inference.probabilityPlayerHasValue(
          next, dumpCard.value, nextHandSize, myHand, projectedTable,
        );
        if (probCapture > 0.05) {
          score -= Math.round(probCapture * 20);
          if (dumpCard.suit === 'denara')
            score -= Math.round(probCapture * (14 + denariCloseness * 20));
          if (dumpCard.value === 7)
            score -= Math.round(probCapture * (10 + primieraCloseness * 20));
        }
      }
    }

    // Anti-scopa: penalise leaving a table the opponent can immediately sweep
    if (inference && nextIsOpp && nextHandSize > 0) {
      const tableAfter = captured.length > 0
        ? state.table.filter(c => !captured.some(cap => cap.id === c.id))
        : [...state.table, move.card];
      if (tableAfter.length > 0) {
        const tableSum = tableAfter.reduce((s, c) => s + c.value, 0);
        const prob = inference.probabilityPlayerHasValue(next, tableSum, nextHandSize, myHand, tableAfter);
        score -= prob * 60;
      }
    }

    if (score > bestScore) {
      bestScore = score;
      bestMove = move;
    }
  }

  return bestMove;
}

// ===========================================================================
// MASTER — forced check → endgame solve → PIMC search
// ===========================================================================

async function masterMove(
  state: GameState,
  playerIdx: PlayerIndex,
  tracker: CardTracker | undefined,
  onProgress: ((progress: AIDecisionProgress) => void) | undefined,
  profile: SearchProfile,
  startedAt: number,
  timing: SearchTimingContext,
  rng: RandomSource,
  inference: CardInferenceEngine | null,
): Promise<AIMove> {
  const cardsRemaining = state.players.reduce((s, p) => s + p.hand.length, 0);
  const legalMoves = getLegalMoves(state, playerIdx);

  if (legalMoves.length === 1) {
    reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
      cardsRemaining,
      sampleCount: 1,
      maxDepth: 1,
      completedDepth: 1,
      rootMoveCount: 1,
      timedOut: false,
      aspirationExpansions: 0,
    });
    return legalMoves[0];
  }

  // Forced moves
  const forced = checkForcedMove(legalMoves, state.table);
  if (forced) {
    reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
      cardsRemaining,
      sampleCount: 0,
      maxDepth: 0,
      completedDepth: 0,
      rootMoveCount: legalMoves.length,
      timedOut: false,
      aspirationExpansions: 0,
    });
    return forced;
  }

  // Deterministic endgame solve
  if (inference) {
    const endgameMove = solveEndgame(state, playerIdx, inference, legalMoves);
    if (endgameMove) {
      reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
        cardsRemaining,
        sampleCount: 0,
        maxDepth: 0,
        completedDepth: 0,
        rootMoveCount: legalMoves.length,
        timedOut: false,
        aspirationExpansions: 0,
      });
      return endgameMove;
    }
  }

  // Early-game: empty table → no captures possible, dump the safest card.
  if (state.table.length === 0) {
    const safe = pickSafestEmptyTableDump(state.players[playerIdx].hand, legalMoves);
    if (safe) {
      reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
        cardsRemaining,
        sampleCount: 0,
        maxDepth: 0,
        completedDepth: 0,
        rootMoveCount: legalMoves.length,
        timedOut: false,
        aspirationExpansions: 0,
      });
      return safe;
    }
  }

  const myTeam = teamOf(playerIdx);
  const categoryStates = getCategoryStates(state, myTeam);
  const parityState = analyzeTableParity(state.table);
  const effectiveInference = inference ?? buildFallbackInference(tracker);

  await timing.yieldToHost();

  const deadline = startedAt + profile.timeBudgetMs;
  const remainingBudget = Math.max(100, deadline - timing.now());

  reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 0, 0, {
    cardsRemaining,
    sampleCount: profile.sampleCount,
    maxDepth: profile.maxDepth,
    completedDepth: 0,
    rootMoveCount: legalMoves.length,
    timedOut: false,
    aspirationExpansions: 0,
  });

  const pimcOptions: Partial<PIMCOptions> = {
    determinizations: profile.sampleCount ?? 12,
    timeBudgetMs: remainingBudget,
    maxDepthMidgame: Math.min(profile.maxDepth ?? 5, 5),
    maxDepthEndgame: 8,
    stabilityWeight: 0.3,
    timingSource: timing,
  };

  const results = pimcSearch(
    state,
    playerIdx,
    legalMoves,
    effectiveInference,
    categoryStates,
    parityState,
    pimcOptions,
    rng,
    tracker,
  );

  reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
    cardsRemaining,
    sampleCount: profile.sampleCount,
    maxDepth: profile.maxDepth,
    completedDepth: profile.maxDepth,
    rootMoveCount: legalMoves.length,
    timedOut: timing.now() >= deadline,
    aspirationExpansions: 0,
  });

  return results[0]?.move ?? legalMoves[0];
}

// ===========================================================================
// Main entry point
// ===========================================================================

export async function chooseMove(
  state: GameState,
  playerIdx: PlayerIndex,
  difficulty: Difficulty = 'advanced',
  tracker?: CardTracker,
  onProgress?: (progress: AIDecisionProgress) => void,
  options?: AIChooseMoveOptions,
): Promise<AIMove> {
  const timing = createSearchTimingContext(options?.timingSource);
  const startedAt = timing.now();
  const profile = getSearchProfile(state, difficulty, options?.profileOverride);
  const rng = options?.rng ?? Math.random;
  const inference = options?.inference ?? null;

  reportDecisionProgress(onProgress, difficulty, startedAt, timing, profile.timeBudgetMs, 0, 0);

  switch (difficulty) {
    case 'beginner': {
      const move = beginnerMove(state, playerIdx, tracker, rng);
      reportDecisionProgress(onProgress, difficulty, startedAt, timing, profile.timeBudgetMs, 1, 1);
      return move;
    }
    case 'advanced': {
      const move = advancedMove(state, playerIdx, tracker, inference);
      reportDecisionProgress(onProgress, difficulty, startedAt, timing, profile.timeBudgetMs, 1, 1);
      return move;
    }
    case 'master':
      return masterMove(state, playerIdx, tracker, onProgress, profile, startedAt, timing, rng, inference);
  }
}
