pub mod seed;

use std::collections::HashMap;
use std::path::Path;

use rusqlite::Connection;
use serde::{Deserialize, Serialize};

use crate::error::Result;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClueEntry {
    pub text: String,
    pub difficulty: u8,
}

pub struct ClueDatabase {
    json_clues: HashMap<String, Vec<ClueEntry>>,
    db: Option<std::sync::Mutex<Connection>>,
}

impl ClueDatabase {
    pub fn empty() -> Self {
        Self {
            json_clues: HashMap::new(),
            db: None,
        }
    }

    pub fn load_json(path: &Path) -> Result<Self> {
        let content = std::fs::read_to_string(path)?;
        let raw: HashMap<String, Vec<ClueEntry>> = serde_json::from_str(&content)?;
        let json_clues = raw
            .into_iter()
            .map(|(k, v)| (k.to_uppercase(), v))
            .collect();
        Ok(Self {
            json_clues,
            db: None,
        })
    }

    pub fn load_sqlite(db_path: &Path) -> Result<Self> {
        let db = Connection::open(db_path)
            .map_err(|e| crate::error::CrucivError::Dictionary(e.to_string()))?;
        Ok(Self {
            json_clues: HashMap::new(),
            db: Some(std::sync::Mutex::new(db)),
        })
    }

    pub fn load(data_dir: &Path) -> Result<Self> {
        let db_path = data_dir.join("clues.db");
        let json_path = data_dir.join("clues.json");

        if db_path.exists() {
            Self::load_sqlite(&db_path)
        } else if json_path.exists() {
            Self::load_json(&json_path)
        } else {
            Ok(Self::empty())
        }
    }

    pub fn get_clue(&self, word: &str, difficulty: u8) -> Result<String> {
        let upper = word.to_uppercase();

        // try sqlite with difficulty matching
        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            if let Some(clue) = query_sqlite(&conn, &upper, difficulty) {
                return Ok(clue);
            }
        }

        // fall back to json
        if let Some(entries) = self.json_clues.get(&upper) {
            if let Some(entry) = entries.first() {
                return Ok(entry.text.clone());
            }
        }

        // last resort placeholder so unclued words still render as fill-in-the-blanks
        Ok(format!("___ ({} letter word)", word.len()))
    }

    /// Get word difficulty scores for dictionary boosting.
    /// Returns a map of word -> difficulty score (1.0=easy to 6.0=hard).
    pub fn word_difficulties(&self) -> std::collections::HashMap<String, f64> {
        let mut result = std::collections::HashMap::new();
        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            if let Ok(mut stmt) = conn.prepare("SELECT word, score FROM word_difficulty") {
                if let Ok(rows) = stmt.query_map([], |row| {
                    Ok((row.get::<_, String>(0)?, row.get::<_, f64>(1)?))
                }) {
                    for r in rows.flatten() {
                        result.insert(r.0, r.1);
                    }
                }
            };
        }
        result
    }

    pub fn word_count(&self) -> usize {
        let json_count = self.json_clues.len();
        let db_count = self
            .db
            .as_ref()
            .map(|db| {
                let conn = db.lock().unwrap();
                conn.query_row("SELECT COUNT(DISTINCT word) FROM clues", [], |row| {
                    row.get::<_, usize>(0)
                })
                .unwrap_or(0)
            })
            .unwrap_or(0);
        json_count + db_count
    }

    pub fn all_words(&self) -> std::collections::HashSet<String> {
        let mut words: std::collections::HashSet<String> = self
            .json_clues
            .keys()
            .filter(|w| w.chars().all(|c| c.is_alphabetic()))
            .cloned()
            .collect();
        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            if let Ok(mut stmt) = conn.prepare("SELECT DISTINCT word FROM clues") {
                if let Ok(rows) = stmt.query_map([], |row| row.get::<_, String>(0)) {
                    for w in rows.flatten() {
                        if w.chars().all(|c| c.is_alphabetic()) {
                            words.insert(w);
                        }
                    }
                }
            };
        }
        words
    }

    /// Words that have at least one non-cryptic clue.
    /// Use this for dictionary filtering to avoid placing words
    /// that can't actually be clued in normal puzzles.
    pub fn clueable_words(&self) -> std::collections::HashSet<String> {
        let mut words: std::collections::HashSet<String> = self
            .json_clues
            .keys()
            .filter(|w| w.chars().all(|c| c.is_alphabetic()))
            .cloned()
            .collect();
        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            if let Ok(mut stmt) =
                conn.prepare("SELECT DISTINCT word FROM clues WHERE source != 'cryptic'")
            {
                if let Ok(rows) = stmt.query_map([], |row| row.get::<_, String>(0)) {
                    for w in rows.flatten() {
                        if w.chars().all(|c| c.is_alphabetic()) {
                            words.insert(w);
                        }
                    }
                }
            };
        }
        words
    }

    pub fn suggestions(&self, word: &str, limit: usize) -> Vec<String> {
        let upper = word.to_uppercase();
        let mut result = Vec::new();
        if let Some(entries) = self.json_clues.get(&upper) {
            for e in entries.iter().take(limit) {
                result.push(e.text.clone());
            }
        }
        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            if let Ok(mut stmt) =
                conn.prepare("SELECT DISTINCT clue FROM clues WHERE word = ?1 LIMIT ?2")
            {
                if let Ok(rows) = stmt.query_map(
                    rusqlite::params![upper, (limit - result.len()) as i64],
                    |row| row.get::<_, String>(0),
                ) {
                    for r in rows.flatten() {
                        if !result.contains(&r) {
                            result.push(r);
                        }
                    }
                }
            };
        }
        result.truncate(limit);
        result
    }

    pub fn lookup(&self, word: &str, difficulty: u8) -> Option<ClueEntry> {
        let upper = word.to_uppercase();

        if let Some(entries) = self.json_clues.get(&upper) {
            if let Some(entry) = entries.iter().find(|e| e.difficulty == difficulty) {
                return Some(entry.clone());
            }
            if let Some(entry) = entries.first() {
                return Some(entry.clone());
            }
        }

        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            if let Some(clue) = query_sqlite(&conn, &upper, difficulty) {
                return Some(ClueEntry {
                    text: clue,
                    difficulty,
                });
            }
        }

        None
    }

    pub fn db_ref(&self) -> Option<std::sync::MutexGuard<'_, Connection>> {
        self.db.as_ref().map(|m| m.lock().unwrap())
    }

    pub fn find_word_for_clue(&self, clue_text: &str) -> Option<String> {
        for (word, entries) in &self.json_clues {
            for e in entries {
                if e.text == clue_text {
                    return Some(word.clone());
                }
            }
        }
        None
    }

    pub fn random_clues(&self, count: usize) -> Vec<(String, String)> {
        let mut result = Vec::new();
        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            // pick random word+clue pairs, prefer common words (3-8 letters)
            if let Ok(mut stmt) = conn.prepare(
                "SELECT word, clue FROM clues WHERE length(word) BETWEEN 3 AND 8 ORDER BY RANDOM() LIMIT ?1"
            ) {
                if let Ok(rows) = stmt.query_map(rusqlite::params![count as i64], |row| {
                    Ok((row.get::<_, String>(1)?, row.get::<_, String>(0)?))
                }) {
                    for r in rows.flatten() {
                        result.push(r);
                    }
                }
            };
        }
        if result.is_empty() {
            // fallback to json clues
            for (word, entries) in self.json_clues.iter().take(count) {
                if let Some(e) = entries.first() {
                    result.push((e.text.clone(), word.clone()));
                }
            }
        }
        result
    }

    pub fn random_obscure_clues(&self, count: usize) -> Vec<(String, String)> {
        let mut result = Vec::new();
        if let Some(db) = &self.db {
            let conn = db.lock().unwrap();
            if let Ok(mut stmt) = conn.prepare(
                "SELECT word, clue FROM clues WHERE length(word) >= 7 AND source != 'cryptic' ORDER BY RANDOM() LIMIT ?1"
            ) {
                if let Ok(rows) = stmt.query_map(rusqlite::params![count as i64], |row| {
                    Ok((row.get::<_, String>(1)?, row.get::<_, String>(0)?))
                }) {
                    for r in rows.flatten() {
                        result.push(r);
                    }
                }
            };
        }
        if result.is_empty() {
            for (word, entries) in self
                .json_clues
                .iter()
                .filter(|(w, _)| w.len() >= 7)
                .take(count)
            {
                if let Some(e) = entries.first() {
                    result.push((e.text.clone(), word.clone()));
                }
            }
        }
        if result.is_empty() {
            return self.random_clues(count);
        }
        result
    }

    /// Returns (clue_text, real_answer, vec_of_fake_clues)
    pub fn random_clues_with_fakes(
        &self,
        count: usize,
        fakes: usize,
    ) -> Vec<(String, String, Vec<String>)> {
        let mut result = Vec::new();
        let clues = self.random_clues(count + count * fakes);
        if clues.len() < count + fakes {
            // not enough data, just return what we have with empty fakes
            for (clue, word) in clues.into_iter().take(count) {
                result.push((clue, word, Vec::new()));
            }
            return result;
        }
        let main: Vec<_> = clues[..count].to_vec();
        let extra: Vec<_> = clues[count..].to_vec();
        for (i, (clue, word)) in main.into_iter().enumerate() {
            let fake_clues: Vec<String> = extra
                .iter()
                .filter(|(_, w)| w != &word)
                .skip(i * fakes)
                .take(fakes)
                .map(|(c, _)| c.clone())
                .collect();
            result.push((clue, word, fake_clues));
        }
        result
    }
}

// cryptic clue database (separate schema: answer/clue/definition)
/// Cryptic clue access - reads from the same clues.db using source='cryptic'
pub struct CrypticClueDb {
    db: std::sync::Mutex<Connection>,
}

impl CrypticClueDb {
    pub fn load(path: &Path) -> Result<Self> {
        let db = Connection::open(path)
            .map_err(|e| crate::error::CrucivError::Dictionary(e.to_string()))?;
        Ok(Self {
            db: std::sync::Mutex::new(db),
        })
    }

    pub fn get_clue(&self, word: &str, _difficulty: u8) -> Result<String> {
        let upper = word.to_uppercase();
        let conn = self.db.lock().unwrap();
        conn.query_row(
            "SELECT clue FROM clues WHERE word = ?1 AND source = 'cryptic' ORDER BY RANDOM() LIMIT 1",
            [&upper],
            |row| row.get::<_, String>(0),
        ).map_err(|_| crate::error::CrucivError::MissingClue(word.to_string()))
    }

    pub fn get_clue_with_meta(
        &self,
        word: &str,
    ) -> Option<(String, Option<String>, Option<String>)> {
        let upper = word.to_uppercase();
        let conn = self.db.lock().unwrap();
        conn.query_row(
            "SELECT clue, definition, wordplay_type FROM clues WHERE word = ?1 AND source = 'cryptic' ORDER BY RANDOM() LIMIT 1",
            [&upper],
            |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?, row.get::<_, Option<String>>(2)?)),
        ).ok()
    }

    pub fn suggestions(&self, word: &str, limit: usize) -> Vec<String> {
        let upper = word.to_uppercase();
        let mut result = Vec::new();
        let conn = self.db.lock().unwrap();
        if let Ok(mut stmt) = conn.prepare(
            "SELECT DISTINCT clue FROM clues WHERE word = ?1 AND source = 'cryptic' LIMIT ?2",
        ) {
            if let Ok(rows) = stmt.query_map(rusqlite::params![upper, limit as i64], |row| {
                row.get::<_, String>(0)
            }) {
                for r in rows.flatten() {
                    result.push(r);
                }
            }
        }
        result
    }

    pub fn word_count(&self) -> usize {
        let conn = self.db.lock().unwrap();
        conn.query_row(
            "SELECT COUNT(DISTINCT word) FROM clues WHERE source = 'cryptic'",
            [],
            |row| row.get::<_, usize>(0),
        )
        .unwrap_or(0)
    }

    pub fn all_words(&self) -> std::collections::HashSet<String> {
        let mut words = std::collections::HashSet::new();
        let conn = self.db.lock().unwrap();
        if let Ok(mut stmt) = conn.prepare(
            "SELECT DISTINCT word FROM clues WHERE source = 'cryptic' AND LENGTH(word) >= 3",
        ) {
            if let Ok(rows) = stmt.query_map([], |row| row.get::<_, String>(0)) {
                for w in rows.flatten() {
                    if w.chars().all(|c| c.is_alphabetic()) {
                        words.insert(w);
                    }
                }
            }
        }
        words
    }
}

fn query_sqlite(db: &Connection, word: &str, difficulty: u8) -> Option<String> {
    if difficulty <= 1 {
        // very easy / easy: pick shortest straightforward non-cryptic clue
        // exclude fill-in-blanks (___), abbreviation markers, and foreign clues
        let easy_filter = "AND source != 'cryptic' AND clue NOT LIKE '%\\_\\_\\_%' ESCAPE '\\' AND clue NOT LIKE '%bbr%' AND clue NOT LIKE '%org.%' AND clue NOT LIKE '%Var.%' AND clue NOT LIKE '%: %' AND clue NOT LIKE '%Sp.%' AND clue NOT LIKE '%Fr.%' AND clue NOT LIKE '%Lat.%' AND clue NOT LIKE '%Ger.%'";
        return db.query_row(
            &format!("SELECT clue FROM clues WHERE word = ?1 AND difficulty <= ?2 {} ORDER BY LENGTH(clue) ASC LIMIT 1", easy_filter),
            rusqlite::params![word, difficulty],
            |row| row.get::<_, String>(0),
        ).ok().or_else(|| {
            // widen to medium, still filtering junk
            db.query_row(
                &format!("SELECT clue FROM clues WHERE word = ?1 AND difficulty <= 2 {} ORDER BY LENGTH(clue) ASC LIMIT 1", easy_filter),
                [word],
                |row| row.get::<_, String>(0),
            ).ok()
        }).or_else(|| {
            // any non-cryptic clue, prefer easier
            db.query_row(
                "SELECT clue FROM clues WHERE word = ?1 AND source != 'cryptic' ORDER BY difficulty ASC, LENGTH(clue) ASC LIMIT 1",
                [word],
                |row| row.get::<_, String>(0),
            ).ok()
        });
    }

    // medium/hard: random non-cryptic clue at matching difficulty, fall back to closest
    match db.query_row(
        "SELECT clue FROM clues WHERE word = ?1 AND difficulty = ?2 AND source != 'cryptic' ORDER BY RANDOM() LIMIT 1",
        rusqlite::params![word, difficulty],
        |row| row.get::<_, String>(0),
    ) {
        Ok(clue) => Some(clue),
        Err(rusqlite::Error::QueryReturnedNoRows) => {
            match db.query_row(
                "SELECT clue FROM clues WHERE word = ?1 AND source != 'cryptic' ORDER BY ABS(difficulty - ?2), RANDOM() LIMIT 1",
                rusqlite::params![word, difficulty],
                |row| row.get::<_, String>(0),
            ) {
                Ok(clue) => Some(clue),
                Err(rusqlite::Error::QueryReturnedNoRows) => None,
                Err(e) => {
                    tracing::warn!("clue query error for {}: {}", word, e);
                    None
                }
            }
        }
        Err(e) => {
            tracing::warn!("clue query error for {}: {}", word, e);
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_db() -> ClueDatabase {
        let mut clues = HashMap::new();
        clues.insert(
            "CAT".to_string(),
            vec![
                ClueEntry {
                    text: "A furry pet".into(),
                    difficulty: 1,
                },
                ClueEntry {
                    text: "Feline friend".into(),
                    difficulty: 2,
                },
                ClueEntry {
                    text: "Felix, for one".into(),
                    difficulty: 3,
                },
            ],
        );
        clues.insert(
            "DOG".to_string(),
            vec![ClueEntry {
                text: "Man's best friend".into(),
                difficulty: 1,
            }],
        );
        ClueDatabase {
            json_clues: clues,
            db: None,
        }
    }

    #[test]
    fn exact_difficulty_match() {
        let db = test_db();
        let clue = db.lookup("CAT", 2).unwrap();
        assert_eq!(clue.text, "Feline friend");
    }

    #[test]
    fn fallback_to_adjacent() {
        let db = test_db();
        let clue = db.lookup("DOG", 2).unwrap();
        assert_eq!(clue.text, "Man's best friend");
    }

    #[test]
    fn missing_word_gets_fallback() {
        let db = test_db();
        let clue = db.get_clue("XYZ", 1).unwrap();
        assert!(clue.contains("letter word"));
    }
}
