use std::collections::VecDeque;
use std::sync::atomic::Ordering;
use std::time::Instant;

use rand::rngs::StdRng;
use rand::Rng;
use rand::SeedableRng;

use crate::config::GenerationConfig;
use crate::dict::Dictionary;
use crate::error::{CrucivError, Result};
use crate::grid::slot::Slot;
use crate::grid::Grid;

use super::state::SolverState;

pub struct SolveResult {
    pub grid: Grid,
    pub word_assignments: Vec<(usize, usize)>,
}

pub fn solve(
    grid: &Grid,
    slots: &[Slot],
    dict: &Dictionary,
    config: &GenerationConfig,
) -> Result<SolveResult> {
    let start = Instant::now();
    let timeout = std::time::Duration::from_secs(config.timeout_seconds);

    let mut rng = match config.seed {
        Some(s) => StdRng::seed_from_u64(s),
        None => StdRng::from_os_rng(),
    };

    let total_restarts = config.max_restarts.max(slots.len());
    let bt_budget = (config.max_backtrack as u64).max(slots.len() as u64 * 2000);

    for restart in 0..total_restarts {
        if start.elapsed() > timeout {
            return Err(CrucivError::Timeout(config.timeout_seconds));
        }
        if let Some(ref flag) = config.cancelled {
            if flag.load(Ordering::Relaxed) {
                return Err(CrucivError::Generation("cancelled".into()));
            }
        }

        let mut state = SolverState::new(slots, grid.width, grid.height, dict.word_count());
        state.init_domains(slots, &dict.index);

        // run initial domain propagation to detect infeasible cells early
        if !propagate_all_domains(&mut state, slots, dict) {
            tracing::debug!(
                "restart {}: initial domain propagation found infeasible cell",
                restart + 1
            );
            continue;
        }

        let mut bt = 0u64;
        let rand_top = config.rand_top_base + restart.min(5);

        let result = backtrack(
            &mut state, slots, dict, config, &start, timeout, &mut rng, &mut bt, rand_top,
            bt_budget,
        );

        if let Some(assignments) = result {
            let mut filled = grid.clone();
            for &(si, wi) in &assignments {
                let word = &dict.get(wi).text;
                for (pos, (r, c)) in slots[si].cells().iter().enumerate() {
                    filled.set(
                        *r,
                        *c,
                        crate::grid::Cell::Letter(word.as_bytes()[pos] as char),
                    );
                }
            }
            return Ok(SolveResult {
                grid: filled,
                word_assignments: assignments,
            });
        }

        tracing::debug!("restart {} failed ({} backtracks)", restart + 1, bt);
    }

    Err(CrucivError::Generation(format!(
        "failed to fill grid after {} restarts",
        total_restarts
    )))
}

fn backtrack(
    state: &mut SolverState,
    slots: &[Slot],
    dict: &Dictionary,
    config: &GenerationConfig,
    start: &Instant,
    timeout: std::time::Duration,
    rng: &mut StdRng,
    bt: &mut u64,
    rand_top: usize,
    bt_budget: u64,
) -> Option<Vec<(usize, usize)>> {
    if *bt > bt_budget {
        return None;
    }
    if *bt % 512 == 0 {
        if start.elapsed() > timeout {
            return None;
        }
        if let Some(ref flag) = config.cancelled {
            if flag.load(Ordering::Relaxed) {
                return None;
            }
        }
    }

    let slot_idx = match pick_slot(state, slots, dict) {
        Some(i) => i,
        None => {
            return Some(
                state
                    .assignments
                    .iter()
                    .enumerate()
                    .filter_map(|(i, a)| a.map(|w| (i, w)))
                    .collect(),
            );
        }
    };

    let slot = &slots[slot_idx];
    let constraints = state.current_constraints(slot);

    let all_candidates =
        dict.candidates_filtered(slot.length, &constraints, config.frequency_threshold);

    // score and filter candidates
    let mut scored: Vec<(usize, f64)> = all_candidates
        .iter()
        .filter(|&&w| !state.is_used(w))
        .map(|&w| {
            let word = dict.get(w);
            let mut s = word.frequency;
            let bytes = word.text.as_bytes();
            let mut seen = [false; 26];
            let mut unique = 0;
            for &b in bytes {
                let idx = (b - b'A') as usize;
                if !seen[idx] {
                    seen[idx] = true;
                    unique += 1;
                }
            }
            s += (unique as f64 / bytes.len() as f64) * 0.1;
            // penalize recently used words so the solver picks fresh ones
            if let Some(ref staleness) = config.staleness {
                if let Some(&penalty) = staleness.get(&word.text) {
                    s *= penalty;
                }
            }
            (w, s)
        })
        .collect();

    if scored.is_empty() {
        return None;
    }

    scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());

    // randomize top candidates
    let top_n = scored.len().min(rand_top);
    if top_n > 1 {
        for i in 0..top_n - 1 {
            let j = rng.random_range(i..top_n);
            scored.swap(i, j);
        }
    }

    scored.truncate(24);

    // filter out words related to already-placed words
    let placed_texts: Vec<&str> = state
        .assignments
        .iter()
        .filter_map(|a| a.map(|wi| dict.get(wi).text.as_str()))
        .collect();
    if !placed_texts.is_empty() {
        scored.retain(|(w, _)| {
            let text = &dict.get(*w).text;
            !placed_texts
                .iter()
                .any(|p| super::candidate::is_related(text, p))
        });
    }

    // LCV ordering
    if scored.len() > 1 && scored.len() <= 20 && slots[slot_idx].intersections.len() >= 2 {
        lcv_sort(&mut scored, slot_idx, state, slots, dict);
    }

    let max_try = scored.len().min(16);

    for i in 0..max_try {
        let (word_idx, _) = scored[i];
        let word_bytes: Vec<u8> = dict.get(word_idx).text.bytes().collect();

        // save state for undo
        let cells = slot.cells();
        let mut prev_letters: Vec<(usize, usize, Option<u8>)> = Vec::with_capacity(cells.len());
        let mut prev_domains: Vec<(usize, usize, u32)> = Vec::new();

        for (pos, &(r, c)) in cells.iter().enumerate() {
            prev_letters.push((r, c, state.get_letter(r, c)));
            state.set_letter(r, c, word_bytes[pos]);
        }
        state.assignments[slot_idx] = Some(word_idx);
        state.mark_used(word_idx);

        // AC-3 propagation with domain narrowing
        let ac3_ok = ac3_propagate(state, slots, dict, slot_idx, &mut prev_domains);

        if ac3_ok {
            if let Some(result) = backtrack(
                state, slots, dict, config, start, timeout, rng, bt, rand_top, bt_budget,
            ) {
                return Some(result);
            }
        }

        *bt += 1;

        // undo
        state.assignments[slot_idx] = None;
        state.unmark_used(word_idx);
        for &(r, c, p) in &prev_letters {
            match p {
                Some(l) => state.set_letter(r, c, l),
                None => state.clear_letter(r, c),
            }
        }
        for &(r, c, d) in &prev_domains {
            state.set_domain(r, c, d);
        }
    }

    None
}

/// Compute the set of letters possible at a given position in a slot,
/// given the current constraints. Returns a 26-bit mask.
fn compute_slot_domain_at(state: &SolverState, slot: &Slot, pos: usize, dict: &Dictionary) -> u32 {
    let constraints = state.current_constraints(slot);
    let len = slot.length;
    let chunks = match dict.index.chunk_count(len) {
        Some(c) => c,
        None => return 0,
    };
    let n = dict.index.count_of_length(len);
    if n == 0 {
        return 0;
    }

    // build intersection bitset for all existing constraints
    let mut result = vec![u64::MAX; chunks];
    let last_bits = n % 64;
    if last_bits > 0 {
        result[chunks - 1] = (1u64 << last_bits) - 1;
    }
    for &(cpos, letter) in &constraints {
        if cpos == pos {
            continue;
        } // skip the position we're computing domain for
        if let Some(bits) = dict.index.get_letter_bits(len, cpos, letter) {
            for (i, chunk) in result.iter_mut().enumerate() {
                *chunk &= bits[i];
            }
        } else {
            return 0;
        }
    }

    // now union all letters that appear at `pos` across remaining candidates
    let mut domain = 0u32;
    for letter in 0u8..26 {
        if let Some(bits) = dict.index.get_letter_bits(len, pos, b'A' + letter) {
            for (i, &chunk) in result.iter().enumerate() {
                if chunk & bits[i] != 0 {
                    domain |= 1u32 << letter;
                    break;
                }
            }
        }
    }
    domain
}

/// Full domain propagation for all unassigned cells.
/// Returns false if any cell has an empty domain.
fn propagate_all_domains(state: &mut SolverState, slots: &[Slot], dict: &Dictionary) -> bool {
    let mut changed = true;
    while changed {
        changed = false;
        for slot in slots {
            if state.assignments[slot.index].is_some() {
                continue;
            }
            for (pos, &(r, c)) in slot.cells().iter().enumerate() {
                if state.get_letter(r, c).is_some() {
                    continue;
                }
                let new_domain = compute_slot_domain_at(state, slot, pos, dict);
                let old_domain = state.get_domain(r, c);
                let narrowed = old_domain & new_domain;
                if narrowed != old_domain {
                    if narrowed == 0 {
                        return false;
                    }
                    state.set_domain(r, c, narrowed);
                    changed = true;
                }
            }
        }
    }
    true
}

/// AC-3 propagation after placing a word.
/// Checks crossing slots and narrows cell domains.
/// Saves changed domains for undo.
fn ac3_propagate(
    state: &mut SolverState,
    slots: &[Slot],
    dict: &Dictionary,
    placed: usize,
    prev_domains: &mut Vec<(usize, usize, u32)>,
) -> bool {
    let mut queue: VecDeque<usize> = VecDeque::new();
    let mut checked = vec![false; slots.len()];
    checked[placed] = true;

    for ix in &slots[placed].intersections {
        if state.assignments[ix.other_slot].is_none() {
            queue.push_back(ix.other_slot);
        }
    }

    while let Some(si) = queue.pop_front() {
        if checked[si] || state.assignments[si].is_some() {
            continue;
        }
        checked[si] = true;

        let slot = &slots[si];
        let constraints = state.current_constraints(slot);
        let count = dict.index.candidate_count(slot.length, &constraints);

        if count == 0 {
            return false;
        }

        // narrow cell domains for unconstrained positions in this slot
        let mut domain_changed = false;
        for (pos, &(r, c)) in slot.cells().iter().enumerate() {
            if state.get_letter(r, c).is_some() {
                continue;
            }
            let new_domain = compute_slot_domain_at(state, slot, pos, dict);
            let old_domain = state.get_domain(r, c);
            let narrowed = old_domain & new_domain;
            if narrowed == 0 {
                return false;
            }
            if narrowed != old_domain {
                prev_domains.push((r, c, old_domain));
                state.set_domain(r, c, narrowed);
                domain_changed = true;
            }
        }

        // if domains changed, propagate to crossings
        if domain_changed || count <= 20 {
            for ix in &slot.intersections {
                if !checked[ix.other_slot] && state.assignments[ix.other_slot].is_none() {
                    queue.push_back(ix.other_slot);
                }
            }
        }
    }

    true
}

// Full MRV: check candidate_count for ALL unassigned slots.
// Tiebreak by degree (number of unassigned crossings).
fn pick_slot(state: &SolverState, slots: &[Slot], dict: &Dictionary) -> Option<usize> {
    let mut best_idx = None;
    let mut best_count = usize::MAX;
    let mut best_degree = 0usize;

    for (i, a) in state.assignments.iter().enumerate() {
        if a.is_some() {
            continue;
        }
        let slot = &slots[i];
        let constraints = state.current_constraints(slot);
        let count = dict.index.candidate_count(slot.length, &constraints);

        if count == 0 {
            return Some(i); // force failure fast
        }

        let degree = slot
            .intersections
            .iter()
            .filter(|ix| state.assignments[ix.other_slot].is_none())
            .count();

        if count < best_count || (count == best_count && degree > best_degree) {
            best_count = count;
            best_degree = degree;
            best_idx = Some(i);
        }
    }

    best_idx
}

// LCV: sort candidates by how much freedom they leave in crossing slots.
fn lcv_sort(
    scored: &mut Vec<(usize, f64)>,
    slot_idx: usize,
    state: &SolverState,
    slots: &[Slot],
    dict: &Dictionary,
) {
    let slot = &slots[slot_idx];
    let crossings: Vec<_> = slot
        .intersections
        .iter()
        .filter(|ix| state.assignments[ix.other_slot].is_none())
        .collect();

    if crossings.is_empty() {
        return;
    }

    let mut freedom: Vec<(usize, f64, i64)> = scored
        .iter()
        .map(|&(wi, score)| {
            let word = dict.get(wi);
            let bytes = word.text.as_bytes();
            let mut total_freedom = 0i64;

            for ix in &crossings {
                let letter = bytes[ix.self_pos];
                let other = &slots[ix.other_slot];
                let mut other_constraints = state.current_constraints(other);
                other_constraints.push((ix.other_pos, letter));
                let count = dict.index.candidate_count(other.length, &other_constraints);
                total_freedom += count as i64;
            }

            (wi, score, total_freedom)
        })
        .collect();

    freedom.sort_by(|a, b| b.2.cmp(&a.2).then(b.1.partial_cmp(&a.1).unwrap()));

    *scored = freedom.into_iter().map(|(wi, s, _)| (wi, s)).collect();
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::grid::slot::extract_slots;
    use crate::grid::Cell;

    #[test]
    fn solve_5x5_real_dict() {
        use std::path::Path;
        let path = Path::new("../data/wordlists/enable.txt");
        if !path.exists() {
            return;
        }
        let dict = Dictionary::load(path, None, 3).unwrap();
        let grid = Grid::new(5, 5);
        let slots = extract_slots(&grid, 3);
        let config = GenerationConfig {
            width: 5,
            height: 5,
            timeout_seconds: 10,
            frequency_threshold: 0.0,
            max_restarts: 5,
            seed: Some(42),
            ..Default::default()
        };
        let result = solve(&grid, &slots, &dict, &config);
        assert!(result.is_ok());
    }

    #[test]
    fn solve_with_real_dict() {
        use std::path::Path;
        let path = Path::new("../data/wordlists/enable.txt");
        if !path.exists() {
            return;
        }
        let dict = Dictionary::load(path, None, 3).unwrap();
        let mut grid = Grid::new(5, 5);
        grid.set(2, 2, Cell::Black);
        let slots = extract_slots(&grid, 3);
        let config = GenerationConfig {
            width: 5,
            height: 5,
            timeout_seconds: 10,
            frequency_threshold: 0.0,
            max_restarts: 5,
            seed: Some(42),
            ..Default::default()
        };
        let result = solve(&grid, &slots, &dict, &config);
        assert!(result.is_ok());
    }
}
