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

use crate::config::GenerationConfig;
use crate::error::{CrucivError, Result};

use super::{Cell, Grid};
use super::slot::extract_slots;
use super::symmetry::mirror_coords;

const MAX_WORD_LEN_HUGE: usize = 7;  // grids 25x25+
const MAX_WORD_LEN_LARGE: usize = 9; // grids 19x19+

/// Try to load a grid from pre-made templates first (fast path for 15x15 and 21x21).
/// Falls back to random generation if no templates available.
pub fn generate_pattern(config: &GenerationConfig) -> Result<Grid> {
    let mut rng = match config.seed {
        Some(s) => StdRng::seed_from_u64(s),
        None => StdRng::from_os_rng(),
    };

    // try template-based generation for large grids
    if config.width >= 15 && config.height >= 15 {
        if let Some(grid) = load_template(config, &mut rng) {
            return Ok(grid);
        }
    }

    for _ in 0..100 {
        let grid = try_generate(config, &mut rng);
        if let Some(grid) = grid {
            return Ok(grid);
        }
    }

    Err(CrucivError::Generation(
        "failed to generate valid grid pattern after 100 attempts".into(),
    ))
}

fn load_template(config: &GenerationConfig, rng: &mut StdRng) -> Option<Grid> {
    let data_dir = std::env::var("DATA_DIR").unwrap_or_else(|_| "../data".into());
    let filename = format!("grids_{}x{}.txt", config.width, config.height);
    let path = std::path::Path::new(&data_dir).join("grid-templates").join(&filename);

    let content = std::fs::read_to_string(&path).ok()?;
    let templates: Vec<&str> = content.split("\n\n").filter(|s| !s.trim().is_empty()).collect();
    if templates.is_empty() { return None; }

    // try a few random templates
    for _ in 0..20 {
        let idx = rng.random_range(0..templates.len());
        let template = templates[idx].trim();
        let lines: Vec<&str> = template.lines().collect();
        if lines.len() != config.height { continue; }
        if lines[0].len() != config.width { continue; }

        let mut grid = Grid::new(config.width, config.height);
        for (r, line) in lines.iter().enumerate() {
            for (c, ch) in line.chars().enumerate() {
                if ch == '#' {
                    grid.set(r, c, Cell::Black);
                }
            }
        }

        if is_connected(&grid) && has_valid_words(&grid, config.min_word_length) {
            let slots = extract_slots(&grid, config.min_word_length);
            if !slots.is_empty() {
                tracing::debug!("loaded {}x{} template (index {})", config.width, config.height, idx);
                return Some(grid);
            }
        }
    }

    None
}


fn try_generate(config: &GenerationConfig, rng: &mut StdRng) -> Option<Grid> {
    let w = config.width;
    let h = config.height;

    let max_word_len = if w >= 25 || h >= 25 { MAX_WORD_LEN_HUGE }
                       else if w >= 19 || h >= 19 { MAX_WORD_LEN_LARGE }
                       else { w.max(h) };

    let min_density = if w >= 25 || h >= 25 { 0.30 }
                      else if w >= 19 || h >= 19 { 0.25 }
                      else { 0.0 };
    let effective_density = config.black_cell_density.max(min_density);

    // for high density (sparse puzzles), carve slots out of a black grid
    // for normal density, add black cells to a white grid
    let mut grid = if effective_density > 0.50 {
        try_generate_sparse(w, h, effective_density, config, rng)?
    } else {
        try_generate_dense(w, h, effective_density, config, rng)?
    };

    if has_long_runs(&grid, max_word_len) {
        break_long_runs(&mut grid, max_word_len, config, rng);
    }

    if !is_connected(&grid) || !has_valid_words(&grid, config.min_word_length) {
        return None;
    }

    let slots = extract_slots(&grid, config.min_word_length);
    if slots.is_empty() {
        return None;
    }

    Some(grid)
}

/// Standard approach: start white, add black cells while maintaining connectivity
fn try_generate_dense(
    w: usize, h: usize, density: f64, config: &GenerationConfig, rng: &mut StdRng,
) -> Option<Grid> {
    let mut grid = Grid::new(w, h);
    let total = w * h;
    let target_black = (total as f64 * density) as usize;

    let mut positions: Vec<(usize, usize)> = Vec::new();
    for r in 0..h {
        for c in 0..w {
            positions.push((r, c));
        }
    }
    shuffle(&mut positions, rng);

    let mut placed = 0;
    for &(r, c) in &positions {
        if placed >= target_black { break; }
        if grid.get(r, c).is_black() { continue; }

        let coords = mirror_coords(r, c, w, h, config.symmetry);
        let would_place = coords.iter().filter(|&&(mr, mc)| !grid.get(mr, mc).is_black()).count();
        if placed + would_place > target_black + 2 { continue; }

        for &(mr, mc) in &coords { grid.set(mr, mc, Cell::Black); }

        if !has_valid_words(&grid, config.min_word_length) || !is_connected(&grid) {
            for &(mr, mc) in &coords { grid.set(mr, mc, Cell::Empty); }
            continue;
        }

        placed = grid.black_count();
    }

    Some(grid)
}

/// Sparse approach: start black, carve connected word slots
fn try_generate_sparse(
    w: usize, h: usize, density: f64, config: &GenerationConfig, rng: &mut StdRng,
) -> Option<Grid> {
    let mut grid = Grid::new_black(w, h);
    let total = w * h;
    let target_white = ((1.0 - density) * total as f64).max(6.0) as usize;
    let min_len = config.min_word_length;

    // start with one slot near the center to seed connectivity
    let cr = h / 2;
    let cc = w / 2;
    let first_len = min_len + rng.random_range(0..=2);
    let start_c = cc.saturating_sub(first_len / 2);
    for i in 0..first_len {
        if start_c + i < w {
            let coords = mirror_coords(cr, start_c + i, w, h, config.symmetry);
            for &(mr, mc) in &coords { grid.set(mr, mc, Cell::Empty); }
        }
    }

    // carve more slots, preferring positions that intersect existing white cells
    for _ in 0..300 {
        if grid.white_count() >= target_white { break; }

        let horizontal = rng.random_bool(0.5);
        let len = min_len + rng.random_range(0..=3);

        // try to find a position that crosses an existing white cell
        let mut best = None;
        for _ in 0..20 {
            let r = rng.random_range(0..h);
            let c = rng.random_range(0..w);

            let (end_r, end_c) = if horizontal { (r, c + len - 1) } else { (r + len - 1, c) };
            if end_r >= h || end_c >= w { continue; }

            // check if this slot touches any existing white cell
            let mut touches = false;
            for i in 0..len {
                let (pr, pc) = if horizontal { (r, c + i) } else { (r + i, c) };
                if grid.get(pr, pc).is_white() { touches = true; break; }
                // also check adjacent cells
                for (dr, dc) in [(-1i32,0),(1,0),(0,-1i32),(0,1)] {
                    let nr = pr as i32 + dr;
                    let nc = pc as i32 + dc;
                    if nr >= 0 && nc >= 0 && (nr as usize) < h && (nc as usize) < w {
                        if grid.get(nr as usize, nc as usize).is_white() { touches = true; break; }
                    }
                }
                if touches { break; }
            }

            // first slot always ok, subsequent must touch existing
            if grid.white_count() <= first_len * 2 || touches {
                best = Some((r, c));
                break;
            }
        }

        let Some((r, c)) = best else { continue };

        for i in 0..len {
            let (pr, pc) = if horizontal { (r, c + i) } else { (r + i, c) };
            if pr < h && pc < w {
                let coords = mirror_coords(pr, pc, w, h, config.symmetry);
                for &(mr, mc) in &coords { grid.set(mr, mc, Cell::Empty); }
            }
        }
    }

    if !is_connected(&grid) || !has_valid_words(&grid, min_len) {
        return None;
    }

    let slots = extract_slots(&grid, min_len);
    if slots.len() < 2 { return None; }

    Some(grid)
}

fn has_long_runs(grid: &Grid, max_len: usize) -> bool {
    for r in 0..grid.height {
        let mut run = 0;
        for c in 0..grid.width {
            if grid.get(r, c).is_white() {
                run += 1;
                if run > max_len { return true; }
            } else {
                run = 0;
            }
        }
    }
    for c in 0..grid.width {
        let mut run = 0;
        for r in 0..grid.height {
            if grid.get(r, c).is_white() {
                run += 1;
                if run > max_len { return true; }
            } else {
                run = 0;
            }
        }
    }
    false
}

fn break_long_runs(grid: &mut Grid, max_len: usize, config: &GenerationConfig, _rng: &mut StdRng) {
    for _ in 0..100 {
        if !has_long_runs(grid, max_len) {
            break;
        }

        let mut broke_something = false;

        // find and break a long row run
        'rows: for r in 0..grid.height {
            let mut start = None;
            let mut run = 0;
            for c in 0..=grid.width {
                if c < grid.width && grid.get(r, c).is_white() {
                    if start.is_none() { start = Some(c); }
                    run += 1;
                } else {
                    if run > max_len {
                        let s = start.unwrap();
                        let mid = s + run / 2;
                        if try_place_black(grid, r, mid, config) {
                            broke_something = true;
                            break 'rows;
                        }
                    }
                    start = None;
                    run = 0;
                }
            }
        }

        // find and break a long column run
        'cols: for c in 0..grid.width {
            let mut start = None;
            let mut run = 0;
            for r in 0..=grid.height {
                if r < grid.height && grid.get(r, c).is_white() {
                    if start.is_none() { start = Some(r); }
                    run += 1;
                } else {
                    if run > max_len {
                        let s = start.unwrap();
                        let mid = s + run / 2;
                        if try_place_black(grid, mid, c, config) {
                            broke_something = true;
                            break 'cols;
                        }
                    }
                    start = None;
                    run = 0;
                }
            }
        }

        if !broke_something {
            break;
        }
    }
}

fn try_place_black(grid: &mut Grid, r: usize, c: usize, config: &GenerationConfig) -> bool {
    let coords = mirror_coords(r, c, grid.width, grid.height, config.symmetry);
    for &(mr, mc) in &coords {
        grid.set(mr, mc, Cell::Black);
    }
    if is_connected(grid) && has_valid_words(grid, config.min_word_length) {
        true
    } else {
        for &(mr, mc) in &coords {
            grid.set(mr, mc, Cell::Empty);
        }
        false
    }
}

fn has_valid_words(grid: &Grid, min_len: usize) -> bool {
    // every white cell must belong to at least one run of min_len+ in some direction
    // first compute across run length for each cell
    let w = grid.width;
    let h = grid.height;
    let mut across_len = vec![vec![0usize; w]; h];
    for r in 0..h {
        let mut start = 0;
        while start < w {
            if !grid.get(r, start).is_white() { start += 1; continue; }
            let mut end = start;
            while end < w && grid.get(r, end).is_white() { end += 1; }
            let len = end - start;
            for c in start..end { across_len[r][c] = len; }
            start = end;
        }
    }
    // compute down run length for each cell
    let mut down_len = vec![vec![0usize; w]; h];
    for c in 0..w {
        let mut start = 0;
        while start < h {
            if !grid.get(start, c).is_white() { start += 1; continue; }
            let mut end = start;
            while end < h && grid.get(end, c).is_white() { end += 1; }
            let len = end - start;
            for r in start..end { down_len[r][c] = len; }
            start = end;
        }
    }
    // check: every white cell must be in valid-length runs in BOTH directions
    // this ensures every cell is "checked" (solvable through crossings)
    for r in 0..h {
        for c in 0..w {
            if !grid.get(r, c).is_white() { continue; }
            if across_len[r][c] < min_len || down_len[r][c] < min_len {
                return false;
            }
        }
    }
    // also reject any run of 2..min_len (too short to be a word but not a single crossing cell)
    for r in 0..h {
        let mut run = 0;
        for c in 0..w {
            if grid.get(r, c).is_white() {
                run += 1;
            } else {
                if run > 0 && run < min_len {
                    return false;
                }
                run = 0;
            }
        }
        if run > 0 && run < min_len {
            return false;
        }
    }

    for c in 0..w {
        let mut run = 0;
        for r in 0..h {
            if grid.get(r, c).is_white() {
                run += 1;
            } else {
                if run > 0 && run < min_len {
                    return false;
                }
                run = 0;
            }
        }
        if run > 0 && run < min_len {
            return false;
        }
    }

    true
}

fn is_connected(grid: &Grid) -> bool {
    // BFS from the first white cell - all white cells must be reachable
    let mut start = None;
    let mut white_count = 0;

    for r in 0..grid.height {
        for c in 0..grid.width {
            if grid.get(r, c).is_white() {
                white_count += 1;
                if start.is_none() {
                    start = Some((r, c));
                }
            }
        }
    }

    let Some((sr, sc)) = start else {
        return true; // no white cells, trivially connected
    };

    if white_count == 0 {
        return true;
    }

    let mut visited = vec![vec![false; grid.width]; grid.height];
    let mut queue = std::collections::VecDeque::new();
    queue.push_back((sr, sc));
    visited[sr][sc] = true;
    let mut reached = 1;

    while let Some((r, c)) = queue.pop_front() {
        for (dr, dc) in [(-1i32, 0), (1, 0), (0, -1i32), (0, 1)] {
            let nr = r as i32 + dr;
            let nc = c as i32 + dc;
            if nr < 0 || nc < 0 {
                continue;
            }
            let nr = nr as usize;
            let nc = nc as usize;
            if nr >= grid.height || nc >= grid.width {
                continue;
            }
            if !visited[nr][nc] && grid.get(nr, nc).is_white() {
                visited[nr][nc] = true;
                reached += 1;
                queue.push_back((nr, nc));
            }
        }
    }

    reached == white_count
}

fn shuffle<T>(v: &mut [T], rng: &mut StdRng) {
    for i in (1..v.len()).rev() {
        let j = rng.random_range(0..=i);
        v.swap(i, j);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{GenerationConfig, SymmetryType};

    #[test]
    fn generates_5x5() {
        let config = GenerationConfig {
            width: 5,
            height: 5,
            seed: Some(42),
            ..Default::default()
        };
        let grid = generate_pattern(&config).unwrap();
        assert_eq!(grid.width, 5);
        assert_eq!(grid.height, 5);
        assert!(is_connected(&grid));
    }

    #[test]
    fn generates_9x9() {
        let config = GenerationConfig {
            width: 9,
            height: 9,
            seed: Some(123),
            ..Default::default()
        };
        let grid = generate_pattern(&config).unwrap();
        assert!(is_connected(&grid));
        assert!(has_valid_words(&grid, 3));
    }

    #[test]
    fn generates_15x15() {
        let config = GenerationConfig {
            width: 15,
            height: 15,
            seed: Some(999),
            ..Default::default()
        };
        let grid = generate_pattern(&config).unwrap();
        assert!(is_connected(&grid));
        let slots = extract_slots(&grid, 3);
        assert!(!slots.is_empty());
    }

    #[test]
    fn respects_symmetry_180() {
        let config = GenerationConfig {
            width: 7,
            height: 7,
            symmetry: SymmetryType::Rotational180,
            seed: Some(77),
            ..Default::default()
        };
        let grid = generate_pattern(&config).unwrap();
        for r in 0..7 {
            for c in 0..7 {
                assert_eq!(
                    grid.get(r, c).is_black(),
                    grid.get(6 - r, 6 - c).is_black(),
                    "symmetry violated at ({}, {})",
                    r,
                    c
                );
            }
        }
    }

    #[test]
    fn respects_mirror_symmetry() {
        let config = GenerationConfig {
            width: 7,
            height: 7,
            symmetry: SymmetryType::Mirror,
            seed: Some(55),
            ..Default::default()
        };
        let grid = generate_pattern(&config).unwrap();
        for r in 0..7 {
            for c in 0..7 {
                assert_eq!(
                    grid.get(r, c).is_black(),
                    grid.get(r, 6 - c).is_black(),
                    "mirror symmetry violated at ({}, {})",
                    r,
                    c
                );
            }
        }
    }

    #[test]
    fn no_short_words() {
        let config = GenerationConfig {
            width: 9,
            height: 9,
            min_word_length: 3,
            seed: Some(200),
            ..Default::default()
        };
        let grid = generate_pattern(&config).unwrap();
        assert!(has_valid_words(&grid, 3));
    }

    #[test]
    fn connectivity() {
        let mut g = Grid::new(5, 5);
        // split grid in half with a black column
        for r in 0..5 {
            g.set(r, 2, Cell::Black);
        }
        assert!(!is_connected(&g));
    }

    #[test]
    fn density_in_range() {
        let config = GenerationConfig {
            width: 11,
            height: 11,
            black_cell_density: 0.25,
            seed: Some(300),
            ..Default::default()
        };
        let grid = generate_pattern(&config).unwrap();
        let d = grid.density();
        // allow some tolerance since symmetry constraints mean we can't hit exact density
        assert!(d > 0.10 && d < 0.40, "density {} out of range", d);
    }
}
