use std::collections::HashSet;

use crate::dict::Dictionary;
use crate::grid::Grid;
use crate::grid::slot::extract_slots;
use crate::puzzle::PlacedWord;

pub struct ValidationResult {
    pub errors: Vec<String>,
}

impl ValidationResult {
    pub fn is_valid(&self) -> bool {
        self.errors.is_empty()
    }
}

pub fn validate_puzzle(
    grid: &Grid,
    words: &[PlacedWord],
    dict: &Dictionary,
    min_word_length: usize,
    obscurity_budget: f64,
) -> ValidationResult {
    let mut errors = Vec::new();

    check_min_word_length(&mut errors, words, min_word_length);
    check_no_duplicates(&mut errors, words);
    check_no_orphan_letters(&mut errors, grid, min_word_length);
    check_obscurity(&mut errors, words, dict, obscurity_budget);

    ValidationResult { errors }
}

fn check_min_word_length(errors: &mut Vec<String>, words: &[PlacedWord], min_len: usize) {
    for w in words {
        if w.word.len() < min_len {
            errors.push(format!(
                "word '{}' is {} letters, minimum is {}",
                w.word,
                w.word.len(),
                min_len
            ));
        }
    }
}

fn check_no_duplicates(errors: &mut Vec<String>, words: &[PlacedWord]) {
    let mut seen = HashSet::new();
    for w in words {
        if !seen.insert(&w.word) {
            errors.push(format!("duplicate word: '{}'", w.word));
        }
    }
}

fn check_no_orphan_letters(errors: &mut Vec<String>, grid: &Grid, min_len: usize) {
    let slots = extract_slots(grid, min_len);

    // build set of all cells covered by slots
    let mut covered: HashSet<(usize, usize)> = HashSet::new();
    for slot in &slots {
        for cell in slot.cells() {
            covered.insert(cell);
        }
    }

    // every white cell should be covered by at least one slot
    for r in 0..grid.height {
        for c in 0..grid.width {
            if grid.get(r, c).is_white() && !covered.contains(&(r, c)) {
                errors.push(format!(
                    "orphan letter at ({}, {}) - not part of any word",
                    r, c
                ));
            }
        }
    }
}

fn check_obscurity(
    errors: &mut Vec<String>,
    words: &[PlacedWord],
    dict: &Dictionary,
    budget: f64,
) {
    if words.is_empty() {
        return;
    }

    let obscure_count = words
        .iter()
        .filter(|w| {
            // look up frequency in dictionary
            let freq = dict
                .words
                .iter()
                .find(|dw| dw.text == w.word)
                .map(|dw| dw.frequency)
                .unwrap_or(0.0);
            freq < 0.3
        })
        .count();

    let ratio = obscure_count as f64 / words.len() as f64;
    if ratio > budget {
        errors.push(format!(
            "obscurity budget exceeded: {:.0}% obscure words (max {:.0}%)",
            ratio * 100.0,
            budget * 100.0
        ));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dict::index::WordIndex;
    use crate::dict::word::Word;
    use crate::grid::Cell;
    use crate::grid::slot::Direction;

    fn dummy_dict() -> Dictionary {
        let words = vec![
            Word::new("CAT".into(), 0.8),
            Word::new("DOG".into(), 0.8),
            Word::new("BAT".into(), 0.7),
            Word::new("HAT".into(), 0.7),
            Word::new("RAT".into(), 0.7),
            Word::new("XYZQ".into(), 0.1),
        ];
        let index = WordIndex::build(&words);
        Dictionary { words, index }
    }

    fn placed(word: &str, row: usize, col: usize, dir: Direction) -> PlacedWord {
        PlacedWord {
            word: word.to_string(),
            row,
            col,
            direction: dir,
            clue_number: 1,
        }
    }

    #[test]
    fn valid_puzzle_passes() {
        let dict = dummy_dict();
        let words = vec![
            placed("CAT", 0, 0, Direction::Across),
            placed("DOG", 1, 0, Direction::Across),
        ];
        let grid = Grid::new(3, 2);
        let result = validate_puzzle(&grid, &words, &dict, 3, 0.2);
        assert!(result.is_valid());
    }

    #[test]
    fn catches_short_words() {
        let dict = dummy_dict();
        let words = vec![placed("AB", 0, 0, Direction::Across)];
        let grid = Grid::new(2, 1);
        let result = validate_puzzle(&grid, &words, &dict, 3, 0.2);
        assert!(!result.is_valid());
        assert!(result.errors[0].contains("minimum is 3"));
    }

    #[test]
    fn catches_duplicates() {
        let dict = dummy_dict();
        let words = vec![
            placed("CAT", 0, 0, Direction::Across),
            placed("CAT", 1, 0, Direction::Across),
        ];
        let grid = Grid::new(3, 2);
        let result = validate_puzzle(&grid, &words, &dict, 3, 0.2);
        assert!(!result.is_valid());
        assert!(result.errors[0].contains("duplicate"));
    }

    #[test]
    fn catches_orphan_letters() {
        let mut grid = Grid::new(3, 3);
        // create a grid where position (1,1) is white but isolated by blacks around it
        // but the runs through it are too short
        grid.set(0, 1, Cell::Black);
        grid.set(1, 0, Cell::Black);
        grid.set(1, 2, Cell::Black);
        grid.set(2, 1, Cell::Black);
        // (1,1) is white but has no run >= 3 in any direction

        let dict = dummy_dict();
        let result = validate_puzzle(&grid, &[], &dict, 3, 0.2);
        assert!(!result.is_valid());
        assert!(result.errors.iter().any(|e| e.contains("orphan")));
    }
}
