use serde::Serialize;
use serde_json::Value;
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;

#[derive(Clone, Debug, Serialize)]
pub struct DiscoveryReport {
    pub project_root: String,
    pub identity: ProjectIdentity,
    pub manifests: Vec<ProjectManifest>,
    pub paths: DiscoveryPaths,
    pub commands: DiscoveryCommands,
    pub architecture: ArchitectureReport,
    pub quality: QualityReport,
    pub operations: OperationsReport,
    pub data_contracts: DataContractReport,
    pub integrations: IntegrationReport,
    pub domain_language: DomainLanguageReport,
    pub code_intelligence: Vec<CodeIntelligenceStatus>,
    pub capabilities: Vec<CapabilityRecommendation>,
    pub risks: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct ProjectIdentity {
    pub name: String,
    pub lifecycle: String,
    pub stack: Vec<String>,
    pub project_type: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct ProjectManifest {
    pub path: String,
    pub kind: String,
    pub name: Option<String>,
    pub version: Option<String>,
    pub package_manager: Option<String>,
    pub workspace: bool,
    pub scripts: Vec<String>,
    pub dependencies: Vec<String>,
    pub signals: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct DiscoveryPaths {
    pub source: Vec<String>,
    pub tests: Vec<String>,
    pub docs: Vec<String>,
    pub migrations: Vec<String>,
    pub infra: Vec<String>,
    pub config: Vec<String>,
    pub artifacts: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct DiscoveryCommands {
    pub install: Vec<String>,
    pub lint: Vec<String>,
    pub typecheck: Vec<String>,
    pub test: Vec<String>,
    pub build: Vec<String>,
    pub not_verified: Vec<String>,
    pub missing: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct ArchitectureReport {
    pub summary: Vec<String>,
    pub entrypoints: Vec<ArchitectureEvidence>,
    pub components: Vec<ArchitectureEvidence>,
    pub layers: Vec<ArchitectureEvidence>,
    pub routing: Vec<ArchitectureEvidence>,
    pub conventions: Vec<String>,
    pub confidence: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct ArchitectureEvidence {
    pub path: String,
    pub kind: String,
    pub role: String,
    pub evidence: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct QualityReport {
    pub test_frameworks: Vec<String>,
    pub test_paths: Vec<String>,
    pub commands: Vec<String>,
    pub coverage: Vec<String>,
    pub gaps: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct OperationsReport {
    pub ci: Vec<String>,
    pub deploy: Vec<String>,
    pub containers: Vec<String>,
    pub env_files: Vec<String>,
    pub env_vars: Vec<String>,
    pub observability: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct DataContractReport {
    pub migrations: Vec<String>,
    pub schemas: Vec<String>,
    pub api_contracts: Vec<String>,
    pub orm: Vec<String>,
    pub seeds_fixtures: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct IntegrationReport {
    pub external_services: Vec<String>,
    pub auth: Vec<String>,
    pub webhooks: Vec<String>,
    pub queues_jobs: Vec<String>,
    pub storage: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct DomainLanguageReport {
    pub context_files: Vec<String>,
    pub context_map: Option<String>,
    pub adr_dirs: Vec<String>,
    pub recommendation: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct CodeIntelligenceStatus {
    pub id: String,
    pub available: bool,
    pub indexed: bool,
    pub command: String,
    pub index_marker: String,
    pub suggested_commands: Vec<String>,
    pub fallback: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct CapabilityRecommendation {
    pub id: String,
    pub title: String,
    pub source: String,
    pub source_url: String,
    pub trigger: String,
    pub stages: Vec<String>,
    pub local_skill: String,
    pub mode: String,
    pub risk: String,
    pub fallback: String,
}

pub fn discover_project(root: &Path) -> DiscoveryReport {
    let root = crate::runtime::platform::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
    let stack = detect_stack(&root);
    let manifests = detect_manifests(&root);
    let paths = detect_paths(&root);
    let commands = detect_commands(&root);
    let data_contracts = detect_data_contracts(&root);
    let integrations = detect_integrations(&root);
    let architecture = detect_architecture(&root, &stack, &manifests, &paths, &data_contracts);
    let quality = detect_quality(&root, &commands, &manifests, &paths);
    let operations = detect_operations(&root);
    DiscoveryReport {
        project_root: root.display().to_string(),
        identity: ProjectIdentity {
            name: detect_project_name(&root, &manifests),
            lifecycle: if root.join(".git").is_dir() || root.join("sdd.config.yaml").exists() {
                "brownfield".to_string()
            } else {
                "greenfield".to_string()
            },
            project_type: detect_project_type(&root, &stack),
            stack,
        },
        manifests,
        paths,
        commands,
        architecture,
        quality,
        operations,
        data_contracts,
        integrations,
        domain_language: detect_domain_language(&root),
        code_intelligence: detect_code_intelligence(&root),
        capabilities: capability_recommendations(&root),
        risks: detect_risks(&root),
    }
}

fn detect_project_name(root: &Path, manifests: &[ProjectManifest]) -> String {
    manifests
        .iter()
        .find_map(|manifest| manifest.name.clone())
        .or_else(|| {
            root.file_name()
                .and_then(|item| item.to_str())
                .map(str::to_string)
        })
        .unwrap_or_else(|| "projeto".to_string())
}

fn detect_manifests(root: &Path) -> Vec<ProjectManifest> {
    let mut manifests = Vec::new();

    if root.join("package.json").exists() {
        let text = read_optional(root.join("package.json"));
        let json = serde_json::from_str::<Value>(&text).ok();
        let scripts = json
            .as_ref()
            .and_then(|value| value.get("scripts"))
            .and_then(Value::as_object)
            .map(|scripts| {
                scripts
                    .iter()
                    .map(|(name, command)| {
                        format!("{name}: {}", command.as_str().unwrap_or("<não textual>"))
                    })
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let dependencies = package_dependencies(json.as_ref());
        let workspace = json
            .as_ref()
            .and_then(|value| value.get("workspaces"))
            .is_some()
            || root.join("pnpm-workspace.yaml").exists()
            || root.join("turbo.json").exists()
            || root.join("nx.json").exists();
        let mut signals = Vec::new();
        if workspace {
            signals.push("workspace/monorepo".to_string());
        }
        if dependencies.iter().any(|dep| dep.contains("react")) {
            signals.push("React".to_string());
        }
        if dependencies.iter().any(|dep| dep == "next") || root.join("next.config.js").exists() {
            signals.push("Next.js".to_string());
        }
        if dependencies.iter().any(|dep| dep == "vite") || root.join("vite.config.ts").exists() {
            signals.push("Vite".to_string());
        }
        if dependencies.iter().any(|dep| {
            ["express", "fastify", "hono", "@nestjs/core", "koa"].contains(&dep.as_str())
        }) {
            signals.push("Node API".to_string());
        }
        manifests.push(ProjectManifest {
            path: "package.json".to_string(),
            kind: "node".to_string(),
            name: json
                .as_ref()
                .and_then(|value| value.get("name"))
                .and_then(Value::as_str)
                .map(str::to_string),
            version: json
                .as_ref()
                .and_then(|value| value.get("version"))
                .and_then(Value::as_str)
                .map(str::to_string),
            package_manager: Some(node_runner(root)),
            workspace,
            scripts,
            dependencies,
            signals,
        });
    }

    if root.join("Cargo.toml").exists() {
        let text = read_optional(root.join("Cargo.toml"));
        manifests.push(ProjectManifest {
            path: "Cargo.toml".to_string(),
            kind: "rust".to_string(),
            name: toml_string_value(&text, "name"),
            version: toml_string_value(&text, "version"),
            package_manager: Some("cargo".to_string()),
            workspace: text.contains("[workspace]"),
            scripts: Vec::new(),
            dependencies: cargo_dependencies(&text),
            signals: cargo_signals(root, &text),
        });
    }

    if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
        let pyproject = read_optional(root.join("pyproject.toml"));
        let requirements = read_optional(root.join("requirements.txt"));
        let combined = format!("{pyproject}\n{requirements}");
        manifests.push(ProjectManifest {
            path: if root.join("pyproject.toml").exists() {
                "pyproject.toml".to_string()
            } else {
                "requirements.txt".to_string()
            },
            kind: "python".to_string(),
            name: toml_string_value(&pyproject, "name"),
            version: toml_string_value(&pyproject, "version"),
            package_manager: python_package_manager(root),
            workspace: root.join("uv.lock").exists(),
            scripts: python_scripts(&pyproject),
            dependencies: python_dependencies(&combined),
            signals: python_signals(root, &combined),
        });
    }

    if root.join("go.mod").exists() {
        let text = read_optional(root.join("go.mod"));
        manifests.push(ProjectManifest {
            path: "go.mod".to_string(),
            kind: "go".to_string(),
            name: text
                .lines()
                .find_map(|line| line.trim().strip_prefix("module "))
                .map(str::to_string),
            version: None,
            package_manager: Some("go".to_string()),
            workspace: root.join("go.work").exists(),
            scripts: Vec::new(),
            dependencies: go_dependencies(&text),
            signals: go_signals(root, &text),
        });
    }

    for (path, kind) in [
        ("pom.xml", "jvm"),
        ("build.gradle", "jvm"),
        ("build.gradle.kts", "jvm"),
        ("Gemfile", "ruby"),
        ("composer.json", "php"),
        ("pubspec.yaml", "dart/flutter"),
        ("Package.swift", "swift"),
    ] {
        if root.join(path).exists() {
            manifests.push(ProjectManifest {
                path: path.to_string(),
                kind: kind.to_string(),
                name: None,
                version: None,
                package_manager: None,
                workspace: false,
                scripts: Vec::new(),
                dependencies: Vec::new(),
                signals: vec![format!("manifest {path}")],
            });
        }
    }

    manifests
}

fn package_dependencies(json: Option<&Value>) -> Vec<String> {
    let mut dependencies = BTreeSet::new();
    for section in [
        "dependencies",
        "devDependencies",
        "peerDependencies",
        "optionalDependencies",
    ] {
        if let Some(map) = json
            .and_then(|value| value.get(section))
            .and_then(Value::as_object)
        {
            dependencies.extend(map.keys().cloned());
        }
    }
    dependencies.into_iter().collect()
}

fn toml_string_value(text: &str, key: &str) -> Option<String> {
    text.lines().find_map(|line| {
        let trimmed = line.trim();
        let value = trimmed.strip_prefix(&format!("{key} = "))?;
        Some(value.trim_matches('"').trim_matches('\'').to_string())
    })
}

fn cargo_dependencies(text: &str) -> Vec<String> {
    let mut dependencies = BTreeSet::new();
    let mut in_dependencies = false;
    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_dependencies = matches!(
                trimmed,
                "[dependencies]"
                    | "[dev-dependencies]"
                    | "[build-dependencies]"
                    | "[workspace.dependencies]"
            );
            continue;
        }
        if in_dependencies {
            if let Some((name, _)) = trimmed.split_once('=') {
                let name = name.trim();
                if !name.is_empty() {
                    dependencies.insert(name.to_string());
                }
            }
        }
    }
    dependencies.into_iter().collect()
}

fn cargo_signals(root: &Path, text: &str) -> Vec<String> {
    let lower = text.to_lowercase();
    let mut signals = Vec::new();
    if root.join("src/main.rs").exists() {
        signals.push("binário Rust".to_string());
    }
    if root.join("src/lib.rs").exists() {
        signals.push("biblioteca Rust".to_string());
    }
    for (needle, label) in [
        ("axum", "Axum API"),
        ("actix-web", "Actix Web API"),
        ("rocket", "Rocket API"),
        ("tonic", "gRPC/Tonic"),
        ("ratatui", "TUI Ratatui"),
        ("clap", "CLI Clap"),
        ("serde", "serialização Serde"),
        ("sqlx", "SQLx"),
        ("diesel", "Diesel ORM"),
    ] {
        if lower.contains(needle) {
            signals.push(label.to_string());
        }
    }
    signals
}

fn python_package_manager(root: &Path) -> Option<String> {
    if root.join("uv.lock").exists() {
        Some("uv".to_string())
    } else if root.join("poetry.lock").exists() {
        Some("poetry".to_string())
    } else if root.join("requirements.txt").exists() {
        Some("pip".to_string())
    } else if root.join("pyproject.toml").exists() {
        Some("python".to_string())
    } else {
        None
    }
}

fn python_scripts(pyproject: &str) -> Vec<String> {
    let mut scripts = Vec::new();
    let mut in_scripts = false;
    for line in pyproject.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_scripts = matches!(
                trimmed,
                "[project.scripts]" | "[tool.poetry.scripts]" | "[tool.hatch.build.targets.wheel]"
            );
            continue;
        }
        if in_scripts && trimmed.contains('=') {
            scripts.push(trimmed.to_string());
        }
    }
    scripts
}

fn python_dependencies(text: &str) -> Vec<String> {
    let mut dependencies = BTreeSet::new();
    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('[') {
            continue;
        }
        let normalized = trimmed
            .split(['=', '<', '>', '~', ';', '['])
            .next()
            .unwrap_or(trimmed)
            .trim()
            .trim_matches('"')
            .trim_matches('\'');
        if normalized
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.')
            && normalized.len() > 1
        {
            dependencies.insert(normalized.to_string());
        }
    }
    dependencies.into_iter().collect()
}

fn python_signals(root: &Path, text: &str) -> Vec<String> {
    let lower = text.to_lowercase();
    let mut signals = Vec::new();
    for (needle, label) in [
        ("fastapi", "FastAPI"),
        ("django", "Django"),
        ("flask", "Flask"),
        ("pytest", "pytest"),
        ("pydantic", "Pydantic"),
        ("sqlalchemy", "SQLAlchemy"),
        ("pandas", "Data/analytics"),
    ] {
        if lower.contains(needle) {
            signals.push(label.to_string());
        }
    }
    if root.join("manage.py").exists() {
        signals.push("Django manage.py".to_string());
    }
    signals
}

fn go_dependencies(text: &str) -> Vec<String> {
    text.lines()
        .filter_map(|line| {
            let trimmed = line.trim();
            if trimmed.starts_with("require ") {
                trimmed
                    .trim_start_matches("require ")
                    .split_whitespace()
                    .next()
                    .map(str::to_string)
            } else if trimmed.contains('/') && !trimmed.starts_with("//") {
                trimmed.split_whitespace().next().map(str::to_string)
            } else {
                None
            }
        })
        .collect::<BTreeSet<_>>()
        .into_iter()
        .collect()
}

fn go_signals(root: &Path, text: &str) -> Vec<String> {
    let lower = text.to_lowercase();
    let mut signals = Vec::new();
    if root.join("cmd").is_dir() {
        signals.push("cmd/ entrypoints".to_string());
    }
    for (needle, label) in [
        ("gin-gonic/gin", "Gin API"),
        ("gofiber/fiber", "Fiber API"),
        ("grpc", "gRPC"),
        ("cobra", "CLI Cobra"),
    ] {
        if lower.contains(needle) {
            signals.push(label.to_string());
        }
    }
    signals
}

fn detect_stack(root: &Path) -> Vec<String> {
    let mut stack = Vec::new();
    let package = read_optional(root.join("package.json")).to_lowercase();
    let pyproject = read_optional(root.join("pyproject.toml")).to_lowercase();
    let requirements = read_optional(root.join("requirements.txt")).to_lowercase();
    let cargo = read_optional(root.join("Cargo.toml")).to_lowercase();
    let go_mod = read_optional(root.join("go.mod")).to_lowercase();

    if root.join("Cargo.toml").exists() {
        stack.push("rust".to_string());
    }
    if root.join("package.json").exists() {
        stack.push("node".to_string());
    }
    if contains_any(
        &package,
        &["react", "next", "vite", "svelte", "vue", "angular"],
    ) {
        stack.push("frontend".to_string());
    }
    if contains_any(&package, &["react-native", "expo"]) {
        stack.push("mobile-react-native".to_string());
    }
    if contains_any(
        &package,
        &["express", "fastify", "hono", "nestjs", "koa", "trpc"],
    ) {
        stack.push("node-api".to_string());
    }
    if root.join("pyproject.toml").exists()
        || root.join("requirements.txt").exists()
        || root.join("uv.lock").exists()
    {
        stack.push("python".to_string());
    }
    if contains_any(
        &format!("{pyproject}\n{requirements}"),
        &["fastapi", "django", "flask"],
    ) {
        stack.push("python-api".to_string());
    }
    if root.join("go.mod").exists() {
        stack.push("go".to_string());
    }
    if contains_any(&go_mod, &["gin-gonic/gin", "gofiber/fiber", "grpc"]) {
        stack.push("go-api".to_string());
    }
    if contains_any(&cargo, &["axum", "actix-web", "rocket", "tonic", "warp"]) {
        stack.push("rust-api".to_string());
    }
    if is_monorepo(root) {
        stack.push("monorepo".to_string());
    }
    if is_infra(root) {
        stack.push("infra".to_string());
    }
    if stack.is_empty() {
        stack.push("generic".to_string());
    }
    stack.sort();
    stack.dedup();
    stack
}

fn detect_project_type(root: &Path, stack: &[String]) -> String {
    if stack.iter().any(|item| item.contains("api")) {
        "api/service".to_string()
    } else if stack.iter().any(|item| item == "frontend") {
        "frontend".to_string()
    } else if root.join("src/main.rs").exists() || root.join("src/bin").is_dir() {
        "cli/app".to_string()
    } else if stack.iter().any(|item| item == "infra") {
        "infra".to_string()
    } else {
        "project".to_string()
    }
}

fn detect_paths(root: &Path) -> DiscoveryPaths {
    DiscoveryPaths {
        source: existing_paths(
            root,
            &[
                "src", "app", "apps", "packages", "lib", "crates", "cmd", "internal", "plugins",
            ],
        ),
        tests: existing_paths(root, &["tests", "test", "__tests__", "e2e", "spec"]),
        docs: existing_paths(root, &["docs", "README.md", "CONTEXT.md", "CONTEXT-MAP.md"]),
        migrations: existing_paths(
            root,
            &["migrations", "db/migrate", "prisma/migrations", "alembic"],
        ),
        infra: existing_paths(
            root,
            &[
                "infra",
                "terraform",
                "k8s",
                ".github/workflows",
                ".gitlab-ci.yml",
                "Dockerfile",
            ],
        ),
        config: existing_paths(
            root,
            &[
                "sdd.config.yaml",
                "package.json",
                "Cargo.toml",
                "pyproject.toml",
                "go.mod",
                ".env.example",
                ".env.sample",
            ],
        ),
        artifacts: existing_paths(root, &["docs", ".sdd/intelligence", ".sdd/memory"]),
    }
}

fn detect_commands(root: &Path) -> DiscoveryCommands {
    let package = read_optional(root.join("package.json"));
    let cargo = root.join("Cargo.toml").exists();
    let pyproject = read_optional(root.join("pyproject.toml"));
    let requirements = root.join("requirements.txt").exists();
    let mut commands = DiscoveryCommands {
        install: Vec::new(),
        lint: Vec::new(),
        typecheck: Vec::new(),
        test: Vec::new(),
        build: Vec::new(),
        not_verified: Vec::new(),
        missing: Vec::new(),
    };

    if !package.is_empty() {
        let runner = node_runner(root);
        push_if_script(&mut commands.lint, &package, &runner, "lint");
        push_if_script(&mut commands.typecheck, &package, &runner, "typecheck");
        push_if_script(&mut commands.test, &package, &runner, "test");
        push_if_script(&mut commands.build, &package, &runner, "build");
        commands.install.push(match runner.as_str() {
            "pnpm" => "pnpm install".to_string(),
            "yarn" => "yarn install".to_string(),
            "bun" => "bun install".to_string(),
            _ => "npm install".to_string(),
        });
    }
    if cargo {
        commands
            .lint
            .push("cargo clippy --all-targets --all-features -- -D warnings".to_string());
        commands.typecheck.push("cargo check".to_string());
        commands.test.push("cargo test".to_string());
        commands.build.push("cargo build --release".to_string());
    }
    if !pyproject.is_empty() || requirements {
        if pyproject.contains("ruff") || root.join("ruff.toml").exists() {
            commands.lint.push("ruff check .".to_string());
        }
        if pyproject.contains("mypy") || root.join("mypy.ini").exists() {
            commands.typecheck.push("mypy .".to_string());
        }
        commands.test.push("pytest".to_string());
        if requirements {
            commands
                .install
                .push("python -m pip install -r requirements.txt".to_string());
        }
    }
    if root.join("go.mod").exists() {
        commands.typecheck.push("go test ./...".to_string());
        commands.test.push("go test ./...".to_string());
        commands.build.push("go build ./...".to_string());
    }

    for (label, items) in [
        ("lint", &commands.lint),
        ("typecheck", &commands.typecheck),
        ("test", &commands.test),
        ("build", &commands.build),
    ] {
        if items.is_empty() {
            commands.missing.push(label.to_string());
        } else {
            commands.not_verified.extend(
                items
                    .iter()
                    .map(|item| format!("{item} (derivado, não executado)")),
            );
        }
    }
    commands
}

fn detect_architecture(
    root: &Path,
    stack: &[String],
    manifests: &[ProjectManifest],
    paths: &DiscoveryPaths,
    data_contracts: &DataContractReport,
) -> ArchitectureReport {
    let entrypoints = detect_entrypoints(root, manifests);
    let components = detect_components(root);
    let layers = detect_layers(root);
    let routing = detect_routing(root, manifests);
    let conventions = detect_conventions(root);
    let mut summary = Vec::new();

    if !entrypoints.is_empty() {
        summary.push(format!(
            "Entrypoints reais detectados: {}.",
            entrypoints
                .iter()
                .map(|item| format!("`{}` ({})", item.path, item.kind))
                .collect::<Vec<_>>()
                .join(", ")
        ));
    } else {
        summary.push(
            "Nenhum entrypoint canônico foi encontrado no inventário estático; confirmar comando de inicialização no próximo checkpoint.".to_string(),
        );
    }

    if stack.iter().any(|item| item == "monorepo") {
        summary.push(
            "Projeto com sinais de monorepo/workspaces; mudanças devem declarar app/package alvo e testes afetados.".to_string(),
        );
    }
    if stack.iter().any(|item| item == "rust") && root.join("src/main.rs").exists() {
        summary.push(
            "Aplicação Rust com binário em `src/main.rs`; contratos e comandos derivam de `Cargo.toml`.".to_string(),
        );
    }
    if stack.iter().any(|item| item == "frontend") {
        summary.push(
            "Interface frontend detectada; revisar rotas, componentes, design system e validação visual quando houver mudança de UI.".to_string(),
        );
    }
    if stack.iter().any(|item| item.contains("api")) {
        summary.push(
            "Serviço/API detectado; revisar contratos, handlers/rotas, autenticação, erros e observabilidade na Tech Spec.".to_string(),
        );
    }
    if !data_contracts.migrations.is_empty() || !data_contracts.schemas.is_empty() {
        summary.push(
            "Contratos de dados presentes; mudanças em persistência precisam de plano de migração, compatibilidade e rollback.".to_string(),
        );
    }
    if !paths.artifacts.is_empty() {
        summary.push(format!(
            "Artefatos SDD detectados em {}; usar traceability map como fonte local de contexto aprovado.",
            paths.artifacts.join(", ")
        ));
    }
    if summary.len() == 1 && entrypoints.is_empty() {
        summary.push(
            "A arquitetura ainda não possui sinais suficientes para inferência de alto nível; trate o discovery como pendência de onboarding.".to_string(),
        );
    }

    let confidence = if !entrypoints.is_empty() && !manifests.is_empty() && !components.is_empty() {
        "alta: manifests, entrypoints e componentes foram encontrados".to_string()
    } else if !manifests.is_empty() || !entrypoints.is_empty() {
        "média: há manifests ou entrypoints, mas faltam componentes ou convenções claras"
            .to_string()
    } else {
        "baixa: sem manifest/entrypoint suficiente no inventário estático".to_string()
    };

    ArchitectureReport {
        summary,
        entrypoints,
        components,
        layers,
        routing,
        conventions,
        confidence,
    }
}

fn detect_entrypoints(root: &Path, manifests: &[ProjectManifest]) -> Vec<ArchitectureEvidence> {
    let mut out = Vec::new();
    for (path, kind, role) in [
        ("src/main.rs", "rust-bin", "Binário principal Rust"),
        ("src/lib.rs", "rust-lib", "Biblioteca Rust compartilhada"),
        ("src/bin", "rust-bin-dir", "Binários Rust adicionais"),
        ("src/main.ts", "node-entry", "Entrypoint TypeScript"),
        ("src/main.tsx", "frontend-entry", "Entrypoint frontend"),
        ("src/index.ts", "node-entry", "Entrypoint Node/Worker"),
        ("src/server.ts", "node-server", "Servidor Node"),
        ("server.ts", "node-server", "Servidor Node"),
        ("worker.ts", "worker", "Worker"),
        ("src/App.tsx", "react-app", "Componente raiz React"),
        (
            "app/page.tsx",
            "next-app-router",
            "Página raiz Next App Router",
        ),
        (
            "pages/_app.tsx",
            "next-pages-router",
            "Bootstrap Next Pages Router",
        ),
        ("main.py", "python-entry", "Entrypoint Python"),
        ("app.py", "python-entry", "Entrypoint Python"),
        ("manage.py", "django-entry", "Entrypoint Django"),
        ("cmd", "go-cmd", "Entrypoints Go por comando"),
        ("main.go", "go-entry", "Entrypoint Go"),
        (
            "wrangler.toml",
            "cloudflare-worker",
            "Configuração Cloudflare Worker",
        ),
    ] {
        push_arch_if_exists(
            root,
            &mut out,
            path,
            kind,
            role,
            &[format!("path `{path}` existe")],
        );
    }

    for file in collect_files_with_name(root, "main.go", 4, 8) {
        if file != "main.go" {
            push_arch_path(
                &mut out,
                file.clone(),
                "go-entry",
                "Entrypoint Go aninhado",
                vec![format!("arquivo `{file}` encontrado")],
            );
        }
    }

    for manifest in manifests {
        if manifest.kind == "node" {
            for script in &manifest.scripts {
                if script.starts_with("dev:")
                    || script.starts_with("start:")
                    || script.starts_with("serve:")
                {
                    push_arch_path(
                        &mut out,
                        manifest.path.clone(),
                        "script",
                        "Script de execução declarado no manifest",
                        vec![script.clone()],
                    );
                }
            }
        }
    }
    dedup_arch(out)
}

fn detect_components(root: &Path) -> Vec<ArchitectureEvidence> {
    let mut out = Vec::new();
    for (path, kind, role) in [
        ("apps", "workspace", "Aplicações do monorepo"),
        ("packages", "workspace", "Pacotes compartilhados"),
        ("crates", "rust-workspace", "Crates Rust"),
        ("src", "source", "Código fonte principal"),
        ("src/domain", "domain", "Domínio/regras centrais"),
        (
            "src/runtime",
            "runtime",
            "Runtime e integrações operacionais",
        ),
        ("src/tui", "interface", "Interface TUI"),
        ("src/components", "ui", "Componentes de UI"),
        ("app", "frontend-router", "App router/frontend"),
        ("pages", "frontend-router", "Pages router/frontend"),
        ("routes", "routing", "Rotas HTTP"),
        ("controllers", "routing", "Controllers"),
        ("services", "application", "Serviços de aplicação"),
        ("repositories", "data", "Acesso a dados"),
        ("bot/src", "bot", "Bot/integração conversacional"),
        ("schemas", "contracts", "Schemas e contratos"),
        ("templates", "templates", "Templates de artefatos"),
        (
            "clients",
            "client-surfaces",
            "Superfícies geradas para clientes",
        ),
        ("plugins", "plugins", "Plugins e skills distribuíveis"),
        (
            "adapters",
            "integrations",
            "Adapters de ferramentas externas",
        ),
        ("presets", "configuration", "Presets de instalação"),
        ("docs", "documentation", "Documentação e artefatos locais"),
    ] {
        push_arch_if_exists(
            root,
            &mut out,
            path,
            kind,
            role,
            &[format!("path `{path}` existe")],
        );
    }
    dedup_arch(out)
}

fn detect_layers(root: &Path) -> Vec<ArchitectureEvidence> {
    let mut out = Vec::new();
    for (path, kind, role) in [
        ("src/domain", "domain", "Domínio isolado"),
        ("src/runtime", "runtime", "Runtime/plataforma"),
        ("src/tui", "presentation", "Apresentação TUI"),
        (
            "src/main.rs",
            "composition-root",
            "CLI/composição principal",
        ),
        ("schemas", "contracts", "Contratos versionados"),
        ("templates", "artifact-templates", "Templates de artefatos"),
        ("bot/src", "integration", "Integração externa/bot"),
        ("tests", "verification", "Testes automatizados"),
    ] {
        push_arch_if_exists(
            root,
            &mut out,
            path,
            kind,
            role,
            &[format!("path `{path}` existe")],
        );
    }
    dedup_arch(out)
}

fn detect_routing(root: &Path, manifests: &[ProjectManifest]) -> Vec<ArchitectureEvidence> {
    let mut out = Vec::new();
    for (path, kind, role) in [
        ("app/api", "next-api", "Rotas API Next App Router"),
        ("pages/api", "next-api", "Rotas API Next Pages Router"),
        ("src/routes", "routes", "Rotas de aplicação"),
        ("routes", "routes", "Rotas de aplicação"),
        ("controllers", "controllers", "Controllers HTTP"),
        ("src/server.ts", "node-server", "Servidor Node"),
        ("src/main.rs", "cli-router", "Despacho de comandos CLI"),
        (".claude/commands", "agent-commands", "Comandos Claude"),
        (".opencode/commands", "agent-commands", "Comandos OpenCode"),
        (".trae/commands", "agent-commands", "Comandos Trae"),
    ] {
        push_arch_if_exists(
            root,
            &mut out,
            path,
            kind,
            role,
            &[format!("path `{path}` existe")],
        );
    }
    let dep_text = dependency_text(manifests).to_lowercase();
    for (needle, kind, role) in [
        ("hono", "hono", "Roteamento Hono provável por dependência"),
        (
            "express",
            "express",
            "Roteamento Express provável por dependência",
        ),
        (
            "fastify",
            "fastify",
            "Roteamento Fastify provável por dependência",
        ),
        (
            "@nestjs/core",
            "nestjs",
            "Módulos/controllers NestJS prováveis por dependência",
        ),
        ("axum", "axum", "Rotas Axum prováveis por dependência"),
        (
            "actix-web",
            "actix",
            "Rotas Actix Web prováveis por dependência",
        ),
        ("clap", "clap", "Roteamento de CLI via Clap"),
    ] {
        if dep_text.contains(needle) {
            push_arch_path(
                &mut out,
                "manifest".to_string(),
                kind,
                role,
                vec![format!("dependência `{needle}` detectada")],
            );
        }
    }
    dedup_arch(out)
}

fn detect_conventions(root: &Path) -> Vec<String> {
    let mut conventions = Vec::new();
    for (path, description) in [
        ("AGENTS.md", "Instruções portáveis para agentes"),
        ("CLAUDE.md", "Instruções específicas para Claude Code"),
        ("sdd.config.yaml", "Configuração SDD do projeto"),
        (
            "schemas/sdd-contract.yaml",
            "Contrato canônico do fluxo SDD",
        ),
        (
            "schemas/artifact-sections.json",
            "Schema de validação de artefatos",
        ),
        (
            "templates/traceability-map.yaml",
            "Template de rastreabilidade local",
        ),
        (
            "templates/artifact-index.yaml",
            "Template do índice de artefatos",
        ),
        (".github/workflows", "CI GitHub Actions"),
        ("README.md", "Documentação inicial do projeto"),
        ("CONTEXT.md", "Glossário/contexto de domínio"),
        ("CONTEXT-MAP.md", "Mapa de contexto/domínio"),
    ] {
        if root.join(path).exists() {
            conventions.push(format!("{description}: `{path}`"));
        }
    }
    if conventions.is_empty() {
        conventions.push(
            "não encontrado no inventário; registrar convenções no Project Discovery".to_string(),
        );
    }
    conventions
}

fn detect_quality(
    root: &Path,
    commands: &DiscoveryCommands,
    manifests: &[ProjectManifest],
    paths: &DiscoveryPaths,
) -> QualityReport {
    let dep_text = dependency_text(manifests).to_lowercase();
    let mut test_frameworks = Vec::new();
    for (needle, label) in [
        ("vitest", "Vitest"),
        ("jest", "Jest"),
        ("playwright", "Playwright"),
        ("cypress", "Cypress"),
        ("pytest", "pytest"),
        ("rstest", "rstest"),
        ("cargo", "cargo test"),
        ("go test", "go test"),
    ] {
        if dep_text.contains(needle)
            || commands.test.iter().any(|command| command.contains(needle))
            || (needle == "cargo" && root.join("Cargo.toml").exists())
        {
            push_unique(&mut test_frameworks, label.to_string());
        }
    }
    let test_paths = paths.tests.clone();
    let commands = [
        commands.lint.clone(),
        commands.typecheck.clone(),
        commands.test.clone(),
        commands.build.clone(),
    ]
    .concat();
    let coverage = collect_existing(
        root,
        &[
            "coverage",
            "coverage/lcov.info",
            ".nyc_output",
            "tarpaulin-report.html",
            "target/llvm-cov",
        ],
    );
    let mut gaps = Vec::new();
    if test_paths.is_empty() {
        gaps.push("paths de teste não encontrados no inventário".to_string());
    }
    if commands.is_empty() {
        gaps.push("comandos de qualidade não derivados dos manifests".to_string());
    }
    if coverage.is_empty() {
        gaps.push("coverage não encontrado; tratar como não verificado".to_string());
    }
    QualityReport {
        test_frameworks,
        test_paths,
        commands,
        coverage,
        gaps,
    }
}

fn detect_operations(root: &Path) -> OperationsReport {
    let ci = collect_existing(
        root,
        &[
            ".github/workflows",
            ".gitlab-ci.yml",
            "bitbucket-pipelines.yml",
            ".circleci",
            "Jenkinsfile",
        ],
    );
    let deploy = collect_existing(
        root,
        &[
            "vercel.json",
            "netlify.toml",
            "wrangler.toml",
            "fly.toml",
            "render.yaml",
            "railway.toml",
            "terraform",
            "infra",
            "k8s",
        ],
    );
    let containers = collect_existing(root, &["Dockerfile", "docker-compose.yml", "compose.yaml"]);
    let env_files = collect_existing(
        root,
        &[
            ".env.example",
            ".env.sample",
            ".env.local.example",
            "appsettings.json",
            "wrangler.toml",
        ],
    );
    let env_vars = collect_env_var_names(root, &env_files);
    let observability = detect_observability(root, &env_vars);
    OperationsReport {
        ci,
        deploy,
        containers,
        env_files,
        env_vars,
        observability,
    }
}

fn detect_data_contracts(root: &Path) -> DataContractReport {
    let migrations = collect_existing(
        root,
        &[
            "migrations",
            "db/migrate",
            "prisma/migrations",
            "alembic",
            "drizzle",
        ],
    );
    let mut schemas = collect_existing(
        root,
        &[
            "schemas",
            "schema",
            "prisma/schema.prisma",
            "drizzle",
            "src/schema.ts",
            "src/db/schema.ts",
        ],
    );
    schemas.extend(collect_files_by_extensions(
        root,
        &["graphql", "proto"],
        5,
        40,
    ));
    schemas.sort();
    schemas.dedup();
    let mut api_contracts = collect_files_named(
        root,
        &[
            "openapi.yaml",
            "openapi.yml",
            "openapi.json",
            "swagger.yaml",
            "schema.graphql",
        ],
        6,
        40,
    );
    api_contracts.extend(collect_files_by_extensions(root, &["proto"], 6, 40));
    api_contracts.sort();
    api_contracts.dedup();
    let all_text = project_signal_text(root).to_lowercase();
    let mut orm = Vec::new();
    for (needle, label) in [
        ("prisma", "Prisma"),
        ("drizzle", "Drizzle"),
        ("typeorm", "TypeORM"),
        ("sequelize", "Sequelize"),
        ("knex", "Knex"),
        ("sqlx", "SQLx"),
        ("diesel", "Diesel"),
        ("sqlalchemy", "SQLAlchemy"),
        ("mongoose", "Mongoose"),
    ] {
        if all_text.contains(needle) {
            orm.push(label.to_string());
        }
    }
    let seeds_fixtures = collect_existing(
        root,
        &[
            "seeds",
            "seed",
            "fixtures",
            "tests/fixtures",
            "test/fixtures",
            "prisma/seed.ts",
        ],
    );
    DataContractReport {
        migrations,
        schemas,
        api_contracts,
        orm,
        seeds_fixtures,
    }
}

fn detect_integrations(root: &Path) -> IntegrationReport {
    let signal = project_signal_text(root).to_lowercase();
    let env_files = collect_existing(
        root,
        &[
            ".env.example",
            ".env.sample",
            ".env.local.example",
            "appsettings.json",
            "wrangler.toml",
        ],
    );
    let env_vars = collect_env_var_names(root, &env_files)
        .join("\n")
        .to_lowercase();
    let combined = format!("{signal}\n{env_vars}");
    let external_services = detect_named_signals(
        &combined,
        &[
            ("openai", "OpenAI"),
            ("anthropic", "Anthropic"),
            ("slack", "Slack"),
            ("github", "GitHub"),
            ("gitlab", "GitLab"),
            ("bitbucket", "Bitbucket"),
            ("jira", "Jira"),
            ("confluence", "Confluence"),
            ("stripe", "Stripe"),
            ("sendgrid", "SendGrid"),
            ("resend", "Resend"),
            ("twilio", "Twilio"),
            ("firebase", "Firebase"),
            ("supabase", "Supabase"),
            ("cloudflare", "Cloudflare"),
            ("aws", "AWS"),
            ("s3", "S3"),
            ("r2", "Cloudflare R2"),
            ("sentry", "Sentry"),
            ("datadog", "Datadog"),
        ],
    );
    let auth = detect_named_signals(
        &combined,
        &[
            ("auth0", "Auth0"),
            ("clerk", "Clerk"),
            ("nextauth", "NextAuth"),
            ("jwt", "JWT"),
            ("oauth", "OAuth"),
            ("session", "Sessões"),
            ("api_key", "API keys"),
            ("token", "Tokens"),
        ],
    );
    let webhooks = detect_named_signals(
        &combined,
        &[
            ("webhook", "Webhooks"),
            ("stripe_webhook", "Stripe webhook"),
            ("github_webhook", "GitHub webhook"),
            ("slack_signing", "Slack signing secret"),
        ],
    );
    let queues_jobs = detect_named_signals(
        &combined,
        &[
            ("bullmq", "BullMQ"),
            ("bull", "Bull"),
            ("queue", "Filas"),
            ("cron", "Cron"),
            ("worker", "Workers/jobs"),
            ("celery", "Celery"),
            ("sidekiq", "Sidekiq"),
        ],
    );
    let storage = detect_named_signals(
        &combined,
        &[
            ("postgres", "PostgreSQL"),
            ("mysql", "MySQL"),
            ("sqlite", "SQLite"),
            ("redis", "Redis"),
            ("mongodb", "MongoDB"),
            ("s3", "S3"),
            ("r2", "Cloudflare R2"),
            ("bucket", "Object storage"),
        ],
    );
    IntegrationReport {
        external_services,
        auth,
        webhooks,
        queues_jobs,
        storage,
    }
}

fn detect_domain_language(root: &Path) -> DomainLanguageReport {
    let context_files = collect_named_files(root, &["CONTEXT.md"]);
    let context_map = if root.join("CONTEXT-MAP.md").exists() {
        Some("CONTEXT-MAP.md".to_string())
    } else {
        None
    };
    let adr_dirs = collect_adr_dirs(root);
    let recommendation = if context_files.is_empty() && adr_dirs.is_empty() {
        "Criar glossário/ADRs apenas quando decisões ou termos de domínio forem resolvidos."
            .to_string()
    } else {
        "Usar linguagem canônica e ADRs existentes antes de PRD, Tech Spec, Execution e Review."
            .to_string()
    };
    DomainLanguageReport {
        context_files,
        context_map,
        adr_dirs,
        recommendation,
    }
}

fn detect_code_intelligence(root: &Path) -> Vec<CodeIntelligenceStatus> {
    let codegraph_indexed = root.join(".codegraph").is_dir();
    let mut codegraph_commands = Vec::new();
    if !codegraph_indexed {
        codegraph_commands.push("codegraph init -i .".to_string());
    } else {
        codegraph_commands.push("codegraph sync .".to_string());
    }
    codegraph_commands.extend([
        "codegraph status .".to_string(),
        "codegraph files --path . --json".to_string(),
        "codegraph query \"<symbol>\" --path . --json".to_string(),
        "codegraph context \"<task>\" --path . --format markdown".to_string(),
        "git diff --name-only | codegraph affected --path . --stdin --quiet".to_string(),
    ]);
    vec![
        CodeIntelligenceStatus {
            id: "codegraph".to_string(),
            available: command_available("codegraph"),
            indexed: codegraph_indexed,
            command: "codegraph".to_string(),
            index_marker: ".codegraph/".to_string(),
            suggested_commands: codegraph_commands,
            fallback:
                "Use `rg --files`, `rg`, leitura focada e testes afetados derivados dos manifests."
                    .to_string(),
        },
        CodeIntelligenceStatus {
            id: "lexa".to_string(),
            available: command_available("lexa"),
            indexed: root.join(".lexa").is_dir(),
            command: "lexa".to_string(),
            index_marker: ".lexa/graph.lexa".to_string(),
            suggested_commands: vec![
                "lexa status".to_string(),
                "lexa files".to_string(),
                "lexa brief \"<task>\"".to_string(),
                "lexa audit".to_string(),
            ],
            fallback: "Use `rg --files`, `rg`, outlines manuais por arquivo e Context Pack SDD."
                .to_string(),
        },
    ]
}

pub fn capability_recommendations(root: &Path) -> Vec<CapabilityRecommendation> {
    let mut items = vec![
        CapabilityRecommendation {
            id: "code-intelligence".to_string(),
            title: "Code intelligence opcional".to_string(),
            source: "SDD + CodeGraph + Lexa".to_string(),
            source_url:
                "https://github.com/colbymchenry/codegraph, https://github.com/anvia-hq/lexa"
                    .to_string(),
            trigger: "Discovery, Tech Spec, Execution ou Review em código brownfield."
                .to_string(),
            stages: vec!["project-discovery", "techspec", "execution", "review"]
                .into_iter()
                .map(str::to_string)
                .collect(),
            local_skill: ".agents/skills/code-intelligence/SKILL.md".to_string(),
            mode: "optional".to_string(),
            risk: "Índice pode estar ausente ou obsoleto; verificar status antes de confiar."
                .to_string(),
            fallback: "`rg --files`, `rg`, leitura focada e comandos reais do projeto.".to_string(),
        },
        CapabilityRecommendation {
            id: "execution-discipline".to_string(),
            title: "Execução disciplinada".to_string(),
            source: "Superpowers + Matt Pocock Skills".to_string(),
            source_url: "https://github.com/obra/superpowers/tree/main/skills, https://github.com/mattpocock/skills".to_string(),
            trigger: "Qualquer task de implementação, bugfix ou performance.".to_string(),
            stages: vec!["execution", "review"].into_iter().map(str::to_string).collect(),
            local_skill: ".agents/skills/execution-discipline/SKILL.md".to_string(),
            mode: "adapted".to_string(),
            risk: "Pode virar ritual sem evidência; exigir teste/loop/verificação específicos.".to_string(),
            fallback: "Aplicar checklist local de teste, diagnóstico e validação fresca.".to_string(),
        },
        CapabilityRecommendation {
            id: "domain-language".to_string(),
            title: "Linguagem de domínio e ADRs leves".to_string(),
            source: "Matt Pocock Skills".to_string(),
            source_url: "https://github.com/mattpocock/skills".to_string(),
            trigger: "Termos ambíguos, domínio rico, PRD/Tech Spec com decisões difíceis.".to_string(),
            stages: vec!["idea", "prd", "techspec", "execution", "memory"]
                .into_iter()
                .map(str::to_string)
                .collect(),
            local_skill: ".agents/skills/domain-language/SKILL.md".to_string(),
            mode: "adapted".to_string(),
            risk: "Não transformar glossário em spec nem criar ADR para decisão trivial.".to_string(),
            fallback: "Registrar termos no artifact SDD e apontar para ADRs existentes.".to_string(),
        },
        CapabilityRecommendation {
            id: "architecture-deepening".to_string(),
            title: "Deepening arquitetural".to_string(),
            source: "Matt Pocock Skills".to_string(),
            source_url: "https://github.com/mattpocock/skills".to_string(),
            trigger: "Acoplamento, módulos rasos, baixa testabilidade ou refactor grande.".to_string(),
            stages: vec!["project-discovery", "techspec", "refinement", "review"]
                .into_iter()
                .map(str::to_string)
                .collect(),
            local_skill: ".agents/skills/architecture-deepening/SKILL.md".to_string(),
            mode: "adapted".to_string(),
            risk: "Não refatorar automaticamente; propor candidatos e exigir checkpoint.".to_string(),
            fallback: "Documentar fricção e converter em task/ADR futura.".to_string(),
        },
    ];

    let package = read_optional(root.join("package.json")).to_lowercase();
    let has_frontend = contains_any(
        &package,
        &[
            "react",
            "next",
            "vite",
            "svelte",
            "vue",
            "astro",
            "tailwind",
            "storybook",
        ],
    ) || root.join("src/components").is_dir();
    if has_frontend {
        items.push(CapabilityRecommendation {
            id: "taste-ui".to_string(),
            title: "Qualidade visual contextual".to_string(),
            source: "Taste Skill".to_string(),
            source_url: "https://github.com/Leonxlnx/taste-skill".to_string(),
            trigger: "Landing page, portfolio, redesign ou UI visual detectada.".to_string(),
            stages: vec!["prd", "techspec", "execution", "review"]
                .into_iter()
                .map(str::to_string)
                .collect(),
            local_skill: ".agents/skills/frontend-design/SKILL.md".to_string(),
            mode: "adapted".to_string(),
            risk: "Não aplicar estética de marketing em dashboard operacional.".to_string(),
            fallback: "Usar design-flow/frontend-design/visual-review do SDD.".to_string(),
        });
    }
    items
}

fn detect_risks(root: &Path) -> Vec<String> {
    let mut risks = Vec::new();
    if is_monorepo(root) {
        risks.push(
            "monorepo: roteamento de contexto e testes afetados precisam ser explícitos"
                .to_string(),
        );
    }
    if is_infra(root) {
        risks.push("infra/deploy: exigir checkpoint antes de mudanças operacionais".to_string());
    }
    if !collect_adr_dirs(root).is_empty() {
        risks.push("arquitetura documentada: respeitar ADRs antes de propor mudanças".to_string());
    }
    if !existing_paths(
        root,
        &["migrations", "db/migrate", "prisma/migrations", "alembic"],
    )
    .is_empty()
    {
        risks.push("dados/migrações: exigir data-contracts e plano de reversão".to_string());
    }
    if risks.is_empty() {
        risks.push(
            "sem riscos críticos detectados no inventário estático; confirmar no PRD/Tech Spec"
                .to_string(),
        );
    }
    risks
}

fn dependency_text(manifests: &[ProjectManifest]) -> String {
    manifests
        .iter()
        .flat_map(|manifest| {
            [
                manifest.dependencies.join("\n"),
                manifest.signals.join("\n"),
                manifest.scripts.join("\n"),
            ]
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn project_signal_text(root: &Path) -> String {
    [
        read_optional(root.join("package.json")),
        read_optional(root.join("Cargo.toml")),
        read_optional(root.join("pyproject.toml")),
        read_optional(root.join("requirements.txt")),
        read_optional(root.join("go.mod")),
        read_optional(root.join("wrangler.toml")),
        read_optional(root.join("sdd.config.yaml")),
    ]
    .join("\n")
}

fn collect_existing(root: &Path, candidates: &[&str]) -> Vec<String> {
    existing_paths(root, candidates)
}

fn collect_files_with_name(root: &Path, name: &str, max_depth: usize, limit: usize) -> Vec<String> {
    collect_files_named(root, &[name], max_depth, limit)
}

fn collect_files_named(root: &Path, names: &[&str], max_depth: usize, limit: usize) -> Vec<String> {
    let mut out = Vec::new();
    for entry in WalkDir::new(root)
        .max_depth(max_depth)
        .into_iter()
        .filter_entry(|entry| !is_ignored_entry(entry.file_name().to_string_lossy().as_ref()))
        .flatten()
    {
        if out.len() >= limit {
            break;
        }
        if entry.file_type().is_file() {
            let name = entry.file_name().to_string_lossy();
            if names.iter().any(|candidate| name == *candidate) {
                out.push(relative_path(root, entry.path()));
            }
        }
    }
    out.sort();
    out.dedup();
    out
}

fn collect_files_by_extensions(
    root: &Path,
    extensions: &[&str],
    max_depth: usize,
    limit: usize,
) -> Vec<String> {
    let mut out = Vec::new();
    for entry in WalkDir::new(root)
        .max_depth(max_depth)
        .into_iter()
        .filter_entry(|entry| !is_ignored_entry(entry.file_name().to_string_lossy().as_ref()))
        .flatten()
    {
        if out.len() >= limit {
            break;
        }
        if entry.file_type().is_file() {
            let ext = entry
                .path()
                .extension()
                .and_then(|item| item.to_str())
                .unwrap_or("");
            if extensions.contains(&ext) {
                out.push(relative_path(root, entry.path()));
            }
        }
    }
    out.sort();
    out.dedup();
    out
}

fn collect_env_var_names(root: &Path, env_files: &[String]) -> Vec<String> {
    let mut vars = BTreeSet::new();
    for rel in env_files {
        let text = read_optional(root.join(rel));
        for line in text.lines() {
            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }
            if let Some((name, _)) = trimmed.split_once('=') {
                let name = name.trim();
                if name
                    .chars()
                    .all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
                {
                    vars.insert(name.to_string());
                }
            }
        }
    }
    vars.into_iter().collect()
}

fn detect_observability(root: &Path, env_vars: &[String]) -> Vec<String> {
    let signal = format!(
        "{}\n{}",
        project_signal_text(root).to_lowercase(),
        env_vars.join("\n").to_lowercase()
    );
    detect_named_signals(
        &signal,
        &[
            ("sentry", "Sentry"),
            ("opentelemetry", "OpenTelemetry"),
            ("otel", "OpenTelemetry"),
            ("datadog", "Datadog"),
            ("prometheus", "Prometheus"),
            ("analytics", "Analytics"),
            ("logfire", "Logfire"),
            ("new_relic", "New Relic"),
            ("grafana", "Grafana"),
        ],
    )
}

fn detect_named_signals(text: &str, signals: &[(&str, &str)]) -> Vec<String> {
    let mut out = Vec::new();
    for (needle, label) in signals {
        if text.contains(needle) {
            push_unique(&mut out, (*label).to_string());
        }
    }
    out
}

fn push_arch_if_exists(
    root: &Path,
    out: &mut Vec<ArchitectureEvidence>,
    path: &str,
    kind: &str,
    role: &str,
    evidence: &[String],
) {
    if root.join(path).exists() {
        push_arch_path(out, path.to_string(), kind, role, evidence.to_vec());
    }
}

fn push_arch_path(
    out: &mut Vec<ArchitectureEvidence>,
    path: String,
    kind: &str,
    role: &str,
    evidence: Vec<String>,
) {
    out.push(ArchitectureEvidence {
        path,
        kind: kind.to_string(),
        role: role.to_string(),
        evidence,
    });
}

fn dedup_arch(items: Vec<ArchitectureEvidence>) -> Vec<ArchitectureEvidence> {
    let mut seen = BTreeSet::new();
    let mut out = Vec::new();
    for item in items {
        let key = format!("{}::{}::{}", item.path, item.kind, item.role);
        if seen.insert(key) {
            out.push(item);
        }
    }
    out
}

fn push_unique(out: &mut Vec<String>, value: String) {
    if !out.iter().any(|item| item == &value) {
        out.push(value);
    }
}

fn existing_paths(root: &Path, candidates: &[&str]) -> Vec<String> {
    candidates
        .iter()
        .filter(|candidate| root.join(candidate).exists())
        .map(|candidate| (*candidate).to_string())
        .collect()
}

fn collect_named_files(root: &Path, names: &[&str]) -> Vec<String> {
    let mut out = Vec::new();
    for entry in WalkDir::new(root)
        .max_depth(4)
        .into_iter()
        .filter_entry(|entry| !is_ignored_entry(entry.file_name().to_string_lossy().as_ref()))
        .flatten()
    {
        if entry.file_type().is_file() {
            let name = entry.file_name().to_string_lossy();
            if names.iter().any(|candidate| name == *candidate) {
                out.push(relative_path(root, entry.path()));
            }
        }
    }
    out.sort();
    out
}

fn collect_adr_dirs(root: &Path) -> Vec<String> {
    let mut out = Vec::new();
    for candidate in ["docs/adr", "docs/adrs", "adr", "adrs"] {
        if root.join(candidate).is_dir() {
            out.push(candidate.to_string());
        }
    }
    for entry in WalkDir::new(root)
        .max_depth(4)
        .into_iter()
        .filter_entry(|entry| !is_ignored_entry(entry.file_name().to_string_lossy().as_ref()))
        .flatten()
    {
        if entry.file_type().is_dir() {
            let rel = relative_path(root, entry.path());
            if rel.ends_with("/docs/adr") || rel.ends_with("/docs/adrs") {
                out.push(rel);
            }
        }
    }
    out.sort();
    out.dedup();
    out
}

fn is_ignored_entry(name: &str) -> bool {
    crate::runtime::optimization::DEFAULT_IGNORED_PATHS.contains(&name)
}

fn read_optional(path: PathBuf) -> String {
    fs::read_to_string(path).unwrap_or_default()
}

fn contains_any(text: &str, terms: &[&str]) -> bool {
    terms.iter().any(|term| text.contains(term))
}

fn node_runner(root: &Path) -> String {
    if root.join("pnpm-lock.yaml").exists() {
        "pnpm".to_string()
    } else if root.join("yarn.lock").exists() {
        "yarn".to_string()
    } else if root.join("bun.lockb").exists() || root.join("bun.lock").exists() {
        "bun".to_string()
    } else {
        "npm".to_string()
    }
}

fn push_if_script(target: &mut Vec<String>, package: &str, runner: &str, script: &str) {
    if package.contains(&format!("\"{script}\"")) {
        target.push(format!("{runner} run {script}"));
    }
}

fn command_available(command: &str) -> bool {
    if command.contains('/') || command.contains('\\') {
        return Path::new(command).is_file();
    }
    let Some(paths) = env::var_os("PATH") else {
        return false;
    };
    env::split_paths(&paths).any(|path| path.join(command).is_file())
}

fn is_monorepo(root: &Path) -> bool {
    root.join("pnpm-workspace.yaml").exists()
        || root.join("turbo.json").exists()
        || root.join("nx.json").exists()
        || read_optional(root.join("package.json")).contains("\"workspaces\"")
        || (root.join("apps").is_dir() && root.join("packages").is_dir())
}

fn is_infra(root: &Path) -> bool {
    root.join("infra").is_dir()
        || root.join("terraform").is_dir()
        || root.join("k8s").is_dir()
        || root.join(".github/workflows").is_dir()
}

fn relative_path(root: &Path, path: &Path) -> String {
    path.strip_prefix(root)
        .unwrap_or(path)
        .to_string_lossy()
        .replace('\\', "/")
}

#[allow(dead_code)]
fn _command_output(command: &str, args: &[&str]) -> Option<String> {
    let output = Command::new(command).args(args).output().ok()?;
    if output.status.success() {
        Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
    } else {
        None
    }
}
