//! Skill trust & lifecycle governance (arXiv 2602.12430, *Agent Skills for Large
//! Language Models: Architecture, Acquisition, Security, and the Path Forward*).
//!
//! See `docs/proposals/skill-trust-governance.md`. Agent skills are
//! filesystem-based capability packages loaded on demand — a supply-chain
//! surface (the survey reports **26.1% of community-contributed skills contain
//! vulnerabilities**). The paper's defense is a four-tier, gate-based permission
//! model mapping skill *provenance* to graduated *deployment capabilities*.
//!
//! This is that model as a pure decision core: classify a skill's provenance
//! into a [`TrustTier`], then gate the [`PermissionTier`] capability ceiling that
//! tier permits. It reuses CAR's existing capability vocabulary
//! ([`crate::permission::PermissionTier`]) and the `car-memgine`
//! skill-degradation rule, rather than inventing parallel concepts. Distinct
//! from the *action-level* permission gate: this governs *which skills may load,
//! and at what ceiling, by provenance* — before they run.

use crate::permission::{ApprovalDecision, ApprovalLedger, PermissionTier};
use serde::{Deserialize, Serialize};

/// Where a skill came from — one provenance signal among several.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SkillSource {
    /// Shipped/endorsed by the official skill registry.
    Official,
    /// From the operator's own first-party catalog.
    FirstParty,
    /// Community-contributed.
    Community,
    /// Provenance unknown.
    Unknown,
}

impl Default for SkillSource {
    fn default() -> Self {
        SkillSource::Unknown
    }
}

/// The provenance + lifecycle signals a skill carries. The caller folds these
/// from `car-bundle` (signature), a scanner (vulnerabilities), and the memgine
/// skill stats (track record); omitted fields default conservatively.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SkillProvenance {
    /// The skill bundle carries an ed25519 signature (`car-bundle`).
    #[serde(default)]
    pub signed: bool,
    /// The signature verified against a trusted keyring.
    #[serde(default)]
    pub signer_trusted: bool,
    /// A vulnerability scan was run.
    #[serde(default)]
    pub scanned: bool,
    /// Scan findings — any > 0 makes the skill untrusted (the 26.1% problem).
    #[serde(default)]
    pub vulnerabilities: u64,
    #[serde(default)]
    pub source: SkillSource,
    /// Lifecycle track record (from the memgine skill stats).
    #[serde(default)]
    pub success_count: u64,
    #[serde(default)]
    pub fail_count: u64,
}

/// The four trust tiers, ascending. `Ord` so a ceiling comparison is direct.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
    Untrusted,
    Community,
    Verified,
    Official,
}

/// What the gate decided about a skill's requested deployment capability.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeploymentOutcome {
    /// Granted at the requested tier.
    Allow,
    /// Granted, but capped below the requested tier.
    Downgrade,
    /// Not permitted to deploy at all.
    Deny,
}

/// The full gate decision, with the trust tier, the capability ceiling that tier
/// allows, what was actually granted, and a human-readable reason.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SkillDeploymentDecision {
    pub trust: TrustTier,
    /// The max capability this trust tier permits; `None` ⇒ deny deployment.
    pub ceiling: Option<PermissionTier>,
    /// `min(requested, ceiling)`; `None` when denied.
    pub granted: Option<PermissionTier>,
    pub outcome: DeploymentOutcome,
    pub reason: String,
}

/// Whether a skill's track record marks it degraded — the same rule
/// `car-memgine` uses to auto-degrade a skill (`fail > success + 2`).
fn degraded(p: &SkillProvenance) -> bool {
    p.fail_count > p.success_count + 2
}

/// Classify a skill's provenance into a trust tier. Deterministic. The two hard
/// demotions to `Untrusted` encode the paper's lifecycle + security points.
pub fn classify_trust(p: &SkillProvenance) -> TrustTier {
    if degraded(p) {
        return TrustTier::Untrusted; // lifecycle demotion
    }
    if p.vulnerabilities > 0 {
        return TrustTier::Untrusted; // vulnerable ⇒ untrusted regardless of signature
    }
    if p.signed && p.signer_trusted && p.scanned && p.source == SkillSource::Official {
        TrustTier::Official
    } else if p.signed && p.scanned {
        TrustTier::Verified
    } else if p.scanned {
        TrustTier::Community
    } else {
        TrustTier::Untrusted
    }
}

/// The capability ceiling a trust tier permits. `None` ⇒ deny deployment.
pub fn ceiling(tier: TrustTier) -> Option<PermissionTier> {
    match tier {
        TrustTier::Official => Some(PermissionTier::FullAccess),
        TrustTier::Verified => Some(PermissionTier::SandboxEdit),
        TrustTier::Community => Some(PermissionTier::ReadOnly),
        TrustTier::Untrusted => None,
    }
}

/// Gate a skill's requested deployment capability against its provenance.
/// Grants `min(requested, ceiling)`. Pure and deterministic.
pub fn gate_skill_deployment(
    p: &SkillProvenance,
    requested: PermissionTier,
) -> SkillDeploymentDecision {
    let trust = classify_trust(p);
    let ceiling = ceiling(trust);
    match ceiling {
        None => SkillDeploymentDecision {
            trust,
            ceiling,
            granted: None,
            outcome: DeploymentOutcome::Deny,
            reason: format!(
                "skill is {trust:?}: deployment denied (unsigned/unscanned, vulnerable, or \
                 degraded provenance)"
            ),
        },
        Some(cap) => {
            let granted = requested.min(cap);
            let outcome = if granted == requested {
                DeploymentOutcome::Allow
            } else {
                DeploymentOutcome::Downgrade
            };
            let reason = match outcome {
                DeploymentOutcome::Allow => format!(
                    "skill is {trust:?}: granted {} (≤ {} ceiling)",
                    granted.as_str(),
                    cap.as_str()
                ),
                DeploymentOutcome::Downgrade => format!(
                    "skill is {trust:?}: requested {} exceeds the {} ceiling — downgraded to {}",
                    requested.as_str(),
                    cap.as_str(),
                    granted.as_str()
                ),
                DeploymentOutcome::Deny => unreachable!(),
            };
            SkillDeploymentDecision {
                trust,
                ceiling,
                granted: Some(granted),
                outcome,
                reason,
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Slice 4 — load-time HITL enforcement bridge.
//
// `gate_skill_deployment` decides *what a skill's provenance earns*. This
// section decides *what actually happens at load time* once a human's standing
// decisions are taken into account — the analogue of `flow_gate::enforce_flow`,
// reusing the same durable `ApprovalLedger` substrate (survey §5.2.5). It is the
// piece that turns the gate from "advisory verdict" into "the skill does or does
// not deploy."
// ---------------------------------------------------------------------------

/// A stable fingerprint for a skill-deployment escalation, so an operator's
/// decision to override a `Deny` is remembered across runs via the
/// [`ApprovalLedger`]. Keyed by the skill name and the *requested* tier (the
/// capability being escalated) — the same skill asking for the same tier maps to
/// the same ledger entry.
pub fn skill_deployment_fingerprint(skill_name: &str, requested: PermissionTier) -> String {
    format!("skill_deploy:{skill_name}:{}", requested.as_str())
}

/// A denied skill deployment awaiting a human decision (an operator may choose
/// to override the provenance verdict).
#[derive(Debug, Clone, Serialize)]
pub struct PendingSkillApproval {
    pub fingerprint: String,
    pub skill_name: String,
    pub requested: PermissionTier,
    pub decision: SkillDeploymentDecision,
}

/// The result of enforcing a [`SkillDeploymentDecision`] against the approval
/// ledger — the load-time verdict the runtime acts on.
#[derive(Debug, Clone, Serialize)]
pub struct SkillDeploymentEnforcement {
    /// True when the skill may deploy now, at `effective_tier`.
    pub deploy: bool,
    /// The tier to deploy at when `deploy` is true; `None` when blocked or
    /// pending.
    pub effective_tier: Option<PermissionTier>,
    /// True when an operator override lifted a `Deny` (deploying at the
    /// requested tier despite the provenance verdict).
    pub overridden: bool,
    /// True when deployment is refused: a `Deny` an operator explicitly
    /// rejected.
    pub blocked: bool,
    /// Set when a human decision is required before the skill can deploy.
    pub pending: Option<PendingSkillApproval>,
    pub reason: String,
}

/// Enforce a [`SkillDeploymentDecision`] against the durable [`ApprovalLedger`]
/// at load time.
///
/// - `Allow` → deploy at the granted tier autonomously.
/// - `Downgrade` → deploy at the *capped* granted tier autonomously: a downgrade
///   is the gate's own safe mitigation (the skill still loads, just with less
///   capability), so it is not a hazard and needs no human. The host should
///   surface the downgrade, but it does not block load.
/// - `Deny` → the hazard. Resolved against the ledger by
///   [`skill_deployment_fingerprint`]: an operator who previously **approved**
///   the override deploys at the *requested* tier (`overridden`); **rejected** →
///   `blocked`; **unseen** → `pending` (needs HITL — the skill does not deploy
///   until a human decides).
///
/// The caller records the human decision via the same `permission.approve` /
/// `permission.reject` path the rest of the HITL surface uses, keyed by the
/// pending `fingerprint`.
pub fn enforce_deployment(
    decision: &SkillDeploymentDecision,
    skill_name: &str,
    requested: PermissionTier,
    ledger: &ApprovalLedger,
) -> SkillDeploymentEnforcement {
    match decision.outcome {
        DeploymentOutcome::Allow | DeploymentOutcome::Downgrade => SkillDeploymentEnforcement {
            deploy: true,
            effective_tier: decision.granted,
            overridden: false,
            blocked: false,
            pending: None,
            reason: decision.reason.clone(),
        },
        DeploymentOutcome::Deny => {
            let fp = skill_deployment_fingerprint(skill_name, requested);
            match ledger.lookup(&fp).map(|r| r.decision) {
                Some(ApprovalDecision::Approved) => SkillDeploymentEnforcement {
                    deploy: true,
                    effective_tier: Some(requested),
                    overridden: true,
                    blocked: false,
                    pending: None,
                    reason: format!(
                        "skill '{skill_name}' was denied by provenance, but an operator approved \
                         deploying it at {} anyway",
                        requested.as_str()
                    ),
                },
                Some(ApprovalDecision::Rejected) => SkillDeploymentEnforcement {
                    deploy: false,
                    effective_tier: None,
                    overridden: false,
                    blocked: true,
                    pending: None,
                    reason: format!(
                        "skill '{skill_name}' deployment was rejected by an operator"
                    ),
                },
                None => SkillDeploymentEnforcement {
                    deploy: false,
                    effective_tier: None,
                    overridden: false,
                    blocked: false,
                    pending: Some(PendingSkillApproval {
                        fingerprint: fp,
                        skill_name: skill_name.to_string(),
                        requested,
                        decision: decision.clone(),
                    }),
                    reason: format!(
                        "skill '{skill_name}' denied by provenance — awaiting an operator decision \
                         to override"
                    ),
                },
            }
        }
    }
}

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

    fn prov() -> SkillProvenance {
        SkillProvenance::default()
    }

    #[test]
    fn official_signed_scanned_is_official_full_access() {
        let p = SkillProvenance {
            signed: true,
            signer_trusted: true,
            scanned: true,
            source: SkillSource::Official,
            ..prov()
        };
        assert_eq!(classify_trust(&p), TrustTier::Official);
        let d = gate_skill_deployment(&p, PermissionTier::FullAccess);
        assert_eq!(d.outcome, DeploymentOutcome::Allow);
        assert_eq!(d.granted, Some(PermissionTier::FullAccess));
    }

    #[test]
    fn signed_scanned_non_official_is_verified() {
        let p = SkillProvenance {
            signed: true,
            scanned: true,
            source: SkillSource::Community,
            ..prov()
        };
        assert_eq!(classify_trust(&p), TrustTier::Verified);
        // Requesting full access is downgraded to the SandboxEdit ceiling.
        let d = gate_skill_deployment(&p, PermissionTier::FullAccess);
        assert_eq!(d.outcome, DeploymentOutcome::Downgrade);
        assert_eq!(d.granted, Some(PermissionTier::SandboxEdit));
    }

    #[test]
    fn scanned_unsigned_is_community_read_only() {
        let p = SkillProvenance {
            scanned: true,
            ..prov()
        };
        assert_eq!(classify_trust(&p), TrustTier::Community);
        // ReadOnly request is allowed at the ceiling.
        let d = gate_skill_deployment(&p, PermissionTier::ReadOnly);
        assert_eq!(d.outcome, DeploymentOutcome::Allow);
        // A SandboxEdit request downgrades to ReadOnly.
        let d2 = gate_skill_deployment(&p, PermissionTier::SandboxEdit);
        assert_eq!(d2.outcome, DeploymentOutcome::Downgrade);
        assert_eq!(d2.granted, Some(PermissionTier::ReadOnly));
    }

    #[test]
    fn unsigned_unscanned_is_untrusted_denied() {
        let d = gate_skill_deployment(&prov(), PermissionTier::ReadOnly);
        assert_eq!(d.trust, TrustTier::Untrusted);
        assert_eq!(d.outcome, DeploymentOutcome::Deny);
        assert_eq!(d.granted, None);
    }

    #[test]
    fn vulnerabilities_force_untrusted_even_if_signed() {
        // Fully signed + official, but the scan found a vuln → untrusted, denied.
        let p = SkillProvenance {
            signed: true,
            signer_trusted: true,
            scanned: true,
            vulnerabilities: 1,
            source: SkillSource::Official,
            ..prov()
        };
        assert_eq!(classify_trust(&p), TrustTier::Untrusted);
        assert_eq!(
            gate_skill_deployment(&p, PermissionTier::ReadOnly).outcome,
            DeploymentOutcome::Deny
        );
    }

    #[test]
    fn degraded_track_record_demotes_to_untrusted() {
        // An otherwise-official skill that's failing in the field (fail > success+2).
        let p = SkillProvenance {
            signed: true,
            signer_trusted: true,
            scanned: true,
            source: SkillSource::Official,
            success_count: 1,
            fail_count: 5,
            ..prov()
        };
        assert_eq!(classify_trust(&p), TrustTier::Untrusted);
    }

    #[test]
    fn healthy_track_record_does_not_demote() {
        let p = SkillProvenance {
            signed: true,
            signer_trusted: true,
            scanned: true,
            source: SkillSource::Official,
            success_count: 10,
            fail_count: 3,
            ..prov()
        };
        assert_eq!(classify_trust(&p), TrustTier::Official);
    }

    // --- Slice 4: load-time HITL enforcement ---

    use crate::permission::{ApprovalDecision, ApprovalLedger, ApprovalRecord};

    fn approval(fp: &str, decision: ApprovalDecision) -> ApprovalRecord {
        ApprovalRecord {
            fingerprint: fp.to_string(),
            required_tier: PermissionTier::FullAccess,
            decision,
            reviewer: "human".into(),
            reason: "test".into(),
            evidence: None,
            decided_at: "2026-06-30T00:00:00Z".into(),
        }
    }

    #[test]
    fn enforce_allow_deploys_at_granted() {
        let p = SkillProvenance {
            signed: true,
            signer_trusted: true,
            scanned: true,
            source: SkillSource::Official,
            ..prov()
        };
        let d = gate_skill_deployment(&p, PermissionTier::FullAccess);
        let e = enforce_deployment(&d, "deployer", PermissionTier::FullAccess, &ApprovalLedger::new());
        assert!(e.deploy);
        assert_eq!(e.effective_tier, Some(PermissionTier::FullAccess));
        assert!(!e.overridden && !e.blocked && e.pending.is_none());
    }

    #[test]
    fn enforce_downgrade_deploys_autonomously_at_cap() {
        // Community → ReadOnly ceiling; a SandboxEdit request downgrades but still
        // deploys, with no human in the loop.
        let p = SkillProvenance {
            scanned: true,
            ..prov()
        };
        let d = gate_skill_deployment(&p, PermissionTier::SandboxEdit);
        assert_eq!(d.outcome, DeploymentOutcome::Downgrade);
        let e = enforce_deployment(&d, "capped", PermissionTier::SandboxEdit, &ApprovalLedger::new());
        assert!(e.deploy, "a downgrade is a safe mitigation, not a hazard");
        assert_eq!(e.effective_tier, Some(PermissionTier::ReadOnly));
        assert!(e.pending.is_none() && !e.blocked);
    }

    #[test]
    fn enforce_deny_unseen_is_pending() {
        let d = gate_skill_deployment(&prov(), PermissionTier::ReadOnly);
        assert_eq!(d.outcome, DeploymentOutcome::Deny);
        let e = enforce_deployment(&d, "shady", PermissionTier::ReadOnly, &ApprovalLedger::new());
        assert!(!e.deploy && !e.blocked);
        let pending = e.pending.expect("a novel deny awaits a human");
        assert_eq!(pending.skill_name, "shady");
        assert_eq!(
            pending.fingerprint,
            skill_deployment_fingerprint("shady", PermissionTier::ReadOnly)
        );
    }

    #[test]
    fn enforce_deny_previously_approved_is_overridden() {
        let d = gate_skill_deployment(&prov(), PermissionTier::SandboxEdit);
        let fp = skill_deployment_fingerprint("trusted-anyway", PermissionTier::SandboxEdit);
        let mut ledger = ApprovalLedger::new();
        ledger.record(approval(&fp, ApprovalDecision::Approved)).unwrap();
        let e = enforce_deployment(&d, "trusted-anyway", PermissionTier::SandboxEdit, &ledger);
        assert!(e.deploy && e.overridden);
        // The operator override grants the *requested* tier despite the deny.
        assert_eq!(e.effective_tier, Some(PermissionTier::SandboxEdit));
    }

    #[test]
    fn enforce_deny_previously_rejected_is_blocked() {
        let d = gate_skill_deployment(&prov(), PermissionTier::ReadOnly);
        let fp = skill_deployment_fingerprint("nope", PermissionTier::ReadOnly);
        let mut ledger = ApprovalLedger::new();
        ledger.record(approval(&fp, ApprovalDecision::Rejected)).unwrap();
        let e = enforce_deployment(&d, "nope", PermissionTier::ReadOnly, &ledger);
        assert!(!e.deploy && e.blocked && e.pending.is_none());
    }

    #[test]
    fn fingerprint_is_stable_per_skill_and_tier() {
        assert_eq!(
            skill_deployment_fingerprint("s", PermissionTier::ReadOnly),
            skill_deployment_fingerprint("s", PermissionTier::ReadOnly)
        );
        assert_ne!(
            skill_deployment_fingerprint("s", PermissionTier::ReadOnly),
            skill_deployment_fingerprint("s", PermissionTier::FullAccess)
        );
    }
}
