use std::{fs, path::PathBuf};

use clap::Parser;
use lib::{BlockPalette, OutlineMode, TextBuilder, TtfFont};

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// The text you want to generate
    #[arg(short, long, required_unless_present = "list_palettes")]
    text: Option<String>,

    /// Path to the .ttf font file
    #[arg(short, long, required_unless_present = "list_palettes")]
    font: Option<PathBuf>,

    /// How many blocks deep the text should be
    #[arg(short, long, default_value_t = 1)]
    depth: u32,

    /// Name of a built-in palette preset (from the palettes/ directory)
    #[arg(short, long, conflicts_with = "palette_file")]
    palette: Option<String>,

    /// Path to a JSON palette file
    #[arg(long)]
    palette_file: Option<PathBuf>,

    /// List available palette presets and exit
    #[arg(long)]
    list_palettes: bool,

    /// Output file path (without extension)
    #[arg(short, long, default_value = "output")]
    out: PathBuf,

    /// Output format(s): mcfunction, litematica, all (overrides others), or repeatable values
    #[arg(long, value_name = "FORMAT")]
    format: Vec<String>,

    /// Height of the text in blocks
    #[arg(long, default_value_t = 16.0)]
    size: f32,

    /// Generate a drop shadow
    #[arg(long)]
    shadow: bool,

    /// Shadow X offset in blocks (positive = right)
    #[arg(long, default_value_t = 1)]
    shadow_x: i32,

    /// Shadow Y offset in blocks (negative = down)
    #[arg(long, default_value_t = -1)]
    shadow_y: i32,

    /// Outline connectivity: 4 (cardinal only) or 8 (includes corners)
    #[arg(long, default_value_t = 8, value_parser = clap::value_parser!(u8).range(4..=8))]
    outline_mode: u8,

    /// Extra blocks between each character (overrides font default)
    #[arg(long)]
    letter_spacing: Option<u32>,

    /// Width in blocks inserted for a space character
    #[arg(long, default_value_t = 4)]
    word_spacing: u32,

    /// Cell size in pixels for PNG export (8-64)
    #[arg(long, default_value_t = 16, value_parser = clap::value_parser!(u32).range(8..=64))]
    cell_size: u32,
}

fn palettes_dir() -> PathBuf {
    std::env::current_exe()
        .unwrap()
        .parent()
        .unwrap()
        .join("palettes")
}

fn load_palette_by_name(name: &str) -> anyhow::Result<BlockPalette> {
    let path = palettes_dir().join(format!("{}.json", name));
    let json = fs::read_to_string(&path)
        .map_err(|_| anyhow::anyhow!("palette '{}' not found in {:?}", name, palettes_dir()))?;
    Ok(serde_json::from_str(&json)?)
}

pub fn run() -> anyhow::Result<()> {
    let cli = Cli::parse();

    if cli.list_palettes {
        let dir = palettes_dir();
        let entries = fs::read_dir(&dir)
            .map_err(|_| anyhow::anyhow!("palettes directory not found at {:?}", dir))?;
        println!("Available palettes:");
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) == Some("json") {
                if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
                    println!("  {}", stem);
                }
            }
        }
        return Ok(());
    }

    let text = cli.text.unwrap();
    let font_path = cli.font.unwrap();

    let palette = if let Some(path) = cli.palette_file {
        let json = fs::read_to_string(&path)
            .map_err(|e| anyhow::anyhow!("failed to read palette file {:?}: {}", path, e))?;
        serde_json::from_str(&json)?
    } else {
        let name = cli.palette.as_deref().unwrap_or("default");
        load_palette_by_name(name)?
    };

    tracing::info!("using palette: {}", palette.name);
    tracing::info!("loading font from: {:?}", font_path);
    let font_bytes = fs::read(&font_path)?;

    let font = TtfFont::from_bytes(&font_bytes, cli.size).map_err(|e| anyhow::anyhow!(e))?;

    tracing::info!("generating voxel grid for text: '{}'", text);
    let outline_mode = if cli.outline_mode == 4 {
        OutlineMode::FourConnected
    } else {
        OutlineMode::EightConnected
    };
    let mut builder = TextBuilder::new(&font)
        .with_depth(cli.depth)
        .with_shadow(cli.shadow)
        .with_shadow_offset(cli.shadow_x, cli.shadow_y)
        .with_outline_mode(outline_mode)
        .with_word_spacing(cli.word_spacing);
    if let Some(ls) = cli.letter_spacing {
        builder = builder.with_letter_spacing(ls);
    }
    let grid = builder.generate(&text);

    tracing::info!(
        "grid generated: {}x{}x{}",
        grid.width,
        grid.height,
        grid.depth
    );

    let all_names = exporters::available_names();

    let raw_formats: Vec<&str> = if cli.format.is_empty() {
        vec!["mcfunction"]
    } else if cli.format.iter().any(|f| f == "all") {
        all_names.clone()
    } else {
        cli.format.iter().map(String::as_str).collect()
    };

    // Deduplicate while preserving order
    let mut seen = std::collections::HashSet::new();
    let format_names: Vec<&str> = raw_formats
        .into_iter()
        .filter(|&name| seen.insert(name))
        .collect();

    // Build and validate in one pass; route "png" through build_png to honour --cell-size
    let exporters_to_run = format_names
        .iter()
        .map(|&name| -> anyhow::Result<Box<dyn lib::StructureExporter>> {
            if name == "png" {
                Ok(exporters::build_png(&palette, cli.cell_size))
            } else {
                exporters::build(name, &palette)
                    .ok_or_else(|| anyhow::anyhow!(
                        "unknown format '{}'. Available: {}",
                        name,
                        all_names.join(", ")
                    ))
            }
        })
        .collect::<anyhow::Result<Vec<_>>>()?;

    for (name, exporter) in format_names.iter().zip(exporters_to_run) {
        let output_bytes = exporter.export(&grid)?;
        let mut out_path = cli.out.clone();
        out_path.set_extension(exporter.file_extension());
        fs::write(&out_path, &output_bytes)?;
        tracing::info!("saved {} to: {:?}", name, out_path);
    }

    Ok(())
}
