use crate::config::SymmetryType;
use crate::grid::{Cell, Grid};
use crate::grid::slot::Direction;
use crate::puzzle::{Clue, PlacedWord, Puzzle};

pub fn from_puz(data: &[u8]) -> Result<Puzzle, String> {
    if data.len() < 52 {
        return Err("file too short for .puz format".into());
    }
    if &data[2..14] != b"ACROSS&DOWN\0" {
        return Err("missing ACROSS&DOWN magic".into());
    }

    let width = data[44] as usize;
    let height = data[45] as usize;
    let num_clues = u16::from_le_bytes([data[46], data[47]]) as usize;

    if width == 0 || height == 0 || width > 50 || height > 50 {
        return Err(format!("invalid grid size {}x{}", width, height));
    }

    let board_size = width * height;
    let solution_start = 52;
    let state_start = solution_start + board_size;
    let strings_start = state_start + board_size;

    if data.len() < strings_start {
        return Err("file truncated before grid data".into());
    }

    let solution = &data[solution_start..state_start];

    // build grid
    let mut grid = Grid::new(width, height);
    for i in 0..board_size {
        let r = i / width;
        let c = i % width;
        let b = solution[i];
        if b == b'.' {
            grid.set(r, c, Cell::Black);
        } else if b.is_ascii_alphabetic() {
            grid.set(r, c, Cell::Letter((b as char).to_ascii_uppercase()));
        } else {
            grid.set(r, c, Cell::Empty);
        }
    }

    // parse null-terminated strings after the state grid
    let string_data = &data[strings_start..];
    let strings = parse_null_strings(string_data);

    // strings[0] = title, strings[1] = author, strings[2] = copyright
    // strings[3..3+num_clues] = clue texts, rest = notes
    if strings.len() < 3 + num_clues {
        return Err(format!("expected {} clue strings, found {}", num_clues, strings.len().saturating_sub(3)));
    }

    let clue_strings: Vec<&str> = strings[3..3 + num_clues].iter().map(|s| s.as_str()).collect();

    // number the cells and assign clues
    let (clues_across, clues_down, words) = number_and_assign(
        &grid, width, height, &clue_strings,
    )?;

    Ok(Puzzle {
        grid,
        words,
        clues_across,
        clues_down,
        width,
        height,
        symmetry: SymmetryType::None,
        difficulty_score: 50,
        generation_time_ms: 0,
        theme: None,
        theme_entries: vec![],
        is_cryptic: false,
    })
}

fn parse_null_strings(data: &[u8]) -> Vec<String> {
    let mut result = Vec::new();
    let mut start = 0;
    for (i, &b) in data.iter().enumerate() {
        if b == 0 {
            let s = String::from_utf8_lossy(&data[start..i]).to_string();
            result.push(s);
            start = i + 1;
        }
    }
    result
}

fn number_and_assign(
    grid: &Grid,
    width: usize,
    height: usize,
    clue_strings: &[&str],
) -> Result<(Vec<Clue>, Vec<Clue>, Vec<PlacedWord>), String> {
    let mut clues_across = Vec::new();
    let mut clues_down = Vec::new();
    let mut words = Vec::new();
    let mut clue_idx = 0;
    let mut number = 1u32;

    for r in 0..height {
        for c in 0..width {
            if grid.get(r, c).is_black() {
                continue;
            }

            let starts_across = (c == 0 || grid.get(r, c.wrapping_sub(1)).is_black() || c.wrapping_sub(1) >= width)
                && c + 1 < width && !grid.get(r, c + 1).is_black();

            let starts_down = (r == 0 || grid.get(r.wrapping_sub(1), c).is_black() || r.wrapping_sub(1) >= height)
                && r + 1 < height && !grid.get(r + 1, c).is_black();

            if !starts_across && !starts_down {
                continue;
            }

            let num = number;
            number += 1;

            if starts_across {
                if clue_idx >= clue_strings.len() {
                    return Err("ran out of clue strings".into());
                }
                let answer = read_word(grid, r, c, width, height, Direction::Across);
                words.push(PlacedWord {
                    word: answer.clone(),
                    row: r,
                    col: c,
                    direction: Direction::Across,
                    clue_number: num,
                });
                clues_across.push(Clue {
                    number: num,
                    direction: Direction::Across,
                    text: clue_strings[clue_idx].to_string(),
                    answer,
                    definition: None,
                    wordplay_type: None,
                });
                clue_idx += 1;
            }

            if starts_down {
                if clue_idx >= clue_strings.len() {
                    return Err("ran out of clue strings".into());
                }
                let answer = read_word(grid, r, c, width, height, Direction::Down);
                words.push(PlacedWord {
                    word: answer.clone(),
                    row: r,
                    col: c,
                    direction: Direction::Down,
                    clue_number: num,
                });
                clues_down.push(Clue {
                    number: num,
                    direction: Direction::Down,
                    text: clue_strings[clue_idx].to_string(),
                    answer,
                    definition: None,
                    wordplay_type: None,
                });
                clue_idx += 1;
            }
        }
    }

    Ok((clues_across, clues_down, words))
}

fn read_word(grid: &Grid, row: usize, col: usize, width: usize, height: usize, dir: Direction) -> String {
    let mut word = String::new();
    let (mut r, mut c) = (row, col);
    loop {
        if r >= height || c >= width { break; }
        match grid.get(r, c) {
            Cell::Letter(ch) => word.push(ch),
            Cell::Empty => word.push('_'),
            Cell::Black => break,
        }
        match dir {
            Direction::Across => c += 1,
            Direction::Down => r += 1,
        }
    }
    word
}
