use std::path::PathBuf;
use std::time::Instant;

use clap::{Parser, ValueEnum};

use cruciverb_core::clue::seed::load_clue_database;
use cruciverb_core::config::{DifficultyPreset, GenerationConfig, SymmetryType};
use cruciverb_core::dict::Dictionary;
use cruciverb_core::puzzle::generate_full_puzzle;
use cruciverb_core::validate::validate_puzzle;

#[derive(Parser)]
#[command(name = "cruciverb", about = "Crossword puzzle generator")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(clap::Subcommand)]
enum Commands {
    Generate(GenerateArgs),
    ImportClues(ImportCluesArgs),
}

#[derive(Parser)]
struct ImportCluesArgs {
    #[arg(help = "Path to CSV file (columns: Date,Word,Clue)")]
    csv: PathBuf,

    #[arg(short, long, default_value = "../data/clues.db")]
    output: PathBuf,
}

#[derive(Parser)]
struct GenerateArgs {
    #[arg(short, long, default_value = "15")]
    size: usize,

    #[arg(short, long, value_enum)]
    difficulty: Option<CliDifficulty>,

    #[arg(long, value_enum, default_value = "rotational180")]
    symmetry: CliSymmetry,

    #[arg(short, long)]
    output: Option<PathBuf>,

    #[arg(long)]
    seed: Option<u64>,

    #[arg(long, default_value = "0.22")]
    density: f64,

    #[arg(long, default_value = "../data/wordlists/enable.txt")]
    wordlist: PathBuf,

    #[arg(long)]
    blocklist: Option<PathBuf>,

    #[arg(long, default_value = "../data")]
    data_dir: PathBuf,
}

#[derive(Clone, ValueEnum)]
enum CliDifficulty {
    Glyph,
    Rune,
    Scroll,
    Codex,
    Grimoire,
}

#[derive(Clone, ValueEnum)]
enum CliSymmetry {
    Rotational180,
    Rotational90,
    Diagonal,
    Mirror,
    None,
}

impl From<CliSymmetry> for SymmetryType {
    fn from(s: CliSymmetry) -> Self {
        match s {
            CliSymmetry::Rotational180 => SymmetryType::Rotational180,
            CliSymmetry::Rotational90 => SymmetryType::Rotational90,
            CliSymmetry::Diagonal => SymmetryType::Diagonal,
            CliSymmetry::Mirror => SymmetryType::Mirror,
            CliSymmetry::None => SymmetryType::None,
        }
    }
}

fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    let cli = Cli::parse();

    match cli.command {
        Commands::Generate(args) => generate(args),
        Commands::ImportClues(args) => import_clues(args),
    }
}

fn generate(args: GenerateArgs) {
    let start = Instant::now();

    let config = match &args.difficulty {
        Some(d) => {
            let preset = match d {
                CliDifficulty::Glyph => DifficultyPreset::Glyph,
                CliDifficulty::Rune => DifficultyPreset::Rune,
                CliDifficulty::Scroll => DifficultyPreset::Scroll,
                CliDifficulty::Codex => DifficultyPreset::Codex,
                CliDifficulty::Grimoire => DifficultyPreset::Grimoire,
            };
            let mut c = GenerationConfig::from_preset(preset);
            c.seed = args.seed;
            c.symmetry = args.symmetry.into();
            c
        }
        None => GenerationConfig {
            width: args.size,
            height: args.size,
            symmetry: args.symmetry.into(),
            black_cell_density: args.density,
            seed: args.seed,
            rand_top_base: GenerationConfig::rand_top_for_size(args.size),
            ..Default::default()
        },
    };

    let difficulty: u8 = match &args.difficulty {
        Some(CliDifficulty::Glyph) => 0,
        Some(CliDifficulty::Rune) => 1,
        Some(CliDifficulty::Scroll) => 2,
        Some(CliDifficulty::Codex) => 3,
        Some(CliDifficulty::Grimoire) => 3,
        None => 1,
    };

    eprintln!("loading dictionary...");
    let mut dict = Dictionary::load(
        &args.wordlist,
        args.blocklist.as_deref(),
        config.min_word_length,
    )
    .unwrap_or_else(|e| {
        eprintln!("failed to load wordlist: {}", e);
        std::process::exit(1);
    });
    eprintln!("loaded {} words", dict.word_count());

    let clue_db = load_clue_database(&args.data_dir).unwrap_or_else(|e| {
        eprintln!("warning: failed to load clue database: {}", e);
        cruciverb_core::clue::ClueDatabase::empty()
    });
    eprintln!("loaded {} clue entries", clue_db.word_count());

    let clue_words = clue_db.all_words();
    eprintln!("boosting {} words with clue coverage", clue_words.len());
    dict.boost_from_clue_words(&clue_words);

    eprintln!("generating {}x{} puzzle...", config.width, config.height);
    let puzzle = generate_full_puzzle(&config, &dict, &clue_db, difficulty).unwrap_or_else(|e| {
        eprintln!("generation failed: {}", e);
        std::process::exit(1);
    });

    eprintln!("solved in {:.2}s", start.elapsed().as_secs_f64());

    let validation = validate_puzzle(
        &puzzle.grid,
        &puzzle.words,
        &dict,
        config.min_word_length,
        config.obscurity_budget,
    );
    if !validation.is_valid() {
        eprintln!("validation warnings:");
        for err in &validation.errors {
            eprintln!("  - {}", err);
        }
    }

    let json = puzzle.to_json().expect("failed to serialize puzzle");

    match &args.output {
        Some(path) => {
            std::fs::write(path, &json).unwrap_or_else(|e| {
                eprintln!("failed to write output: {}", e);
                std::process::exit(1);
            });
            eprintln!("puzzle written to {}", path.display());
        }
        None => println!("{}", json),
    }

    eprintln!(
        "total time: {:.2}s | {} words placed | {}x{} grid",
        start.elapsed().as_secs_f64(),
        puzzle.words.len(),
        config.width,
        config.height,
    );
}

fn import_clues(args: ImportCluesArgs) {
    use rusqlite::Connection;
    use std::collections::HashMap;
    use std::io::{BufRead, BufReader};

    eprintln!("importing clues from {}...", args.csv.display());

    let db = Connection::open(&args.output).unwrap_or_else(|e| {
        eprintln!("failed to open database: {}", e);
        std::process::exit(1);
    });

    db.execute_batch(
        "
        DROP TABLE IF EXISTS clues;
        DROP TABLE IF EXISTS word_difficulty;
        CREATE TABLE clues (
            id INTEGER PRIMARY KEY,
            word TEXT NOT NULL,
            clue TEXT NOT NULL,
            difficulty INTEGER NOT NULL DEFAULT 1
        );
        CREATE TABLE word_difficulty (
            word TEXT PRIMARY KEY,
            score REAL NOT NULL
        );
        CREATE INDEX idx_clues_word ON clues(word);
        CREATE INDEX idx_clues_diff ON clues(word, difficulty);
    ",
    )
    .unwrap();

    let file = std::fs::File::open(&args.csv).unwrap_or_else(|e| {
        eprintln!("failed to open CSV: {}", e);
        std::process::exit(1);
    });
    let reader = BufReader::new(file);

    // day-of-week difficulty: Mon=1 (easy) through Sat=6 (hard), Sun=3 (medium-large)
    // map to 3 tiers: easy (1-2), medium (3-4), hard (5-6)
    fn day_of_week(date_str: &str) -> Option<u32> {
        let parts: Vec<&str> = date_str.split('/').collect();
        if parts.len() != 3 {
            return None;
        }
        let m: i32 = parts[0].parse().ok()?;
        let d: i32 = parts[1].parse().ok()?;
        let y: i32 = parts[2].parse().ok()?;
        let (y, m) = if m <= 2 { (y - 1, m + 12) } else { (y, m) };
        let dow = ((d + 13 * (m + 1) / 5 + y + y / 4 - y / 100 + y / 400) % 7 + 7) % 7;
        let weight = match dow {
            2 => 1, // Monday
            3 => 2, // Tuesday
            4 => 3, // Wednesday
            5 => 4, // Thursday
            6 => 5, // Friday
            0 => 6, // Saturday
            1 => 3, // Sunday (medium)
            _ => 3,
        };
        Some(weight)
    }

    fn weight_to_tier(w: u32) -> u8 {
        if w <= 2 {
            1
        } else if w <= 4 {
            2
        } else {
            3
        }
    }

    db.execute_batch("BEGIN").unwrap();

    let mut count = 0u64;
    let mut word_day_scores: HashMap<String, Vec<u32>> = HashMap::new();
    let mut stmt = db
        .prepare("INSERT INTO clues (word, clue, difficulty) VALUES (?1, ?2, ?3)")
        .unwrap();

    for (i, line) in reader.lines().enumerate() {
        let line = match line {
            Ok(l) => l,
            Err(_) => continue,
        };
        if i == 0 && line.starts_with("Date") {
            continue;
        }

        let fields = parse_csv_line(&line);
        let Some((date, word, clue)) = fields else {
            continue;
        };

        if word.is_empty() || !word.bytes().all(|b| b.is_ascii_alphabetic()) {
            continue;
        }

        let upper = word.to_uppercase();
        let day_score = day_of_week(&date).unwrap_or(3);
        let tier = weight_to_tier(day_score);

        stmt.execute(rusqlite::params![upper, clue, tier]).unwrap();
        word_day_scores.entry(upper).or_default().push(day_score);
        count += 1;

        if count % 100_000 == 0 {
            eprintln!("  {} clues imported...", count);
        }
    }

    // compute per-word difficulty scores
    let mut diff_stmt = db
        .prepare("INSERT INTO word_difficulty (word, score) VALUES (?1, ?2)")
        .unwrap();
    for (word, scores) in &word_day_scores {
        let avg: f64 = scores.iter().map(|&s| s as f64).sum::<f64>() / scores.len() as f64;
        diff_stmt.execute(rusqlite::params![word, avg]).unwrap();
    }

    db.execute_batch("COMMIT").unwrap();
    eprintln!(
        "imported {} clues for {} words to {}",
        count,
        word_day_scores.len(),
        args.output.display()
    );
}

fn parse_csv_line(line: &str) -> Option<(String, String, String)> {
    let chars = line.chars();
    let mut fields: Vec<String> = Vec::new();
    let mut field = String::new();
    let mut in_quotes = false;

    for ch in chars {
        match ch {
            '"' => in_quotes = !in_quotes,
            ',' if !in_quotes => {
                fields.push(field.clone());
                field.clear();
            }
            _ => field.push(ch),
        }
    }
    fields.push(field);

    if fields.len() >= 3 {
        Some((
            fields[0].trim().to_string(),
            fields[1].trim().to_string(),
            fields[2..].join(",").trim().to_string(),
        ))
    } else {
        None
    }
}
