// Ferrous Solitaire — interactive browser game.
//
// Architecture:
//   - `SolitaireGame` (Rust/WASM via solitaire_core) owns all rule logic.
//   - This file owns the DOM renderer, drag-and-drop input, and the game loop.
//   - Cards are persistent DOM elements keyed by `card.id`; positions are
//     updated via `transform: translate(...)` so the browser can animate
//     flights on the compositor thread.
//
// Pile name convention (mirrors solitaire_wasm::SolitaireGame::move_cards):
//   "stock" | "waste" | "foundation-0".."foundation-3" | "tableau-0".."tableau-6"

import init, { SolitaireGame } from "/web/pkg/solitaire_wasm.js";

// ── Layout constants (must match game.css --card-w / --card-h / --gap / --pad)
const CARD_W    = 80;
const CARD_H    = 120;   // 2:3 ratio matching 256×384 card PNGs
const GAP       = 12;
const PAD       = 20;   // board inner padding — cards start at (PAD, PAD)
const FAN       = 28;   // vertical offset per fanned tableau card
const WASTE_FAN = 18;   // horizontal offset for draw-3 waste fan

// Natural board dimensions — used for scale-to-fit calculation.
const BOARD_W = PAD * 2 + 7 * CARD_W + 6 * GAP;            // 672
const BOARD_H = PAD * 2 + CARD_H + 28 + CARD_H + 12 * FAN; // 644

// Pile origins in board-element coordinates (include PAD so (0,0) = board edge).
const TOP_Y    = PAD;
const BOTTOM_Y = PAD + CARD_H + 28;

const colX = (c) => PAD + c * (CARD_W + GAP);

const PILE_ORIGIN = {
    stock:          { x: colX(0), y: TOP_Y },
    waste:          { x: colX(1), y: TOP_Y },
    "foundation-0": { x: colX(3), y: TOP_Y },
    "foundation-1": { x: colX(4), y: TOP_Y },
    "foundation-2": { x: colX(5), y: TOP_Y },
    "foundation-3": { x: colX(6), y: TOP_Y },
    "tableau-0":    { x: colX(0), y: BOTTOM_Y },
    "tableau-1":    { x: colX(1), y: BOTTOM_Y },
    "tableau-2":    { x: colX(2), y: BOTTOM_Y },
    "tableau-3":    { x: colX(3), y: BOTTOM_Y },
    "tableau-4":    { x: colX(4), y: BOTTOM_Y },
    "tableau-5":    { x: colX(5), y: BOTTOM_Y },
    "tableau-6":    { x: colX(6), y: BOTTOM_Y },
};

// Foundation suit hints shown when the slot is empty.
const FOUND_SUIT_HINT = ["♠", "♥", "♦", "♣"];

const SUIT_CODE   = { clubs: "C", diamonds: "D", hearts: "H", spades: "S" };
const RANK_LABELS = ["","A","2","3","4","5","6","7","8","9","10","J","Q","K"];
const RED_SUITS   = new Set(["diamonds", "hearts"]);

// ── Theme ─────────────────────────────────────────────────────────────────────
let cardTheme = localStorage.getItem("fs_theme") || "classic";

function preloadTheme(theme) {
    const suits = Object.values(SUIT_CODE);
    const ranks = RANK_LABELS.slice(1);
    for (const r of ranks) for (const s of suits) {
        new Image().src = `/assets/cards/faces/${theme}/${r}${s}.png`;
    }
    new Image().src = `/assets/cards/backs/${theme}/back_0.png`;
}

// Preload both themes on load so switching is instant.
preloadTheme("classic");
preloadTheme("dark");

// ── Persistence ──────────────────────────────────────────────────────────────
const LS_SAVE_KEY = "fs_game_save";

function saveState() {
    if (!game) return;
    try {
        const gameState = game.serialize();
        if (typeof gameState !== "string") return;
        localStorage.setItem(LS_SAVE_KEY, JSON.stringify({ gameState, elapsedSecs, drawThree }));
    } catch (e) {
        // localStorage may be unavailable (private browsing quota, etc.) — never block gameplay.
        console.warn("fs: save failed", e);
    }
}

function clearSave() {
    try { localStorage.removeItem(LS_SAVE_KEY); } catch { /* ignore */ }
}

function loadSave() {
    try {
        const raw = localStorage.getItem(LS_SAVE_KEY);
        if (!raw) return null;
        const save = JSON.parse(raw);
        return save?.gameState ? save : null;
    } catch { return null; }
}

// ── State ────────────────────────────────────────────────────────────────────
let game      = null;
let snap      = null;   // last rendered GameSnapshot
let drawThree = false;

// Persistent card-id → DOM element map.
const cardEls = new Map();

// Drag state
let drag = null;
// drag = {
//   fromPile: string,
//   fromIndex: number,       // index of bottom dragged card in its pile
//   cardIds: number[],       // ids bottom→top
//   startX: number, startY: number,   // board-relative pointer start
// }

// Timer
let timerInterval = null;
let elapsedSecs   = 0;

// Auto-complete
let acTimer = null;

// Current scale factor applied to #board.
let boardScale = 1.0;

// ── DOM refs ─────────────────────────────────────────────────────────────────
const board      = document.getElementById("card-area");
const hudScore   = document.getElementById("hud-score");
const hudMoves   = document.getElementById("hud-moves");
const hudTimer   = document.getElementById("hud-timer");
const hudStock   = document.getElementById("hud-stock");
const hudSeed    = document.getElementById("hud-seed");
const btnUndo      = document.getElementById("btn-undo");
const btnBoardUndo = document.getElementById("btn-board-undo");
const btnNew     = document.getElementById("btn-new");
const chkDraw3   = document.getElementById("chk-draw3");
const btnTheme   = document.getElementById("btn-theme");
const winOverlay    = document.getElementById("win-overlay");
const winScore      = document.getElementById("win-score");
const winMoves      = document.getElementById("win-moves");
const winTime       = document.getElementById("win-time");
const btnWinNew     = document.getElementById("btn-win-new");
const noMovesBanner = document.getElementById("no-moves-banner");

// ── Scale to fit ─────────────────────────────────────────────────────────────
// Scales #card-area to fill #board without overflowing either dimension.
// boardRelative() divides by boardScale to keep hit-testing correct.
function scaleBoard() {
    // Measure the actual rendered #board element — more reliable than
    // computing window.innerHeight minus estimated header height, which
    // breaks under different browser chrome / OS scaling factors.
    const outerBoard = document.getElementById("board");
    const bw = outerBoard.clientWidth;
    const bh = outerBoard.clientHeight;
    boardScale = Math.min(bw / BOARD_W, bh / BOARD_H, 2.0);
    board.style.transform       = `scale(${boardScale})`;
    board.style.transformOrigin = "center center";
}

function syncThemeButton() {
    if (btnTheme) btnTheme.textContent = cardTheme === "classic" ? "Dark" : "Classic";
}

// ── Bootstrap ────────────────────────────────────────────────────────────────
async function bootstrap() {
    await init();
    syncThemeButton();

    buildSlots();
    scaleBoard();
    window.addEventListener("resize", scaleBoard);
    attachHandlers();

    const saved = loadSave();
    if (saved) {
        showResumeDialog(saved);
    } else {
        const params    = new URLSearchParams(window.location.search);
        const rawSeed   = Number(params.get("seed"));
        const urlSeed   = params.has("seed") && Number.isFinite(rawSeed) && rawSeed > 0
            ? Math.floor(rawSeed)
            : randomSeed();
        drawThree       = params.has("draw3");
        chkDraw3.checked = drawThree;
        startGame(urlSeed);
    }
}

function showResumeDialog(saved) {
    const overlay = document.getElementById("resume-overlay");
    if (overlay) overlay.classList.remove("hidden");

    document.getElementById("btn-resume").onclick = () => {
        if (overlay) overlay.classList.add("hidden");
        resumeGame(saved);
    };
    document.getElementById("btn-resume-new").onclick = () => {
        clearSave();
        if (overlay) overlay.classList.add("hidden");
        drawThree = false;
        chkDraw3.checked = false;
        startGame(randomSeed());
    };
}

function resumeGame(saved) {
    let restored;
    try {
        restored = SolitaireGame.from_saved(saved.gameState);
    } catch (e) {
        console.warn("fs: restore failed, starting new game", e);
        clearSave();
        startGame(randomSeed());
        return;
    }

    game = restored;
    drawThree = !!saved.drawThree;
    elapsedSecs = saved.elapsedSecs || 0;
    chkDraw3.checked = drawThree;

    const displaySeed = Math.round(game.seed());
    hudSeed.textContent = `seed ${displaySeed}`;
    winOverlay.classList.add("hidden");
    cardEls.clear();
    board.querySelectorAll(".card, .recycle-label").forEach(el => el.remove());

    const url = new URL(window.location);
    url.searchParams.set("seed", displaySeed);
    if (drawThree) url.searchParams.set("draw3", "");
    else           url.searchParams.delete("draw3");
    history.replaceState(null, "", url);

    const s = game.state();
    snap = s;
    render(s);
    if (!s.is_won) startTimer();
}

function randomSeed() {
    return Math.floor(Math.random() * 9007199254740991);
}

function startGame(seed) {
    if (acTimer)    { clearInterval(acTimer);    acTimer    = null; }
    if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
    elapsedSecs = 0;
    updateTimerDisplay();

    game = new SolitaireGame(seed, drawThree);
    snap = game.state();

    const displaySeed = Math.round(game.seed());
    hudSeed.textContent = `seed ${displaySeed}`;
    winOverlay.classList.add("hidden");
    cardEls.clear();
    board.querySelectorAll(".card, .recycle-label").forEach(el => el.remove());

    // Persist seed in URL so the game can be shared / refreshed.
    const url = new URL(window.location);
    url.searchParams.set("seed", displaySeed);
    if (drawThree) url.searchParams.set("draw3", "");
    else           url.searchParams.delete("draw3");
    history.replaceState(null, "", url);

    render(snap);
    startTimer();
}

// ── Timer ────────────────────────────────────────────────────────────────────
function startTimer() {
    // Idempotent: never stack a second interval. The visibilitychange handler
    // and startGame can both call this, and a stray call (e.g. a load-time
    // visibilitychange while a timer is already running) would otherwise leak
    // the old interval and make elapsedSecs increment twice per second.
    if (timerInterval) return;
    timerInterval = setInterval(() => {
        elapsedSecs++;
        updateTimerDisplay();
    }, 1000);
}

function stopTimer() {
    if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
}

function updateTimerDisplay() {
    const m = Math.floor(elapsedSecs / 60);
    const s = elapsedSecs % 60;
    if (hudTimer) hudTimer.textContent = `${m}:${s.toString().padStart(2, "0")}`;
}

// ── Slot placeholders ─────────────────────────────────────────────────────────
function buildSlots() {
    for (const [pile, origin] of Object.entries(PILE_ORIGIN)) {
        const el = document.createElement("div");
        el.className = "slot";
        el.dataset.pile = pile;
        el.style.transform = `translate(${origin.x}px, ${origin.y}px)`;

        if (pile.startsWith("foundation-")) {
            const slot = parseInt(pile.split("-")[1]);
            const hint = document.createElement("div");
            hint.className = "slot-hint";
            hint.textContent = FOUND_SUIT_HINT[slot];
            el.appendChild(hint);
        }
        board.appendChild(el);
    }
}

// ── Card position math ────────────────────────────────────────────────────────
function cardPos(pileName, indexInPile, pileLength) {
    const origin = PILE_ORIGIN[pileName];
    let x = origin.x;
    let y = origin.y;

    if (pileName === "waste" && drawThree && pileLength >= 2) {
        const fanStart = Math.max(0, pileLength - 3);
        const fanPos   = indexInPile - fanStart;
        if (fanPos > 0) x += fanPos * WASTE_FAN;
    } else if (pileName.startsWith("tableau-")) {
        y += indexInPile * FAN;
    }
    return { x, y };
}

function cardZ(pileName, indexInPile) {
    return 10 + indexInPile;
}

// ── Renderer ──────────────────────────────────────────────────────────────────
function render(s) {
    snap = s;

    hudScore.textContent = `Score: ${s.score}`;
    hudMoves.textContent = `Moves: ${s.move_count}`;
    if (hudStock) hudStock.textContent = `Stock: ${s.stock.length}`;
    btnUndo.disabled           = s.undo_stack_len === 0;
    btnBoardUndo.disabled      = s.undo_stack_len === 0;

    const visible = new Map();
    const addPile = (name, cards) =>
        cards.forEach((c, i) => visible.set(c.id, { pile: name, idx: i, card: c, total: cards.length }));

    addPile("stock",  s.stock);
    addPile("waste",  s.waste);
    s.foundations.forEach((f, i) => addPile(`foundation-${i}`, f));
    s.tableaus.forEach((t, i)    => addPile(`tableau-${i}`, t));

    for (const [id, info] of visible) {
        let el = cardEls.get(id);
        if (!el) {
            el = document.createElement("div");
            el.dataset.cardId = id;
            cardEls.set(id, el);
            board.appendChild(el);
        }
        updateCardEl(el, info.card, info.pile, info.idx, info.total);
    }

    for (const [id, el] of cardEls) {
        if (!visible.has(id)) { el.remove(); cardEls.delete(id); }
    }

    // Foundation suit hints: hide when pile has cards.
    s.foundations.forEach((f, i) => {
        const slotEl = board.querySelector(`.slot[data-pile="foundation-${i}"]`);
        if (slotEl) {
            const hint = slotEl.querySelector(".slot-hint");
            if (hint) hint.style.visibility = f.length > 0 ? "hidden" : "";
        }
    });

    // Recycle indicator on empty stock.
    let recycleEl = board.querySelector(".recycle-label");
    if (s.stock.length === 0 && s.waste.length > 0) {
        if (!recycleEl) {
            recycleEl = document.createElement("div");
            recycleEl.className = "recycle-label";
            recycleEl.textContent = "↺";
            board.appendChild(recycleEl);
        }
        const o = PILE_ORIGIN.stock;
        recycleEl.style.transform = `translate(${o.x}px, ${o.y}px)`;
    } else if (recycleEl) {
        recycleEl.remove();
    }

    // Clear drag highlights left from pointer-move.
    board.querySelectorAll(".slot.drop-active").forEach(e => e.classList.remove("drop-active"));
    board.querySelectorAll(".card.drop-target").forEach(e => e.classList.remove("drop-target"));

    if (s.is_auto_completable && !s.is_won && !acTimer) {
        stopTimer(); // freeze elapsed time at the moment the player's last move completes
        acTimer = setInterval(doAutoCompleteStep, 380);
    }
    if (s.is_won) {
        clearSave();
        stopTimer();
        if (acTimer) { clearInterval(acTimer); acTimer = null; }
        if (noMovesBanner) noMovesBanner.classList.add("hidden");
        // Delay slightly so the last card's CSS transition finishes before
        // the win overlay covers the board. Card transitions are ~260 ms.
        setTimeout(() => showWin(s), 320);
    } else {
        // If the player undid out of auto-complete, restart the timer —
        // stopTimer() was called when auto-complete began, but no code path
        // before here restarts it after an undo.
        if (!s.is_auto_completable && !timerInterval) {
            startTimer();
        }
        saveState();
        const noMoves = !s.has_moves && !s.is_auto_completable;
        if (noMovesBanner) noMovesBanner.classList.toggle("hidden", !noMoves);
    }
}

function updateCardEl(el, card, pileName, idx, total) {
    const pos = cardPos(pileName, idx, total);
    el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
    el.style.zIndex    = cardZ(pileName, idx);

    if (!card.face_up) {
        el.className = "card face-down";
        el.style.backgroundImage = `url('/assets/cards/backs/${cardTheme}/back_0.png')`;
        el.innerHTML = "";
    } else {
        const isRed = RED_SUITS.has(card.suit);
        el.className = `card ${isRed ? "red" : "black"}`;
        el.style.backgroundImage = `url('/assets/cards/faces/${cardTheme}/${RANK_LABELS[card.rank]}${SUIT_CODE[card.suit]}.png')`;
        el.innerHTML = "";
    }
}

// ── Win overlay ───────────────────────────────────────────────────────────────
function showWin(s) {
    winScore.textContent = `Score: ${s.score}`;
    winMoves.textContent = `${s.move_count} moves`;
    const m = Math.floor(elapsedSecs / 60);
    const sec = elapsedSecs % 60;
    if (winTime) winTime.textContent = `${m}:${sec.toString().padStart(2, "0")}`;
    winOverlay.classList.remove("hidden");
    submitReplay(s);
}

function buildReplayPayload(s) {
    if (!game || !s) return null;
    let moves;
    try {
        moves = game.replay_moves();
        if (!Array.isArray(moves) || moves.length === 0) return null;
    } catch (e) {
        console.warn("fs: replay export failed", e);
        return null;
    }
    return {
        schema_version: 2,
        seed: Math.round(game.seed()),
        draw_mode: drawThree ? "DrawThree" : "DrawOne",
        mode: "Classic",
        time_seconds: Math.max(1, elapsedSecs),
        final_score: s.score,
        recorded_at: new Date().toISOString().slice(0, 10),
        moves,
        win_move_index: moves.length - 1,
    };
}

async function submitReplay(s) {
    const token = localStorage.getItem('fs_token');
    if (!token || !game) return;
    const payload = buildReplayPayload(s);
    if (!payload) return;
    try {
        await fetch('/api/replays', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
            body: JSON.stringify(payload),
        });
    } catch (_) { /* best-effort — never block the win screen */ }
}

// ── Auto-complete ─────────────────────────────────────────────────────────────
function doAutoCompleteStep() {
    if (!game || !snap?.is_auto_completable) {
        clearInterval(acTimer); acTimer = null; return;
    }
    const result = game.auto_complete_step();
    if (result?.ok) render(result.snapshot);
    else { clearInterval(acTimer); acTimer = null; }
}

// ── Illegal move flash ────────────────────────────────────────────────────────
function flashIllegal(cardIds) {
    for (const id of cardIds) {
        const el = cardEls.get(id);
        if (!el) continue;
        // Remove any in-progress shake before restarting. Reading offsetWidth
        // forces a synchronous layout flush so the browser sees the removal
        // before we re-add the class, restarting the animation from frame 0.
        el.classList.remove("illegal");
        el.style.removeProperty("--card-tx");
        void el.offsetWidth; // flush layout — do not remove
        el.style.setProperty("--card-tx", el.style.transform || "translate(0,0)");
        el.classList.add("illegal");
        el.addEventListener("animationend", () => {
            el.classList.remove("illegal");
            el.style.removeProperty("--card-tx");
        }, { once: true });
    }
}

// ── Input ─────────────────────────────────────────────────────────────────────
function attachHandlers() {
    const doUndo = () => { const r = game.undo(); if (r.ok) render(r.snapshot); };
    btnUndo.addEventListener("click", doUndo);
    btnBoardUndo.addEventListener("click", doUndo);
    btnNew.addEventListener("click", () => startGame(randomSeed()));
    document.getElementById("btn-no-moves-undo")?.addEventListener("click", doUndo);
    document.getElementById("btn-no-moves-new")?.addEventListener("click", () => startGame(randomSeed()));
    btnWinNew.addEventListener("click", () => startGame(randomSeed()));
    chkDraw3.addEventListener("change", () => {
        drawThree = chkDraw3.checked;
        startGame(randomSeed());
    });
    btnTheme.addEventListener("click", () => {
        cardTheme = cardTheme === "classic" ? "dark" : "classic";
        localStorage.setItem("fs_theme", cardTheme);
        syncThemeButton();
        if (game) render(game.state());
    });
    const doDraw = () => { const r = game.draw(); if (r.ok) render(r.snapshot); };

    document.addEventListener("keydown", (e) => {
        const tag = e.target?.tagName;
        if (e.target?.isContentEditable || tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
        if (e.key === "z" || e.key === "Z" || e.key === "u" || e.key === "U") {
            e.preventDefault();
            doUndo();
            return;
        }
        if (e.key === "n" || e.key === "N") {
            startGame(randomSeed());
            return;
        }
        if (!e.repeat && (e.code === "Space" || e.key === " ")) {
            e.preventDefault();
            doDraw();
        }
    });

    // Pause the game timer while the tab is hidden so background time doesn't
    // inflate the player's recorded game duration.
    document.addEventListener("visibilitychange", () => {
        if (document.hidden) {
            stopTimer();
        } else if (snap && !snap.is_won && !snap.is_auto_completable) {
            startTimer();
        }
    });

    board.addEventListener("pointerdown",   onPointerDown);
    board.addEventListener("pointermove",   onPointerMove);
    board.addEventListener("pointerup",     onPointerUp);
    board.addEventListener("pointercancel", onPointerCancel);
    board.addEventListener("click",         onBoardClick);
    board.addEventListener("dblclick",      onBoardDblClick);
    board.addEventListener("contextmenu", (e) => {
        e.preventDefault();
        if (drag) return;
        const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
        const hit = hitTestCard(bx, by);
        if (!hit || !hit.card.face_up) return;
        const cards = getPileCards(hit.pileName);
        if (!cards) return;
        let fromIndex = hit.cardIndex;
        if (!hit.pileName.startsWith("tableau-")) {
            if (fromIndex !== cards.length - 1) return;
        }
        smartMove(hit.pileName, fromIndex);
    });
}

// ── Coordinate helpers ────────────────────────────────────────────────────────
// Returns cursor position in board-element coordinates
// (0,0 = board element top-left corner, which is the padding edge).
// Divides by boardScale because getBoundingClientRect() returns the SCALED
// visual rect; we need coordinates in the natural (pre-scale) system.
function boardRelative(clientX, clientY) {
    const rect = board.getBoundingClientRect();
    return {
        x: (clientX - rect.left) / boardScale,
        y: (clientY - rect.top)  / boardScale,
    };
}

function hitTestCard(bx, by) {
    const pileOrder = [
        "waste",
        "foundation-0","foundation-1","foundation-2","foundation-3",
        "tableau-0","tableau-1","tableau-2","tableau-3","tableau-4","tableau-5","tableau-6",
        "stock",
    ];

    let best = null, bestZ = -1;

    for (const pileName of pileOrder) {
        const cards = getPileCards(pileName);
        if (!cards) continue;
        for (let i = 0; i < cards.length; i++) {
            const pos = cardPos(pileName, i, cards.length);
            if (bx >= pos.x && bx <= pos.x + CARD_W &&
                by >= pos.y && by <= pos.y + CARD_H) {
                const z = cardZ(pileName, i);
                if (z > bestZ) {
                    bestZ = z;
                    best  = { pileName, cardIndex: i, card: cards[i] };
                }
            }
        }
    }
    return best;
}

function getPileCards(pileName) {
    if (!snap) return null;
    if (pileName === "stock") return snap.stock;
    if (pileName === "waste") return snap.waste;
    if (pileName.startsWith("foundation-")) return snap.foundations[parseInt(pileName.split("-")[1])];
    if (pileName.startsWith("tableau-"))    return snap.tableaus   [parseInt(pileName.split("-")[1])];
    return null;
}

// Drop-target: tableau has tall hit areas; foundations use their slot box.
function findDropTarget(bx, by) {
    for (let c = 0; c < 7; c++) {
        const pile    = `tableau-${c}`;
        const cards   = snap.tableaus[c];
        const origin  = PILE_ORIGIN[pile];
        const bottomY = cards.length > 0
            ? origin.y + (cards.length - 1) * FAN + CARD_H
            : origin.y + CARD_H;
        if (bx >= origin.x && bx <= origin.x + CARD_W && by >= origin.y && by <= bottomY)
            return pile;
    }
    for (let s = 0; s < 4; s++) {
        const pile   = `foundation-${s}`;
        const origin = PILE_ORIGIN[pile];
        if (bx >= origin.x && bx <= origin.x + CARD_W &&
            by >= origin.y && by <= origin.y + CARD_H)
            return pile;
    }
    return null;
}

// ── Pointer handlers ──────────────────────────────────────────────────────────
function onPointerDown(e) {
    if (e.button !== 0 && e.pointerType === "mouse") return;
    if (drag) return;

    const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
    const hit = hitTestCard(bx, by);
    if (!hit || !hit.card.face_up) return;

    const cards = getPileCards(hit.pileName);
    if (!cards) return;

    let fromIndex = hit.cardIndex;
    if (!hit.pileName.startsWith("tableau-")) {
        fromIndex = cards.length - 1;
        if (hit.cardIndex !== fromIndex) return;
    }

    const draggedCards = cards.slice(fromIndex);
    if (draggedCards.some(c => !c.face_up)) return;

    drag = {
        fromPile:  hit.pileName,
        fromIndex,
        cardIds:   draggedCards.map(c => c.id),
        startX: bx,
        startY: by,
    };

    drag.cardIds.forEach((id, i) => {
        const el = cardEls.get(id);
        if (el) { el.classList.add("selected"); el.style.zIndex = 500 + i; }
    });

    board.setPointerCapture(e.pointerId);
    e.preventDefault();
}

function onPointerMove(e) {
    if (!drag) return;
    const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
    const dx = bx - drag.startX;
    const dy = by - drag.startY;

    const cards = getPileCards(drag.fromPile);
    drag.cardIds.forEach((id, i) => {
        const el = cardEls.get(id);
        if (!el) return;
        const base = cardPos(drag.fromPile, drag.fromIndex + i, cards ? cards.length : 1);
        el.style.transform = `translate(${base.x + dx}px, ${base.y + dy}px)`;
    });

    // Highlight drop target.
    board.querySelectorAll(".slot.drop-active").forEach(e => e.classList.remove("drop-active"));
    board.querySelectorAll(".card.drop-target").forEach(e => e.classList.remove("drop-target"));
    const targetPile = findDropTarget(bx, by);
    if (targetPile) {
        const slotEl = board.querySelector(`.slot[data-pile="${targetPile}"]`);
        if (slotEl) slotEl.classList.add("drop-active");
        const targetCards = getPileCards(targetPile);
        if (targetCards?.length > 0) {
            const topEl = cardEls.get(targetCards[targetCards.length - 1].id);
            if (topEl) topEl.classList.add("drop-target");
        }
    }

    e.preventDefault();
}

function onPointerUp(e) {
    if (!drag) return;
    board.querySelectorAll(".slot.drop-active").forEach(e => e.classList.remove("drop-active"));
    board.querySelectorAll(".card.drop-target").forEach(e => e.classList.remove("drop-target"));

    const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
    const targetPile = findDropTarget(bx, by);

    let moved = false;
    let illegalAttempt = false;

    if (targetPile && targetPile !== drag.fromPile) {
        const r = game.move_cards(drag.fromPile, targetPile, drag.cardIds.length);
        if (r.ok) {
            moved = true;
            drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
            render(r.snapshot);
        } else {
            illegalAttempt = true;
        }
    }

    if (!moved) {
        drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
        render(snap); // snap cards back first — then animate so shake plays on settled positions
        if (illegalAttempt) flashIllegal(drag.cardIds);
    }

    drag = null;
}

function onPointerCancel() {
    if (!drag) return;
    drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
    render(snap);
    drag = null;
}

// ── Click / dblclick ──────────────────────────────────────────────────────────
function onBoardClick(e) {
    if (drag || snap?.is_won) return;
    const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
    const stock = PILE_ORIGIN.stock;
    if (bx >= stock.x && bx <= stock.x + CARD_W && by >= stock.y && by <= stock.y + CARD_H) {
        const r = game.draw();
        if (r.ok) render(r.snapshot);
    }
}

// Try to move cards from pileName starting at fromIndex to the best valid target.
// Tries foundations first (single card only), then tableau columns.
// Returns true if a move was made.
function smartMove(pileName, fromIndex) {
    const cards = getPileCards(pileName);
    if (!cards || fromIndex >= cards.length) return false;
    const count = cards.length - fromIndex;

    if (count === 1) {
        for (let s = 0; s < 4; s++) {
            const r = game.move_cards(pileName, `foundation-${s}`, 1);
            if (r.ok) { render(r.snapshot); return true; }
        }
    }

    for (let t = 0; t < 7; t++) {
        const target = `tableau-${t}`;
        if (target === pileName) continue;
        const r = game.move_cards(pileName, target, count);
        if (r.ok) { render(r.snapshot); return true; }
    }

    return false;
}

function onBoardDblClick(e) {
    if (drag || snap?.is_won) return;
    const { x: bx, y: by } = boardRelative(e.clientX, e.clientY);
    const hit = hitTestCard(bx, by);
    if (!hit || !hit.card.face_up) return;

    const cards = getPileCards(hit.pileName);
    if (!cards) return;

    let fromIndex = hit.cardIndex;
    if (!hit.pileName.startsWith("tableau-")) {
        if (fromIndex !== cards.length - 1) return;
    }

    if (!smartMove(hit.pileName, fromIndex)) flashIllegal([cards[fromIndex].id]);
}

// ── Avatar ────────────────────────────────────────────────────────────────────
async function loadAvatar() {
    const token = localStorage.getItem("fs_token");
    if (!token) return;
    try {
        const res = await fetch("/api/me", {
            headers: { Authorization: "Bearer " + token },
        });
        if (!res.ok) return;
        const me = await res.json();
        const link = document.getElementById("hud-avatar");
        const img  = document.getElementById("hud-avatar-img");
        const init = document.getElementById("hud-avatar-initials");
        link.style.display = "flex";
        if (me.avatar_url) {
            img.src = me.avatar_url;
            img.style.display = "block";
            init.style.display = "none";
        } else {
            img.style.display = "none";
            init.textContent = (me.username || "P")[0].toUpperCase();
        }
    } catch { /* not signed in — avatar stays hidden */ }
}

function debugStateKey(state) {
    if (!state) return "missing";
    if (Array.isArray(state.stock) || Array.isArray(state.tableaus)) {
        const out = [];
        const push = cards => {
            for (const c of cards || []) out.push(`${c.id}:${c.face_up ? 1 : 0}`);
            out.push("|");
        };
        push(state.stock);
        push(state.waste);
        for (const pile of state.foundations || []) push(pile);
        for (const pile of state.tableaus || []) push(pile);
        return out.join("");
    }
    return JSON.stringify(state);
}

function orderBaselineDebugMoves(legalMoves) {
    const foundationSingles = [];
    const moveKind = [];
    const rest = [];
    for (let i = 0; i < legalMoves.length; i++) {
        const move = legalMoves[i];
        if (
            move?.kind === "move" &&
            typeof move.to === "string" &&
            move.to.startsWith("foundation-") &&
            move.count === 1
        ) {
            foundationSingles.push(i);
        } else if (move?.kind === "move") {
            moveKind.push(i);
        } else {
            rest.push(i);
        }
    }
    return [...foundationSingles, ...moveKind, ...rest];
}

function runDebugAutoplay(options = {}) {
    if (!game) return { ok: false, reason: "game_not_ready", step: 0 };

    const maxSteps = Number.isInteger(options.maxSteps) && options.maxSteps > 0 ? options.maxSteps : 220;
    const maxVisitsPerState =
        Number.isInteger(options.maxVisitsPerState) && options.maxVisitsPerState > 0
            ? options.maxVisitsPerState
            : 2;
    const policy = options.policy === "baseline" ? "baseline" : "loop_aware";
    const seen = new Map();

    function simulatedVisitCount(legalMoveIndex) {
        let saved = null;
        try {
            saved = game.serialize();
        } catch {
            return null;
        }
        if (typeof saved !== "string" || saved.length === 0) return null;

        const applied = game.debug_apply_legal_move(legalMoveIndex);
        if (!applied?.ok) {
            try { game = SolitaireGame.from_saved(saved); } catch {}
            return null;
        }
        const nextKey = debugStateKey(applied.snapshot);
        try {
            game = SolitaireGame.from_saved(saved);
        } catch {
            return null;
        }
        return seen.get(nextKey) || 0;
    }

    for (let step = 0; step < maxSteps; step++) {
        const snap = game.debug_snapshot();
        if (!snap?.state || !snap?.invariants) {
            return { ok: false, reason: "missing_snapshot", step };
        }
        if (!snap.invariants.state_ok) {
            return { ok: false, reason: "invariant_failed", step, snapshot: snap };
        }
        if (snap.state.is_won) {
            return { ok: true, terminal: "won", step, snapshot: snap };
        }

        const key = debugStateKey(snap.state);
        const visits = (seen.get(key) || 0) + 1;
        seen.set(key, visits);
        if (visits > maxVisitsPerState) {
            return { ok: true, terminal: "cycle", step, snapshot: snap };
        }

        const legalMoves = game.debug_legal_moves();
        if (!Array.isArray(legalMoves) || legalMoves.length === 0) {
            return { ok: true, terminal: "no_moves", step, snapshot: snap };
        }

        const ordered = orderBaselineDebugMoves(legalMoves);
        let idx = ordered[0];
        if (policy === "loop_aware" && ordered.length > 1) {
            let bestIdx = ordered[0];
            let bestVisitCount = Number.MAX_SAFE_INTEGER;
            for (const candidate of ordered) {
                const visitCount = simulatedVisitCount(candidate);
                if (visitCount === null) continue;
                if (visitCount < bestVisitCount) {
                    bestVisitCount = visitCount;
                    bestIdx = candidate;
                    if (visitCount === 0) break;
                }
            }
            idx = bestIdx;
        }

        const result = game.debug_apply_legal_move(idx);
        if (!result?.ok) {
            return {
                ok: false,
                reason: "apply_failed",
                step,
                idx,
                error: result?.error ?? "unknown_error",
            };
        }
        if (result.snapshot) render(result.snapshot);
    }

    const finalSnap = game.debug_snapshot();
    return { ok: !!finalSnap?.invariants?.state_ok, terminal: "step_budget", snapshot: finalSnap };
}

// ── Debug API (engine-first automation surface) ───────────────────────────────
// Playwright and other automation harnesses use this object instead of pixel
// analysis or hardcoded coordinates. Every operation delegates to the Rust
// rules engine exported by `solitaire_wasm`.
window.__FERROUS_DEBUG__ = {
    seed() {
        return game ? Math.round(game.seed()) : null;
    },
    state() {
        return game ? game.state() : null;
    },
    legalMoves() {
        return game ? game.debug_legal_moves() : [];
    },
    moveHistory() {
        return game ? game.debug_move_history() : [];
    },
    snapshot() {
        return game ? game.debug_snapshot() : null;
    },
    serialize() {
        return game ? game.serialize() : null;
    },
    // Reset to a fresh seeded game in place (no page reload). Lets the cycle
    // regression harness reuse one page across hundreds of games instead of
    // navigating per game.
    newGame(seed, drawThreeMode) {
        drawThree = !!drawThreeMode;
        startGame(seed ?? randomSeed());
        return game ? game.state() : null;
    },
    applyLegalMove(index) {
        if (!game) return { ok: false, error: "game_not_ready" };
        const result = game.debug_apply_legal_move(index);
        if (result?.ok && result.snapshot) render(result.snapshot);
        return result;
    },
    applyMove(move) {
        if (!game) return { ok: false, error: "game_not_ready" };
        const payload = typeof move === "string" ? move : JSON.stringify(move);
        const result = game.debug_apply_move_json(payload);
        if (result?.ok && result.snapshot) render(result.snapshot);
        return result;
    },
    failureReport() {
        if (!game) return null;
        const debug = game.debug_snapshot();
        return {
            seed: Math.round(game.seed()),
            moveHistory: debug?.move_history ?? [],
            currentState: debug?.state ?? game.state(),
            stateJson: debug?.state_json ?? null,
            legalMoves: debug?.legal_moves ?? [],
            invariants: debug?.invariants ?? null,
        };
    },
    replayPayload() {
        if (!game) return null;
        return buildReplayPayload(snap ?? game.state());
    },
    runAutoplay(options) {
        return runDebugAutoplay(options);
    },
};

// ── Start ─────────────────────────────────────────────────────────────────────
bootstrap().catch(console.error);
loadAvatar();
