//! Manifest format, canonicalization, and ed25519 signing for
//! CAR contributed-agent bundles (Parslee-ai/car#182).
//!
//! ## Scope
//!
//! This crate owns the on-disk shape and crypto primitives shared
//! between the supervisor (loads + verifies installed agents),
//! the CLI (publishes + signs new agents), and the registry
//! (serves signed manifests). It is `no-runtime` — pure data,
//! pure functions, no async, no I/O outside `read_to_string` for
//! tests. The supervisor and CLI hold the I/O.
//!
//! ## Phase status
//!
//! - **Phase 1** (`car-registry`): the manifest format landed
//!   inline at `car_registry::manifest`. The supervisor dual-reads
//!   legacy `agents.json` and the new `~/.car/agents/<id>/manifest.toml`
//!   layout. Signature verification was stubbed out.
//! - **Phase 2** (this crate): types extracted here; ed25519
//!   sign/verify added; manifest-level canonicalization landed.
//!   The supervisor wires verification with warn-but-not-reject
//!   semantics so existing setups keep working while operators
//!   sign their agents.
//! - **Phase 3+**: full-bundle canonicalization (multi-file:
//!   `identity.md`, `skills.jsonl`, `policies.json`, …) per
//!   `docs/agent-bundle-spec.md §canonicalization`. Today's
//!   `canonical_manifest_bytes` covers only the single
//!   `manifest.toml` file — sufficient for `external_process`
//!   bundles which carry no auxiliary data files.

use std::collections::BTreeMap;
use std::path::PathBuf;

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

#[derive(Debug, thiserror::Error)]
pub enum BundleError {
    #[error("manifest is not valid TOML: {0}")]
    InvalidToml(String),
    #[error("manifest is not valid JSON: {0}")]
    InvalidJson(String),
    #[error("manifest validation failed: {0}")]
    Validation(String),
    #[error("signature verification failed: {0}")]
    SignatureInvalid(String),
    #[error("publisher key is malformed: {0}")]
    KeyMalformed(String),
    #[error("missing publisher info on signed manifest")]
    PublisherMissing,
    #[error("bundle I/O error: {0}")]
    Io(#[from] std::io::Error),
}

/// `manifest.toml` top-level structure. One file per installed
/// agent at `~/.car/agents/<id>/manifest.toml`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentManifest {
    pub agent: AgentIdentity,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub publisher: Option<PublisherInfo>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub runtime: Option<RuntimeRequirements>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lifecycle: Option<LifecyclePolicy>,
    pub transport: TransportSpec,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub capabilities: Option<CapabilityDeclarations>,
}

/// `[agent]` block.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentIdentity {
    pub id: String,
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub namespace: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub license: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub homepage: Option<String>,
}

/// `[publisher]` block. The `signature` field is the base64-
/// encoded ed25519 signature over the canonicalized manifest
/// (i.e., the manifest with `publisher.signature` cleared).
/// `key_id` is the ed25519 public key, base64-encoded (32 bytes
/// raw → 44 char base64).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PublisherInfo {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub key_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct RuntimeRequirements {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub car_min_version: Option<String>,
    #[serde(default = "default_bundle_format_version")]
    pub bundle_format_version: u32,
}

fn default_bundle_format_version() -> u32 {
    1
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LifecyclePolicy {
    #[serde(default)]
    pub stateful: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub persistence: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_inference_complexity: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TransportSpec {
    PureData,
    ExternalProcess(ExternalProcessTransport),
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExternalProcessTransport {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sha256: Option<String>,
    /// Optional `https://` URL the publisher hosts the binary at.
    /// When present, `car install` fetches the binary from this
    /// URL, verifies the digest against `sha256`, and writes the
    /// resulting file at the local `command` path before adoption
    /// (Parslee-ai/car#182 phase 5). Mutually exclusive with
    /// `health_url`. Locally-developed manifests can leave this
    /// unset and ship `command` pointing at a binary the
    /// developer placed there manually.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub binary_url: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub health_url: Option<String>,
    #[serde(default)]
    pub args: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<PathBuf>,
    #[serde(default)]
    pub env: BTreeMap<String, String>,
    #[serde(default)]
    pub restart: RestartPolicy,
    #[serde(default = "default_max_restarts")]
    pub max_restarts: u32,
    #[serde(default = "default_backoff")]
    pub backoff_secs: u64,
    #[serde(default)]
    pub auto_start: bool,
    #[serde(default)]
    pub token: String,
}

fn default_max_restarts() -> u32 {
    10
}

fn default_backoff() -> u64 {
    5
}

/// Restart policy mirrors the supervisor's surface. Re-declared
/// here (rather than re-exported from `car-registry`) so this
/// crate stays standalone — `car-registry` depends on
/// `car-bundle`, not the other way around.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RestartPolicy {
    Never,
    #[default]
    OnFailure,
    Always,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapabilityDeclarations {
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub required: BTreeMap<String, Vec<String>>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub optional: BTreeMap<String, Vec<String>>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub denied: BTreeMap<String, Vec<String>>,
}

impl AgentManifest {
    pub fn is_pure_data(&self) -> bool {
        matches!(self.transport, TransportSpec::PureData)
    }

    pub fn is_remote_service(&self) -> bool {
        matches!(
            &self.transport,
            TransportSpec::ExternalProcess(t) if t.health_url.is_some() && t.command.is_none()
        )
    }

    /// Parse a `manifest.toml` text. Does NOT verify the signature
    /// — pair with [`verify_signature`] when verification is
    /// required.
    pub fn from_toml_str(text: &str) -> Result<Self, BundleError> {
        toml::from_str(text).map_err(|e| BundleError::InvalidToml(e.to_string()))
    }

    /// Serialize back to canonical TOML text. Round-trips through
    /// `to_string_pretty` and back via `from_toml_str`.
    pub fn to_toml_string(&self) -> Result<String, BundleError> {
        toml::to_string_pretty(self).map_err(|e| BundleError::InvalidToml(e.to_string()))
    }
}

// ---------------------------------------------------------------------
// Canonicalization
// ---------------------------------------------------------------------

/// Produce canonical bytes of a manifest for signing / verification.
///
/// Canonicalization rules for the phase-2 single-file form:
///
/// 1. The `publisher.signature` field is cleared (a signature
///    cannot sign itself).
/// 2. The manifest is serialized to JSON (not TOML — TOML lacks a
///    spec-mandated canonical form; serde_json with sorted keys
///    via `BTreeMap` is well-defined).
/// 3. Whitespace is stripped (no pretty-printing).
/// 4. Output is UTF-8 bytes, LF line endings.
///
/// Multi-file bundle canonicalization (per
/// `docs/agent-bundle-spec.md §canonicalization`) lands in a later
/// phase when pure-data bundles need it.
pub fn canonical_manifest_bytes(manifest: &AgentManifest) -> Result<Vec<u8>, BundleError> {
    let mut cleared = manifest.clone();
    if let Some(pub_info) = cleared.publisher.as_mut() {
        pub_info.signature = None;
    }
    // JSON because it has a deterministic canonical form when
    // keys are sorted, and the spec is unambiguous. TOML's
    // round-trip whitespace handling is loose enough that two
    // serializers can disagree on the bytes.
    serde_json::to_vec(&cleared).map_err(|e| BundleError::InvalidJson(e.to_string()))
}

/// SHA-256 hex digest of the canonical manifest bytes. Useful for
/// content-addressed lookups (registry caching, etc.) without
/// requiring signature verification.
pub fn manifest_digest_hex(manifest: &AgentManifest) -> Result<String, BundleError> {
    let bytes = canonical_manifest_bytes(manifest)?;
    let mut hasher = Sha256::new();
    hasher.update(&bytes);
    Ok(hex(&hasher.finalize()))
}

/// SHA-256 hex digest of an arbitrary byte slice. Used by
/// `car install` to verify binaries fetched via
/// `transport.binary_url` against the manifest's
/// `transport.sha256` (Parslee-ai/car#182 phase 5).
pub fn sha256_hex(bytes: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    hex(&hasher.finalize())
}

/// Verify a byte slice matches a hex-encoded SHA-256 digest.
/// Comparison is constant-time-ish (case-insensitive hex
/// equality), and returns an error rather than a bool so the
/// failure message can name the expected + actual digests.
pub fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<(), BundleError> {
    let actual = sha256_hex(bytes);
    if actual.eq_ignore_ascii_case(expected_hex) {
        Ok(())
    } else {
        Err(BundleError::SignatureInvalid(format!(
            "sha256 mismatch: expected `{expected_hex}`, got `{actual}`"
        )))
    }
}

fn hex(bytes: &[u8]) -> String {
    let mut out = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        out.push_str(&format!("{:02x}", b));
    }
    out
}

// ---------------------------------------------------------------------
// Signing + verification
// ---------------------------------------------------------------------

/// Sign a manifest in place: writes the public key id into
/// `publisher.key_id`, then serializes canonical bytes (which
/// clear the signature but include the key_id), signs them with
/// `key`, and writes the base64 signature into
/// `publisher.signature`. Replaces any existing signature.
///
/// Ordering matters: key_id MUST be set BEFORE computing canonical
/// bytes for signing, so verification sees the same input. A
/// previous draft set key_id after computing the bytes and the
/// signature failed to verify — caught by
/// `sign_then_verify_round_trip`.
pub fn sign_manifest(manifest: &mut AgentManifest, key: &SigningKey) -> Result<(), BundleError> {
    let pub_info = manifest
        .publisher
        .get_or_insert_with(PublisherInfo::default);
    pub_info.key_id = Some(encode_base64(key.verifying_key().as_bytes()));
    pub_info.signature = None;
    let bytes = canonical_manifest_bytes(manifest)?;
    let signature = key.sign(&bytes);
    // Re-borrow — the canonical-bytes call took an immutable view.
    manifest.publisher.as_mut().unwrap().signature = Some(encode_base64(&signature.to_bytes()));
    Ok(())
}

/// Verify a manifest's signature against the embedded
/// `publisher.key_id`. Returns `Ok(())` on success, `Err(...)` on
/// any failure (missing publisher, malformed key, mismatched
/// signature). A manifest with no `publisher` block is treated as
/// unsigned and rejected — callers that want to accept unsigned
/// manifests should not call this function.
pub fn verify_signature(manifest: &AgentManifest) -> Result<(), BundleError> {
    let pub_info = manifest
        .publisher
        .as_ref()
        .ok_or(BundleError::PublisherMissing)?;
    let key_b64 = pub_info
        .key_id
        .as_deref()
        .ok_or_else(|| BundleError::KeyMalformed("missing key_id".into()))?;
    let sig_b64 = pub_info
        .signature
        .as_deref()
        .ok_or_else(|| BundleError::SignatureInvalid("missing signature".into()))?;
    let key_bytes = decode_base64(key_b64)
        .map_err(|e| BundleError::KeyMalformed(format!("key_id base64: {e}")))?;
    let key_arr: [u8; 32] = key_bytes
        .as_slice()
        .try_into()
        .map_err(|_| BundleError::KeyMalformed("key_id must be 32 bytes".into()))?;
    let verifying =
        VerifyingKey::from_bytes(&key_arr).map_err(|e| BundleError::KeyMalformed(e.to_string()))?;
    let sig_bytes = decode_base64(sig_b64)
        .map_err(|e| BundleError::SignatureInvalid(format!("signature base64: {e}")))?;
    let sig_arr: [u8; 64] = sig_bytes
        .as_slice()
        .try_into()
        .map_err(|_| BundleError::SignatureInvalid("signature must be 64 bytes".into()))?;
    let signature = Signature::from_bytes(&sig_arr);
    let bytes = canonical_manifest_bytes(manifest)?;
    verifying
        .verify(&bytes, &signature)
        .map_err(|e| BundleError::SignatureInvalid(e.to_string()))
}

fn encode_base64(bytes: &[u8]) -> String {
    use base64::Engine;
    base64::engine::general_purpose::STANDARD.encode(bytes)
}

fn decode_base64(s: &str) -> Result<Vec<u8>, String> {
    use base64::Engine;
    base64::engine::general_purpose::STANDARD
        .decode(s)
        .map_err(|e| e.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::SigningKey;
    use rand_core::OsRng;

    fn sample_manifest() -> AgentManifest {
        AgentManifest {
            agent: AgentIdentity {
                id: "ui-improver".into(),
                name: "UI Improvement".into(),
                namespace: Some("parslee".into()),
                version: Some("0.1.0".into()),
                description: Some("Issues A2UI patches".into()),
                license: Some("Apache-2.0".into()),
                homepage: None,
            },
            publisher: None,
            runtime: Some(RuntimeRequirements {
                car_min_version: Some("0.8.0".into()),
                bundle_format_version: 1,
            }),
            lifecycle: Some(LifecyclePolicy {
                stateful: true,
                persistence: Some("host".into()),
                default_inference_complexity: Some("low".into()),
            }),
            transport: TransportSpec::ExternalProcess(ExternalProcessTransport {
                command: Some("/usr/local/bin/ui-improver".into()),
                binary_url: None,
                sha256: Some("abc123".into()),
                health_url: None,
                args: vec!["--mode".into(), "a2ui".into()],
                cwd: None,
                env: BTreeMap::new(),
                restart: RestartPolicy::OnFailure,
                max_restarts: 10,
                backoff_secs: 5,
                auto_start: false,
                token: String::new(),
            }),
            capabilities: None,
        }
    }

    #[test]
    fn round_trip_through_toml() {
        let m = sample_manifest();
        let text = m.to_toml_string().unwrap();
        let round = AgentManifest::from_toml_str(&text).unwrap();
        assert_eq!(round.agent.id, m.agent.id);
        assert_eq!(round.agent.namespace, m.agent.namespace);
        match (&round.transport, &m.transport) {
            (TransportSpec::ExternalProcess(a), TransportSpec::ExternalProcess(b)) => {
                assert_eq!(a.command, b.command);
                assert_eq!(a.sha256, b.sha256);
                assert_eq!(a.args, b.args);
            }
            _ => panic!("transport kind drift after round-trip"),
        }
    }

    #[test]
    fn contrib_template_manifest_parses_external_process_command() {
        let text = include_str!("../../../examples/contrib-template/manifest.toml");
        let manifest = AgentManifest::from_toml_str(text).unwrap();

        match manifest.transport {
            TransportSpec::ExternalProcess(transport) => {
                assert_eq!(
                    transport.command.as_deref(),
                    Some("/absolute/path/to/contrib-template/agent.sh")
                );
            }
            TransportSpec::PureData => panic!("contrib template must use external_process"),
        }
    }

    #[test]
    fn canonical_bytes_clear_signature_for_signing() {
        let mut m = sample_manifest();
        m.publisher = Some(PublisherInfo {
            key_id: Some("abc".into()),
            signature: Some("REAL_SIG".into()),
        });
        let bytes = canonical_manifest_bytes(&m).unwrap();
        let s = std::str::from_utf8(&bytes).unwrap();
        // The signature is cleared in the canonical form — otherwise
        // a signature couldn't sign itself.
        assert!(!s.contains("REAL_SIG"));
        // key_id stays in (it's a claim, not the signature).
        assert!(s.contains("abc"));
    }

    #[test]
    fn manifest_digest_is_stable() {
        let m = sample_manifest();
        let a = manifest_digest_hex(&m).unwrap();
        let b = manifest_digest_hex(&m).unwrap();
        assert_eq!(a, b);
        assert_eq!(a.len(), 64); // SHA-256 = 32 bytes hex = 64 chars
    }

    #[test]
    fn sign_then_verify_round_trip() {
        let mut m = sample_manifest();
        let key = SigningKey::generate(&mut OsRng);
        sign_manifest(&mut m, &key).unwrap();
        verify_signature(&m).expect("freshly signed manifest must verify");
    }

    #[test]
    fn verify_fails_on_tampered_manifest() {
        let mut m = sample_manifest();
        let key = SigningKey::generate(&mut OsRng);
        sign_manifest(&mut m, &key).unwrap();
        // Tamper with a field after signing.
        if let TransportSpec::ExternalProcess(ref mut t) = m.transport {
            t.command = Some("/tmp/malicious".into());
        }
        let err = verify_signature(&m).expect_err("tampered manifest must fail verify");
        assert!(matches!(err, BundleError::SignatureInvalid(_)));
    }

    #[test]
    fn verify_fails_on_missing_publisher() {
        let m = sample_manifest();
        let err = verify_signature(&m).expect_err("unsigned manifest must error");
        assert!(matches!(err, BundleError::PublisherMissing));
    }

    #[test]
    fn verify_fails_on_wrong_key() {
        let mut m = sample_manifest();
        let key = SigningKey::generate(&mut OsRng);
        sign_manifest(&mut m, &key).unwrap();
        // Substitute a different key_id.
        let other = SigningKey::generate(&mut OsRng);
        m.publisher.as_mut().unwrap().key_id =
            Some(encode_base64(other.verifying_key().as_bytes()));
        let err = verify_signature(&m).expect_err("wrong key_id must fail verify");
        assert!(matches!(err, BundleError::SignatureInvalid(_)));
    }

    #[test]
    fn sha256_hex_matches_known_value() {
        // Empty input → SHA-256 of zero bytes.
        let empty = sha256_hex(b"");
        assert_eq!(
            empty,
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );
        // Known vector for "abc".
        let abc = sha256_hex(b"abc");
        assert_eq!(
            abc,
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }

    #[test]
    fn verify_sha256_accepts_match_rejects_mismatch_and_is_case_insensitive() {
        let bytes = b"agent-binary-payload";
        let digest = sha256_hex(bytes);
        verify_sha256(bytes, &digest).expect("matching digest must verify");
        verify_sha256(bytes, &digest.to_uppercase())
            .expect("verify is case-insensitive on the hex digest");
        let err = verify_sha256(bytes, "00".repeat(32).as_str())
            .expect_err("non-matching digest must fail");
        assert!(matches!(err, BundleError::SignatureInvalid(_)));
    }

    #[test]
    fn signing_is_idempotent_with_same_key() {
        // Signing the same manifest twice with the same key
        // produces identical bytes — ed25519 is deterministic.
        let mut a = sample_manifest();
        let mut b = sample_manifest();
        let key = SigningKey::generate(&mut OsRng);
        sign_manifest(&mut a, &key).unwrap();
        sign_manifest(&mut b, &key).unwrap();
        assert_eq!(
            a.publisher.as_ref().unwrap().signature,
            b.publisher.as_ref().unwrap().signature
        );
    }
}
