//! Turn commitment and verification
//!
//! This module provides the core verification logic for committed turns.
//! Both the browser engine and the on-chain pallet use `verify_and_apply_turn`
//! to validate that a player's turn is legal.

use alloc::vec;
use alloc::vec::Vec;

use crate::error::{GameError, GameResult};
use crate::rng::{BattleRng, XorShiftRng};
use crate::state::ShopState;
use crate::types::{
    CardId, CommitTurnAction, CompareOp, ShopAbility, ShopCondition, ShopEffect, ShopMatcher,
    ShopScope, ShopTarget, ShopTrigger, StatType, TurnAction,
};
use crate::BattleResult;

const SHOP_START_SALT: u64 = 0x5348_4f50_0000_0001;
const SHOP_BUY_SALT: u64 = 0x5348_4f50_0000_0002;
const SHOP_SELL_SALT: u64 = 0x5348_4f50_0000_0003;
const SHOP_AFTER_LOSS_SALT: u64 = 0x5348_4f50_0000_0004;
const SHOP_AFTER_WIN_SALT: u64 = 0x5348_4f50_0000_0005;
const SHOP_AFTER_DRAW_SALT: u64 = 0x5348_4f50_0000_0006;

#[derive(Clone)]
struct ShopPendingAbility {
    source_slot: Option<usize>,
    source_on_board: bool,
    ability: ShopAbility,
}

/// Apply `OnShopStart` triggers for all units currently on the board.
///
/// This should be called exactly once when entering a new shop phase.
pub fn apply_shop_start_triggers(state: &mut ShopState) {
    apply_shop_start_triggers_with_result(state, None);
}

/// Apply shop-start triggers for all units currently on the board.
///
/// Runs `OnShopStart`, then one of `AfterLoss` / `AfterWin` / `AfterDraw`
/// if a previous battle result is provided.
pub fn apply_shop_start_triggers_with_result(
    state: &mut ShopState,
    previous_battle_result: Option<BattleResult>,
) {
    state.shop_mana = state.shop_mana.clamp(0, state.mana_limit);
    let mut start_rng = shop_rng(state, SHOP_START_SALT);
    execute_shop_trigger(state, ShopTrigger::OnShopStart, None, None, &mut start_rng);

    if let Some(result) = previous_battle_result {
        let (trigger, salt) = match result {
            BattleResult::Defeat => (ShopTrigger::AfterLoss, SHOP_AFTER_LOSS_SALT),
            BattleResult::Victory => (ShopTrigger::AfterWin, SHOP_AFTER_WIN_SALT),
            BattleResult::Draw => (ShopTrigger::AfterDraw, SHOP_AFTER_DRAW_SALT),
        };
        let mut outcome_rng = shop_rng(state, salt);
        execute_shop_trigger(state, trigger, None, None, &mut outcome_rng);
    }

    state.shop_mana = state.shop_mana.clamp(0, state.mana_limit);
}

/// Apply `OnBuy` triggers for a successful shop buy action.
pub fn apply_on_buy_triggers(state: &mut ShopState, action_index: usize, bought_slot: usize) {
    if bought_slot >= state.board.len() {
        return;
    }

    let mut rng = shop_rng(state, SHOP_BUY_SALT.wrapping_add(action_index as u64));
    execute_shop_trigger(state, ShopTrigger::OnBuy, Some(bought_slot), None, &mut rng);
}

/// Apply `OnSell` triggers for a successful shop sell action.
pub fn apply_on_sell_triggers(
    state: &mut ShopState,
    action_index: usize,
    sold_card_id: CardId,
    sold_slot: usize,
) {
    if sold_slot >= state.board.len() {
        return;
    }

    let mut rng = shop_rng(state, SHOP_SELL_SALT.wrapping_add(action_index as u64));
    execute_shop_trigger(
        state,
        ShopTrigger::OnSell,
        None,
        Some((sold_card_id, sold_slot)),
        &mut rng,
    );
}

/// Find the nearest empty board slot to `target`, preferring higher indices first.
pub fn find_nearest_empty_board_slot<T>(board: &[Option<T>], target: usize) -> Option<usize> {
    if target >= board.len() {
        return None;
    }
    if board[target].is_none() {
        return Some(target);
    }

    let len = board.len();
    for distance in 1..len {
        let right = target + distance;
        if right < len && board[right].is_none() {
            return Some(right);
        }

        if distance <= target {
            let left = target - distance;
            if board[left].is_none() {
                return Some(left);
            }
        }
    }

    None
}

/// Validate a hand-to-board insert and return the empty slot that should be
/// shifted toward `target`, if any.
pub fn prepare_board_slot_for_insert<T>(
    board: &[Option<T>],
    target: usize,
) -> GameResult<Option<usize>> {
    if target >= board.len() {
        return Err(GameError::InvalidBoardSlot {
            index: target as u32,
        });
    }
    if board[target].is_none() {
        return Ok(None);
    }

    let empty_slot = find_nearest_empty_board_slot(board, target).ok_or(GameError::BoardFull)?;
    Ok(Some(empty_slot))
}

/// Shift board units toward `target` to make room for an insert.
pub fn apply_board_insert_shift<T>(board: &mut [Option<T>], empty_slot: usize, target: usize) {
    if empty_slot > target {
        for i in (target..empty_slot).rev() {
            board.swap(i + 1, i);
        }
    } else {
        for i in empty_slot..target {
            board.swap(i, i + 1);
        }
    }
}

/// Validate that a `MoveBoard` action represents an occupied-to-occupied reorder.
///
/// `MoveBoard` is reserved for shifting a unit into another occupied slot while
/// sliding intermediate units. Direct moves into empty slots should use
/// `SwapBoard` instead.
pub fn validate_move_board_positions<T>(
    board: &[Option<T>],
    from: usize,
    to: usize,
) -> GameResult<()> {
    if from >= board.len() {
        return Err(GameError::InvalidBoardSlot { index: from as u32 });
    }
    if to >= board.len() {
        return Err(GameError::InvalidBoardSlot { index: to as u32 });
    }
    if from == to {
        return Err(GameError::InvalidBoardMove {
            from_slot: from as u32,
            to_slot: to as u32,
        });
    }
    if board[from].is_none() {
        return Err(GameError::BoardSlotEmpty { index: from as u32 });
    }
    if board[to].is_none() {
        return Err(GameError::BoardSlotEmpty { index: to as u32 });
    }

    Ok(())
}

/// Apply a validated `MoveBoard` action by shifting intermediate units.
pub fn apply_move_board_positions<T>(board: &mut [Option<T>], from: usize, to: usize) {
    if from < to {
        for i in from..to {
            board.swap(i, i + 1);
        }
    } else {
        for i in (to..from).rev() {
            board.swap(i, i + 1);
        }
    }
}

/// Verify and apply a committed turn action to the game state.
///
/// This function executes actions sequentially in order:
/// 1. Validates each action as it's processed
/// 2. Tracks mana changes (earned from burns, spent on plays)
/// 3. Ensures mana never goes negative and respects mana_limit
/// 4. Updates board state as actions are applied
/// 5. Removes used hand cards at the end
pub fn verify_and_apply_turn(state: &mut ShopState, action: &CommitTurnAction) -> GameResult<()> {
    let hand_size = state.hand.len();

    // Track mana and used cards
    let mut current_mana: i32 = state.shop_mana.clamp(0, state.mana_limit);
    let mut hand_used = vec![false; hand_size];

    // Process each action in order
    for (action_index, turn_action) in action.actions.iter().enumerate() {
        match turn_action {
            TurnAction::BurnFromHand { hand_index } => {
                let hi = *hand_index as usize;

                // Validate index
                if hi >= hand_size {
                    return Err(GameError::InvalidHandIndex { index: *hand_index });
                }

                // Check not already used
                if hand_used[hi] {
                    return Err(GameError::CardAlreadyUsed { index: *hand_index });
                }

                // Get burn value and add mana (capped at mana_limit)
                let card_id = state.hand[hi];
                let burn_value = state
                    .card_pool
                    .get(&card_id)
                    .map(|c| c.economy.burn_value)
                    .unwrap_or(0);

                current_mana = (current_mana + burn_value).min(state.mana_limit);
                hand_used[hi] = true;
            }

            TurnAction::PlayFromHand {
                hand_index,
                board_slot,
            } => {
                let hi = *hand_index as usize;
                let bs = *board_slot as usize;

                // Validate hand index
                if hi >= hand_size {
                    return Err(GameError::InvalidHandIndex { index: *hand_index });
                }

                // Check hand card not already used
                if hand_used[hi] {
                    return Err(GameError::CardAlreadyUsed { index: *hand_index });
                }

                // Validate board slot
                if bs >= state.board.len() {
                    return Err(GameError::InvalidBoardSlot { index: *board_slot });
                }

                // Get card info
                let card_id = state.hand[hi];
                let play_cost = state
                    .card_pool
                    .get(&card_id)
                    .map(|c| c.economy.play_cost)
                    .unwrap_or(0);

                // Check we have enough mana
                if current_mana < play_cost {
                    return Err(GameError::NotEnoughMana {
                        have: current_mana,
                        need: play_cost,
                    });
                }

                let insert_shift = prepare_board_slot_for_insert(&state.board, bs)?;
                if let Some(empty_slot) = insert_shift {
                    apply_board_insert_shift(&mut state.board, empty_slot, bs);
                }

                // Deduct mana and place unit
                current_mana -= play_cost;
                hand_used[hi] = true;
                state.board[bs] = Some(crate::types::BoardUnit::new(card_id));

                state.shop_mana = current_mana;
                apply_on_buy_triggers(state, action_index, bs);
                current_mana = state.shop_mana;
            }

            TurnAction::BurnFromBoard { board_slot } => {
                let bs = *board_slot as usize;

                // Validate board slot
                if bs >= state.board.len() {
                    return Err(GameError::InvalidBoardBurn { index: *board_slot });
                }

                // Check slot has a unit
                let sold_unit = state.board[bs]
                    .take()
                    .ok_or(GameError::InvalidBoardBurn { index: *board_slot })?;

                // Get burn value and add mana (capped at mana_limit)
                let burn_value = state
                    .card_pool
                    .get(&sold_unit.card_id)
                    .map(|c| c.economy.burn_value)
                    .unwrap_or(0);

                current_mana = (current_mana + burn_value).min(state.mana_limit);

                state.shop_mana = current_mana;
                apply_on_sell_triggers(state, action_index, sold_unit.card_id, bs);
                current_mana = state.shop_mana;
            }

            TurnAction::SwapBoard { slot_a, slot_b } => {
                let sa = *slot_a as usize;
                let sb = *slot_b as usize;

                // Validate both slots
                if sa >= state.board.len() {
                    return Err(GameError::InvalidBoardSlot { index: *slot_a });
                }
                if sb >= state.board.len() {
                    return Err(GameError::InvalidBoardSlot { index: *slot_b });
                }

                // Swap positions
                state.board.swap(sa, sb);
            }

            TurnAction::MoveBoard { from_slot, to_slot } => {
                let from = *from_slot as usize;
                let to = *to_slot as usize;

                validate_move_board_positions(&state.board, from, to)?;
                apply_move_board_positions(&mut state.board, from, to);
            }
        }
    }

    // Remove used hand cards (sort descending to preserve indices)
    let mut hand_indices_to_remove: Vec<usize> = hand_used
        .iter()
        .enumerate()
        .filter(|(_, &used)| used)
        .map(|(i, _)| i)
        .collect();

    hand_indices_to_remove.sort_unstable_by(|a, b| b.cmp(a));

    for idx in hand_indices_to_remove {
        state.hand.remove(idx);
    }

    state.shop_mana = current_mana;

    Ok(())
}

fn shop_rng(state: &ShopState, salt: u64) -> XorShiftRng {
    let round_mix = (state.round.max(0) as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
    XorShiftRng::seed_from_u64(state.game_seed ^ round_mix ^ salt)
}

fn execute_shop_trigger<R: BattleRng>(
    state: &mut ShopState,
    trigger: ShopTrigger,
    trigger_source_slot: Option<usize>,
    sold_source: Option<(CardId, usize)>,
    rng: &mut R,
) {
    let mut pending = Vec::new();

    for (slot, board_unit) in state.board.iter().enumerate() {
        let Some(board_unit) = board_unit else {
            continue;
        };

        let Some(card) = state.card_pool.get(&board_unit.card_id) else {
            continue;
        };

        for ability in &card.shop_abilities {
            if ability.trigger == trigger {
                pending.push(ShopPendingAbility {
                    source_slot: Some(slot),
                    source_on_board: true,
                    ability: ability.clone(),
                });
            }
        }
    }

    if let Some((sold_card_id, sold_slot)) = sold_source {
        if let Some(card) = state.card_pool.get(&sold_card_id) {
            for ability in &card.shop_abilities {
                if ability.trigger == trigger {
                    pending.push(ShopPendingAbility {
                        source_slot: Some(sold_slot),
                        source_on_board: false,
                        ability: ability.clone(),
                    });
                }
            }
        }
    }

    for pending_ability in pending {
        if !shop_conditions_pass(
            state,
            &pending_ability.ability.conditions,
            pending_ability.source_slot,
            pending_ability.source_on_board,
            trigger_source_slot,
        ) {
            continue;
        }

        apply_shop_effect(
            state,
            &pending_ability.ability.effect,
            pending_ability.source_slot,
            pending_ability.source_on_board,
            trigger_source_slot,
            rng,
        );
    }
}

fn apply_shop_effect<R: BattleRng>(
    state: &mut ShopState,
    effect: &ShopEffect,
    source_slot: Option<usize>,
    source_on_board: bool,
    trigger_source_slot: Option<usize>,
    rng: &mut R,
) {
    match effect {
        ShopEffect::ModifyStatsPermanent {
            health,
            attack,
            target,
        } => {
            let targets = resolve_shop_targets(
                state,
                target,
                source_slot,
                source_on_board,
                trigger_source_slot,
                rng,
            );
            for slot in targets {
                if let Some(unit) = state.board.get_mut(slot).and_then(|s| s.as_mut()) {
                    unit.perm_health = unit.perm_health.saturating_add(*health);
                    unit.perm_attack = unit.perm_attack.saturating_add(*attack);
                }
            }
            cleanup_dead_units(state);
        }
        ShopEffect::SpawnUnit {
            card_id,
            spawn_location,
        } => {
            let Some(_spawn_card) = state.card_pool.get(card_id) else {
                return;
            };
            let empty_slot = match spawn_location {
                crate::types::SpawnLocation::Front | crate::types::SpawnLocation::DeathPosition => {
                    state.board.iter().position(|s| s.is_none())
                }
                crate::types::SpawnLocation::Back => state.board.iter().rposition(|s| s.is_none()),
            };
            let Some(empty_slot) = empty_slot else {
                return;
            };

            state.board[empty_slot] = Some(crate::types::BoardUnit::new(*card_id));
        }
        ShopEffect::Destroy { target } => {
            let targets = resolve_shop_targets(
                state,
                target,
                source_slot,
                source_on_board,
                trigger_source_slot,
                rng,
            );
            for slot in targets {
                state.board[slot] = None;
            }
        }
        ShopEffect::GainMana { amount } => {
            state.shop_mana = state
                .shop_mana
                .saturating_add(*amount)
                .clamp(0, state.mana_limit);
        }
    }
}

fn resolve_shop_targets<R: BattleRng>(
    state: &ShopState,
    target: &ShopTarget,
    source_slot: Option<usize>,
    source_on_board: bool,
    trigger_source_slot: Option<usize>,
    rng: &mut R,
) -> Vec<usize> {
    match target {
        ShopTarget::Position { scope, index } => {
            if *scope == ShopScope::SelfUnit {
                resolve_self_position_target(state, source_slot, source_on_board, *index)
            } else {
                resolve_absolute_position_target(
                    state,
                    *scope,
                    source_slot,
                    source_on_board,
                    trigger_source_slot,
                    *index,
                )
            }
        }
        ShopTarget::Random { scope, count } => {
            let mut candidates = resolve_scope_slots(
                state,
                *scope,
                source_slot,
                source_on_board,
                trigger_source_slot,
            );
            if candidates.is_empty() {
                return Vec::new();
            }

            rng.shuffle(&mut candidates);
            candidates.truncate((*count as usize).min(candidates.len()));
            candidates
        }
        ShopTarget::Standard {
            scope,
            stat,
            order,
            count,
        } => {
            let mut candidates = resolve_scope_slots(
                state,
                *scope,
                source_slot,
                source_on_board,
                trigger_source_slot,
            );
            candidates.sort_by(|a, b| {
                let a_val = shop_stat_value(state, *a, *stat).unwrap_or(0);
                let b_val = shop_stat_value(state, *b, *stat).unwrap_or(0);
                let cmp = match order {
                    crate::types::SortOrder::Ascending => a_val.cmp(&b_val),
                    crate::types::SortOrder::Descending => b_val.cmp(&a_val),
                };
                if cmp == core::cmp::Ordering::Equal {
                    a.cmp(b)
                } else {
                    cmp
                }
            });
            candidates.truncate((*count as usize).min(candidates.len()));
            candidates
        }
        ShopTarget::All { scope } => resolve_scope_slots(
            state,
            *scope,
            source_slot,
            source_on_board,
            trigger_source_slot,
        ),
    }
}

fn resolve_self_position_target(
    state: &ShopState,
    source_slot: Option<usize>,
    source_on_board: bool,
    index: i32,
) -> Vec<usize> {
    let Some(source_slot) = source_slot else {
        return Vec::new();
    };

    let candidate = if index == 0 {
        if source_on_board {
            Some(source_slot)
        } else {
            None
        }
    } else {
        let offset = source_slot as i32 + index;
        if offset < 0 {
            None
        } else {
            Some(offset as usize)
        }
    };

    let Some(slot) = candidate else {
        return Vec::new();
    };

    if slot >= state.board.len() {
        return Vec::new();
    }

    if state.board[slot].is_some() {
        vec![slot]
    } else {
        Vec::new()
    }
}

fn resolve_absolute_position_target(
    state: &ShopState,
    scope: ShopScope,
    source_slot: Option<usize>,
    source_on_board: bool,
    trigger_source_slot: Option<usize>,
    index: i32,
) -> Vec<usize> {
    let candidates = resolve_scope_slots(
        state,
        scope,
        source_slot,
        source_on_board,
        trigger_source_slot,
    );
    if candidates.is_empty() {
        return Vec::new();
    }

    if index == -1 {
        return candidates
            .last()
            .copied()
            .map(|s| vec![s])
            .unwrap_or_default();
    }

    let idx = index as usize;
    if idx >= state.board.len() {
        return Vec::new();
    }

    if candidates.contains(&idx) {
        vec![idx]
    } else {
        Vec::new()
    }
}

fn resolve_scope_slots(
    state: &ShopState,
    scope: ShopScope,
    source_slot: Option<usize>,
    source_on_board: bool,
    trigger_source_slot: Option<usize>,
) -> Vec<usize> {
    let occupied: Vec<usize> = state
        .board
        .iter()
        .enumerate()
        .filter_map(|(i, slot)| slot.as_ref().map(|_| i))
        .collect();

    match scope {
        ShopScope::SelfUnit => {
            if source_on_board {
                source_slot
                    .filter(|idx| state.board[*idx].is_some())
                    .map(|idx| vec![idx])
                    .unwrap_or_default()
            } else {
                Vec::new()
            }
        }
        ShopScope::Allies => occupied,
        ShopScope::All => occupied,
        ShopScope::AlliesOther => {
            if source_on_board {
                occupied
                    .into_iter()
                    .filter(|idx| Some(*idx) != source_slot)
                    .collect()
            } else {
                occupied
            }
        }
        ShopScope::TriggerSource => trigger_source_slot
            .filter(|idx| state.board[*idx].is_some())
            .map(|idx| vec![idx])
            .unwrap_or_default(),
    }
}

fn shop_conditions_pass(
    state: &ShopState,
    conditions: &[ShopCondition],
    source_slot: Option<usize>,
    source_on_board: bool,
    trigger_source_slot: Option<usize>,
) -> bool {
    for condition in conditions {
        match condition {
            ShopCondition::Is(matcher) => {
                if !shop_matcher_pass(
                    state,
                    matcher,
                    source_slot,
                    source_on_board,
                    trigger_source_slot,
                ) {
                    return false;
                }
            }
            ShopCondition::AnyOf(matchers) => {
                if !matchers.iter().any(|matcher| {
                    shop_matcher_pass(
                        state,
                        matcher,
                        source_slot,
                        source_on_board,
                        trigger_source_slot,
                    )
                }) {
                    return false;
                }
            }
        }
    }
    true
}

fn shop_matcher_pass(
    state: &ShopState,
    matcher: &ShopMatcher,
    source_slot: Option<usize>,
    source_on_board: bool,
    trigger_source_slot: Option<usize>,
) -> bool {
    match matcher {
        ShopMatcher::UnitCount { scope, op, value } => {
            let count = resolve_scope_slots(
                state,
                *scope,
                source_slot,
                source_on_board,
                trigger_source_slot,
            )
            .len() as u32;
            compare_u32(count, *op, *value)
        }
        ShopMatcher::StatValueCompare {
            scope,
            stat,
            op,
            value,
        } => {
            let targets = resolve_scope_slots(
                state,
                *scope,
                source_slot,
                source_on_board,
                trigger_source_slot,
            );
            if targets.is_empty() {
                return false;
            }

            targets.iter().any(|slot| {
                shop_stat_value(state, *slot, *stat)
                    .map(|actual| compare_i32(actual, *op, *value))
                    .unwrap_or(false)
            })
        }
        ShopMatcher::IsPosition { scope, index } => {
            let targets = resolve_scope_slots(
                state,
                *scope,
                source_slot,
                source_on_board,
                trigger_source_slot,
            );
            if targets.is_empty() {
                return false;
            }

            let desired = if *index == -1 {
                targets.last().copied()
            } else if *index >= 0 {
                Some(*index as usize)
            } else {
                None
            };

            desired.map(|idx| targets.contains(&idx)).unwrap_or(false)
        }
    }
}

fn shop_stat_value(state: &ShopState, slot: usize, stat: StatType) -> Option<i32> {
    let unit = state.board.get(slot)?.as_ref()?;
    let card = state.card_pool.get(&unit.card_id)?;

    match stat {
        StatType::Health => Some(card.stats.health.saturating_add(unit.perm_health)),
        StatType::Attack => Some(card.stats.attack.saturating_add(unit.perm_attack)),
        StatType::Mana => Some(card.economy.play_cost),
    }
}

fn cleanup_dead_units(state: &mut ShopState) {
    for idx in 0..state.board.len() {
        let should_remove = state.board[idx]
            .as_ref()
            .and_then(|unit| state.card_pool.get(&unit.card_id).map(|card| (unit, card)))
            .map(|(unit, card)| card.stats.health.saturating_add(unit.perm_health) <= 0)
            .unwrap_or(false);
        if should_remove {
            state.board[idx] = None;
        };
    }
}

fn compare_i32(actual: i32, op: CompareOp, expected: i32) -> bool {
    match op {
        CompareOp::GreaterThan => actual > expected,
        CompareOp::LessThan => actual < expected,
        CompareOp::Equal => actual == expected,
        CompareOp::GreaterThanOrEqual => actual >= expected,
        CompareOp::LessThanOrEqual => actual <= expected,
    }
}

fn compare_u32(actual: u32, op: CompareOp, expected: u32) -> bool {
    match op {
        CompareOp::GreaterThan => actual > expected,
        CompareOp::LessThan => actual < expected,
        CompareOp::Equal => actual == expected,
        CompareOp::GreaterThanOrEqual => actual >= expected,
        CompareOp::LessThanOrEqual => actual <= expected,
    }
}
