import {
  Card, Suit, SUITS, Player, PlayerIndex, GameState,
  TeamScore, ScoreBreakdown, PRIMIERA_VALUES, Capture, DealerRelativeRole
} from './types';

export type RandomSource = () => number;

// ---------------------------------------------------------------------------
// Deck
// ---------------------------------------------------------------------------

export function buildDeck(): Card[] {
  const deck: Card[] = [];
  for (const suit of SUITS) {
    for (let v = 1; v <= 10; v++) {
      deck.push({ suit, value: v, id: `${suit}_${v}` });
    }
  }
  return deck;
}

export function shuffle<T>(arr: T[], rng: RandomSource = Math.random): T[] {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

// ---------------------------------------------------------------------------
// Capture logic
// ---------------------------------------------------------------------------

/**
 * Find all valid capture combinations for a played card against the table.
 * Rules:
 *  - If the table contains a card of the same value, you MUST take it (and only it)
 *    unless the table has multiple cards of that value (take exactly one)
 *  - If no direct match exists, you may take any subset of table cards that sums to the played value
 * Returns array of capture sets (each is a list of cards taken from table).
 */
export function findCaptures(played: Card, table: Card[]): Card[][] {
  // Each direct-match card is a separate single-card capture option
  const directMatches = table.filter(c => c.value === played.value);
  if (directMatches.length > 0) {
    return directMatches.map((directMatch): Card[] => [directMatch]);
  }

  const results: Card[][] = [];
  // Only sum captures are legal when no direct match is available.
  const subsets = getSubsets(table);
  for (const subset of subsets) {
    if (subset.length >= 2) {
      const sum = subset.reduce((acc, c) => acc + c.value, 0);
      if (sum === played.value) {
        results.push(subset);
      }
    }
  }
  return results;
}

function getSubsets(cards: Card[]): Card[][] {
  const result: Card[][] = [[]];
  for (const card of cards) {
    const newSubsets = result.map(s => [...s, card]);
    result.push(...newSubsets);
  }
  return result;
}

export function canCapture(played: Card, table: Card[]): boolean {
  return findCaptures(played, table).length > 0;
}

// ---------------------------------------------------------------------------
// Game state initialisation
// ---------------------------------------------------------------------------

export function nextPlayer(playerIdx: PlayerIndex, steps = 1): PlayerIndex {
  return ((playerIdx + steps) % 4) as PlayerIndex;
}

export function getOpeningPlayerForDealer(dealer: PlayerIndex): PlayerIndex {
  return nextPlayer(dealer);
}

export function getDealerRelativeOrder(
  dealer: PlayerIndex
): [PlayerIndex, PlayerIndex, PlayerIndex, PlayerIndex] {
  const firstHand = getOpeningPlayerForDealer(dealer);
  const secondHand = nextPlayer(firstHand);
  const thirdHand = nextPlayer(secondHand);
  return [firstHand, secondHand, thirdHand, dealer];
}

export function getDealerRelativeRole(
  dealer: PlayerIndex,
  playerIdx: PlayerIndex
): DealerRelativeRole {
  const [firstHand, secondHand, thirdHand] = getDealerRelativeOrder(dealer);

  if (playerIdx === firstHand) return 'first-hand';
  if (playerIdx === secondHand) return 'second-hand';
  if (playerIdx === thirdHand) return 'third-hand';
  return 'dealer';
}

export function createInitialState(dealer: PlayerIndex = 3, rng: RandomSource = Math.random): GameState {
  const deck = shuffle(buildDeck(), rng);
  const startingPlayer = getOpeningPlayerForDealer(dealer);

  const players: [Player, Player, Player, Player] = [
    { index: 0, hand: [], pile: [], scope: 0, isHuman: true,  name: 'Tu' },
    { index: 1, hand: [], pile: [], scope: 0, isHuman: false, name: 'AI Ovest' },
    { index: 2, hand: [], pile: [], scope: 0, isHuman: false, name: 'Compagno' },
    { index: 3, hand: [], pile: [], scope: 0, isHuman: false, name: 'AI Est' },
  ];

  // Deal 10 cards each — Scopone Scientifico deals all at once
  for (let i = 0; i < 4; i++) {
    players[i].hand = deck.splice(0, 10);
  }

  // No initial table cards in Scopone Scientifico
  const table: Card[] = [];

  const emptyTeamScore = (): TeamScore => ({
    cards: 0, scope: 0, denari: 0, settebello: false, primiera: 0, roundPoints: 0, totalPoints: 0,
  });

  return {
    players,
    table,
    matchStartingPlayer: startingPlayer,
    dealer,
    currentPlayer: startingPlayer,
    roundOver: false,
    gameOver: false,
    teamScores: [emptyTeamScore(), emptyTeamScore()],
    lastCapturTeam: null,
    roundNumber: 1,
  };
}

// ---------------------------------------------------------------------------
// Play a card
// ---------------------------------------------------------------------------

/**
 * Apply a move to the game state (immutably).
 * If captureChoice is provided, use that capture set; otherwise use the first valid capture.
 * Returns the new state and what was captured (null if nothing).
 */
export function applyMove(
  state: GameState,
  playerIdx: PlayerIndex,
  card: Card,
  captureChoice?: Card[]
): { nextState: GameState; capture: Capture | null; isScopa: boolean } {
  const state2 = cloneState(state);
  const player = state2.players[playerIdx];

  // Remove card from hand
  player.hand = player.hand.filter(c => c.id !== card.id);

  const captures = findCaptures(card, state2.table);
  let capturedCards: Card[] = [];
  let isScopa = false;

  if (captures.length > 0) {
    const chosen = captureChoice ?? captures[0];
    capturedCards = chosen;

    // Remove captured cards from table
    const capturedIds = new Set(chosen.map(c => c.id));
    state2.table = state2.table.filter(c => !capturedIds.has(c.id));

    // Add played card + captured to player's pile
    player.pile.push(card, ...capturedCards);

    // Scopa: cleared the table (but NOT on the last play of the round)
    if (state2.table.length === 0) {
      const allHandsEmptyNow = state2.players.every(p => p.hand.length === 0);
      if (!allHandsEmptyNow) {
        player.scope += 1;
        isScopa = true;
      }
    }

    // Track which team made last capture
    state2.lastCapturTeam = (playerIdx === 0 || playerIdx === 2) ? 0 : 1;
  } else {
    // No capture — add to table
    state2.table.push(card);
  }

  // Advance turn
  state2.currentPlayer = nextPlayer(playerIdx);

  // Check if round is over (all hands empty)
  const allHandsEmpty = state2.players.every(p => p.hand.length === 0);
  if (allHandsEmpty) {
    // Remaining table cards go to last capturing team
    if (state2.table.length > 0 && state2.lastCapturTeam !== null) {
      const lastTeamPlayers = state2.lastCapturTeam === 0 ? [0, 2] : [1, 3];
      // Give to first player of that team
      state2.players[lastTeamPlayers[0]].pile.push(...state2.table);
      state2.table = [];
    }
    state2.roundOver = true;
    calculateScores(state2);
  }

  return {
    nextState: state2,
    capture: capturedCards.length > 0 ? { played: card, captured: capturedCards } : null,
    isScopa,
  };
}

// ---------------------------------------------------------------------------
// Scoring
// ---------------------------------------------------------------------------

function calculateScores(state: GameState): void {
  const team0 = [state.players[0], state.players[2]];
  const team1 = [state.players[1], state.players[3]];

  const breakdown: ScoreBreakdown = scoreRound(team0, team1);

  const t0 = state.teamScores[0];
  const t1 = state.teamScores[1];

  // Cards
  t0.cards = team0.reduce((s, p) => s + p.pile.length, 0);
  t1.cards = team1.reduce((s, p) => s + p.pile.length, 0);

  // Denari
  const denari0 = team0.flatMap(p => p.pile).filter(c => c.suit === 'denara');
  const denari1 = team1.flatMap(p => p.pile).filter(c => c.suit === 'denara');
  t0.denari = denari0.length;
  t1.denari = denari1.length;

  // Settebello
  t0.settebello = team0.flatMap(p => p.pile).some(c => c.suit === 'denara' && c.value === 7);
  t1.settebello = !t0.settebello;

  // Scope
  t0.scope = team0.reduce((s, p) => s + p.scope, 0);
  t1.scope = team1.reduce((s, p) => s + p.scope, 0);

  // Primiera
  t0.primiera = calcPrimiera(team0.flatMap(p => p.pile));
  t1.primiera = calcPrimiera(team1.flatMap(p => p.pile));

  // Points this round
  let p0 = 0;
  let p1 = 0;

  if (breakdown.cartePoint === 0) p0++;
  else if (breakdown.cartePoint === 1) p1++;

  if (breakdown.denariPoint === 0) p0++;
  else if (breakdown.denariPoint === 1) p1++;

  if (breakdown.settebelloPoint === 0) p0++;
  else p1++;

  if (breakdown.primieraPoint === 0) p0++;
  else if (breakdown.primieraPoint === 1) p1++;

  p0 += breakdown.scopeTeam0;
  p1 += breakdown.scopeTeam1;

  t0.roundPoints = p0;
  t1.roundPoints = p1;
  t0.totalPoints += p0;
  t1.totalPoints += p1;
}

function scoreRound(team0: Player[], team1: Player[]): ScoreBreakdown {
  const pile0 = team0.flatMap(p => p.pile);
  const pile1 = team1.flatMap(p => p.pile);

  const cards0 = pile0.length;
  const cards1 = pile1.length;

  const denari0 = pile0.filter(c => c.suit === 'denara').length;
  const denari1 = pile1.filter(c => c.suit === 'denara').length;

  const hasSette0 = pile0.some(c => c.suit === 'denara' && c.value === 7);

  const prim0 = calcPrimiera(pile0);
  const prim1 = calcPrimiera(pile1);

  const scope0 = team0.reduce((s, p) => s + p.scope, 0);
  const scope1 = team1.reduce((s, p) => s + p.scope, 0);

  return {
    cartePoint: cards0 > cards1 ? 0 : cards1 > cards0 ? 1 : null,
    denariPoint: denari0 > denari1 ? 0 : denari1 > denari0 ? 1 : null,
    settebelloPoint: hasSette0 ? 0 : 1,
    primieraPoint: prim0 > prim1 ? 0 : prim1 > prim0 ? 1 : null,
    scopeTeam0: scope0,
    scopeTeam1: scope1,
  };
}

export function calcPrimiera(pile: Card[]): number {
  // Best card per suit, using primiera values
  let total = 0;
  for (const suit of SUITS) {
    const cards = pile.filter(c => c.suit === suit);
    if (cards.length === 0) return 0; // can't score primiera without all 4 suits
    const best = Math.max(...cards.map(c => PRIMIERA_VALUES[c.value]));
    total += best;
  }
  return total;
}

export function getScoreBreakdown(state: GameState): ScoreBreakdown {
  const team0 = [state.players[0], state.players[2]];
  const team1 = [state.players[1], state.players[3]];
  return scoreRound(team0, team1);
}

export function getMatchOutcome(teamScores: [TeamScore, TeamScore]): {
  winner: 0 | 1 | null;
  continueMatch: boolean;
} {
  const [team0, team1] = teamScores;
  const thresholdReached = team0.totalPoints >= 11 || team1.totalPoints >= 11;

  if (!thresholdReached) {
    return {
      winner: null,
      continueMatch: true,
    };
  }

  if (team0.totalPoints === team1.totalPoints) {
    return {
      winner: null,
      continueMatch: true,
    };
  }

  return {
    winner: team0.totalPoints > team1.totalPoints ? 0 : 1,
    continueMatch: false,
  };
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function cloneCard(c: Card): Card {
  return { suit: c.suit, value: c.value, id: c.id };
}

function clonePlayer(p: Player): Player {
  return {
    index: p.index,
    hand: p.hand.map(cloneCard),
    pile: p.pile.map(cloneCard),
    scope: p.scope,
    isHuman: p.isHuman,
    name: p.name,
  };
}

function cloneTeamScore(ts: TeamScore): TeamScore {
  return { ...ts };
}

export function cloneState(state: GameState): GameState {
  return {
    players: [
      clonePlayer(state.players[0]),
      clonePlayer(state.players[1]),
      clonePlayer(state.players[2]),
      clonePlayer(state.players[3]),
    ],
    table: state.table.map(cloneCard),
    matchStartingPlayer: state.matchStartingPlayer,
    dealer: state.dealer,
    currentPlayer: state.currentPlayer,
    roundOver: state.roundOver,
    gameOver: state.gameOver,
    teamScores: [cloneTeamScore(state.teamScores[0]), cloneTeamScore(state.teamScores[1])],
    lastCapturTeam: state.lastCapturTeam,
    roundNumber: state.roundNumber,
  };
}

function deepClone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

export function teamOf(playerIdx: PlayerIndex): 0 | 1 {
  return (playerIdx === 0 || playerIdx === 2) ? 0 : 1;
}
