use crate::manifest::RunnerManifest;
use crate::model::{
    CapabilityProbeOutcome, Check, CheckEvidence, CheckStatus, EvidenceKind, PackageProbeOutcome,
    ProbeHostReport, ReceiptSummary, ToolProbeOutcome,
};
use crate::probe::{
    CommandOutcome, ProbeRequest, build_tool_command, capability_outcome_from,
    capability_outcome_skipped, capability_probe_argv, missing_package_outcome,
    missing_tool_outcome, package_outcome_from, package_probe_for, requested_packages,
    requested_tools, tool_outcome_from,
};
use std::process::Command;

pub fn probe_host(manifest: &RunnerManifest, request: &ProbeRequest<'_>) -> ProbeHostReport {
    let mut report = ProbeHostReport {
        manifest_path: String::new(),
        image_name: manifest.image.name.clone(),
        tools: Vec::new(),
        packages: Vec::new(),
        capabilities: Vec::new(),
        summary: ReceiptSummary::default(),
        checks: Vec::new(),
    };

    for (name, declared) in requested_tools(request.tools, manifest) {
        match declared {
            Some(entry) => {
                let argv = build_tool_command(entry);
                let outcome = run(&argv);
                let probe_outcome = tool_outcome_from(name, Some(entry.version.as_str()), outcome);
                push_tool_check(&mut report.checks, &probe_outcome);
                report.tools.push(probe_outcome);
            }
            None => {
                let outcome = missing_tool_outcome(
                    name,
                    "probe.tool_not_in_manifest: manifest does not declare this tool",
                );
                report.checks.push(
                    Check::fail(
                        "probe.tool_not_in_manifest",
                        format!("tool '{name}' is not declared in the manifest"),
                    )
                    .at(format!("tool/{name}")),
                );
                report.tools.push(outcome);
            }
        }
    }

    for (name, declared) in requested_packages(request.packages, manifest) {
        match declared {
            Some(pkg) => match package_probe_for(pkg) {
                Some(spec) => {
                    let outcome = run(&spec.argv);
                    let probe_outcome = package_outcome_from(pkg, &spec, outcome);
                    push_package_check(&mut report.checks, &probe_outcome);
                    report.packages.push(probe_outcome);
                }
                None => {
                    report.checks.push(Check::skip(
                        "probe.package_manager_unsupported",
                        format!(
                            "package '{name}' uses unsupported manager '{}' for host probes",
                            pkg.manager
                        ),
                    ));
                    let outcome = PackageProbeOutcome {
                        name: pkg.name.clone(),
                        manager: pkg.manager.clone(),
                        expected_version: Some(pkg.version.clone()),
                        observed_version: None,
                        classification: crate::model::Compatibility::Simulated,
                        status: CheckStatus::Skip,
                        reason: Some(format!(
                            "probe.package_manager_unsupported: '{}'",
                            pkg.manager
                        )),
                        command: None,
                        exit_code: None,
                        stdout_excerpt: None,
                        stderr_excerpt: None,
                    };
                    report.packages.push(outcome);
                }
            },
            None => {
                let outcome = missing_package_outcome(
                    name,
                    "probe.package_not_in_manifest: manifest does not declare this package",
                );
                report.checks.push(Check::fail(
                    "probe.package_not_in_manifest",
                    format!("package '{name}' is not declared in the manifest"),
                ));
                report.packages.push(outcome);
            }
        }
    }

    for name in request.capabilities {
        if !manifest.has_capability(name) {
            report.checks.push(Check::fail(
                "probe.capability_not_in_manifest",
                format!("capability '{name}' is not declared in the manifest"),
            ));
            report.capabilities.push(CapabilityProbeOutcome {
                name: name.clone(),
                classification: crate::model::Compatibility::Unsupported,
                status: CheckStatus::Fail,
                reason: Some(
                    "probe.capability_not_in_manifest: manifest does not declare this capability"
                        .to_string(),
                ),
                command: None,
                exit_code: None,
                stdout_excerpt: None,
                stderr_excerpt: None,
            });
            continue;
        }
        match capability_probe_argv(name) {
            Some(argv) => {
                let outcome = run(&argv);
                let probe = capability_outcome_from(name, outcome);
                push_capability_check(&mut report.checks, &probe);
                report.capabilities.push(probe);
            }
            None => {
                report.checks.push(Check::skip(
                    "probe.capability_no_host_probe",
                    format!("no host probe defined for capability '{name}'"),
                ));
                report.capabilities.push(capability_outcome_skipped(
                    name,
                    "probe.capability_no_host_probe: capability accepted from manifest, no host probe",
                ));
            }
        }
    }

    report.summary = ReceiptSummary::from_checks(&report.checks);
    report
}

fn run(argv: &[String]) -> CommandOutcome {
    if argv.is_empty() {
        return CommandOutcome::from_spawn_error(
            String::new(),
            std::io::Error::new(std::io::ErrorKind::InvalidInput, "empty argv"),
        );
    }
    let display = shell_join(argv);
    let mut cmd = Command::new(&argv[0]);
    for a in &argv[1..] {
        cmd.arg(a);
    }
    match cmd.output() {
        Ok(output) => CommandOutcome::from_output(display, output),
        Err(err) => CommandOutcome::from_spawn_error(display, err),
    }
}

fn shell_join(argv: &[String]) -> String {
    argv.iter()
        .map(|a| {
            if a.chars().all(|c| {
                c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':' | '=' | ',')
            }) {
                a.clone()
            } else {
                format!("\"{}\"", a.replace('"', "\\\""))
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}

fn push_tool_check(checks: &mut Vec<Check>, outcome: &ToolProbeOutcome) {
    let id = match outcome.status {
        CheckStatus::Pass => "probe.tool_exact",
        CheckStatus::Warn => "probe.tool_drift",
        CheckStatus::Fail => "probe.tool_unsupported",
        CheckStatus::Skip => "probe.tool_skipped",
    };
    let msg = match (&outcome.expected_version, &outcome.observed_version) {
        (Some(exp), Some(obs)) => {
            format!("tool '{}' expected {} observed {}", outcome.name, exp, obs)
        }
        (Some(exp), None) => format!("tool '{}' expected {} (no probe output)", outcome.name, exp),
        (None, Some(obs)) => format!("tool '{}' observed {} (no expectation)", outcome.name, obs),
        (None, None) => format!("tool '{}' probe failed", outcome.name),
    };
    let evidence = CheckEvidence {
        kind: EvidenceKind::HostCommand,
        command: outcome.command.clone(),
        exit_code: outcome.exit_code,
        stdout: outcome.stdout_excerpt.clone(),
        stderr: outcome.stderr_excerpt.clone(),
        image: None,
        subject: Some(format!("tool/{}", outcome.name)),
    };
    let check = match outcome.status {
        CheckStatus::Pass => Check::pass(id, msg),
        CheckStatus::Warn => Check::warn(id, msg),
        CheckStatus::Fail => Check::fail(id, msg),
        CheckStatus::Skip => Check::skip(id, msg),
    };
    checks.push(check.with_evidence(evidence));
}

fn push_package_check(checks: &mut Vec<Check>, outcome: &PackageProbeOutcome) {
    let id = match outcome.status {
        CheckStatus::Pass => "probe.package_exact",
        CheckStatus::Warn => "probe.package_drift",
        CheckStatus::Fail => "probe.package_unsupported",
        CheckStatus::Skip => "probe.package_skipped",
    };
    let msg = match (&outcome.expected_version, &outcome.observed_version) {
        (Some(exp), Some(obs)) => format!(
            "package '{}' ({manager}) expected {exp} observed {obs}",
            outcome.name,
            manager = outcome.manager
        ),
        (Some(exp), None) => format!(
            "package '{}' ({manager}) expected {exp} (no probe output)",
            outcome.name,
            manager = outcome.manager
        ),
        _ => format!("package '{}' probe inconclusive", outcome.name),
    };
    let evidence = CheckEvidence {
        kind: EvidenceKind::HostCommand,
        command: outcome.command.clone(),
        exit_code: outcome.exit_code,
        stdout: outcome.stdout_excerpt.clone(),
        stderr: outcome.stderr_excerpt.clone(),
        image: None,
        subject: Some(format!("package/{}", outcome.name)),
    };
    let check = match outcome.status {
        CheckStatus::Pass => Check::pass(id, msg),
        CheckStatus::Warn => Check::warn(id, msg),
        CheckStatus::Fail => Check::fail(id, msg),
        CheckStatus::Skip => Check::skip(id, msg),
    };
    checks.push(check.with_evidence(evidence));
}

fn push_capability_check(checks: &mut Vec<Check>, outcome: &CapabilityProbeOutcome) {
    let id = match outcome.status {
        CheckStatus::Pass => "probe.capability_exact",
        CheckStatus::Warn => "probe.capability_drift",
        CheckStatus::Fail => "probe.capability_unsupported",
        CheckStatus::Skip => "probe.capability_skipped",
    };
    let msg = format!("capability '{}' host probe", outcome.name);
    let evidence = CheckEvidence {
        kind: EvidenceKind::HostCommand,
        command: outcome.command.clone(),
        exit_code: outcome.exit_code,
        stdout: outcome.stdout_excerpt.clone(),
        stderr: outcome.stderr_excerpt.clone(),
        image: None,
        subject: Some(format!("capability/{}", outcome.name)),
    };
    let check = match outcome.status {
        CheckStatus::Pass => Check::pass(id, msg),
        CheckStatus::Warn => Check::warn(id, msg),
        CheckStatus::Fail => Check::fail(id, msg),
        CheckStatus::Skip => Check::skip(id, msg),
    };
    checks.push(check.with_evidence(evidence));
}

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

    fn sample() -> RunnerManifest {
        serde_json::from_str(
            r#"{
            "schema_version": 1,
            "image": { "name": "img", "os": { "family": "linux", "arch": "x64" } },
            "tools": [{ "name": "echo-tool", "version": "1.0.0" }],
            "packages": [{ "manager": "apt", "name": "missing-pkg", "version": "9.9.9" }],
            "capabilities": ["docker"]
        }"#,
        )
        .unwrap()
    }

    #[test]
    fn unknown_tool_in_request_records_failure() {
        let m = sample();
        let request = ProbeRequest {
            tools: &["nonexistent-tool".to_string()],
            packages: &[],
            capabilities: &[],
        };
        let report = probe_host(&m, &request);
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "probe.tool_not_in_manifest")
        );
    }

    #[test]
    fn unknown_capability_in_request_records_failure() {
        let m = sample();
        let request = ProbeRequest {
            tools: &[],
            packages: &[],
            capabilities: &["fake-cap".to_string()],
        };
        let report = probe_host(&m, &request);
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "probe.capability_not_in_manifest")
        );
    }

    #[test]
    fn package_not_in_manifest_records_failure() {
        let m = sample();
        let request = ProbeRequest {
            tools: &[],
            packages: &["not-declared".to_string()],
            capabilities: &[],
        };
        let report = probe_host(&m, &request);
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "probe.package_not_in_manifest")
        );
    }
}
