use anyhow::{bail, Result};
use std::io::IsTerminal;
use weave::FilterMesh;

#[derive(Clone)]
pub struct PatternEntry {
    pub category: &'static str,
    pub tool: &'static str,
    pub savings: &'static str,
    pub sample: &'static str,
}

pub static ALL_PATTERNS: &[PatternEntry] = &[
    // VCS
    PatternEntry {
        category: "vcs",
        tool: "git",
        savings: "80–95%",
        sample: "git log --oneline -20",
    },
    PatternEntry {
        category: "vcs",
        tool: "gh",
        savings: "70–85%",
        sample: "gh pr list",
    },
    PatternEntry {
        category: "vcs",
        tool: "jj",
        savings: "75–90%",
        sample: "jj log",
    },
    // Build
    PatternEntry {
        category: "build",
        tool: "cargo",
        savings: "65–90%",
        sample: "cargo build --release",
    },
    PatternEntry {
        category: "build",
        tool: "go",
        savings: "60–85%",
        sample: "go build ./...",
    },
    PatternEntry {
        category: "build",
        tool: "make",
        savings: "55–80%",
        sample: "make all",
    },
    PatternEntry {
        category: "build",
        tool: "gradle",
        savings: "60–85%",
        sample: "gradle build",
    },
    PatternEntry {
        category: "build",
        tool: "bazel",
        savings: "65–85%",
        sample: "bazel build //...",
    },
    PatternEntry {
        category: "build",
        tool: "nx",
        savings: "60–80%",
        sample: "nx build my-app",
    },
    PatternEntry {
        category: "build",
        tool: "vite",
        savings: "55–75%",
        sample: "vite build",
    },
    PatternEntry {
        category: "build",
        tool: "turbo",
        savings: "60–80%",
        sample: "turbo build",
    },
    PatternEntry {
        category: "build",
        tool: "zig",
        savings: "60–85%",
        sample: "zig build",
    },
    PatternEntry {
        category: "build",
        tool: "flutter",
        savings: "55–80%",
        sample: "flutter build apk",
    },
    PatternEntry {
        category: "build",
        tool: "swift",
        savings: "60–80%",
        sample: "swift build",
    },
    PatternEntry {
        category: "build",
        tool: "dotnet",
        savings: "60–80%",
        sample: "dotnet build",
    },
    PatternEntry {
        category: "build",
        tool: "buf",
        savings: "55–75%",
        sample: "buf generate",
    },
    PatternEntry {
        category: "build",
        tool: "rustfmt",
        savings: "70–90%",
        sample: "cargo fmt --check",
    },
    // Test
    PatternEntry {
        category: "test",
        tool: "jest",
        savings: "70–90%",
        sample: "jest --testPathPattern auth",
    },
    PatternEntry {
        category: "test",
        tool: "pytest",
        savings: "70–90%",
        sample: "pytest tests/ -v",
    },
    PatternEntry {
        category: "test",
        tool: "vitest",
        savings: "70–90%",
        sample: "vitest run",
    },
    PatternEntry {
        category: "test",
        tool: "playwright",
        savings: "75–90%",
        sample: "playwright test",
    },
    PatternEntry {
        category: "test",
        tool: "rspec",
        savings: "70–85%",
        sample: "rspec spec/",
    },
    // Lint
    PatternEntry {
        category: "lint",
        tool: "clippy",
        savings: "55–80%",
        sample: "cargo clippy -- -D warnings",
    },
    PatternEntry {
        category: "lint",
        tool: "eslint",
        savings: "55–80%",
        sample: "eslint src/",
    },
    PatternEntry {
        category: "lint",
        tool: "ruff",
        savings: "60–85%",
        sample: "ruff check src/",
    },
    PatternEntry {
        category: "lint",
        tool: "mypy",
        savings: "55–75%",
        sample: "mypy src/",
    },
    PatternEntry {
        category: "lint",
        tool: "golangci",
        savings: "55–80%",
        sample: "golangci-lint run",
    },
    PatternEntry {
        category: "lint",
        tool: "biome",
        savings: "55–80%",
        sample: "biome check src/",
    },
    PatternEntry {
        category: "lint",
        tool: "pylint",
        savings: "55–75%",
        sample: "pylint src/",
    },
    PatternEntry {
        category: "lint",
        tool: "flake8",
        savings: "55–75%",
        sample: "flake8 src/",
    },
    PatternEntry {
        category: "lint",
        tool: "trivy",
        savings: "50–75%",
        sample: "trivy image myapp:latest",
    },
    PatternEntry {
        category: "lint",
        tool: "semgrep",
        savings: "55–80%",
        sample: "semgrep --config auto",
    },
    PatternEntry {
        category: "lint",
        tool: "hadolint",
        savings: "50–70%",
        sample: "hadolint Dockerfile",
    },
    PatternEntry {
        category: "lint",
        tool: "stylelint",
        savings: "50–70%",
        sample: "stylelint '**/*.css'",
    },
    PatternEntry {
        category: "lint",
        tool: "grype",
        savings: "55–75%",
        sample: "grype dir:.",
    },
    PatternEntry {
        category: "lint",
        tool: "swiftlint",
        savings: "50–70%",
        sample: "swiftlint lint",
    },
    PatternEntry {
        category: "lint",
        tool: "rubocop",
        savings: "50–70%",
        sample: "rubocop lib/",
    },
    // Package
    PatternEntry {
        category: "pkg",
        tool: "npm",
        savings: "60–80%",
        sample: "npm install",
    },
    PatternEntry {
        category: "pkg",
        tool: "pnpm",
        savings: "60–80%",
        sample: "pnpm install",
    },
    PatternEntry {
        category: "pkg",
        tool: "yarn",
        savings: "60–80%",
        sample: "yarn install",
    },
    PatternEntry {
        category: "pkg",
        tool: "bun",
        savings: "60–80%",
        sample: "bun install",
    },
    PatternEntry {
        category: "pkg",
        tool: "pip",
        savings: "60–80%",
        sample: "pip install -r requirements.txt",
    },
    PatternEntry {
        category: "pkg",
        tool: "poetry",
        savings: "60–80%",
        sample: "poetry install",
    },
    PatternEntry {
        category: "pkg",
        tool: "uv",
        savings: "65–80%",
        sample: "uv sync",
    },
    PatternEntry {
        category: "pkg",
        tool: "deno",
        savings: "55–75%",
        sample: "deno cache deps.ts",
    },
    PatternEntry {
        category: "pkg",
        tool: "gem",
        savings: "55–75%",
        sample: "bundle install",
    },
    PatternEntry {
        category: "pkg",
        tool: "mix",
        savings: "55–75%",
        sample: "mix deps.get",
    },
    PatternEntry {
        category: "pkg",
        tool: "apt",
        savings: "65–85%",
        sample: "apt-get install -y curl",
    },
    PatternEntry {
        category: "pkg",
        tool: "brew",
        savings: "60–80%",
        sample: "brew install ripgrep",
    },
    // Infra
    PatternEntry {
        category: "infra",
        tool: "kubectl",
        savings: "60–85%",
        sample: "kubectl get pods -n production",
    },
    PatternEntry {
        category: "infra",
        tool: "docker",
        savings: "60–85%",
        sample: "docker build -t myapp .",
    },
    PatternEntry {
        category: "infra",
        tool: "helm",
        savings: "60–80%",
        sample: "helm upgrade myapp ./chart",
    },
    PatternEntry {
        category: "infra",
        tool: "terraform",
        savings: "60–80%",
        sample: "terraform plan",
    },
    PatternEntry {
        category: "infra",
        tool: "aws",
        savings: "60–80%",
        sample: "aws ecs describe-services",
    },
    PatternEntry {
        category: "infra",
        tool: "gcloud",
        savings: "60–80%",
        sample: "gcloud run services list",
    },
    PatternEntry {
        category: "infra",
        tool: "az",
        savings: "60–80%",
        sample: "az aks get-credentials",
    },
    PatternEntry {
        category: "infra",
        tool: "pulumi",
        savings: "60–80%",
        sample: "pulumi up",
    },
    PatternEntry {
        category: "infra",
        tool: "argocd",
        savings: "60–80%",
        sample: "argocd app list",
    },
    PatternEntry {
        category: "infra",
        tool: "fly",
        savings: "55–75%",
        sample: "fly deploy",
    },
    PatternEntry {
        category: "infra",
        tool: "ansible",
        savings: "55–75%",
        sample: "ansible-playbook site.yml",
    },
    PatternEntry {
        category: "infra",
        tool: "vercel",
        savings: "55–75%",
        sample: "vercel deploy",
    },
    PatternEntry {
        category: "infra",
        tool: "wrangler",
        savings: "55–75%",
        sample: "wrangler deploy",
    },
    PatternEntry {
        category: "infra",
        tool: "supabase",
        savings: "55–75%",
        sample: "supabase db push",
    },
    // Database
    PatternEntry {
        category: "db",
        tool: "psql",
        savings: "55–75%",
        sample: "psql -c '\\dt'",
    },
    PatternEntry {
        category: "db",
        tool: "mysql",
        savings: "55–75%",
        sample: "mysql -e 'SHOW TABLES'",
    },
    PatternEntry {
        category: "db",
        tool: "prisma",
        savings: "60–80%",
        sample: "prisma migrate dev",
    },
    PatternEntry {
        category: "db",
        tool: "redis",
        savings: "50–70%",
        sample: "redis-cli info",
    },
    PatternEntry {
        category: "db",
        tool: "mongosh",
        savings: "50–70%",
        sample: "mongosh --eval 'db.stats()'",
    },
    // AI / Data
    PatternEntry {
        category: "ai",
        tool: "mlflow",
        savings: "50–70%",
        sample: "mlflow experiments list",
    },
    PatternEntry {
        category: "ai",
        tool: "ollama",
        savings: "50–70%",
        sample: "ollama list",
    },
    PatternEntry {
        category: "ai",
        tool: "dbt",
        savings: "55–75%",
        sample: "dbt run",
    },
    PatternEntry {
        category: "ai",
        tool: "spark",
        savings: "55–75%",
        sample: "spark-submit job.py",
    },
    PatternEntry {
        category: "ai",
        tool: "alembic",
        savings: "55–75%",
        sample: "alembic upgrade head",
    },
    PatternEntry {
        category: "ai",
        tool: "flyway",
        savings: "55–75%",
        sample: "flyway migrate",
    },
    // System
    PatternEntry {
        category: "sys",
        tool: "curl",
        savings: "50–75%",
        sample: "curl -I https://api.example.com/health",
    },
    PatternEntry {
        category: "sys",
        tool: "jq",
        savings: "50–70%",
        sample: "curl ... | jq '.'",
    },
    PatternEntry {
        category: "sys",
        tool: "env",
        savings: "50–70%",
        sample: "env | grep AWS",
    },
    PatternEntry {
        category: "sys",
        tool: "systemd",
        savings: "55–75%",
        sample: "systemctl status myservice",
    },
    PatternEntry {
        category: "sys",
        tool: "mise",
        savings: "50–70%",
        sample: "mise list",
    },
];

pub const CATEGORIES: &[(&str, &str)] = &[
    ("vcs", "Version control — git, gh, jj"),
    (
        "build",
        "Build tools — cargo, go, make, gradle, bazel, zig, …",
    ),
    (
        "test",
        "Test runners — jest, pytest, vitest, playwright, rspec",
    ),
    (
        "lint",
        "Linters & scanners — clippy, eslint, ruff, trivy, semgrep, …",
    ),
    (
        "pkg",
        "Package managers — npm, pnpm, pip, poetry, uv, gem, …",
    ),
    (
        "infra",
        "Infrastructure — kubectl, docker, helm, terraform, aws, …",
    ),
    ("db", "Databases — psql, mysql, prisma, redis, mongosh"),
    (
        "ai",
        "AI / Data — mlflow, ollama, dbt, spark, alembic, flyway",
    ),
    ("sys", "System utilities — curl, jq, env, systemd, mise"),
];

pub fn handle(json: bool, category: Option<String>) -> Result<()> {
    let filtered = match &category {
        Some(cat) => {
            let cat_lower = cat.to_lowercase();
            // Validate category
            if !CATEGORIES.iter().any(|(k, _)| *k == cat_lower.as_str()) {
                let valid: Vec<&str> = CATEGORIES.iter().map(|(k, _)| *k).collect();
                bail!(
                    "unknown category '{}' — valid categories: {}",
                    cat,
                    valid.join(", ")
                );
            }
            ALL_PATTERNS
                .iter()
                .filter(|p| p.category == cat_lower.as_str())
                .collect::<Vec<_>>()
        }
        None => ALL_PATTERNS.iter().collect(),
    };

    if json {
        return print_json(&filtered);
    }

    // Authoritative count comes from the live FilterMesh, not the curated sample list.
    let engine_count = FilterMesh::default_mesh().node_count();
    print_table(&filtered, category.as_deref(), engine_count);
    Ok(())
}

fn print_table(patterns: &[&PatternEntry], category_filter: Option<&str>, engine_count: usize) {
    let is_tty = std::io::stdout().is_terminal();
    let sep = "  ─────────────────────────────────────────────────────────────────────────";

    println!();
    let header = if let Some(cat) = category_filter {
        format!("bctx patterns  — category: {cat}")
    } else {
        format!(
            "bctx domain compressors  — {} built-in across {} categories",
            engine_count,
            CATEGORIES.len()
        )
    };

    if is_tty {
        println!("  \x1b[1m{header}\x1b[0m");
    } else {
        println!("  {header}");
    }
    println!("{sep}");
    println!(
        "  {:<10}  {:<18}  {:<12}  SAMPLE COMMAND",
        "CATEGORY", "TOOL", "SAVINGS"
    );
    println!("{sep}");

    let mut last_cat = "";
    for p in patterns {
        let cat_col = if p.category != last_cat {
            last_cat = p.category;
            if is_tty {
                format!("\x1b[33m{:<10}\x1b[0m", p.category)
            } else {
                format!("{:<10}", p.category)
            }
        } else {
            format!("{:<10}", "")
        };

        let tool_col = if is_tty {
            format!("\x1b[36m{:<18}\x1b[0m", p.tool)
        } else {
            format!("{:<18}", p.tool)
        };

        println!(
            "  {}  {}  {:<12}  {}",
            cat_col, tool_col, p.savings, p.sample
        );
    }

    println!("{sep}");
    println!();

    if category_filter.is_none() {
        println!(
            "  Showing {} representative tools · {} compressor nodes active",
            patterns.len(),
            engine_count
        );
        println!("  (some tools have several subcommand-specific compressors, e.g. cargo-build / cargo-test)");
        println!();
        println!("  Filter by category:  bctx patterns --category vcs");
        println!("  Machine-readable:    bctx patterns --json");
        println!();
        println!("  Categories:");
        for (key, desc) in CATEGORIES {
            if is_tty {
                println!("    \x1b[33m{:<8}\x1b[0m  {}", key, desc);
            } else {
                println!("    {:<8}  {}", key, desc);
            }
        }
        println!();
    }
}

fn print_json(patterns: &[&PatternEntry]) -> Result<()> {
    let arr: Vec<serde_json::Value> = patterns
        .iter()
        .map(|p| {
            serde_json::json!({
                "category": p.category,
                "tool": p.tool,
                "savings": p.savings,
                "sample": p.sample,
            })
        })
        .collect();
    println!("{}", serde_json::to_string_pretty(&arr)?);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_patterns_have_non_empty_fields() {
        for p in ALL_PATTERNS {
            assert!(
                !p.category.is_empty(),
                "empty category for tool '{}'",
                p.tool
            );
            assert!(!p.tool.is_empty(), "empty tool name");
            assert!(!p.savings.is_empty(), "empty savings for tool '{}'", p.tool);
            assert!(!p.sample.is_empty(), "empty sample for tool '{}'", p.tool);
        }
    }

    #[test]
    fn all_patterns_use_known_categories() {
        let valid: Vec<&str> = CATEGORIES.iter().map(|(k, _)| *k).collect();
        for p in ALL_PATTERNS {
            assert!(
                valid.contains(&p.category),
                "unknown category '{}' for tool '{}'",
                p.category,
                p.tool
            );
        }
    }

    #[test]
    fn categories_list_has_nine_entries() {
        assert_eq!(CATEGORIES.len(), 9);
    }

    #[test]
    fn categories_have_unique_keys() {
        let keys: Vec<&str> = CATEGORIES.iter().map(|(k, _)| *k).collect();
        let unique: std::collections::HashSet<_> = keys.iter().collect();
        assert_eq!(
            keys.len(),
            unique.len(),
            "duplicate category keys: {keys:?}"
        );
    }

    #[test]
    fn total_pattern_count_exceeds_seventy() {
        assert!(
            ALL_PATTERNS.len() > 70,
            "expected > 70 patterns, got {}",
            ALL_PATTERNS.len()
        );
    }

    #[test]
    fn filter_vcs_returns_only_vcs_entries() {
        let vcs: Vec<_> = ALL_PATTERNS
            .iter()
            .filter(|p| p.category == "vcs")
            .collect();
        assert!(!vcs.is_empty(), "no vcs patterns found");
        for p in &vcs {
            assert_eq!(p.category, "vcs");
        }
    }

    #[test]
    fn filter_unknown_category_returns_error() {
        let result = handle(false, Some("unknowncategory".to_string()));
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("unknown category"), "unexpected error: {msg}");
        assert!(
            msg.contains("valid categories"),
            "missing category list: {msg}"
        );
    }

    #[test]
    fn handle_table_all_succeeds() {
        // Exercises the full handle() → print_table() code path for all patterns.
        assert!(handle(false, None).is_ok());
    }

    #[test]
    fn handle_table_filtered_succeeds() {
        // Exercises handle() → print_table() with a valid category filter.
        assert!(handle(false, Some("build".to_string())).is_ok());
    }

    #[test]
    fn handle_json_all_succeeds() {
        // Exercises handle() → print_json() for all patterns.
        assert!(handle(true, None).is_ok());
    }

    #[test]
    fn handle_json_filtered_succeeds() {
        // Exercises handle() → print_json() with a category filter.
        assert!(handle(true, Some("infra".to_string())).is_ok());
    }

    #[test]
    fn handle_category_lookup_is_case_insensitive() {
        // Category matching must be case-insensitive so "VCS" and "vcs" both work.
        assert!(handle(false, Some("VCS".to_string())).is_ok());
        assert!(handle(true, Some("Build".to_string())).is_ok());
    }

    #[test]
    fn json_flag_produces_valid_json_for_all() {
        let patterns: Vec<_> = ALL_PATTERNS.iter().collect();
        let arr: Vec<serde_json::Value> = patterns
            .iter()
            .map(|p| serde_json::json!({"category": p.category, "tool": p.tool}))
            .collect();
        let text = serde_json::to_string_pretty(&arr).expect("serialise");
        let parsed: Vec<serde_json::Value> = serde_json::from_str(&text).expect("parse");
        assert_eq!(parsed.len(), ALL_PATTERNS.len());
    }

    #[test]
    fn json_flag_produces_valid_json_for_category() {
        let build: Vec<_> = ALL_PATTERNS
            .iter()
            .filter(|p| p.category == "build")
            .collect();
        let arr: Vec<serde_json::Value> = build
            .iter()
            .map(|p| serde_json::json!({"tool": p.tool, "savings": p.savings}))
            .collect();
        let text = serde_json::to_string_pretty(&arr).expect("serialise");
        let _parsed: Vec<serde_json::Value> = serde_json::from_str(&text).expect("parse");
    }

    #[test]
    fn no_duplicate_tool_names_within_same_category() {
        for (cat, _) in CATEGORIES {
            let tools: Vec<_> = ALL_PATTERNS
                .iter()
                .filter(|p| p.category == *cat)
                .map(|p| p.tool)
                .collect();
            let unique: std::collections::HashSet<_> = tools.iter().collect();
            assert_eq!(
                tools.len(),
                unique.len(),
                "duplicate tool in category '{cat}': {tools:?}"
            );
        }
    }
}
