mod cache;
mod cli;
mod config;
mod plugin;
mod plugins;
mod report;
mod scanner;
mod utils;

use std::path::PathBuf;

use anyhow::Result;
use clap::Parser;
use crossterm::style::{style, Stylize};

use cache::Cache;
use cli::Cli;
use config::Config;

fn main() -> Result<()> {
    // Install Ctrl+C handler so the terminal is restored cleanly (cursor, raw mode, etc.)
    ctrlc::set_handler(move || {
        let _ = crossterm::execute!(std::io::stderr(), crossterm::cursor::Show);
        let _ = crossterm::terminal::disable_raw_mode();
        std::process::exit(130);
    })?;

    let args = Cli::parse();
    let cfg = Config::load();

    // Merge CLI args with config (CLI wins)
    let min_size_str = if args.min_size != "100MB" {
        args.min_size.clone()
    } else {
        cfg.min_size.unwrap_or_else(|| args.min_size.clone())
    };
    let min_size = cli::parse_size(&min_size_str).unwrap_or(100 * 1024 * 1024);

    let max_depth = if args.max_depth != 8 {
        args.max_depth
    } else {
        cfg.max_depth.unwrap_or(args.max_depth)
    };
    let no_global = args.no_global || cfg.no_global.unwrap_or(false);

    // Merge exclude paths from CLI and config
    let mut exclude: Vec<PathBuf> = args.exclude.clone();
    for p in &cfg.exclude {
        let pb = PathBuf::from(p);
        if !exclude.contains(&pb) {
            exclude.push(pb);
        }
    }
    // Canonicalize exclude paths for reliable prefix matching
    exclude = exclude
        .into_iter()
        .map(|p| std::fs::canonicalize(&p).unwrap_or(p))
        .collect();

    // Determine scan roots
    let paths: Vec<PathBuf> = if args.paths.is_empty() {
        vec![utils::home_dir()]
    } else {
        args.paths.clone()
    };

    // Validate paths
    for p in &paths {
        if !p.exists() {
            anyhow::bail!("path does not exist: {}", p.display());
        }
    }

    // Build plugin list with --only / --skip filtering + availability check
    let mut all = plugins::all_plugins();

    // Merge only/skip from config
    let only: Vec<String> = if !args.only.is_empty() {
        args.only.clone()
    } else {
        cfg.only.clone()
    };
    let mut skip = args.skip.clone();
    for s in &cfg.skip {
        if !skip.contains(s) {
            skip.push(s.clone());
        }
    }

    if !only.is_empty() {
        all.retain(|p| only.iter().any(|name| p.name() == name));
        // When explicitly requested, warn about unavailable plugins instead of silently dropping them
        for p in &all {
            if !p.is_available() {
                eprintln!("warning: plugin '{}' is not available (skipped)", p.name());
            }
        }
    }
    if !skip.is_empty() {
        all.retain(|p| !skip.iter().any(|name| p.name() == name));
    }
    all.retain(|p| p.is_available());

    if all.is_empty() {
        println!("No plugins selected. Available plugins:");
        for p in plugins::all_plugins() {
            let avail = if p.is_available() {
                ""
            } else {
                " (unavailable)"
            };
            println!("  {} — {}{}", p.name(), p.description(), avail);
        }
        return Ok(());
    }

    // Open cache
    let cache = Cache::open()?;
    if args.rescan {
        cache.clear()?;
    } else {
        cache.prune_stale();
    }

    if !args.json {
        // Show lifetime stats
        if let Some(stats) = cache.lifetime_stats() {
            println!(
                "  {} freed {} across {} runs ({} targets)\n",
                style("Lifetime:").dim(),
                style(bytesize::ByteSize(stats.total_freed).to_string())
                    .bold()
                    .cyan(),
                style(stats.total_runs).cyan(),
                style(stats.total_targets).cyan(),
            );
        }

        // Print header
        let plugin_names: Vec<&str> = all.iter().map(|p| p.name()).collect();
        println!(
            "  {} scanning {} with {} plugins ({})\n",
            style("rasa").bold().cyan(),
            paths
                .iter()
                .map(|p| p.display().to_string())
                .collect::<Vec<_>>()
                .join(", "),
            all.len(),
            plugin_names.join(", "),
        );
    }

    // Start scan
    let path_refs: Vec<&std::path::Path> = paths.iter().map(|p| p.as_path()).collect();
    let include_global = !no_global;
    let scan = scanner::start_scan(
        &path_refs,
        all,
        min_size,
        include_global,
        max_depth,
        &cache,
        &exclude,
    );

    if args.json {
        // JSON mode: stream targets as NDJSON to stdout
        let stdout = std::io::stdout();
        let mut out = stdout.lock();
        loop {
            match scan.rx.recv() {
                Ok(scanner::ScanEvent::Target(target)) => {
                    let _ = serde_json::to_writer(&mut out, &target);
                    let _ = std::io::Write::write_all(&mut out, b"\n");
                }
                Ok(scanner::ScanEvent::Error(e)) => {
                    eprintln!("{}", e);
                }
                Ok(scanner::ScanEvent::Done) | Err(_) => break,
            }
        }
        return Ok(());
    }

    // Interactive mode
    let result = report::interactive_select(scan, &cache)?;

    // Record run in history
    if result.total_freed > 0 {
        cache.record_run(result.total_freed, result.target_count, result.errors.len());
    }

    report::print_done(result.total_freed, result.errors.len());

    if !result.errors.is_empty() {
        eprintln!("  Errors:");
        for err in &result.errors {
            eprintln!("    {}", err);
        }
    }

    Ok(())
}
