use anyhow::Result;

use crate::model::{
    CapabilityProbeOutcome, Check, CheckReport, CheckStatus, OutputFormat, PackageProbeOutcome,
    ProbeDockerReport, ProbeHostReport, RunnerImageReceipt, SetupNodeReport, ToolProbeOutcome,
};

pub fn render_receipt(receipt: &RunnerImageReceipt, format: OutputFormat) -> Result<String> {
    match format {
        OutputFormat::Text => Ok(render_text(receipt)),
        OutputFormat::Json => Ok(format!("{}\n", serde_json::to_string_pretty(receipt)?)),
        OutputFormat::Markdown => Ok(render_markdown(receipt)),
    }
}

fn render_text(receipt: &RunnerImageReceipt) -> String {
    let mut out = String::new();
    out.push_str(&format!(
        "{} {} ({})\n",
        receipt.tool.name,
        receipt.tool.version,
        receipt.mode.as_str()
    ));
    out.push_str(&format!(
        "summary: {} passed, {} warned, {} failed, {} skipped\n",
        receipt.summary.passed,
        receipt.summary.warnings,
        receipt.summary.failed,
        receipt.summary.skipped
    ));

    if let Some(check) = &receipt.check {
        render_check_text(&mut out, check);
    }
    if let Some(probe) = &receipt.probe_host {
        render_probe_host_text(&mut out, probe);
    }
    if let Some(probe) = &receipt.probe_docker {
        render_probe_docker_text(&mut out, probe);
    }
    if let Some(setup) = &receipt.setup_node {
        render_setup_node_text(&mut out, setup);
    }

    if !receipt.checks.is_empty() {
        out.push_str("\nchecks:\n");
        for check in &receipt.checks {
            push_check_line(&mut out, check, 2);
        }
    }

    out
}

fn render_check_text(out: &mut String, report: &CheckReport) {
    out.push_str(&format!(
        "\nmanifest: {} (image: {}, schema: {})\n",
        report.manifest_path, report.image_name, report.schema_version
    ));
    for c in &report.checks {
        push_check_line(out, c, 2);
    }
}

fn render_probe_host_text(out: &mut String, report: &ProbeHostReport) {
    out.push_str(&format!(
        "\nprobe-host: {} (image: {})\n",
        report.manifest_path, report.image_name
    ));
    if !report.tools.is_empty() {
        out.push_str("  tools:\n");
        for t in &report.tools {
            push_tool_outcome(out, t, 4);
        }
    }
    if !report.packages.is_empty() {
        out.push_str("  packages:\n");
        for p in &report.packages {
            push_package_outcome(out, p, 4);
        }
    }
    if !report.capabilities.is_empty() {
        out.push_str("  capabilities:\n");
        for c in &report.capabilities {
            push_capability_outcome(out, c, 4);
        }
    }
    for c in &report.checks {
        push_check_line(out, c, 2);
    }
}

fn render_probe_docker_text(out: &mut String, report: &ProbeDockerReport) {
    out.push_str(&format!(
        "\nprobe-docker: {} (image: {} <- manifest image {})\n",
        report.manifest_path, report.requested_image, report.image_name
    ));
    out.push_str(&format!(
        "  docker cli available: {}\n",
        report.docker_cli_available
    ));
    if let Some(inspect) = &report.inspect {
        out.push_str(&format!(
            "  inspect: digest={} arch={} os={}\n",
            inspect.digest.clone().unwrap_or_else(|| "?".to_string()),
            inspect
                .architecture
                .clone()
                .unwrap_or_else(|| "?".to_string()),
            inspect.os.clone().unwrap_or_else(|| "?".to_string())
        ));
    }
    if !report.tools.is_empty() {
        out.push_str("  tools:\n");
        for t in &report.tools {
            push_tool_outcome(out, t, 4);
        }
    }
    if !report.packages.is_empty() {
        out.push_str("  packages:\n");
        for p in &report.packages {
            push_package_outcome(out, p, 4);
        }
    }
    if !report.capabilities.is_empty() {
        out.push_str("  capabilities:\n");
        for c in &report.capabilities {
            push_capability_outcome(out, c, 4);
        }
    }
    for c in &report.checks {
        push_check_line(out, c, 2);
    }
}

fn render_setup_node_text(out: &mut String, report: &SetupNodeReport) {
    out.push_str(&format!(
        "\nsetup-node: requested={} resolved={} arch={} classification={}\n",
        report.requested_version,
        report
            .resolved_version
            .clone()
            .unwrap_or_else(|| "<unresolved>".to_string()),
        report.arch,
        report.classification.as_str()
    ));
    if let Some(p) = &report.toolcache_path {
        out.push_str(&format!("  toolcache_path: {p}\n"));
    }
    if let Some(p) = &report.path_prepend {
        out.push_str(&format!("  path_prepend: {p}\n"));
    }
    if let Some(reason) = &report.reason {
        out.push_str(&format!("  reason: {reason}\n"));
    }
    out.push_str(&format!("  check_latest: {}\n", report.check_latest));
    for c in &report.checks {
        push_check_line(out, c, 2);
    }
}

fn push_tool_outcome(out: &mut String, outcome: &ToolProbeOutcome, indent: usize) {
    let pad = " ".repeat(indent);
    let exp = outcome.expected_version.as_deref().unwrap_or("-");
    let obs = outcome.observed_version.as_deref().unwrap_or("-");
    out.push_str(&format!(
        "{pad}{} expected={} observed={} ({}, {:?})\n",
        outcome.name,
        exp,
        obs,
        outcome.classification.as_str(),
        outcome.status
    ));
    if let Some(reason) = &outcome.reason {
        out.push_str(&format!("{pad}  reason: {reason}\n"));
    }
}

fn push_package_outcome(out: &mut String, outcome: &PackageProbeOutcome, indent: usize) {
    let pad = " ".repeat(indent);
    let exp = outcome.expected_version.as_deref().unwrap_or("-");
    let obs = outcome.observed_version.as_deref().unwrap_or("-");
    out.push_str(&format!(
        "{pad}{} ({}) expected={} observed={} ({}, {:?})\n",
        outcome.name,
        outcome.manager,
        exp,
        obs,
        outcome.classification.as_str(),
        outcome.status
    ));
    if let Some(reason) = &outcome.reason {
        out.push_str(&format!("{pad}  reason: {reason}\n"));
    }
}

fn push_capability_outcome(out: &mut String, outcome: &CapabilityProbeOutcome, indent: usize) {
    let pad = " ".repeat(indent);
    out.push_str(&format!(
        "{pad}{} ({}, {:?})\n",
        outcome.name,
        outcome.classification.as_str(),
        outcome.status
    ));
    if let Some(reason) = &outcome.reason {
        out.push_str(&format!("{pad}  reason: {reason}\n"));
    }
}

fn push_check_line(out: &mut String, check: &Check, indent: usize) {
    let pad = " ".repeat(indent);
    out.push_str(&format!(
        "{pad}{} {:<5} {} ({})\n",
        status_symbol(check.status),
        status_word(check.status),
        check.message,
        check.id
    ));
}

fn render_markdown(receipt: &RunnerImageReceipt) -> String {
    let mut out = String::new();
    out.push_str("# gha-runner-image-proof Receipt\n\n");
    out.push_str(&format!(
        "- Tool: `{}` `{}`\n",
        receipt.tool.name, receipt.tool.version
    ));
    out.push_str(&format!("- Mode: `{}`\n", receipt.mode.as_str()));
    out.push_str(&format!("- Checked at: `{}`\n", receipt.checked_at));
    out.push_str(&format!(
        "- Summary: **{} passed**, **{} warned**, **{} failed**, **{} skipped**\n\n",
        receipt.summary.passed,
        receipt.summary.warnings,
        receipt.summary.failed,
        receipt.summary.skipped
    ));

    if let Some(check) = &receipt.check {
        out.push_str(&format!(
            "## Manifest `{}`\n\nimage: `{}` schema_version: `{}`\n\n",
            md_escape(&check.manifest_path),
            md_escape(&check.image_name),
            check.schema_version,
        ));
        markdown_checks_table(&mut out, &check.checks);
    }

    if let Some(probe) = &receipt.probe_host {
        out.push_str(&format!(
            "## Probe (host) — image `{}`\n\n",
            md_escape(&probe.image_name)
        ));
        markdown_tool_table(&mut out, &probe.tools);
        markdown_package_table(&mut out, &probe.packages);
        markdown_capability_table(&mut out, &probe.capabilities);
        markdown_checks_table(&mut out, &probe.checks);
    }

    if let Some(probe) = &receipt.probe_docker {
        out.push_str(&format!(
            "## Probe (docker) — image `{}` (manifest image `{}`)\n\n",
            md_escape(&probe.requested_image),
            md_escape(&probe.image_name)
        ));
        out.push_str(&format!(
            "- Docker CLI available: `{}`\n",
            probe.docker_cli_available
        ));
        if let Some(inspect) = &probe.inspect {
            out.push_str(&format!(
                "- Digest: `{}`\n- Architecture: `{}`\n- OS: `{}`\n\n",
                md_escape(inspect.digest.as_deref().unwrap_or("?")),
                md_escape(inspect.architecture.as_deref().unwrap_or("?")),
                md_escape(inspect.os.as_deref().unwrap_or("?"))
            ));
        }
        markdown_tool_table(&mut out, &probe.tools);
        markdown_package_table(&mut out, &probe.packages);
        markdown_capability_table(&mut out, &probe.capabilities);
        markdown_checks_table(&mut out, &probe.checks);
    }

    if let Some(setup) = &receipt.setup_node {
        out.push_str("## setup-node\n\n");
        out.push_str(&format!(
            "- Requested: `{}`\n- Resolved: `{}`\n- Arch: `{}`\n- Classification: `{}`\n",
            md_escape(&setup.requested_version),
            md_escape(setup.resolved_version.as_deref().unwrap_or("<unresolved>")),
            md_escape(&setup.arch),
            setup.classification.as_str(),
        ));
        if let Some(p) = &setup.toolcache_path {
            out.push_str(&format!("- Toolcache path: `{}`\n", md_escape(p)));
        }
        if let Some(p) = &setup.path_prepend {
            out.push_str(&format!("- PATH prepend: `{}`\n", md_escape(p)));
        }
        if let Some(r) = &setup.reason {
            out.push_str(&format!("- Reason: `{}`\n", md_escape(r)));
        }
        out.push('\n');
        markdown_checks_table(&mut out, &setup.checks);
    }

    out
}

fn markdown_tool_table(out: &mut String, outcomes: &[ToolProbeOutcome]) {
    if outcomes.is_empty() {
        return;
    }
    out.push_str("### Tools\n\n| Name | Expected | Observed | Classification | Status |\n");
    out.push_str("| --- | --- | --- | --- | --- |\n");
    for t in outcomes {
        out.push_str(&format!(
            "| `{}` | `{}` | `{}` | `{}` | `{:?}` |\n",
            md_escape(&t.name),
            md_escape(t.expected_version.as_deref().unwrap_or("-")),
            md_escape(t.observed_version.as_deref().unwrap_or("-")),
            t.classification.as_str(),
            t.status,
        ));
    }
    out.push('\n');
}

fn markdown_package_table(out: &mut String, outcomes: &[PackageProbeOutcome]) {
    if outcomes.is_empty() {
        return;
    }
    out.push_str(
        "### Packages\n\n| Name | Manager | Expected | Observed | Classification | Status |\n",
    );
    out.push_str("| --- | --- | --- | --- | --- | --- |\n");
    for p in outcomes {
        out.push_str(&format!(
            "| `{}` | `{}` | `{}` | `{}` | `{}` | `{:?}` |\n",
            md_escape(&p.name),
            md_escape(&p.manager),
            md_escape(p.expected_version.as_deref().unwrap_or("-")),
            md_escape(p.observed_version.as_deref().unwrap_or("-")),
            p.classification.as_str(),
            p.status,
        ));
    }
    out.push('\n');
}

fn markdown_capability_table(out: &mut String, outcomes: &[CapabilityProbeOutcome]) {
    if outcomes.is_empty() {
        return;
    }
    out.push_str("### Capabilities\n\n| Name | Classification | Status |\n");
    out.push_str("| --- | --- | --- |\n");
    for c in outcomes {
        out.push_str(&format!(
            "| `{}` | `{}` | `{:?}` |\n",
            md_escape(&c.name),
            c.classification.as_str(),
            c.status,
        ));
    }
    out.push('\n');
}

fn markdown_checks_table(out: &mut String, checks: &[Check]) {
    if checks.is_empty() {
        return;
    }
    out.push_str("### Checks\n\n| Status | ID | Message |\n");
    out.push_str("| --- | --- | --- |\n");
    for c in checks {
        out.push_str(&format!(
            "| `{}` | `{}` | {} |\n",
            status_word(c.status),
            md_escape(&c.id),
            md_escape(&c.message),
        ));
    }
    out.push('\n');
}

fn status_symbol(status: CheckStatus) -> &'static str {
    match status {
        CheckStatus::Pass => "[PASS]",
        CheckStatus::Warn => "[WARN]",
        CheckStatus::Fail => "[FAIL]",
        CheckStatus::Skip => "[SKIP]",
    }
}

fn status_word(status: CheckStatus) -> &'static str {
    match status {
        CheckStatus::Pass => "pass",
        CheckStatus::Warn => "warn",
        CheckStatus::Fail => "fail",
        CheckStatus::Skip => "skip",
    }
}

fn md_escape(value: &str) -> String {
    value.replace('|', "\\|").replace('\n', "<br>")
}
