// ---------------------------------------------------------------------------
// Card Inference Engine
// Tracks per-player card constraints and probability distributions.
// Additive to CardTracker — does NOT replace it.
// ---------------------------------------------------------------------------

import { Card, PlayerIndex, Suit, SUITS } from './types';
import { AIMove } from './types';
import { CardTracker } from './card-tracker';

export interface CardInferenceSnapshot {
  cannotHave: Array<[PlayerIndex, string[]]>;
  confirmedHeld: Array<[PlayerIndex, string[]]>;
}

export class CardInferenceEngine {
  private cannotHave: Map<PlayerIndex, Set<string>>;
  private confirmedHeld: Map<PlayerIndex, Set<string>>;
  private tracker: CardTracker;

  constructor(tracker: CardTracker) {
    this.tracker = tracker;
    this.cannotHave = new Map();
    this.confirmedHeld = new Map();
    for (const p of [0, 1, 2, 3] as PlayerIndex[]) {
      this.cannotHave.set(p, new Set());
      this.confirmedHeld.set(p, new Set());
    }
  }

  reset(): void {
    for (const p of [0, 1, 2, 3] as PlayerIndex[]) {
      this.cannotHave.get(p)!.clear();
      this.confirmedHeld.get(p)!.clear();
    }
  }

  /**
   * Call after every move is applied.
   * @param playerIdx  The player who just played
   * @param move       The move that was played (card + capture)
   * @param tableBeforeMove  The table state BEFORE the move was applied
   */
  onMove(playerIdx: PlayerIndex, move: AIMove, tableBeforeMove: Card[]): void {
    // 1. Player confirmed held the card they played
    this.confirmedHeld.get(playerIdx)!.add(move.card.id);

    // 2. If dump (no capture): player could not capture any table card with this card.
    //    Infer: player does NOT hold any card whose value matches any table card
    //    (because if they did, they'd have had a capture option for the dump-value already,
    //    but more importantly: the dump tells us no capture was available for move.card.value,
    //    which means the table does NOT have move.card.value — engine enforces mandatory capture).
    //    Additional inference: for each table card value, player cannot have the matching
    //    card OF THE SAME VALUE as their dumped card (engine rule: if matching value on table, capture is mandatory).
    //    Simplified: if player dumps value V, and table had cards of various values,
    //    it means table had NO card of value V. We can mark: player "cannot have" cards
    //    whose value == any table card value (because those would have been capturable).
    //    Actually the correct inference is: player dumped, so findCaptures(move.card, tableBeforeMove) === [].
    //    This means: no single table card matches move.card.value AND no subset sums to move.card.value.
    //    We mark: player definitely did NOT have a card that could capture from this table state.
    //    The simplest safe inference: mark that the player cannot have any card of the SAME VALUE
    //    as any card on the table before the move (since having such a card would have forced a capture).
    if (move.capture.length === 0 && tableBeforeMove.length > 0) {
      // Player dumped — they had no capture for move.card.
      // Since capture is mandatory in Scopone, this means no card on table matches move.card.value
      // and no subset of table cards sums to move.card.value.
      // Safe inference: player doesn't hold any card of the same value as table cards
      // (because if they had such a card, they'd have been forced to capture it instead).
      // NOTE: This inference applies to OTHER cards in their hand, not the dumped card itself.
      for (const tableCard of tableBeforeMove) {
        // If player had a card of value tableCard.value, they would have captured it
        // (since direct match = mandatory capture). So: player cannot have any card of tableCard.value
        // in their REMAINING hand (they may have had one but already played it — but since we're
        // tracking "cannot have NOW", this is correct: if they just dumped and had tableCard.value,
        // they would have captured instead).
        for (const suit of SUITS) {
          const inferredId = `${suit}_${tableCard.value}`;
          if (inferredId !== move.card.id) {
            this.cannotHave.get(playerIdx)!.add(inferredId);
          }
        }
      }
    }

    // 3. Exhaustion inference: for any value where all 4 suits are now accounted for
    this.applyExhaustionInference();
  }

  private applyExhaustionInference(): void {
    for (let value = 1; value <= 10; value++) {
      const allPlayed = SUITS.every(suit => this.tracker.hasBeenPlayed(`${suit}_${value}`));
      if (allPlayed) {
        for (const p of [0, 1, 2, 3] as PlayerIndex[]) {
          for (const suit of SUITS) {
            this.cannotHave.get(p)!.add(`${suit}_${value}`);
          }
        }
      }
    }
  }

  /**
   * Returns the pool of unseen cards for a given player, filtered by their constraints.
   */
  getConstrainedUnseen(excludePlayerIdx: PlayerIndex, myHand: Card[], table: Card[]): Card[] {
    const unseen = this.tracker.getUnseenCards(myHand, table);
    const excluded = this.cannotHave.get(excludePlayerIdx)!;
    return unseen.filter(c => !excluded.has(c.id));
  }

  /**
   * Hypergeometric probability: P(player holds at least one card of given value).
   */
  probabilityPlayerHasValue(
    playerIdx: PlayerIndex,
    value: number,
    handSize: number,
    myHand: Card[],
    table: Card[],
  ): number {
    if (handSize <= 0) return 0;
    const pool = this.getConstrainedUnseen(playerIdx, myHand, table);
    const matching = pool.filter(c => c.value === value).length;
    if (matching === 0) return 0;
    if (handSize >= pool.length) return 1;

    let probNone = 1;
    for (let i = 0; i < handSize; i++) {
      probNone *= Math.max(0, (pool.length - matching - i)) / (pool.length - i);
    }
    return 1 - probNone;
  }

  /**
   * Returns a likely hand for sampling (for PIMC determinization).
   * Cards are weighted by strategic importance: 7s first, then 6s, then coins, then aces.
   */
  getLikelyHandForSampling(
    playerIdx: PlayerIndex,
    handSize: number,
    myHand: Card[],
    table: Card[],
    rng: () => number = Math.random,
  ): Card[] {
    const pool = this.getConstrainedUnseen(playerIdx, myHand, table);
    if (pool.length === 0) return [];

    // Weight cards by strategic importance
    const weighted = pool.map(card => {
      let weight = 0;
      if (card.value === 7) weight += 4;
      else if (card.value === 6) weight += 3;
      else if (card.value === 1) weight += 2;
      if (card.suit === 'denara') weight += 1;
      weight += rng() * 0.5; // noise to vary sampling
      return { card, weight };
    });

    weighted.sort((a, b) => b.weight - a.weight);
    return weighted.slice(0, Math.min(handSize, pool.length)).map(w => w.card);
  }

  toSnapshot(): CardInferenceSnapshot {
    return {
      cannotHave: ([0, 1, 2, 3] as PlayerIndex[]).map(p =>
        [p, [...this.cannotHave.get(p)!]] as [PlayerIndex, string[]],
      ),
      confirmedHeld: ([0, 1, 2, 3] as PlayerIndex[]).map(p =>
        [p, [...this.confirmedHeld.get(p)!]] as [PlayerIndex, string[]],
      ),
    };
  }

  static fromSnapshot(snapshot: CardInferenceSnapshot, tracker: CardTracker): CardInferenceEngine {
    const engine = new CardInferenceEngine(tracker);
    for (const [p, ids] of snapshot.cannotHave) {
      engine.cannotHave.set(p as PlayerIndex, new Set(ids));
    }
    for (const [p, ids] of snapshot.confirmedHeld) {
      engine.confirmedHeld.set(p as PlayerIndex, new Set(ids));
    }
    return engine;
  }
}
