//! Summarizes project dependencies from lock files and manifests.

use crate::core::guard::never_worse;
use crate::core::tracking;
use crate::core::truncate::{reduced, CAP_WARNINGS};
use anyhow::{Context, Result};
use regex::Regex;
use std::fs;
use std::path::Path;

const MAX_DEPS: usize = CAP_WARNINGS;
// dev deps are secondary to prod — show fewer.
const MAX_DEV_DEPS: usize = reduced(CAP_WARNINGS, 5);

/// Summarize project dependencies
pub fn run(path: &Path, verbose: u8) -> Result<()> {
    let timer = tracking::TimedExecution::start();

    let dir = if path.is_file() {
        path.parent().unwrap_or(Path::new("."))
    } else {
        path
    };

    if verbose > 0 {
        eprintln!("Scanning dependencies in: {}", dir.display());
    }

    let mut found = false;
    let mut rtk = String::new();
    let mut raw = String::new();

    let cargo_path = dir.join("Cargo.toml");
    if cargo_path.exists() {
        found = true;
        raw.push_str(&fs::read_to_string(&cargo_path).unwrap_or_default());
        rtk.push_str("Rust (Cargo.toml):\n");
        rtk.push_str(&summarize_cargo_str(&cargo_path)?);
    }

    let package_path = dir.join("package.json");
    if package_path.exists() {
        found = true;
        raw.push_str(&fs::read_to_string(&package_path).unwrap_or_default());
        rtk.push_str("Node.js (package.json):\n");
        rtk.push_str(&summarize_package_json_str(&package_path)?);
    }

    let requirements_path = dir.join("requirements.txt");
    if requirements_path.exists() {
        found = true;
        raw.push_str(&fs::read_to_string(&requirements_path).unwrap_or_default());
        rtk.push_str("Python (requirements.txt):\n");
        rtk.push_str(&summarize_requirements_str(&requirements_path)?);
    }

    let pyproject_path = dir.join("pyproject.toml");
    if pyproject_path.exists() {
        found = true;
        raw.push_str(&fs::read_to_string(&pyproject_path).unwrap_or_default());
        rtk.push_str("Python (pyproject.toml):\n");
        rtk.push_str(&summarize_pyproject_str(&pyproject_path)?);
    }

    let gomod_path = dir.join("go.mod");
    if gomod_path.exists() {
        found = true;
        raw.push_str(&fs::read_to_string(&gomod_path).unwrap_or_default());
        rtk.push_str("Go (go.mod):\n");
        rtk.push_str(&summarize_gomod_str(&gomod_path)?);
    }

    if !found {
        rtk.push_str(&format!("No dependency files found in {}", dir.display()));
    }

    let shown = never_worse(&raw, &rtk);
    print!("{}", shown);
    timer.track("cat */deps", "rtk deps", &raw, shown);
    Ok(())
}

fn summarize_cargo_str(path: &Path) -> Result<String> {
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    let dep_re =
        Regex::new(r#"^([a-zA-Z0-9_-]+)\s*=\s*(?:"([^"]+)"|.*version\s*=\s*"([^"]+)")"#).unwrap();
    let section_re = Regex::new(r"^\[([^\]]+)\]").unwrap();
    let mut current_section = String::new();
    let mut deps = Vec::new();
    let mut dev_deps = Vec::new();
    let mut out = String::new();

    for line in content.lines() {
        if let Some(caps) = section_re.captures(line) {
            current_section = caps
                .get(1)
                .map(|m| m.as_str().to_string())
                .unwrap_or_default();
        } else if let Some(caps) = dep_re.captures(line) {
            let name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
            let version = caps
                .get(2)
                .or(caps.get(3))
                .map(|m| m.as_str())
                .unwrap_or("*");
            let dep = format!("{} ({})", name, version);
            match current_section.as_str() {
                "dependencies" => deps.push(dep),
                "dev-dependencies" => dev_deps.push(dep),
                _ => {}
            }
        }
    }

    if !deps.is_empty() {
        out.push_str(&format!("  Dependencies ({}):\n", deps.len()));
        for d in deps.iter().take(MAX_DEPS) {
            out.push_str(&format!("    {}\n", d));
        }
        if deps.len() > MAX_DEPS {
            out.push_str(&format!("    ... +{} more\n", deps.len() - MAX_DEPS));
        }
    }
    if !dev_deps.is_empty() {
        out.push_str(&format!("  Dev ({}):\n", dev_deps.len()));
        for d in dev_deps.iter().take(MAX_DEV_DEPS) {
            out.push_str(&format!("    {}\n", d));
        }
        if dev_deps.len() > MAX_DEV_DEPS {
            out.push_str(&format!("    ... +{} more\n", dev_deps.len() - MAX_DEV_DEPS));
        }
    }
    Ok(out)
}

fn summarize_package_json_str(path: &Path) -> Result<String> {
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    let json: serde_json::Value = crate::core::utils::from_json_str(&content)
        .with_context(|| format!("Failed to parse {}", path.display()))?;
    let mut out = String::new();

    if let Some(name) = json.get("name").and_then(|v| v.as_str()) {
        let version = json.get("version").and_then(|v| v.as_str()).unwrap_or("?");
        out.push_str(&format!("  {} @ {}\n", name, version));
    }
    if let Some(deps) = json.get("dependencies").and_then(|v| v.as_object()) {
        out.push_str(&format!("  Dependencies ({}):\n", deps.len()));
        for (i, (name, version)) in deps.iter().enumerate() {
            if i >= MAX_DEPS {
                out.push_str(&format!("    ... +{} more\n", deps.len() - MAX_DEPS));
                break;
            }
            out.push_str(&format!(
                "    {} ({})\n",
                name,
                version.as_str().unwrap_or("*")
            ));
        }
    }
    if let Some(dev_deps) = json.get("devDependencies").and_then(|v| v.as_object()) {
        out.push_str(&format!("  Dev Dependencies ({}):\n", dev_deps.len()));
        for (i, (name, _)) in dev_deps.iter().enumerate() {
            if i >= MAX_DEV_DEPS {
                out.push_str(&format!("    ... +{} more\n", dev_deps.len() - MAX_DEV_DEPS));
                break;
            }
            out.push_str(&format!("    {}\n", name));
        }
    }
    Ok(out)
}

fn summarize_requirements_str(path: &Path) -> Result<String> {
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    let dep_re = Regex::new(r"^([a-zA-Z0-9_-]+)([=<>!~]+.*)?$").unwrap();
    let mut deps = Vec::new();
    let mut out = String::new();

    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if let Some(caps) = dep_re.captures(line) {
            let name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
            let version = caps.get(2).map(|m| m.as_str()).unwrap_or("");
            deps.push(format!("{}{}", name, version));
        }
    }

    out.push_str(&format!("  Packages ({}):\n", deps.len()));
    for d in deps.iter().take(MAX_DEPS) {
        out.push_str(&format!("    {}\n", d));
    }
    if deps.len() > MAX_DEPS {
        out.push_str(&format!("    ... +{} more\n", deps.len() - MAX_DEPS));
    }
    Ok(out)
}

fn summarize_pyproject_str(path: &Path) -> Result<String> {
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    let mut in_deps = false;
    let mut deps = Vec::new();
    let mut out = String::new();

    for line in content.lines() {
        if line.contains("dependencies") && line.contains("[") {
            in_deps = true;
            continue;
        }
        if in_deps {
            if line.trim() == "]" {
                break;
            }
            let line = line
                .trim()
                .trim_matches(|c| c == '"' || c == '\'' || c == ',');
            if !line.is_empty() {
                deps.push(line.to_string());
            }
        }
    }

    if !deps.is_empty() {
        out.push_str(&format!("  Dependencies ({}):\n", deps.len()));
        for d in deps.iter().take(MAX_DEPS) {
            out.push_str(&format!("    {}\n", d));
        }
        if deps.len() > MAX_DEPS {
            out.push_str(&format!("    ... +{} more\n", deps.len() - MAX_DEPS));
        }
    }
    Ok(out)
}

fn summarize_gomod_str(path: &Path) -> Result<String> {
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    let mut module_name = String::new();
    let mut go_version = String::new();
    let mut deps = Vec::new();
    let mut in_require = false;
    let mut out = String::new();

    for line in content.lines() {
        let line = line.trim();
        if line.starts_with("module ") {
            module_name = line.trim_start_matches("module ").to_string();
        } else if line.starts_with("go ") {
            go_version = line.trim_start_matches("go ").to_string();
        } else if line == "require (" {
            in_require = true;
        } else if line == ")" {
            in_require = false;
        } else if in_require && !line.starts_with("//") {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 2 {
                deps.push(format!("{} {}", parts[0], parts[1]));
            }
        } else if line.starts_with("require ") && !line.contains("(") {
            deps.push(line.trim_start_matches("require ").to_string());
        }
    }

    if !module_name.is_empty() {
        out.push_str(&format!("  {} (go {})\n", module_name, go_version));
    }
    if !deps.is_empty() {
        out.push_str(&format!("  Dependencies ({}):\n", deps.len()));
        for d in deps.iter().take(MAX_DEPS) {
            out.push_str(&format!("    {}\n", d));
        }
        if deps.len() > MAX_DEPS {
            out.push_str(&format!("    ... +{} more\n", deps.len() - MAX_DEPS));
        }
    }
    Ok(out)
}

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

    const PACKAGE_JSON: &str =
        r#"{"name": "demo", "version": "1.0.0", "dependencies": {"left-pad": "^1.3.0"}}"#;

    fn summarize(content: &str) -> Result<String> {
        let dir = tempfile::tempdir().expect("tempdir");
        let path = dir.path().join("package.json");
        fs::write(&path, content).expect("write package.json");
        summarize_package_json_str(&path)
    }

    #[test]
    fn test_package_json_plain_parses() {
        let out = summarize(PACKAGE_JSON).expect("plain package.json must parse");
        assert!(out.contains("demo @ 1.0.0"));
        assert!(out.contains("left-pad"));
    }

    #[test]
    fn test_package_json_bom_parses() {
        let bom = format!("\u{feff}{}", PACKAGE_JSON);
        let out = summarize(&bom).expect("BOM-prefixed package.json must parse");
        assert_eq!(out, summarize(PACKAGE_JSON).unwrap());
    }

    #[test]
    fn test_package_json_invalid_has_path_context() {
        let err = summarize("not json").unwrap_err();
        assert!(err.to_string().contains("package.json"));
    }
}
