use super::ReplayPlaybackState;
use chrono::Datelike;
use solitaire_core::game_state::GameState;
use solitaire_core::{Card, Rank, Suit};
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};

/// Pure helper — formats the `GAME #YYYY-DDD` caption for the given
/// state. Returns `None` for `Inactive` / `Completed` (the replay is
/// consumed when transitioning out of `Playing`, so the identifier
/// isn't recoverable from state in those branches); spawn-time
/// callers fall back to an empty string.
///
/// Year + chrono ordinal (`{year}-{ordinal:03}`) gives a compact
/// monotonically-increasing identifier shaped like `2026-127` — same
/// shape as the mockup's `GAME #2024-127` motif.
pub(crate) fn format_game_caption(state: &ReplayPlaybackState) -> Option<String> {
    match state {
        ReplayPlaybackState::Playing { replay, .. } => Some(format!(
            "GAME #{}-{:03}",
            replay.recorded_at.year(),
            replay.recorded_at.ordinal()
        )),
        ReplayPlaybackState::Inactive | ReplayPlaybackState::Completed => None,
    }
}

/// Pure helper — formats the centre progress readout for the given state.
/// Exposed at module scope so the spawn path and the per-frame update
/// path produce the exact same string.
pub(crate) fn format_progress(state: &ReplayPlaybackState) -> String {
    match state.progress() {
        // `MOVE N/M` (uppercase + slash) reads as a Terminal output
        // line and matches the floating-chip motif in the mockup at
        // `docs/ui-mockups/replay-overlay-mobile.html`.
        Some((cursor, total)) => format!("MOVE {cursor}/{total}"),
        None if state.is_completed() => "REPLAY COMPLETE".to_string(),
        None => String::new(),
    }
}

/// Pure helper — formats a [`KlondikePile`] as a short, lowercase,
/// 1-indexed display string for the move-log row. `Foundation(2)`
/// renders as `"foundation 3"` rather than `"foundation 2"` so
/// players see human-friendly numbers; the underlying enum
/// remains 0-indexed.
///
/// Returns `String` rather than `&'static str` because the
/// `Foundation` / `Tableau` variants need formatting; the static
/// variants (`Stock`, `Waste`) still allocate but the cost is
/// trivial against the per-frame update cadence.
pub(crate) fn format_pile(p: &KlondikePile) -> String {
    match p {
        KlondikePile::Stock => "waste".to_string(),
        KlondikePile::Foundation(foundation) => {
            format!("foundation {}", foundation_number(*foundation))
        }
        KlondikePile::Tableau(tableau) => format!("tableau {}", tableau_number(*tableau)),
    }
}

fn foundation_number(foundation: Foundation) -> u8 {
    match foundation {
        Foundation::Foundation1 => 1,
        Foundation::Foundation2 => 2,
        Foundation::Foundation3 => 3,
        Foundation::Foundation4 => 4,
    }
}

fn tableau_number(tableau: Tableau) -> u8 {
    match tableau {
        Tableau::Tableau1 => 1,
        Tableau::Tableau2 => 2,
        Tableau::Tableau3 => 3,
        Tableau::Tableau4 => 4,
        Tableau::Tableau5 => 5,
        Tableau::Tableau6 => 6,
        Tableau::Tableau7 => 7,
    }
}

/// Pure helper — formats a [`KlondikeInstruction`] as the body of a
/// move-log row. `RotateStock` reads as `"stock cycle"`; a `Dst*`
/// instruction reads as `"{from} → {to}"` using [`format_pile`] for
/// each nameable endpoint. The card count is omitted from the row
/// body — at row scale it adds visual noise without meaningful
/// information for the typical 1-card moves.
///
/// The destination pile is always recoverable directly from the
/// instruction. The source pile is shown when it is statically
/// nameable (a `DstFoundation` carries a [`KlondikePile`] source);
/// a `DstTableau`'s source is the runtime-only `KlondikePileStack`
/// type — not re-exported across the `solitaire_core` boundary and so
/// not pattern-matchable here — so its row renders `"→ {to}"` without
/// a leading source label. Faithful full-coordinate decoding lives in
/// [`GameState::instruction_to_piles`] on the playback path; the
/// move-log is a display-only digest.
pub(crate) fn format_move_body(instruction: &KlondikeInstruction) -> String {
    match instruction {
        KlondikeInstruction::RotateStock => "stock cycle".to_string(),
        KlondikeInstruction::DstFoundation(dst) => {
            format!(
                "{} \u{2192} {}",
                format_pile(&dst.src),
                format_pile(&KlondikePile::Foundation(dst.foundation))
            )
        }
        KlondikeInstruction::DstTableau(dst) => {
            format!(
                "\u{2192} {}",
                format_pile(&KlondikePile::Tableau(dst.tableau))
            )
        }
    }
}

/// Pure helper — formats the move-log panel's header text. Reads
/// `▌ MOVE LOG · N/M` while playing, where `N` is the count of
/// moves applied so far and `M` is the total in the replay. The
/// cursor-block prefix (`▌`) matches the splash and replay-banner
/// motifs. Empty in `Inactive` (no replay attached); reads
/// `▌ MOVE LOG · COMPLETE` in `Completed`.
pub(crate) fn format_move_log_header(state: &ReplayPlaybackState) -> String {
    match state {
        ReplayPlaybackState::Playing { replay, cursor, .. } => {
            format!(
                "\u{258C} MOVE LOG \u{00B7} {}/{}",
                cursor,
                replay.moves.len()
            )
        }
        ReplayPlaybackState::Completed => "\u{258C} MOVE LOG \u{00B7} COMPLETE".to_string(),
        ReplayPlaybackState::Inactive => String::new(),
    }
}

/// Pure helper — formats the kth-most-recently-applied move's row
/// text. `k = 1` is the active row (`replay.moves[cursor - 1]`,
/// displayed as `"{cursor} │ {body}"`). `k = 2` is the row above
/// that (`moves[cursor - 2]` displayed as `"{cursor - 1} │ {body}"`),
/// and so on.
///
/// Returns the empty string in any of these cases:
/// - State isn't `Playing` (no replay attached).
/// - `k == 0` (no kth-most-recent for k=0; the active is k=1).
/// - `k > cursor` (not enough history — e.g. cursor=2 has rows
///   for k=1 and k=2 only, k=3 returns empty).
/// - The move list is shorter than expected (defensive guard).
pub(crate) fn format_kth_recent_row(state: &ReplayPlaybackState, k: usize) -> String {
    let ReplayPlaybackState::Playing { replay, cursor, .. } = state else {
        return String::new();
    };
    if k == 0 || k > *cursor {
        return String::new();
    }
    let zero_idx = *cursor - k;
    let Some(m) = replay.moves.get(zero_idx) else {
        return String::new();
    };
    let display_idx = *cursor - k + 1;
    format!("{} \u{2502} {}", display_idx, format_move_body(m))
}

/// Pure helper — formats the kth-NEXT move's row text. `k = 1`
/// is the move that will apply next (`replay.moves[cursor]`,
/// displayed as `cursor + 1`); `k = 2` is the move after that,
/// and so on.
///
/// Returns the empty string in any of these cases:
/// - State isn't `Playing` (no replay attached).
/// - `k == 0` (degenerate; the active is k=1 of *recent*, not
///   *next*).
/// - `cursor + k - 1 >= moves.len()` (not enough remaining
///   replay — late in the move list, the trailing next rows
///   stay empty).
pub(crate) fn format_kth_next_row(state: &ReplayPlaybackState, k: usize) -> String {
    let ReplayPlaybackState::Playing { replay, cursor, .. } = state else {
        return String::new();
    };
    if k == 0 {
        return String::new();
    }
    let zero_idx = *cursor + k - 1;
    let Some(m) = replay.moves.get(zero_idx) else {
        return String::new();
    };
    let display_idx = *cursor + k;
    format!("{} \u{2502} {}", display_idx, format_move_body(m))
}

/// Pure helper — formats the active-row text for the move-log
/// panel. Wraps [`format_kth_recent_row`] with `k=1` and prepends
/// a `▶` focus marker so the active row reads visually distinct
/// from prev rows even before the highlight background lands.
/// Returns empty when there's no row to render (cursor=0 or
/// non-`Playing` state) — never `"▶ "` alone, which would paint
/// a stray prefix.
pub(crate) fn format_active_move_row(state: &ReplayPlaybackState) -> String {
    let body = format_kth_recent_row(state, 1);
    if body.is_empty() {
        return String::new();
    }
    format!("\u{25B6} {body}") // ▶
}

// ---------------------------------------------------------------------------
// Mini-tableau format helpers and update system
// ---------------------------------------------------------------------------

/// Pure helper — short rank symbol. Single character for all ranks
/// except Ten which uses "T" (keeps every card a consistent 2-char
/// wide render: rank-char + suit-glyph). Players familiar with
/// solitaire shorthand read "T" instantly; the suit glyph immediately
/// follows and disambiguates from an ambiguous "T".
pub(crate) fn format_rank_short(rank: Rank) -> &'static str {
    match rank {
        Rank::Ace => "A",
        Rank::Two => "2",
        Rank::Three => "3",
        Rank::Four => "4",
        Rank::Five => "5",
        Rank::Six => "6",
        Rank::Seven => "7",
        Rank::Eight => "8",
        Rank::Nine => "9",
        Rank::Ten => "T",
        Rank::Jack => "J",
        Rank::Queen => "Q",
        Rank::King => "K",
    }
}

/// Pure helper — Unicode suit glyph from FiraMono's covered range
/// (U+2660–U+2666). These four code points are confirmed present in
/// the bundled FiraMono on Android (verified on Pixel 7 / API 34).
pub(crate) fn format_suit_glyph(suit: Suit) -> &'static str {
    match suit {
        Suit::Spades => "\u{2660}",   // ♠
        Suit::Hearts => "\u{2665}",   // ♥
        Suit::Diamonds => "\u{2666}", // ♦
        Suit::Clubs => "\u{2663}",    // ♣
    }
}

/// Pure helper — compact 2-char card label (`rank + suit glyph`) for a
/// known card, or `"--"` for an absent top card (empty pile).
pub(crate) fn format_card_short(card: Option<&(Card, bool)>) -> String {
    match card {
        Some((c, _)) => format!("{}{}", format_rank_short(c.rank()), format_suit_glyph(c.suit())),
        None => "--".to_string(),
    }
}

/// Pure helper — one-line summary of the four foundation tops.
/// Renders as `F: A♠ 7♥ 5♦ K♣` with `--` for any empty slot.
/// Foundation slots are displayed in their natural 0-3 order
/// (matching the visual left-to-right order on screen).
pub(crate) fn format_foundations_row(game: &GameState) -> String {
    let slots = [
        Foundation::Foundation1,
        Foundation::Foundation2,
        Foundation::Foundation3,
        Foundation::Foundation4,
    ]
    .map(|foundation| {
        let cards = game.pile(KlondikePile::Foundation(foundation));
        format_card_short(cards.last())
    });
    format!("F: {} {} {} {}", slots[0], slots[1], slots[2], slots[3])
}

/// Pure helper — one-line stock / waste summary.
/// Renders as `STK:N WST:X♠` where N is the stock card count and
/// X♠ is the top waste card (or `--` when the waste pile is empty).
pub(crate) fn format_stock_waste_row(game: &GameState) -> String {
    let stock_cards = game.stock_cards();
    let waste_cards = game.waste_cards();
    let stock_count = stock_cards.len();
    let waste_top = waste_cards.last();
    format!("STK:{} WST:{}", stock_count, format_card_short(waste_top))
}
