use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;

use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use serde::{Deserialize, Serialize};

use crate::clue::{ClueDatabase, CrypticClueDb};
use crate::config::{GenerationConfig, SymmetryType};
use crate::dict::Dictionary;
use crate::error::{CrucivError, Result};
use crate::grid::pattern::generate_pattern;
use crate::grid::slot::{extract_slots, Direction, Slot};
use crate::grid::Grid;
use crate::solver::{self, SolveResult};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlacedWord {
    pub word: String,
    pub row: usize,
    pub col: usize,
    pub direction: Direction,
    pub clue_number: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Clue {
    pub number: u32,
    pub direction: Direction,
    pub text: String,
    pub answer: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub definition: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub wordplay_type: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Puzzle {
    pub grid: Grid,
    pub words: Vec<PlacedWord>,
    pub clues_across: Vec<Clue>,
    pub clues_down: Vec<Clue>,
    pub width: usize,
    pub height: usize,
    pub symmetry: SymmetryType,
    pub difficulty_score: u32,
    pub generation_time_ms: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub theme: Option<String>,
    #[serde(default)]
    pub theme_entries: Vec<String>,
    #[serde(default)]
    pub is_cryptic: bool,
}

impl Puzzle {
    pub fn to_json(&self) -> serde_json::Result<String> {
        serde_json::to_string_pretty(self)
    }

    pub fn from_json(json: &str) -> serde_json::Result<Self> {
        serde_json::from_str(json)
    }

    pub fn apply_cryptic_clues(&mut self, cryptic_db: &CrypticClueDb) -> usize {
        let mut count = 0;
        for clue in self
            .clues_across
            .iter_mut()
            .chain(self.clues_down.iter_mut())
        {
            if let Some((text, definition, wordplay)) = cryptic_db.get_clue_with_meta(&clue.answer)
            {
                clue.text = text;
                clue.definition = definition;
                clue.wordplay_type = wordplay;
                count += 1;
            }
        }
        if count > 0 {
            self.is_cryptic = true;
        }
        count
    }

    pub fn is_fully_filled(&self) -> bool {
        use crate::grid::Cell;
        for r in 0..self.height {
            for c in 0..self.width {
                if matches!(self.grid.get(r, c), Cell::Empty) {
                    return false;
                }
            }
        }
        // also check every white cell is covered by a word in both directions
        let mut across_covered = vec![vec![false; self.width]; self.height];
        let mut down_covered = vec![vec![false; self.width]; self.height];
        for w in &self.words {
            let len = w.word.len();
            for i in 0..len {
                let (r, c) = match w.direction {
                    Direction::Across => (w.row, w.col + i),
                    Direction::Down => (w.row + i, w.col),
                };
                if r < self.height && c < self.width {
                    match w.direction {
                        Direction::Across => across_covered[r][c] = true,
                        Direction::Down => down_covered[r][c] = true,
                    }
                }
            }
        }
        for r in 0..self.height {
            for c in 0..self.width {
                if matches!(self.grid.get(r, c), Cell::Letter(_)) {
                    if !across_covered[r][c] || !down_covered[r][c] {
                        return false;
                    }
                }
            }
        }
        true
    }
}

pub fn assemble_puzzle(
    solve_result: &SolveResult,
    slots: &[Slot],
    dict: &Dictionary,
    clue_db: &ClueDatabase,
    config: &GenerationConfig,
    difficulty: u8,
    generation_time_ms: u64,
    theme: Option<&str>,
    theme_words: Option<&HashSet<String>>,
) -> Puzzle {
    let mut clue_number = 1u32;
    let mut assigned_numbers: HashMap<(usize, usize), u32> = HashMap::new();
    let mut words = Vec::new();
    let mut clues_across = Vec::new();
    let mut clues_down = Vec::new();

    let mut sorted_slots: Vec<_> = slots.iter().collect();
    sorted_slots.sort_by_key(|s| (s.row, s.col, matches!(s.direction, Direction::Down) as u8));

    for slot in &sorted_slots {
        let num = *assigned_numbers
            .entry((slot.row, slot.col))
            .or_insert_with(|| {
                let n = clue_number;
                clue_number += 1;
                n
            });

        let word_idx = solve_result
            .word_assignments
            .iter()
            .find(|(si, _)| *si == slot.index)
            .map(|(_, wi)| *wi);

        if let Some(wi) = word_idx {
            let word_text = &dict.get(wi).text;

            let clue_text = match clue_db.get_clue(word_text, difficulty) {
                Ok(text) => text,
                Err(_) => clue_db.get_clue(word_text, 3).unwrap_or_else(|_| {
                    tracing::warn!("no clue found for word: {}", word_text);
                    format!("___ ({} letter word)", word_text.len())
                }),
            };

            words.push(PlacedWord {
                word: word_text.clone(),
                row: slot.row,
                col: slot.col,
                direction: slot.direction,
                clue_number: num,
            });

            let clue = Clue {
                number: num,
                direction: slot.direction,
                text: clue_text,
                answer: word_text.clone(),
                definition: None,
                wordplay_type: None,
            };

            match slot.direction {
                Direction::Across => clues_across.push(clue),
                Direction::Down => clues_down.push(clue),
            }
        }
    }

    let theme_entries = if let Some(tw) = theme_words {
        words
            .iter()
            .filter(|w| tw.contains(&w.word))
            .map(|w| w.word.clone())
            .collect()
    } else {
        vec![]
    };

    Puzzle {
        grid: solve_result.grid.clone(),
        words,
        clues_across,
        clues_down,
        width: config.width,
        height: config.height,
        symmetry: config.symmetry,
        difficulty_score: 50,
        generation_time_ms,
        theme: theme.map(|s| s.to_string()),
        theme_entries,
        is_cryptic: false,
    }
}

pub fn generate_full_puzzle(
    config: &GenerationConfig,
    dict: &Dictionary,
    clue_db: &ClueDatabase,
    difficulty: u8,
) -> Result<Puzzle> {
    generate_themed_puzzle(config, dict, clue_db, difficulty, None, None)
}

pub fn generate_themed_puzzle(
    config: &GenerationConfig,
    dict: &Dictionary,
    clue_db: &ClueDatabase,
    difficulty: u8,
    theme: Option<&str>,
    theme_words: Option<&HashSet<String>>,
) -> Result<Puzzle> {
    let start = Instant::now();

    let cores = std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(1)
        .min(8);

    // parallel for larger grids with multiple cores
    if cores > 1 && config.width > 9 {
        return parallel_generate(
            config,
            dict,
            clue_db,
            difficulty,
            theme,
            theme_words,
            cores,
            start,
        );
    }

    sequential_generate(config, dict, clue_db, difficulty, theme, theme_words, start)
}

struct SolvedGrid {
    result: SolveResult,
    slots: Vec<Slot>,
    config: GenerationConfig,
}

fn parallel_generate(
    config: &GenerationConfig,
    dict: &Dictionary,
    clue_db: &ClueDatabase,
    difficulty: u8,
    theme: Option<&str>,
    theme_words: Option<&HashSet<String>>,
    cores: usize,
    start: Instant,
) -> Result<Puzzle> {
    let cancelled = Arc::new(AtomicBool::new(false));
    let (tx, rx) = std::sync::mpsc::channel::<Result<SolvedGrid>>();
    let dict = Arc::new(dict.clone());

    let max_attempts = if config.width >= 25 {
        40
    } else if config.width >= 19 {
        30
    } else {
        20
    };
    let per_worker = (max_attempts + cores - 1) / cores;

    let handles: Vec<_> = (0..cores)
        .map(|i| {
            let tx = tx.clone();
            let cancelled = cancelled.clone();
            let dict = dict.clone();
            let base_seed = config.seed;
            let mut os_rng = base_seed.is_none().then(StdRng::from_os_rng);
            let mut worker_config = config.clone();
            worker_config.cancelled = Some(cancelled.clone());

            let min_word_length = config.min_word_length;

            std::thread::spawn(move || {
                for attempt in 0..per_worker {
                    if cancelled.load(Ordering::Relaxed) {
                        return;
                    }

                    let mut attempt_config = worker_config.clone();
                    // seeded callers (daily puzzles) keep the deterministic series;
                    // unseeded callers get fresh OS entropy per attempt instead of
                    // silently sharing the fixed 0-based series
                    attempt_config.seed = match base_seed {
                        Some(s) => {
                            let wseed = s.wrapping_add(i as u64 * 7919);
                            Some(if attempt > 0 {
                                wseed
                                    .wrapping_mul(6364136223846793005)
                                    .wrapping_add((attempt as u64).wrapping_mul(1442695040888963407))
                            } else {
                                wseed
                            })
                        }
                        None => Some(os_rng.as_mut().unwrap().random::<u64>()),
                    };

                    let grid = match generate_pattern(&attempt_config) {
                        Ok(g) => g,
                        Err(_) => continue,
                    };

                    let slots = extract_slots(&grid, min_word_length);

                    match solver::solve(&grid, &slots, &dict, &attempt_config) {
                        Ok(result) => {
                            let _ = tx.send(Ok(SolvedGrid {
                                result,
                                slots,
                                config: attempt_config,
                            }));
                            return;
                        }
                        Err(_) => continue,
                    }
                }
            })
        })
        .collect();

    drop(tx);

    let mut last_err = None;
    for msg in rx {
        match msg {
            Ok(solved) => {
                let elapsed = start.elapsed().as_millis() as u64;
                let puzzle = assemble_puzzle(
                    &solved.result,
                    &solved.slots,
                    &dict,
                    clue_db,
                    &solved.config,
                    difficulty,
                    elapsed,
                    theme,
                    theme_words,
                );
                if !puzzle.is_fully_filled() {
                    tracing::warn!("rejecting puzzle: not fully filled or uncovered cells");
                    continue;
                }
                cancelled.store(true, Ordering::Relaxed);
                return Ok(puzzle);
            }
            Err(e) => {
                last_err = Some(e);
            }
        }
    }

    // wait for threads to finish
    for h in handles {
        let _ = h.join();
    }

    Err(last_err.unwrap_or_else(|| CrucivError::Generation(generation_hint(config))))
}

fn sequential_generate(
    config: &GenerationConfig,
    dict: &Dictionary,
    clue_db: &ClueDatabase,
    difficulty: u8,
    theme: Option<&str>,
    theme_words: Option<&HashSet<String>>,
    start: Instant,
) -> Result<Puzzle> {
    let max_attempts = if config.width >= 25 {
        40
    } else if config.width >= 19 {
        30
    } else {
        20
    };

    for attempt in 0..max_attempts {
        let mut attempt_config = config.clone();
        if attempt > 0 {
            attempt_config.seed = Some(
                config
                    .seed
                    .unwrap_or(0)
                    .wrapping_mul(6364136223846793005)
                    .wrapping_add((attempt as u64).wrapping_mul(1442695040888963407)),
            );
        }

        let grid = match generate_pattern(&attempt_config) {
            Ok(g) => g,
            Err(_) => continue,
        };

        let slots = extract_slots(&grid, config.min_word_length);

        match solver::solve(&grid, &slots, &dict, &attempt_config) {
            Ok(result) => {
                let elapsed = start.elapsed().as_millis() as u64;
                let puzzle = assemble_puzzle(
                    &result,
                    &slots,
                    dict,
                    clue_db,
                    config,
                    difficulty,
                    elapsed,
                    theme,
                    theme_words,
                );
                if !puzzle.is_fully_filled() {
                    tracing::warn!("rejecting puzzle: not fully filled or uncovered cells");
                    continue;
                }
                return Ok(puzzle);
            }
            Err(_) => continue,
        }
    }

    Err(CrucivError::Generation(generation_hint(config)))
}

fn generation_hint(config: &GenerationConfig) -> String {
    let size = config.width;
    let density = config.black_cell_density;
    let fill_pct = ((1.0 - density) * 100.0).round() as u32;

    if density < 0.12 {
        format!("Could not fill a {}x{} grid at {}% fill - try lowering the fill percentage or reducing the grid size", size, size, fill_pct)
    } else if size >= 19 && density < 0.18 {
        format!("A {}x{} grid at {}% fill is very hard to fill - try lowering the fill to around 75% or below", size, size, fill_pct)
    } else if size >= 15 && density < 0.15 {
        format!("Could not fill a {}x{} grid at {}% fill - large grids need more black cells. Try 78% fill or lower", size, size, fill_pct)
    } else {
        format!("Could not generate a {}x{} puzzle at {}% fill - try a different fill percentage or grid size", size, size, fill_pct)
    }
}
