//! Lint handler — dispatches to linter parsers (#104, #116, #118, #133)
//!
//! Called via flat dispatch: `skim <linter> [args...]`. Supported linters:
//! `biome`, `black`, `dprint`, `eslint`, `gofmt`, `golangci`, `mypy`,
//! `oxlint`, `prettier`, `rubocop`, `ruff`, `rustfmt`, `swiftlint`.

pub(crate) mod biome;
pub(crate) mod black;
pub(crate) mod dprint;
pub(crate) mod eslint;
pub(crate) mod gofmt;
pub(crate) mod golangci;
pub(crate) mod mypy;
pub(crate) mod oxlint;
pub(crate) mod prettier;
pub(crate) mod rubocop;
pub(crate) mod ruff;
pub(crate) mod rustfmt;
pub(crate) mod swiftlint;

use std::collections::BTreeMap;
use std::process::ExitCode;

use super::extract_show_stats;
use crate::output::canonical::{LintGroup, LintIssue, LintResult, LintSeverity};

/// Known linters that the lint handler can dispatch to.
const KNOWN_LINTERS: &[&str] = &[
    "biome",
    "black",
    "dprint",
    "eslint",
    "gofmt",
    "golangci",
    "mypy",
    "oxlint",
    "prettier",
    "rubocop",
    "ruff",
    "rustfmt",
    "swiftlint",
];

/// Entry point for `skim <linter> [args...]` (lint handler).
///
/// If no linter is specified or `--help` / `-h` is passed, prints usage
/// and exits. Otherwise dispatches to the linter-specific handler.
pub(crate) fn run(
    args: &[String],
    analytics: &crate::analytics::AnalyticsConfig,
) -> anyhow::Result<ExitCode> {
    if args.is_empty() || args.iter().any(|a| matches!(a.as_str(), "--help" | "-h")) {
        print_help();
        return Ok(ExitCode::SUCCESS);
    }

    let (filtered_args, show_stats) = extract_show_stats(args);

    let (filtered_args, json_output) = super::extract_json_flag(&filtered_args);

    let Some((linter_name, linter_args)) = filtered_args.split_first() else {
        print_help();
        return Ok(ExitCode::SUCCESS);
    };

    let ctx = super::RunContext {
        show_stats,
        json_output,
        analytics_enabled: analytics.enabled,
        session_id: analytics.session_id.clone(),
    };

    match linter_name.as_str() {
        "biome" => biome::run(linter_args, &ctx),
        "black" => black::run(linter_args, &ctx),
        "dprint" => dprint::run(linter_args, &ctx),
        "eslint" => eslint::run(linter_args, &ctx),
        "gofmt" => gofmt::run(linter_args, &ctx),
        "golangci" => golangci::run(linter_args, &ctx),
        "mypy" => mypy::run(linter_args, &ctx),
        "oxlint" => oxlint::run(linter_args, &ctx),
        "prettier" => prettier::run(linter_args, &ctx),
        "rubocop" => rubocop::run(linter_args, &ctx),
        "ruff" => ruff::run(linter_args, &ctx),
        "rustfmt" => rustfmt::run(linter_args, &ctx),
        "swiftlint" => swiftlint::run(linter_args, &ctx),
        linter => {
            let safe_linter = crate::cmd::sanitize_for_display(linter);
            eprintln!(
                "skim: unknown linter '{safe_linter}'\n\
                 Available linters: {}\n\
                 Run 'skim <linter> --help' for usage information",
                KNOWN_LINTERS.join(", ")
            );
            Ok(ExitCode::FAILURE)
        }
    }
}

fn print_help() {
    println!("skim <linter> [args...]");
    println!();
    println!("  Run linters and parse the output for AI context windows.");
    println!();
    println!("Available linters:");
    for linter in KNOWN_LINTERS {
        println!("  {linter}");
    }
    println!();
    println!("Flags:");
    println!("  --json          Emit structured JSON output");
    println!("  --show-stats    Show token statistics");
    println!();
    println!("Examples:");
    println!("  skim biome check .        Run biome check");
    println!("  skim black src/           Run black --check");
    println!("  skim dprint check .       Run dprint check");
    println!("  skim eslint .             Run eslint");
    println!("  skim gofmt ./...          Run gofmt -l");
    println!("  skim golangci run ./...   Run golangci-lint");
    println!("  skim mypy src/            Run mypy");
    println!("  skim oxlint src/          Run oxlint");
    println!("  skim prettier .           Run prettier --check");
    println!("  skim ruff check .         Run ruff check");
    println!("  skim rustfmt src/         Run rustfmt --check");
    println!("  eslint . 2>&1 | skim eslint  Pipe eslint output");
}

/// Re-export the shared `combine_output` under the name callers expect.
pub(crate) use super::combine_output as combine_stdout_stderr;

/// Group individual lint issues by rule into a `LintResult`.
///
/// Uses `BTreeMap` for deterministic ordering of rule groups.
pub(crate) fn group_issues(tool: &str, issues: Vec<LintIssue>) -> LintResult {
    let mut groups: BTreeMap<String, LintGroup> = BTreeMap::new();
    let mut errors = 0usize;
    let mut warnings = 0usize;
    for issue in issues {
        match issue.severity {
            LintSeverity::Error => errors += 1,
            LintSeverity::Warning => warnings += 1,
            LintSeverity::Info => {}
        }
        let location = format!("{}:{}", issue.file, issue.line);
        let group = groups
            .entry(issue.rule.clone())
            .or_insert_with(|| LintGroup {
                rule: issue.rule,
                count: 0,
                severity: issue.severity,
                locations: Vec::new(),
            });
        group.count += 1;
        group.locations.push(location);
    }
    LintResult::new(
        tool.to_string(),
        errors,
        warnings,
        groups.into_values().collect(),
    )
}

// ============================================================================
// Shared test helpers
// ============================================================================

/// Shared fixture loader for lint parser unit tests.
///
/// Delegates to `test_utils::load_fixture` with the `"lint"` subdir
/// pre-applied, consistent with the pattern used by `gh::test_helpers`.
#[cfg(test)]
pub(crate) fn load_lint_fixture(name: &str) -> String {
    crate::cmd::test_utils::load_fixture("lint", name)
}

// ============================================================================
// Unit tests
// ============================================================================

#[cfg(test)]
mod tests {
    use std::borrow::Cow;

    use super::*;
    use crate::cmd::test_utils::{make_output, make_output_full};
    use crate::output::canonical::{LintIssue, LintSeverity};

    #[test]
    fn test_group_issues_info_severity_not_counted() {
        let issues = vec![
            LintIssue {
                file: "a.ts".to_string(),
                line: 1,
                rule: "info-rule".to_string(),
                message: "informational".to_string(),
                severity: LintSeverity::Info,
            },
            LintIssue {
                file: "a.ts".to_string(),
                line: 2,
                rule: "err-rule".to_string(),
                message: "real error".to_string(),
                severity: LintSeverity::Error,
            },
        ];
        let result = group_issues("test", issues);
        assert_eq!(result.errors, 1);
        assert_eq!(result.warnings, 0);
        // Info issue is grouped but not counted as error or warning
        assert_eq!(result.groups.len(), 2);
    }

    #[test]
    fn test_group_issues_empty() {
        let result = group_issues("test", vec![]);
        assert_eq!(result.errors, 0);
        assert_eq!(result.warnings, 0);
        assert!(result.groups.is_empty());
        assert!(result.as_ref().contains(" OK"));
    }

    #[test]
    fn test_combine_stdout_stderr_empty_stderr() {
        let output = make_output("hello world");
        let combined = combine_stdout_stderr(&output);
        assert_eq!(&*combined, "hello world");
        // When stderr is empty, should borrow (Cow::Borrowed)
        assert!(matches!(combined, Cow::Borrowed(_)));
    }

    #[test]
    fn test_combine_stdout_stderr_with_stderr() {
        let output = make_output_full("out", "err", Some(1));
        let combined = combine_stdout_stderr(&output);
        assert_eq!(&*combined, "out\nerr");
        // When stderr is non-empty, should own (Cow::Owned)
        assert!(matches!(combined, Cow::Owned(_)));
    }

    #[test]
    fn test_combine_stdout_stderr_both_empty() {
        let output = make_output("");
        let combined = combine_stdout_stderr(&output);
        assert_eq!(&*combined, "");
        assert!(matches!(combined, Cow::Borrowed(_)));
    }
}
