/*
  Blackjack (React, no build step)
  ══════════════════════════════════════════════════════════════════════════════
  Six-deck shoe, dealer stands on all 17s, blackjack pays 3:2, with insurance,
  double down and splitting up to four hands. The bankroll and win/loss record
  survive a reload via localStorage; everything else lives in the reducer.

  The domain logic (constants, shoe, hand values, reducer) is duplicated in
  game.js so the unit tests in test/game.test.js can load it without a browser.
  ══════════════════════════════════════════════════════════════════════════════
*/

// ── Domain: Card constants ───────────────────────────────────────────────────
const SUITS       = ["S", "H", "D", "C"];
const SUIT_SYMBOL = { S: "♠", H: "♥", D: "♦", C: "♣" };
const SUIT_COLOR  = { S: "black", C: "black", H: "red", D: "red" };
const RANKS       = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
const RANK_LABEL  = {
  1: "A", 2: "2",  3: "3", 4: "4", 5: "5",  6: "6",  7: "7",
  8: "8", 9: "9", 10: "10", 11: "J", 12: "Q", 13: "K",
};

// ── Domain: Table rules ──────────────────────────────────────────────────────
const DECKS           = 6;      // six-deck shoe
const MIN_BET         = 5;
const START_BANKROLL  = 500;
const MAX_HANDS       = 4;      // three splits
const DEALER_STANDS_ON = 17;    // dealer stands on all 17s, including soft 17
const BLACKJACK_PAYS  = 1.5;    // 3:2
const RESHUFFLE_AT    = 0.25;   // reshuffle once less than 25 % of the shoe is left

// ── Domain: Deck factory ─────────────────────────────────────────────────────
function makeShoe(decks) {
  let id = 1;
  const cards = [];
  for (let d = 0; d < decks; d++)
    for (const suit of SUITS)
      for (const rank of RANKS)
        cards.push({ id: id++, suit, rank });
  return cards;
}

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

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

// ── Domain: Hand evaluation ──────────────────────────────────────────────────
// Aces count 11 until the hand would bust, then 10 at a time drops to 1.
// `soft` is true while an ace is still being counted as 11.
function handValue(cards) {
  let total = 0;
  let aces  = 0;
  for (const c of cards) {
    if (c.rank === 1) { aces++; total += 11; }
    else              { total += c.rank >= 10 ? 10 : c.rank; }
  }
  while (total > 21 && aces > 0) { total -= 10; aces--; }
  return { total, soft: aces > 0 };
}

function cardValue(card) {
  if (card.rank === 1)   return 11;
  if (card.rank >= 10)   return 10;
  return card.rank;
}

// A natural: the first two cards dealt to a hand that was not split.
function isBlackjack(hand) {
  return hand.cards.length === 2
      && !hand.fromSplit
      && handValue(hand.cards).total === 21;
}

function isBust(hand) {
  return handValue(hand.cards).total > 21;
}

function formatMoney(n) {
  const v = Math.round(n * 100) / 100;
  return "$" + (Number.isInteger(v) ? v : v.toFixed(2));
}

// ── Domain: Initial state ────────────────────────────────────────────────────
function newHand(bet, fromSplit, splitAces) {
  return {
    cards:     [],
    bet:       bet,
    done:      false,
    doubled:   false,
    fromSplit: !!fromSplit,
    splitAces: !!splitAces,
    result:    null,   // "win" | "lose" | "push" | "bust" | "blackjack"
    payout:    0,
  };
}

function emptyStats() {
  return { played: 0, won: 0, lost: 0, push: 0, blackjacks: 0 };
}

// `saved` (optional) restores bankroll and stats from localStorage.
function initialState(saved) {
  const bankroll = saved && typeof saved.bankroll === "number"
    ? saved.bankroll
    : START_BANKROLL;
  return {
    shoe:       shuffle(makeShoe(DECKS)),
    bankroll:   bankroll,
    bet:        Math.min(MIN_BET, bankroll),
    hands:      [],
    active:     0,
    dealer:     [],
    holeHidden: true,
    insurance:  0,
    phase:      "betting",   // betting | insurance | player | dealer | settled
    message:    "Place your bet.",
    lastNet:    0,
    stats:      (saved && saved.stats) || emptyStats(),
  };
}

// ── Domain: Shoe helpers ─────────────────────────────────────────────────────
function drawCard(s) {
  if (!s.shoe.length) s.shoe = shuffle(makeShoe(DECKS));
  return s.shoe.pop();
}

function needsShuffle(s) {
  return s.shoe.length < DECKS * 52 * RESHUFFLE_AT;
}

// ── Domain: Action availability (shared by UI and tests) ─────────────────────
function activeHand(s) {
  return s.hands[s.active] || null;
}

function canDeal(s) {
  return s.phase === "betting" && s.bet >= MIN_BET && s.bet <= s.bankroll;
}

function canHit(s) {
  const h = activeHand(s);
  return s.phase === "player" && !!h && !h.done && handValue(h.cards).total < 21;
}

function canDouble(s) {
  const h = activeHand(s);
  return s.phase === "player" && !!h && !h.done
      && h.cards.length === 2 && !h.splitAces
      && s.bankroll >= h.bet;
}

function canSplit(s) {
  const h = activeHand(s);
  if (s.phase !== "player" || !h || h.done) return false;
  if (h.cards.length !== 2)                 return false;
  if (s.hands.length >= MAX_HANDS)          return false;
  if (s.bankroll < h.bet)                   return false;
  // Same rank, or any two ten-valued cards (10, J, Q, K).
  const [a, b] = h.cards;
  return a.rank === b.rank || (cardValue(a) === 10 && cardValue(b) === 10);
}

function insuranceCost(s) {
  return s.hands.length ? s.hands[0].bet / 2 : 0;
}

// ── Domain: Round flow ───────────────────────────────────────────────────────
// Move to the next hand that still needs to act, dealing a second card to a
// freshly split hand on the way. When no hand is left it is the dealer's turn.
function advanceHand(s) {
  for (let i = s.active; i < s.hands.length; i++) {
    const h = s.hands[i];
    if (h.done) continue;
    if (h.cards.length < 2) h.cards.push(drawCard(s));
    // Split aces get exactly one card; 21 needs no decision.
    if (h.splitAces || handValue(h.cards).total >= 21) { h.done = true; continue; }
    s.active  = i;
    s.message = s.hands.length > 1 ? "Playing hand " + (i + 1) + "." : "Your move.";
    return s;
  }

  s.active     = s.hands.length - 1;
  s.holeHidden = false;

  // If every hand busted the dealer does not draw — the round is over.
  if (s.hands.every(isBust)) return settle(s);

  s.phase   = "dealer";
  s.message = "Dealer's turn.";
  return s;
}

// Dealer peeks for blackjack on an Ace or ten-valued up card, and a player
// natural ends the round immediately.
function resolveOpening(s) {
  const up       = s.dealer[0];
  const dealerBJ = handValue(s.dealer).total === 21;

  if (cardValue(up) >= 10 && dealerBJ) {
    s.holeHidden = false;
    return settle(s);
  }
  if (isBlackjack(s.hands[0])) {
    s.hands[0].done = true;
    s.holeHidden    = false;
    return settle(s);
  }
  s.phase   = "player";
  s.message = "Your move.";
  return s;
}

function settle(s) {
  s.holeHidden = false;

  const dTotal   = handValue(s.dealer).total;
  const dealerBJ = s.dealer.length === 2 && dTotal === 21;

  let wagered  = s.insurance;
  let returned = s.insurance > 0 && dealerBJ ? s.insurance * 3 : 0;

  for (const h of s.hands) {
    wagered += h.bet;
    const pTotal = handValue(h.cards).total;
    const pBJ    = isBlackjack(h);

    if (pTotal > 21)               { h.result = "bust";      h.payout = 0; }
    else if (pBJ && !dealerBJ)     { h.result = "blackjack"; h.payout = h.bet * (1 + BLACKJACK_PAYS); }
    else if (pBJ && dealerBJ)      { h.result = "push";      h.payout = h.bet; }
    else if (dealerBJ)             { h.result = "lose";      h.payout = 0; }
    else if (dTotal > 21)          { h.result = "win";       h.payout = h.bet * 2; }
    else if (pTotal > dTotal)      { h.result = "win";       h.payout = h.bet * 2; }
    else if (pTotal === dTotal)    { h.result = "push";      h.payout = h.bet; }
    else                           { h.result = "lose";      h.payout = 0; }

    returned += h.payout;

    s.stats.played++;
    if (h.result === "win" || h.result === "blackjack") s.stats.won++;
    else if (h.result === "push")                       s.stats.push++;
    else                                                s.stats.lost++;
    if (h.result === "blackjack")                       s.stats.blackjacks++;
  }

  s.bankroll += returned;
  s.lastNet   = returned - wagered;
  s.phase     = "settled";
  s.message   = s.lastNet > 0 ? "You win " + formatMoney(s.lastNet) + "."
              : s.lastNet < 0 ? "You lose " + formatMoney(-s.lastNet) + "."
              : "Push — your bet is returned.";
  return s;
}

// ── Domain: Reducer ──────────────────────────────────────────────────────────
function gameReducer(state, action) {
  const s = deepClone(state);

  switch (action.type) {

    case "RESET":
      return initialState();

    case "ADD_CHIP": {
      if (s.phase !== "betting") return state;
      const next = s.bet + action.amount;
      if (next > s.bankroll) return state;
      s.bet = next;
      return s;
    }

    case "CLEAR_BET": {
      if (s.phase !== "betting") return state;
      s.bet = 0;
      return s;
    }

    case "DEAL": {
      if (!canDeal(s)) return state;
      if (needsShuffle(s)) s.shoe = shuffle(makeShoe(DECKS));

      s.bankroll  -= s.bet;
      s.hands      = [newHand(s.bet)];
      s.dealer     = [];
      s.holeHidden = true;
      s.insurance  = 0;
      s.active     = 0;
      s.lastNet    = 0;

      s.hands[0].cards.push(drawCard(s));
      s.dealer.push(drawCard(s));
      s.hands[0].cards.push(drawCard(s));
      s.dealer.push(drawCard(s));

      // Insurance is offered on an Ace up card, before the dealer peeks.
      if (s.dealer[0].rank === 1 && s.bankroll >= insuranceCost(s)) {
        s.phase   = "insurance";
        s.message = "Dealer shows an Ace — insurance?";
        return s;
      }
      return resolveOpening(s);
    }

    case "INSURANCE": {
      if (s.phase !== "insurance") return state;
      if (action.take) {
        const cost = insuranceCost(s);
        if (s.bankroll < cost) return state;
        s.bankroll -= cost;
        s.insurance = cost;
      }
      return resolveOpening(s);
    }

    case "HIT": {
      if (!canHit(s)) return state;
      const h = activeHand(s);
      h.cards.push(drawCard(s));
      const { total } = handValue(h.cards);
      if (total >= 21) {          // bust, or 21 needs no further decision
        h.done = true;
        return advanceHand(s);
      }
      return s;
    }

    case "STAND": {
      if (s.phase !== "player") return state;
      const h = activeHand(s);
      if (!h || h.done) return state;
      h.done = true;
      return advanceHand(s);
    }

    case "DOUBLE": {
      if (!canDouble(s)) return state;
      const h = activeHand(s);
      s.bankroll -= h.bet;
      h.bet      *= 2;
      h.doubled   = true;
      h.cards.push(drawCard(s));
      h.done      = true;
      return advanceHand(s);
    }

    case "SPLIT": {
      if (!canSplit(s)) return state;
      const h = activeHand(s);
      const splitAces = h.cards[0].rank === 1;
      s.bankroll -= h.bet;

      const moved = h.cards.pop();
      h.fromSplit = true;
      h.splitAces = splitAces;

      const extra = newHand(h.bet, true, splitAces);
      extra.cards.push(moved);
      s.hands.splice(s.active + 1, 0, extra);

      return advanceHand(s);   // tops both hands back up to two cards
    }

    case "DEALER_STEP": {
      if (s.phase !== "dealer") return state;
      if (handValue(s.dealer).total < DEALER_STANDS_ON) {
        s.dealer.push(drawCard(s));
        return s;
      }
      return settle(s);
    }

    case "NEXT_ROUND": {
      if (s.phase !== "settled") return state;
      s.hands      = [];
      s.dealer     = [];
      s.holeHidden = true;
      s.insurance  = 0;
      s.active     = 0;
      s.lastNet    = 0;
      s.phase      = "betting";
      if (s.bet > s.bankroll) s.bet = s.bankroll;
      s.message    = s.bankroll < MIN_BET
        ? "Out of chips — start a new game."
        : "Place your bet.";
      return s;
    }

    default:
      return state;
  }
}

// ── UI: constants ────────────────────────────────────────────────────────────
const CHIPS       = [5, 25, 100, 500];
const STORAGE_KEY = "blackjack.v1";
const DEAL_DELAY  = 650;   // ms between the dealer's cards

// ── UI: persistence ──────────────────────────────────────────────────────────
function loadSaved() {
  try {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : null;
  } catch (err) {
    return null;   // private mode / storage disabled — just start fresh
  }
}

function save(game) {
  try {
    window.localStorage.setItem(
      STORAGE_KEY,
      JSON.stringify({ bankroll: game.bankroll, stats: game.stats }),
    );
  } catch (err) { /* ignore */ }
}

// ── UI: labels ───────────────────────────────────────────────────────────────
// A soft hand shows both totals, e.g. "7/17" for A+6.
function totalLabel(cards) {
  const { total, soft } = handValue(cards);
  if (soft && total <= 21) return (total - 10) + "/" + total;
  return String(total);
}

const RESULT_LABEL = {
  win:       "Win",
  lose:      "Lose",
  push:      "Push",
  bust:      "Bust",
  blackjack: "Blackjack!",
};

// ── UI: App ──────────────────────────────────────────────────────────────────
function App() {
  const [game, dispatch] = React.useReducer(
    gameReducer, null, () => initialState(loadSaved()),
  );

  React.useEffect(() => { save(game); }, [game.bankroll, game.stats]);

  // The dealer draws on a timer so the cards land one at a time.
  React.useEffect(() => {
    if (game.phase !== "dealer") return undefined;
    const t = setTimeout(() => dispatch({ type: "DEALER_STEP" }), DEAL_DELAY);
    return () => clearTimeout(t);
  }, [game.phase, game.dealer.length]);

  const deal      = () => dispatch({ type: "DEAL" });
  const nextRound = () => dispatch({ type: "NEXT_ROUND" });

  const newGame = () => {
    if (window.confirm("Start over with " + formatMoney(START_BANKROLL) + " and clear your record?"))
      dispatch({ type: "RESET" });
  };

  // ── Keyboard shortcuts ──────────────────────────────────────────────────────
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      const key = e.key.toLowerCase();

      if (key === "enter" || key === " ") {
        if (game.phase === "betting" && canDeal(game)) { e.preventDefault(); deal(); }
        else if (game.phase === "settled")             { e.preventDefault(); nextRound(); }
        return;
      }
      if (game.phase !== "player") return;
      if (key === "h" && canHit(game))    dispatch({ type: "HIT" });
      if (key === "s")                    dispatch({ type: "STAND" });
      if (key === "d" && canDouble(game)) dispatch({ type: "DOUBLE" });
      if (key === "p" && canSplit(game))  dispatch({ type: "SPLIT" });
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [game]);

  const broke      = game.bankroll < MIN_BET && game.phase === "betting";
  const showResult = game.phase === "settled";

  // ── Render ──────────────────────────────────────────────────────────────────
  return (
    <div className="app">
      <header className="topbar">
        <h1>Blackjack</h1>
        <div className="bank">
          <span className="bankroll" title="Your chips">{formatMoney(game.bankroll)}</span>
          <button className="ghost" onClick={newGame}>New Game</button>
        </div>
      </header>

      <main className="table">
        {/* ── Dealer ──────────────────────────────────────────────────── */}
        <section className="seat dealer-seat">
          <div className="seat-head">
            <span className="seat-label">Dealer</span>
            {game.dealer.length > 0 && (
              <span className="total">
                {game.holeHidden ? handValue([game.dealer[0]]).total + " + ?"
                                 : totalLabel(game.dealer)}
              </span>
            )}
          </div>
          <CardRow cards={game.dealer} holeIndex={game.holeHidden ? 1 : -1} />
        </section>

        <p className={"message" + (game.lastNet > 0 ? " message--good"
                                 : game.lastNet < 0 ? " message--bad" : "")}>
          {game.message}
        </p>

        {/* ── Player ──────────────────────────────────────────────────── */}
        <section className={"hands" + (game.hands.length > 1 ? " hands--split" : "")}>
          {game.hands.length === 0 && (
            <div className="seat">
              <div className="seat-head"><span className="seat-label">You</span></div>
              <div className="empty-seat" />
            </div>
          )}
          {game.hands.map((hand, i) => (
            <HandView
              key={i}
              hand={hand}
              index={i}
              of={game.hands.length}
              active={game.phase === "player" && i === game.active}
              showResult={showResult}
            />
          ))}
        </section>
      </main>

      {/* ── Controls ──────────────────────────────────────────────────── */}
      <footer className="controls">
        {game.phase === "betting" && (
          <BettingControls game={game} dispatch={dispatch} broke={broke}
                           onDeal={deal} onNewGame={newGame} />
        )}

        {game.phase === "insurance" && (
          <div className="control-row">
            <span className="prompt">
              Insurance costs {formatMoney(insuranceCost(game))} and pays 2:1.
            </span>
            <button onClick={() => dispatch({ type: "INSURANCE", take: true })}>
              Take Insurance
            </button>
            <button className="ghost" onClick={() => dispatch({ type: "INSURANCE", take: false })}>
              No Thanks
            </button>
          </div>
        )}

        {game.phase === "player" && (
          <div className="control-row">
            <button onClick={() => dispatch({ type: "HIT" })}    disabled={!canHit(game)}>Hit</button>
            <button onClick={() => dispatch({ type: "STAND" })}>Stand</button>
            <button onClick={() => dispatch({ type: "DOUBLE" })} disabled={!canDouble(game)}>Double</button>
            <button onClick={() => dispatch({ type: "SPLIT" })}  disabled={!canSplit(game)}>Split</button>
          </div>
        )}

        {game.phase === "dealer" && (
          <div className="control-row"><span className="prompt">Dealer is drawing…</span></div>
        )}

        {game.phase === "settled" && (
          <div className="control-row">
            <button className="primary" onClick={nextRound}>Next Round</button>
          </div>
        )}
      </footer>

      <div className="statusbar">
        <span>Won {game.stats.won} · Lost {game.stats.lost} · Push {game.stats.push}</span>
        <span>{game.shoe.length} cards left in the shoe</span>
      </div>

      <details className="rules">
        <summary>House rules &amp; shortcuts</summary>
        <ul>
          <li>{DECKS}-deck shoe, reshuffled when it runs low.</li>
          <li>Blackjack pays 3:2. Insurance pays 2:1.</li>
          <li>Dealer stands on all 17s, soft ones included.</li>
          <li>Double on any first two cards; split up to {MAX_HANDS} hands.</li>
          <li>Split aces get one card each, and 21 after a split is not a blackjack.</li>
          <li>Keys: <kbd>H</kbd> hit · <kbd>S</kbd> stand · <kbd>D</kbd> double · <kbd>P</kbd> split · <kbd>Enter</kbd> deal.</li>
        </ul>
      </details>
    </div>
  );
}

// ── UI: Betting controls ─────────────────────────────────────────────────────
function BettingControls({ game, dispatch, broke, onDeal, onNewGame }) {
  if (broke) {
    return (
      <div className="control-row">
        <span className="prompt">You are out of chips.</span>
        <button className="primary" onClick={onNewGame}>New Game</button>
      </div>
    );
  }
  return (
    <div className="control-row">
      <span className="bet-display">Bet {formatMoney(game.bet)}</span>
      <div className="chips">
        {CHIPS.map((amount) => (
          <button
            key={amount}
            className={"chip chip--" + amount}
            disabled={game.bet + amount > game.bankroll}
            onClick={() => dispatch({ type: "ADD_CHIP", amount })}
            aria-label={"Bet " + formatMoney(amount) + " more"}
          >
            {amount}
          </button>
        ))}
      </div>
      <button className="ghost" onClick={() => dispatch({ type: "CLEAR_BET" })}
              disabled={game.bet === 0}>Clear</button>
      <button className="primary" onClick={onDeal} disabled={!canDeal(game)}>Deal</button>
    </div>
  );
}

// ── UI: One player hand ──────────────────────────────────────────────────────
function HandView({ hand, index, of, active, showResult }) {
  const classes = ["seat", active ? "seat--active" : "",
                   showResult && hand.result ? "seat--" + hand.result : ""]
    .filter(Boolean).join(" ");

  return (
    <div className={classes}>
      <div className="seat-head">
        <span className="seat-label">{of > 1 ? "Hand " + (index + 1) : "You"}</span>
        <span className="total">{totalLabel(hand.cards)}</span>
        <span className="wager">{formatMoney(hand.bet)}{hand.doubled ? " ×2" : ""}</span>
      </div>
      <CardRow cards={hand.cards} holeIndex={-1} />
      {showResult && hand.result && (
        <span className={"result result--" + hand.result}>{RESULT_LABEL[hand.result]}</span>
      )}
    </div>
  );
}

// ── UI: Row of cards ─────────────────────────────────────────────────────────
function CardRow({ cards, holeIndex }) {
  return (
    <div className="card-row">
      {cards.map((card, i) => (
        <CardView key={card.id} card={card} faceUp={i !== holeIndex} />
      ))}
    </div>
  );
}

// ── UI: Card ─────────────────────────────────────────────────────────────────
function CardView({ card, faceUp }) {
  if (!faceUp) return <div className="card face-down" />;

  const color = SUIT_COLOR[card.suit];
  return (
    <div className="card" title={RANK_LABEL[card.rank] + SUIT_SYMBOL[card.suit]}>
      <div className={"corner tl " + color}>
        <span className="rank">{RANK_LABEL[card.rank]}</span>
        <span className="suit">{SUIT_SYMBOL[card.suit]}</span>
      </div>
      <div className={"center-suit " + color}>{SUIT_SYMBOL[card.suit]}</div>
      <div className={"corner br " + color}>
        <span className="rank">{RANK_LABEL[card.rank]}</span>
        <span className="suit">{SUIT_SYMBOL[card.suit]}</span>
      </div>
    </div>
  );
}

// ── Mount ────────────────────────────────────────────────────────────────────
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
