//! The posture assessor: a pure function from a normalized manifest to a
//! scored report. Each dimension is one small function so the scoring rules
//! stay readable and individually testable.

use super::manifest::{DeploymentKind, DeploymentManifest};
use super::model::{Dimension, Evidence, PostureCheck, PostureReport, Status};

/// Score every posture dimension over a manifest.
pub fn assess(manifest: &DeploymentManifest, target: &str) -> PostureReport {
    let checks = vec![
        isolation(manifest),
        filesystem_scope(manifest),
        network_egress(manifest),
        secret_handling(manifest),
        auth_identity(manifest),
        registry_provenance(manifest),
        policy_enforcement(manifest),
        telemetry(manifest),
        audit_logging(manifest),
    ];
    PostureReport::from_checks(manifest.kind.label(), target, checks)
}

/// Isolation: a container with a read-only root and non-root user passes; a
/// bare stdio subprocess fails (observed: it runs with the caller's privileges).
fn isolation(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::Isolation;
    match m.containerized {
        Some(false) => PostureCheck::new(
            d,
            Status::Fail,
            Evidence::Observed,
            "runs as a bare subprocess with the caller's user, filesystem, and network",
        )
        .with_remediation("run the server in a container or sandbox (see docs/runtime-posture.md)"),
        Some(true) => {
            let hardened = m.read_only_root_fs == Some(true) && m.run_as_non_root != Some(false);
            if hardened {
                PostureCheck::new(
                    d,
                    Status::Pass,
                    Evidence::Claimed,
                    "containerized with a read-only root filesystem and non-root user",
                )
            } else {
                PostureCheck::new(
                    d,
                    Status::Warn,
                    Evidence::Claimed,
                    "containerized, but not confirmed read-only-root and non-root",
                )
                .with_remediation("set readOnlyRootFilesystem and runAsNonRoot")
            }
        }
        None => unknown(d, "deployment did not declare whether it is containerized"),
    }
}

/// Filesystem scope: an explicit allowlist passes; otherwise unknown/warn.
fn filesystem_scope(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::FilesystemScope;
    if !m.filesystem_allow.is_empty() {
        PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Claimed,
            &format!("scoped to {} explicit path(s)", m.filesystem_allow.len()),
        )
    } else if m.read_only_root_fs == Some(true) {
        PostureCheck::new(
            d,
            Status::Warn,
            Evidence::Claimed,
            "read-only root, but no explicit filesystem allowlist",
        )
        .with_remediation("declare the exact paths the server may read and write")
    } else if m.kind == DeploymentKind::Stdio {
        PostureCheck::new(
            d,
            Status::Fail,
            Evidence::Observed,
            "no filesystem scoping: a stdio subprocess inherits the caller's access",
        )
        .with_remediation("declare a filesystem allowlist or sandbox the server")
    } else {
        unknown(d, "no filesystem scope declared")
    }
}

/// Network egress: a present allowlist (even empty, meaning deny-all) passes.
fn network_egress(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::NetworkEgress;
    match &m.egress_allow {
        Some(list) if list.is_empty() => PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Claimed,
            "egress denied (empty allowlist)",
        ),
        Some(list) => PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Claimed,
            &format!("egress restricted to {} host(s)", list.len()),
        ),
        None if m.kind == DeploymentKind::Stdio => PostureCheck::new(
            d,
            Status::Fail,
            Evidence::Observed,
            "no egress controls: a stdio subprocess can reach any network the host can",
        )
        .with_remediation("run behind an egress allowlist or a gateway"),
        None => unknown(d, "no egress policy declared"),
    }
}

/// Secret handling: a vault/file source passes, env warns, none/unknown.
fn secret_handling(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::SecretHandling;
    match m.secret_source.as_deref() {
        Some("vault") | Some("file") => PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Claimed,
            "secrets sourced from a managed store",
        ),
        Some("env") => PostureCheck::new(
            d,
            Status::Warn,
            Evidence::Claimed,
            "secrets passed via environment variables",
        )
        .with_remediation("prefer a file or vault mount over inline env secrets"),
        Some("none") => {
            PostureCheck::new(d, Status::Pass, Evidence::Claimed, "no secrets required")
        }
        _ => unknown(d, "secret source not declared"),
    }
}

/// Auth/identity: a real mode passes, "none" fails, missing is unknown.
fn auth_identity(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::AuthIdentity;
    match m.auth_mode.as_deref() {
        Some("none") | Some("") => PostureCheck::new(
            d,
            Status::Fail,
            Evidence::Claimed,
            "no identity enforcement on the server",
        )
        .with_remediation("require OAuth, bearer, or mTLS for production exposure"),
        Some(mode) => PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Claimed,
            &format!("identity enforced via {mode}"),
        ),
        None => unknown(d, "auth mode not declared"),
    }
}

/// Registry provenance: a pinned (digest) source passes, an unpinned one warns.
fn registry_provenance(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::RegistryProvenance;
    match (&m.registry_source, m.registry_pinned, m.registry_signed) {
        (Some(_), Some(true), _) | (Some(_), _, Some(true)) => PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Observed,
            "artifact is digest-pinned or signed",
        ),
        (Some(src), _, _) => PostureCheck::new(
            d,
            Status::Warn,
            Evidence::Observed,
            &format!("sourced from {src} but not digest-pinned or signed"),
        )
        .with_remediation("pin the image by @sha256 digest and sign it"),
        (None, _, _) => unknown(d, "no artifact provenance declared"),
    }
}

/// Policy enforcement: a named engine passes, otherwise unknown.
fn policy_enforcement(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::PolicyEnforcement;
    match m.policy_engine.as_deref() {
        Some(engine) => PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Claimed,
            &format!("tool calls gated by {engine}"),
        ),
        None => unknown(d, "no external policy engine declared"),
    }
}

/// Telemetry: OTel or Prometheus passes, neither warns when stated.
fn telemetry(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::Telemetry;
    if m.otel == Some(true) || m.prometheus == Some(true) {
        PostureCheck::new(
            d,
            Status::Pass,
            Evidence::Claimed,
            "exports OpenTelemetry or Prometheus metrics",
        )
    } else if m.otel == Some(false) && m.prometheus == Some(false) {
        PostureCheck::new(
            d,
            Status::Warn,
            Evidence::Claimed,
            "no telemetry export configured",
        )
        .with_remediation("enable OTel traces or a Prometheus endpoint")
    } else {
        unknown(d, "telemetry readiness not declared")
    }
}

/// Audit logging: an explicit yes passes, explicit no fails, missing unknown.
fn audit_logging(m: &DeploymentManifest) -> PostureCheck {
    let d = Dimension::AuditLogging;
    match m.audit_logging {
        Some(true) => {
            PostureCheck::new(d, Status::Pass, Evidence::Claimed, "audit logging enabled")
        }
        Some(false) => PostureCheck::new(
            d,
            Status::Warn,
            Evidence::Claimed,
            "audit logging explicitly disabled",
        )
        .with_remediation("enable an audit log for tool-call accountability"),
        None => unknown(d, "audit logging not declared"),
    }
}

/// Helper for the common unknown check.
fn unknown(dimension: Dimension, detail: &str) -> PostureCheck {
    PostureCheck::new(dimension, Status::Unknown, Evidence::Unknown, detail)
}
