use std::path::PathBuf;
use std::process;
use std::time::SystemTime;

use clap::Args;
use rsigma_parser::lint::{self, FileLintResult, LintConfig};
use serde::Deserialize;

use crate::output::{OutputCtx, OutputFormat, Painter};

/// Counts returned by `cmd_lint` so the caller can decide the exit code.
pub(crate) struct LintCounts {
    pub errors: usize,
    pub warnings: usize,
    pub infos: usize,
}

/// Arguments for `rsigma rule lint` (and the deprecated `rsigma lint`).
#[derive(Args, Debug)]
pub(crate) struct LintArgs {
    /// Path to a Sigma rule file or directory of rules
    pub path: PathBuf,

    /// JSON schema for additional validation.
    /// Use "default" to download the official Sigma schema (cached for 7 days),
    /// or provide a path to a local schema file.
    #[arg(short, long)]
    pub schema: Option<String>,

    /// Show details for all files, including those that pass
    #[arg(short, long)]
    pub verbose: bool,

    /// Disable specific lint rules (comma-separated).
    /// Example: --disable missing_description,missing_author
    #[arg(long, value_delimiter = ',')]
    pub disable: Vec<String>,

    /// Path to a .rsigma-lint.yml config file.
    /// If omitted, searches for .rsigma-lint.yml in ancestor directories.
    #[arg(long = "config")]
    pub lint_config: Option<PathBuf>,

    /// Exclude paths matching glob patterns (can be repeated).
    /// Patterns are matched against paths relative to the lint root.
    /// Example: --exclude "config/**" --exclude "**/unsupported/**"
    #[arg(long)]
    pub exclude: Vec<String>,

    /// Auto-fix safe lint issues in-place.
    /// Applies format-preserving fixes to files on disk.
    #[arg(long)]
    pub fix: bool,

    /// Minimum severity that causes a non-zero exit code.
    /// 'error' (default): exit 1 only when errors are found.
    /// 'warning': exit 1 when warnings or errors are found.
    /// 'info': exit 1 when any findings (info, warning, error) are found.
    #[arg(long = "fail-level", default_value = "error",
           value_parser = ["error", "warning", "info"])]
    pub fail_level: String,

    /// Allow additional tag namespaces (repeatable).
    /// Example: --tag-namespace myorg --tag-namespace internal
    #[arg(long = "tag-namespace")]
    pub tag_namespaces: Vec<String>,
}

pub(crate) fn cmd_lint(args: LintArgs, ctx: OutputCtx) -> LintCounts {
    let LintArgs {
        path,
        schema,
        verbose,
        disable,
        lint_config: lint_config_path,
        exclude,
        fix: apply_fix,
        fail_level: _,
        tag_namespaces,
    } = args;
    let p = Painter::new(ctx.color);

    // 0. Build lint config from file + CLI flags. `--quiet` suppresses
    //    informational stderr from the config-loading layer below.
    let config = build_lint_config(
        &path,
        disable,
        lint_config_path,
        exclude,
        tag_namespaces,
        ctx,
    );

    // 1. Run built-in lint checks (with suppression)
    let results: Vec<FileLintResult> = if path.is_dir() {
        match lint::lint_yaml_directory_with_config(&path, &config) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("Error: {e}");
                process::exit(crate::exit_code::RULE_ERROR);
            }
        }
    } else {
        match lint::lint_yaml_file_with_config(&path, &config) {
            Ok(r) => vec![r],
            Err(e) => {
                eprintln!("Error: {e}");
                process::exit(crate::exit_code::RULE_ERROR);
            }
        }
    };

    // 2. Optionally run JSON schema validation
    let schema_results = schema.map(|schema_arg| run_schema_validation(&path, &schema_arg));

    // 3. Merge schema warnings into results
    let mut all_results = results;
    if let Some(sr) = schema_results {
        merge_schema_results(&mut all_results, sr);
    }

    // 4. Tally findings. The numeric summary is needed by both the human
    // and machine renderers.
    let mut total_files = 0usize;
    let mut failed_files = 0usize;
    let mut total_errors = 0usize;
    let mut total_warnings = 0usize;
    let mut total_infos = 0usize;
    for result in &all_results {
        total_files += 1;
        total_errors += result.error_count();
        total_warnings += result.warning_count();
        total_infos += result.info_count();
        let has_failures = result
            .warnings
            .iter()
            .any(|w| matches!(w.severity, lint::Severity::Error | lint::Severity::Warning));
        if has_failures {
            failed_files += 1;
        }
    }

    // 5. Machine renderers (`json`, `ndjson`, `csv`, `tsv`) bypass the
    //    coloured human view entirely; the human renderer is the default
    //    when the operator did not explicitly set `--output-format` (the
    //    TTY-aware NDJSON fallback would regress the existing UX).
    let effective_format = if ctx.explicit_format {
        ctx.format
    } else {
        OutputFormat::Table
    };
    match effective_format {
        OutputFormat::Json => {
            crate::output::render_json(
                &lint_envelope(
                    &all_results,
                    total_files,
                    failed_files,
                    total_errors,
                    total_warnings,
                    total_infos,
                ),
                true,
            );
            return LintCounts {
                errors: total_errors,
                warnings: total_warnings,
                infos: total_infos,
            };
        }
        OutputFormat::Ndjson => {
            for entry in lint_finding_rows(&all_results) {
                crate::output::render_ndjson(&entry);
            }
            return LintCounts {
                errors: total_errors,
                warnings: total_warnings,
                infos: total_infos,
            };
        }
        OutputFormat::Csv => {
            render_lint_findings_delimited(&all_results, ',');
            return LintCounts {
                errors: total_errors,
                warnings: total_warnings,
                infos: total_infos,
            };
        }
        OutputFormat::Tsv => {
            render_lint_findings_delimited(&all_results, '\t');
            return LintCounts {
                errors: total_errors,
                warnings: total_warnings,
                infos: total_infos,
            };
        }
        OutputFormat::Table => {
            // Fall through to the existing human-friendly renderer below.
        }
    }

    // 6. Per-file rendering (the human view).
    for result in &all_results {
        let has_failures = result
            .warnings
            .iter()
            .any(|w| matches!(w.severity, lint::Severity::Error | lint::Severity::Warning));

        if result.warnings.is_empty() {
            if verbose {
                println!(
                    "{} {}",
                    p.bold(&result.path.display().to_string()),
                    p.green("OK"),
                );
            }
        } else if has_failures {
            // File header
            println!("{}", p.bold(&result.path.display().to_string()));
            for w in &result.warnings {
                render_lint_warning(w, &p);
            }
            println!(); // blank line between file blocks
        } else {
            // Only info/hint — show if verbose
            if verbose {
                println!("{}", p.bold(&result.path.display().to_string()));
                for w in &result.warnings {
                    render_lint_warning(w, &p);
                }
                println!();
            }
        }
    }

    // 7. Summary. Gated by `ctx.show_stats()` so `--quiet` / `--no-stats`
    //    skip the separator and the human "Checked N file(s)..." line.
    //    The structured `tracing::info!` summary still fires for log
    //    consumers because it is not part of the human output stream.
    if ctx.show_stats() {
        let passed = total_files - failed_files;
        let separator = "─".repeat(60);
        println!("{}", p.dim(&separator));

        let passed_str = format!("{passed} passed");
        let failed_str = format!("{failed_files} failed");
        let errors_str = format!("{total_errors} error(s)");
        let warnings_str = format!("{total_warnings} warning(s)");
        let infos_str = format!("{total_infos} info(s)");

        let passed_colored = if passed > 0 {
            p.green_bold(&passed_str)
        } else {
            p.dim(&passed_str)
        };
        let failed_colored = if failed_files > 0 {
            p.red_bold(&failed_str)
        } else {
            p.dim(&failed_str)
        };
        let errors_colored = if total_errors > 0 {
            p.red(&errors_str)
        } else {
            p.dim(&errors_str)
        };
        let warnings_colored = if total_warnings > 0 {
            p.yellow(&warnings_str)
        } else {
            p.dim(&warnings_str)
        };
        let infos_colored = if total_infos > 0 {
            p.blue(&infos_str)
        } else {
            p.dim(&infos_str)
        };

        println!(
            "Checked {total_files} file(s): {passed_colored}, {failed_colored} ({errors_colored}, {warnings_colored}, {infos_colored})",
        );
    }
    tracing::info!(
        files = total_files,
        passed = total_files - failed_files,
        failed = failed_files,
        errors = total_errors,
        warnings = total_warnings,
        infos = total_infos,
        "Lint summary",
    );

    // 6. Apply fixes if requested
    if apply_fix {
        let fixable: usize = all_results
            .iter()
            .flat_map(|r| &r.warnings)
            .filter(|w| {
                w.fix
                    .as_ref()
                    .is_some_and(|f| f.disposition == lint::FixDisposition::Safe)
            })
            .count();

        if fixable == 0 {
            println!("{}", p.dim("No auto-fixable issues found."));
        } else {
            let result = crate::fix::apply_fixes(&all_results);
            println!(
                "\n{}",
                p.green_bold(&format!(
                    "Applied {} fix(es) across {} file(s).",
                    result.applied, result.files_modified,
                ))
            );
            if result.failed > 0 {
                println!(
                    "{}",
                    p.yellow(&format!(
                        "{} fix(es) could not be applied (conflicts).",
                        result.failed,
                    ))
                );
            }
        }
    }

    LintCounts {
        errors: total_errors,
        warnings: total_warnings,
        infos: total_infos,
    }
}

fn render_lint_warning(w: &lint::LintWarning, p: &Painter) {
    let (severity_label, rule_bracket) = match w.severity {
        lint::Severity::Error => (p.red_bold("error"), p.red(&format!("[{}]", w.rule))),
        lint::Severity::Warning => (p.yellow_bold("warning"), p.yellow(&format!("[{}]", w.rule))),
        lint::Severity::Info => (p.blue("info"), p.blue(&format!("[{}]", w.rule))),
        lint::Severity::Hint => (p.dim("hint"), p.dim(&format!("[{}]", w.rule))),
    };
    println!("  {}{}: {}", severity_label, rule_bracket, w.message);
    let location = if let Some(span) = &w.span {
        format!("{} (line {})", w.path, span.start_line + 1)
    } else {
        w.path.clone()
    };
    println!("    {} {}", p.cyan("-->"), p.cyan(&location));
}

// ---------------------------------------------------------------------------
// JSON Schema validation
// ---------------------------------------------------------------------------

/// Official Sigma detection rule schema URL.
const SCHEMA_URL: &str = "https://raw.githubusercontent.com/SigmaHQ/sigma-specification/main/json-schema/sigma-detection-rule-schema.json";

/// Cache freshness duration: 7 days in seconds.
const CACHE_MAX_AGE_SECS: u64 = 7 * 24 * 60 * 60;

/// Resolve the schema JSON string from the `--schema` argument.
///
/// - `"default"`: download from GitHub and cache in XDG cache dir.
/// - anything else: treat as a local file path.
fn resolve_schema(schema_arg: &str) -> String {
    if schema_arg == "default" {
        resolve_default_schema()
    } else {
        match std::fs::read_to_string(schema_arg) {
            Ok(s) => s,
            Err(e) => {
                eprintln!("Error reading schema file '{schema_arg}': {e}");
                process::exit(crate::exit_code::CONFIG_ERROR);
            }
        }
    }
}

fn resolve_default_schema() -> String {
    let cache_dir = dirs::cache_dir()
        .unwrap_or_else(|| PathBuf::from(".cache"))
        .join("rsigma");
    let cache_path = cache_dir.join("sigma-schema.json");

    // Check if cached copy is fresh
    if let Ok(meta) = std::fs::metadata(&cache_path)
        && let Ok(modified) = meta.modified()
    {
        let age = SystemTime::now()
            .duration_since(modified)
            .unwrap_or_default();
        if age.as_secs() < CACHE_MAX_AGE_SECS
            && let Ok(content) = std::fs::read_to_string(&cache_path)
        {
            eprintln!("Using cached schema: {}", cache_path.display());
            return content;
        }
    }

    // Download
    eprintln!("Downloading schema from {SCHEMA_URL}...");
    match ureq::get(SCHEMA_URL).call() {
        Ok(response) => {
            let body = response.into_body().read_to_string().unwrap_or_else(|e| {
                eprintln!("Error reading schema response: {e}");
                process::exit(crate::exit_code::CONFIG_ERROR);
            });

            // Cache it
            if let Err(e) = std::fs::create_dir_all(&cache_dir) {
                eprintln!("Warning: could not create cache dir: {e}");
            } else if let Err(e) = std::fs::write(&cache_path, &body) {
                eprintln!("Warning: could not cache schema: {e}");
            } else {
                eprintln!("Cached schema at {}", cache_path.display());
            }

            body
        }
        Err(e) => {
            // Offline fallback: use stale cache if available
            if let Ok(content) = std::fs::read_to_string(&cache_path) {
                eprintln!("Warning: schema download failed ({e}), using stale cache");
                content
            } else {
                eprintln!("Error downloading schema: {e}");
                process::exit(crate::exit_code::CONFIG_ERROR);
            }
        }
    }
}

/// Run JSON schema validation on all YAML files at `path`.
fn run_schema_validation(path: &std::path::Path, schema_arg: &str) -> Vec<FileLintResult> {
    let schema_json_str = resolve_schema(schema_arg);
    let schema_value: serde_json::Value = match serde_json::from_str(&schema_json_str) {
        Ok(v) => v,
        Err(e) => {
            eprintln!("Error parsing schema JSON: {e}");
            process::exit(crate::exit_code::CONFIG_ERROR);
        }
    };

    let validator = jsonschema::validator_for(&schema_value).unwrap_or_else(|e| {
        eprintln!("Error compiling JSON schema: {e}");
        process::exit(crate::exit_code::CONFIG_ERROR);
    });

    let mut results = Vec::new();

    if path.is_dir() {
        fn walk_schema(
            dir: &std::path::Path,
            validator: &jsonschema::Validator,
            results: &mut Vec<FileLintResult>,
        ) {
            let Ok(entries) = std::fs::read_dir(dir) else {
                return;
            };
            for entry in entries.flatten() {
                let p = entry.path();
                if p.is_dir() {
                    walk_schema(&p, validator, results);
                } else if matches!(p.extension().and_then(|e| e.to_str()), Some("yml" | "yaml")) {
                    results.push(validate_file_against_schema(&p, validator));
                }
            }
        }
        walk_schema(path, &validator, &mut results);
    } else {
        results.push(validate_file_against_schema(path, &validator));
    }

    results
}

fn validate_file_against_schema(
    path: &std::path::Path,
    validator: &jsonschema::Validator,
) -> FileLintResult {
    let mut warnings = Vec::new();

    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => {
            warnings.push(lint::LintWarning {
                rule: lint::LintRule::FileReadError,
                severity: lint::Severity::Error,
                message: format!("error reading file: {e}"),
                path: "/".to_string(),
                span: None,
                fix: None,
            });
            return FileLintResult {
                path: path.to_path_buf(),
                warnings,
            };
        }
    };

    for doc in yaml_serde::Deserializer::from_str(&content) {
        let yaml_value: yaml_serde::Value = match yaml_serde::Value::deserialize(doc) {
            Ok(v) => v,
            Err(_) => continue,
        };

        // Skip action fragments
        if let Some(m) = yaml_value.as_mapping()
            && let Some(action) = m
                .get(yaml_serde::Value::String("action".into()))
                .and_then(|v| v.as_str())
            && matches!(action, "global" | "reset" | "repeat")
        {
            continue;
        }

        // Convert YAML to JSON for schema validation
        let json_str = match serde_json::to_string(&yaml_value) {
            Ok(s) => s,
            Err(_) => continue,
        };
        let json_value: serde_json::Value = match serde_json::from_str(&json_str) {
            Ok(v) => v,
            Err(_) => continue,
        };

        // Validate
        for error in validator.iter_errors(&json_value) {
            warnings.push(lint::LintWarning {
                rule: lint::LintRule::SchemaViolation,
                severity: lint::Severity::Error,
                message: format!("schema: {error}"),
                path: error.instance_path().to_string(),
                span: None,
                fix: None,
            });
        }
    }

    FileLintResult {
        path: path.to_path_buf(),
        warnings,
    }
}

/// Merge schema validation results into the main lint results.
///
/// For files already in `main_results`, append schema warnings.
/// For files only in `schema_results`, add them as new entries.
fn merge_schema_results(
    main_results: &mut Vec<FileLintResult>,
    schema_results: Vec<FileLintResult>,
) {
    use std::collections::HashMap;

    let mut index: HashMap<PathBuf, usize> = main_results
        .iter()
        .enumerate()
        .map(|(i, r)| (r.path.clone(), i))
        .collect();

    for sr in schema_results {
        if let Some(&idx) = index.get(&sr.path) {
            main_results[idx].warnings.extend(sr.warnings);
        } else {
            let idx = main_results.len();
            index.insert(sr.path.clone(), idx);
            main_results.push(sr);
        }
    }
}

// ---------------------------------------------------------------------------
// Lint config
// ---------------------------------------------------------------------------

/// Build a `LintConfig` from a config file (auto-discovered or explicit) + CLI flags.
fn build_lint_config(
    path: &std::path::Path,
    disable: Vec<String>,
    lint_config_path: Option<PathBuf>,
    exclude: Vec<String>,
    tag_namespaces: Vec<String>,
    ctx: OutputCtx,
) -> LintConfig {
    // Operator-facing progress lines ("Loaded lint config: …") are
    // pure informational and respect `--quiet` via `show_progress()`.
    // Errors and warnings (failed load, bad config) always print.
    let mut config = if let Some(explicit) = lint_config_path {
        match LintConfig::load(&explicit) {
            Ok(c) => {
                if ctx.show_progress() {
                    eprintln!("Loaded lint config: {}", explicit.display());
                }
                c
            }
            Err(e) => {
                eprintln!("Error loading lint config '{}': {e}", explicit.display());
                process::exit(crate::exit_code::CONFIG_ERROR);
            }
        }
    } else if let Some(found) = LintConfig::find_in_ancestors(path) {
        match LintConfig::load(&found) {
            Ok(c) => {
                if ctx.show_progress() {
                    eprintln!("Loaded lint config: {}", found.display());
                }
                c
            }
            Err(e) => {
                eprintln!(
                    "Warning: found .rsigma-lint.yml at {} but failed to load: {e}",
                    found.display()
                );
                LintConfig::default()
            }
        }
    } else {
        LintConfig::default()
    };

    // Merge --disable, --exclude, and --tag-namespace CLI flags
    if !disable.is_empty() || !exclude.is_empty() || !tag_namespaces.is_empty() {
        let cli_config = LintConfig {
            disabled_rules: disable.into_iter().collect(),
            exclude_patterns: exclude,
            tag_namespaces: tag_namespaces
                .into_iter()
                .map(|s| s.to_lowercase())
                .collect(),
            ..Default::default()
        };
        config.merge(&cli_config);
    }

    config
}

// The `Painter` previously defined here moved to `crate::output` so other
// commands (`engine eval`, `rule validate`) can share its
// `--color`/`NO_COLOR`/TTY resolution. This module imports it at the top.

// ---------------------------------------------------------------------------
// Machine renderers (--output-format json|ndjson|csv|tsv)
// ---------------------------------------------------------------------------

/// One row per finding, suitable for `ndjson`/`csv`/`tsv`. The CSV/TSV
/// projection is column-oriented and only carries the columns a spreadsheet
/// reader can use; the full finding object lives in the `Finding` struct
/// behind it.
#[derive(serde::Serialize)]
struct Finding {
    path: String,
    severity: &'static str,
    rule: String,
    message: String,
    line: Option<u32>,
}

impl Finding {
    fn from_lint(path: &std::path::Path, w: &lint::LintWarning) -> Self {
        Self {
            path: path.display().to_string(),
            severity: severity_label(w.severity),
            rule: format!("{}", w.rule),
            message: w.message.clone(),
            line: w.span.as_ref().map(|s| s.start_line + 1),
        }
    }
}

impl crate::output::Tabular for Finding {
    fn headers() -> &'static [&'static str] {
        &["PATH", "SEVERITY", "RULE", "LINE", "MESSAGE"]
    }
    fn row(&self) -> Vec<String> {
        vec![
            self.path.clone(),
            self.severity.to_string(),
            self.rule.clone(),
            self.line.map(|n| n.to_string()).unwrap_or_default(),
            self.message.clone(),
        ]
    }
}

fn severity_label(s: lint::Severity) -> &'static str {
    match s {
        lint::Severity::Error => "error",
        lint::Severity::Warning => "warning",
        lint::Severity::Info => "info",
        lint::Severity::Hint => "hint",
    }
}

/// Flatten the per-file lint results into a stream of [`Finding`] rows.
fn lint_finding_rows(results: &[FileLintResult]) -> Vec<Finding> {
    results
        .iter()
        .flat_map(|r| {
            r.warnings
                .iter()
                .map(move |w| Finding::from_lint(&r.path, w))
        })
        .collect()
}

/// JSON envelope mirroring `config validate --format json`: a top-level
/// summary plus the full findings array.
fn lint_envelope(
    results: &[FileLintResult],
    total_files: usize,
    failed_files: usize,
    total_errors: usize,
    total_warnings: usize,
    total_infos: usize,
) -> serde_json::Value {
    let findings = lint_finding_rows(results);
    serde_json::json!({
        "summary": {
            "files_checked": total_files,
            "files_failed": failed_files,
            "errors": total_errors,
            "warnings": total_warnings,
            "infos": total_infos,
        },
        "findings": findings,
    })
}

fn render_lint_findings_delimited(results: &[FileLintResult], sep: char) {
    use crate::output::Tabular;
    let mut writer = crate::output::DelimitedWriter::new(sep, Finding::headers());
    for f in lint_finding_rows(results) {
        writer.push(&f.row());
    }
}
