import { Card, Suit, SUITS } from './types';

export interface CardTrackerSnapshot {
  playedCardIds: string[];
}

export interface CardTrackerValueRankResidue {
  value: number;
  knownCount: number;
  unseenCount: number;
  hasSingletonUnseenRankResidue: boolean;
  hasPairedUnseenRankResidue: boolean;
}

interface VisibleValueResidueKnowledge {
  unseenCards: Card[];
  unseenCountBySuit: Record<Suit, number>;
  unseenCountByValue: number[];
  valueRankResidues: CardTrackerValueRankResidue[];
}

function normalizeSnapshot(snapshot: CardTrackerSnapshot): CardTrackerSnapshot {
  return {
    playedCardIds: Array.from(new Set(snapshot.playedCardIds)),
  };
}

function createEmptySuitCounts(): Record<Suit, number> {
  const counts = {} as Record<Suit, number>;
  for (const suit of SUITS) {
    counts[suit] = 0;
  }
  return counts;
}

/**
 * Tracks which cards have been played/captured during a round.
 * Used by AI to infer opponent hands WITHOUT cheating.
 */
export class CardTracker {
  private played: Set<string> = new Set(); // card IDs that have been seen

  constructor(snapshot?: CardTrackerSnapshot) {
    if (snapshot) {
      this.restoreSnapshot(snapshot);
    }
  }

  static fromSnapshot(snapshot: CardTrackerSnapshot): CardTracker {
    return new CardTracker(snapshot);
  }

  toSnapshot(): CardTrackerSnapshot {
    return {
      playedCardIds: [...this.played],
    };
  }

  restoreSnapshot(snapshot: CardTrackerSnapshot): void {
    const normalized = normalizeSnapshot(snapshot);
    this.played = new Set(normalized.playedCardIds);
  }

  /** Record a card being played to the table */
  trackPlay(card: Card): void {
    this.played.add(card.id);
  }

  /** Record cards captured from the table */
  trackCapture(cards: Card[]): void {
    for (const c of cards) {
      this.played.add(c.id);
    }
  }

  /** Reset for a new round */
  reset(): void {
    this.played.clear();
  }

  /** Has a specific card been seen played? */
  hasBeenPlayed(cardId: string): boolean {
    return this.played.has(cardId);
  }

  /** Is the settebello (7 of denara) still unseen (not played/captured)? */
  isSettebelloUnseen(): boolean {
    return !this.played.has('denara_7');
  }

  private buildVisibleValueResidueKnowledge(myHand: Card[], table: Card[]): VisibleValueResidueKnowledge {
    const knownCardIds = new Set<string>(this.played);
    for (const card of myHand) {
      knownCardIds.add(card.id);
    }
    for (const card of table) {
      knownCardIds.add(card.id);
    }

    const knownCountByValue = Array.from({ length: 11 }, () => 0);
    const unseenCountByValue = Array.from({ length: 11 }, () => 0);
    const unseenCountBySuit = createEmptySuitCounts();
    const unseenCards: Card[] = [];

    for (const suit of SUITS) {
      for (let value = 1; value <= 10; value++) {
        const id = `${suit}_${value}`;
        if (knownCardIds.has(id)) {
          knownCountByValue[value] += 1;
          continue;
        }

        unseenCountByValue[value] += 1;
        unseenCountBySuit[suit] += 1;
        unseenCards.push({ suit, value, id });
      }
    }

    const valueRankResidues: CardTrackerValueRankResidue[] = [];
    for (let value = 1; value <= 10; value++) {
      const unseenCount = unseenCountByValue[value];
      valueRankResidues.push({
        value,
        knownCount: knownCountByValue[value],
        unseenCount,
        hasSingletonUnseenRankResidue: unseenCount % 2 === 1,
        hasPairedUnseenRankResidue: unseenCount >= 2 && unseenCount % 2 === 0,
      });
    }

    return {
      unseenCards,
      unseenCountBySuit,
      unseenCountByValue,
      valueRankResidues,
    };
  }

  /**
   * Get cards that could be in opponent hands.
   * = full 40-card deck minus: already played, my hand, currently on table
   */
  getUnseenCards(myHand: Card[], table: Card[]): Card[] {
    return this.buildVisibleValueResidueKnowledge(myHand, table).unseenCards;
  }

  /** Count how many cards of a suit are still unseen */
  countRemainingSuit(suit: Suit, myHand: Card[], table: Card[]): number {
    return this.buildVisibleValueResidueKnowledge(myHand, table).unseenCountBySuit[suit];
  }

  /** Count how many unseen cards share a value */
  countRemainingValue(value: number, myHand: Card[], table: Card[]): number {
    return this.getValueRankResidue(value, myHand, table).unseenCount;
  }

  /** Get visible known-count, unseen-count, and same-rank residue for a single value */
  getValueRankResidue(value: number, myHand: Card[], table: Card[]): CardTrackerValueRankResidue {
    const valueRankResidues = this.buildVisibleValueResidueKnowledge(myHand, table).valueRankResidues;
    return valueRankResidues[value - 1] ?? {
      value,
      knownCount: 0,
      unseenCount: 0,
      hasSingletonUnseenRankResidue: false,
      hasPairedUnseenRankResidue: false,
    };
  }

  /** Get visible known-count, unseen-count, and same-rank residue for all card values */
  getValueRankResidueSummary(myHand: Card[], table: Card[]): CardTrackerValueRankResidue[] {
    return this.buildVisibleValueResidueKnowledge(myHand, table).valueRankResidues;
  }

  /** Probability that a hidden hand contains at least one card with the requested value */
  probabilityHandHasValue(value: number, handSize: number, myHand: Card[], table: Card[]): number {
    if (handSize <= 0) return 0;

    const visibleValueResidueKnowledge = this.buildVisibleValueResidueKnowledge(myHand, table);
    const unseen = visibleValueResidueKnowledge.unseenCards;
    const matching = visibleValueResidueKnowledge.unseenCountByValue[value] ?? 0;
    if (matching === 0) return 0;
    if (handSize >= unseen.length) return 1;

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

  /** Get count of all played/seen cards */
  get playedCount(): number {
    return this.played.size;
  }
}
