use std::ffi::OsString;
use std::fmt::Write;
use std::io::{self};
use std::process::{ExitCode, Termination};
use std::time::Instant;

use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use clap::Parser;
use colored::Colorize;
use karva_cache::AggregatedResults;
use karva_cli::{Args, Command, OutputFormat, SnapshotAction, SnapshotCommand, TestCommand};
use karva_collector::CollectedPackage;
use karva_logging::{Printer, set_colored_override, setup_tracing};
use karva_metadata::filter::{NameFilterSet, TagFilterSet};
use karva_metadata::{ProjectMetadata, ProjectOptionsOverrides};
use karva_project::Project;
use karva_project::path::absolute;
use karva_python_semantic::current_python_version;

mod version;
mod watch;

pub fn karva_main(f: impl FnOnce(Vec<OsString>) -> Vec<OsString>) -> ExitStatus {
    run(f).unwrap_or_else(|error| {
        use std::io::Write;

        let mut stderr = std::io::stderr().lock();

        writeln!(stderr, "{}", "Karva failed".red().bold()).ok();
        for cause in error.chain() {
            if let Some(ioerr) = cause.downcast_ref::<io::Error>() {
                if ioerr.kind() == io::ErrorKind::BrokenPipe {
                    return ExitStatus::Success;
                }
            }

            writeln!(stderr, "  {} {cause}", "Cause:".bold()).ok();
        }

        ExitStatus::Error
    })
}

fn run(f: impl FnOnce(Vec<OsString>) -> Vec<OsString>) -> anyhow::Result<ExitStatus> {
    let args = wild::args_os();

    let args = f(
        argfile::expand_args_from(args, argfile::parse_fromfile, argfile::PREFIX)
            .context("Failed to read CLI arguments from file")?,
    );

    let args = Args::parse_from(args);

    match args.command {
        Command::Test(test_args) => test(test_args),
        Command::Snapshot(snapshot_args) => snapshot(snapshot_args),
        Command::Version => version().map(|()| ExitStatus::Success),
    }
}

pub(crate) fn version() -> Result<()> {
    let mut stdout = Printer::default().stream_for_requested_summary().lock();
    if let Some(version_info) = crate::version::version() {
        writeln!(stdout, "karva {}", &version_info)?;
    } else {
        writeln!(stdout, "Failed to get karva version")?;
    }

    Ok(())
}

pub(crate) fn snapshot(args: SnapshotCommand) -> Result<ExitStatus> {
    let cwd = {
        let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
        Utf8PathBuf::from_path_buf(cwd).map_err(|path| {
            anyhow::anyhow!(
                "The current working directory `{}` contains non-Unicode characters.",
                path.display()
            )
        })?
    };

    let printer = Printer::default();
    let mut stdout = printer.stream_for_requested_summary().lock();

    match args.action {
        SnapshotAction::Accept(filter) => {
            let pending = karva_snapshot::storage::find_pending_snapshots(&cwd);
            let resolved = resolve_filter_paths(&filter.paths, &cwd);
            let filtered: Vec<_> = pending
                .iter()
                .filter(|info| matches_filter(&info.pending_path, &resolved))
                .collect();
            if filtered.is_empty() {
                writeln!(stdout, "No pending snapshots found.")?;
                return Ok(ExitStatus::Success);
            }
            let mut accepted = 0;
            for info in &filtered {
                karva_snapshot::storage::accept_pending(&info.pending_path)?;
                writeln!(stdout, "Accepted: {}", info.pending_path)?;
                accepted += 1;
            }
            writeln!(stdout, "\n{accepted} snapshot(s) accepted.")?;
            Ok(ExitStatus::Success)
        }
        SnapshotAction::Reject(filter) => {
            let pending = karva_snapshot::storage::find_pending_snapshots(&cwd);
            let resolved = resolve_filter_paths(&filter.paths, &cwd);
            let filtered: Vec<_> = pending
                .iter()
                .filter(|info| matches_filter(&info.pending_path, &resolved))
                .collect();
            if filtered.is_empty() {
                writeln!(stdout, "No pending snapshots found.")?;
                return Ok(ExitStatus::Success);
            }
            let mut rejected = 0;
            for info in &filtered {
                karva_snapshot::storage::reject_pending(&info.pending_path)?;
                writeln!(stdout, "Rejected: {}", info.pending_path)?;
                rejected += 1;
            }
            writeln!(stdout, "\n{rejected} snapshot(s) rejected.")?;
            Ok(ExitStatus::Success)
        }
        SnapshotAction::Pending(filter) => {
            let pending = karva_snapshot::storage::find_pending_snapshots(&cwd);
            let resolved = resolve_filter_paths(&filter.paths, &cwd);
            let filtered: Vec<_> = pending
                .iter()
                .filter(|info| matches_filter(&info.pending_path, &resolved))
                .collect();
            if filtered.is_empty() {
                writeln!(stdout, "No pending snapshots.")?;
                return Ok(ExitStatus::Success);
            }
            for info in &filtered {
                writeln!(stdout, "{}", info.pending_path)?;
            }
            writeln!(stdout, "\n{} pending snapshot(s).", filtered.len())?;
            Ok(ExitStatus::Success)
        }
        SnapshotAction::Review(filter) => {
            let resolved = resolve_filter_paths(&filter.paths, &cwd);
            // Drop stdout lock before interactive review (it needs stdin/stdout)
            drop(stdout);
            karva_snapshot::review::run_review(&cwd, &resolved)?;
            Ok(ExitStatus::Success)
        }
        SnapshotAction::Prune(prune_args) => {
            {
                use std::io::Write;
                writeln!(
                    std::io::stderr(),
                    "{} Prune uses static analysis and may not detect all unreferenced snapshots.",
                    "warning:".yellow().bold()
                )?;
            }
            let unreferenced = karva_snapshot::storage::find_unreferenced_snapshots(&cwd);
            let resolved = resolve_filter_paths(&prune_args.paths, &cwd);
            let filtered: Vec<_> = unreferenced
                .iter()
                .filter(|info| matches_filter(&info.snap_path, &resolved))
                .collect();
            if filtered.is_empty() {
                writeln!(stdout, "No unreferenced snapshots found.")?;
                return Ok(ExitStatus::Success);
            }
            if prune_args.dry_run {
                for info in &filtered {
                    writeln!(stdout, "Would remove: {} ({})", info.snap_path, info.reason)?;
                }
                writeln!(
                    stdout,
                    "\n{} unreferenced snapshot(s) would be removed.",
                    filtered.len()
                )?;
            } else {
                let mut removed = 0;
                for info in &filtered {
                    karva_snapshot::storage::remove_snapshot(&info.snap_path)?;
                    writeln!(stdout, "Removed: {} ({})", info.snap_path, info.reason)?;
                    removed += 1;
                }
                writeln!(stdout, "\n{removed} snapshot(s) pruned.")?;
            }
            Ok(ExitStatus::Success)
        }
        SnapshotAction::Delete(delete_args) => {
            let all = karva_snapshot::storage::find_all_snapshots(&cwd);
            let resolved = resolve_filter_paths(&delete_args.paths, &cwd);
            let filtered: Vec<_> = all
                .iter()
                .filter(|info| matches_filter(&info.path, &resolved))
                .collect();
            if filtered.is_empty() {
                writeln!(stdout, "No snapshot files found.")?;
                return Ok(ExitStatus::Success);
            }
            if delete_args.dry_run {
                for info in &filtered {
                    writeln!(stdout, "Would delete: {}", info.path)?;
                }
                writeln!(
                    stdout,
                    "\n{} snapshot file(s) would be deleted.",
                    filtered.len()
                )?;
            } else {
                let mut deleted = 0;
                for info in &filtered {
                    karva_snapshot::storage::remove_snapshot(&info.path)?;
                    writeln!(stdout, "Deleted: {}", info.path)?;
                    deleted += 1;
                }
                writeln!(stdout, "\n{deleted} snapshot file(s) deleted.")?;
            }
            Ok(ExitStatus::Success)
        }
    }
}

/// Resolve user-provided filter strings to absolute paths.
fn resolve_filter_paths(filter_paths: &[String], cwd: &Utf8Path) -> Vec<Utf8PathBuf> {
    filter_paths.iter().map(|f| absolute(f, cwd)).collect()
}

/// Check if a snapshot path matches any resolved filter (absolute path prefix match).
/// Returns true if filters is empty (match all).
fn matches_filter(snapshot_path: &Utf8Path, resolved_filters: &[Utf8PathBuf]) -> bool {
    resolved_filters.is_empty()
        || resolved_filters
            .iter()
            .any(|f| snapshot_path.as_str().starts_with(f.as_str()))
}

pub(crate) fn test(args: TestCommand) -> Result<ExitStatus> {
    let verbosity = args.verbosity().level();

    set_colored_override(args.sub_command.color);

    let printer = Printer::new(verbosity, args.sub_command.no_progress.unwrap_or(false));

    let _guard = setup_tracing(verbosity);

    let cwd = {
        let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
        Utf8PathBuf::from_path_buf(cwd)
                .map_err(|path| {
                    anyhow::anyhow!(
                        "The current working directory `{}` contains non-Unicode characters. karva only supports Unicode paths.",
                        path.display()
                    )
                })?
    };

    tracing::debug!(cwd = %cwd, "Working directory");

    let python_version = current_python_version();

    let config_file = args.config_file.as_ref().map(|path| absolute(path, &cwd));

    let mut project_metadata = if let Some(config_file) = &config_file {
        ProjectMetadata::from_config_file(config_file.clone(), &cwd, python_version)?
    } else {
        ProjectMetadata::discover(&cwd, python_version)?
    };

    let sub_command = args.sub_command.clone();

    let no_parallel = args.no_parallel.unwrap_or(false);
    let no_cache = args.no_cache.unwrap_or(false);
    let num_workers = args.num_workers;
    let dry_run = args.dry_run;
    let watch = args.watch;

    if watch && dry_run {
        anyhow::bail!("`--watch` and `--dry-run` cannot be used together");
    }

    let project_options_overrides = ProjectOptionsOverrides::new(config_file, args.into_options());
    project_metadata.apply_overrides(&project_options_overrides);

    let project = Project::from_metadata(project_metadata);

    if dry_run {
        let collected = karva_runner::collect_tests(&project)?;
        print_collected_tests(printer, &collected)?;
        return Ok(ExitStatus::Success);
    }

    let num_workers = if no_parallel {
        1
    } else {
        num_workers.unwrap_or_else(|| karva_static::max_parallelism().get())
    };

    TagFilterSet::new(&sub_command.tag_expressions)?;
    NameFilterSet::new(&sub_command.name_patterns)?;

    let config = karva_runner::ParallelTestConfig {
        num_workers,
        no_cache,
        create_ctrlc_handler: true,
    };

    if watch {
        watch::run_watch_loop(&project, &config, &sub_command, printer)?;
        return Ok(ExitStatus::Success);
    }

    let start_time = Instant::now();

    let result = karva_runner::run_parallel_tests(&project, &config, &sub_command)?;

    print_test_output(
        printer,
        start_time,
        &result,
        sub_command.output_format.as_ref(),
    )?;

    if result.stats.is_success() && result.discovery_diagnostics.is_empty() {
        Ok(ExitStatus::Success)
    } else {
        Ok(ExitStatus::Failure)
    }
}

/// Print test output
pub(crate) fn print_test_output(
    printer: Printer,
    start_time: Instant,
    result: &AggregatedResults,
    output_format: Option<&OutputFormat>,
) -> Result<()> {
    let mut stdout = printer.stream_for_details().lock();

    let is_concise = matches!(output_format, Some(OutputFormat::Concise));

    if (!result.diagnostics.is_empty() || !result.discovery_diagnostics.is_empty())
        && result.stats.total() > 0
        && stdout.is_enabled()
    {
        writeln!(stdout)?;
    }

    if !result.discovery_diagnostics.is_empty() {
        writeln!(stdout, "discovery diagnostics:")?;
        writeln!(stdout)?;
        write!(stdout, "{}", result.discovery_diagnostics)?;

        if is_concise {
            writeln!(stdout)?;
        }
    }

    if !result.diagnostics.is_empty() {
        writeln!(stdout, "diagnostics:")?;
        writeln!(stdout)?;
        write!(stdout, "{}", result.diagnostics)?;

        if is_concise {
            writeln!(stdout)?;
        }
    }

    if (result.diagnostics.is_empty() && result.discovery_diagnostics.is_empty())
        && result.stats.total() > 0
        && stdout.is_enabled()
    {
        writeln!(stdout)?;
    }

    let mut result_stdout = printer.stream_for_failure_summary().lock();

    write!(result_stdout, "{}", result.stats.display(start_time))?;

    Ok(())
}

/// Recursively collect test names from a `CollectedPackage` as `(module_name, function_name)` pairs.
fn collect_test_names(package: &CollectedPackage, tests: &mut Vec<(String, String)>) {
    for module in package.modules.values() {
        let module_name = module.path.module_name().to_string();
        for func in &module.test_function_defs {
            tests.push((module_name.clone(), func.name.to_string()));
        }
    }
    for sub_package in package.packages.values() {
        collect_test_names(sub_package, tests);
    }
}

/// Print collected tests in dry-run mode.
fn print_collected_tests(printer: Printer, collected: &CollectedPackage) -> Result<()> {
    let mut tests = Vec::new();
    collect_test_names(collected, &mut tests);
    tests.sort();

    let mut stdout = printer.stream_for_requested_summary().lock();

    for (module_name, function_name) in &tests {
        writeln!(stdout, "<test> {module_name}::{function_name}")?;
    }

    if !tests.is_empty() {
        writeln!(stdout)?;
    }

    writeln!(stdout, "{} tests collected", tests.len())?;

    Ok(())
}

#[derive(Copy, Clone)]
pub enum ExitStatus {
    /// Checking was successful and there were no errors.
    Success = 0,

    /// Checking was successful but there were errors.
    Failure = 1,

    /// Checking failed.
    Error = 2,
}

impl Termination for ExitStatus {
    fn report(self) -> ExitCode {
        ExitCode::from(self as u8)
    }
}

impl ExitStatus {
    pub const fn to_i32(self) -> i32 {
        self as i32
    }
}
