//! Ed25519 signing of `RunEvidence` artifacts — Phase 5 of the
//! algol→tsafe migration.
//!
//! # Why this module exists
//!
//! Phase 4 lifted the env-injection enforcement pipeline that emits
//! `RunEvidence` (see [`crate::run_evidence`]). The artifact carries
//! BLAKE3 fingerprints of every observed input (contract bytes, injected
//! secrets, denied env names, host identity) but Phase 4 explicitly
//! deferred cryptographic authorship attestation — i.e. *who* produced
//! the artifact — to Phase 5.
//!
//! Phase 5 closes that gap by signing the artifact with an Ed25519
//! keypair held in the tsafe keyring entry under the `tsafe-attest`
//! purpose. The signature lives on the artifact itself
//! ([`crate::run_evidence::RunEvidence::signature`]) so the wire shape
//! stays a single object and old readers parse the unsigned-equivalent
//! payload via `serde(default)`.
//!
//! # Scope (Phase 5, intentional)
//!
//! - Pure-Rust Ed25519 via [`ed25519_dalek`]; no substrate dep yet.
//!   Future phases may refactor the canonical encoder + sign path into a
//!   reusable cohort substrate (see substrate-design.md §1.2 theme 3),
//!   but for now this is the native implementation.
//! - JCS-style canonical encoding (sorted object keys, no whitespace,
//!   no insignificant fractional zeros on integers) — sufficient for
//!   `RunEvidence`'s flat JSON shape today; a strict RFC 8785 encoder is
//!   over-engineered for a struct with no floating-point or duplicate-
//!   keyed fields.
//! - Domain-tag prefix `tsafe.run_evidence.v1\0` prepended before
//!   signing. Prevents cross-protocol forgery if the same key is ever
//!   used for another tsafe artifact in a later phase.
//!
//! # Out of scope (deferred)
//!
//! - PKI / pubkey-trust management. Verification uses the pubkey
//!   embedded in [`SignaturePayload`] (TOFU). Operators are expected to
//!   pin/verify the pubkey out of band post-launch.
//! - Post-quantum signatures. The domain tag includes the
//!   `run_evidence.v1` version so a future PQ family can ship under
//!   `run_evidence.v2`.
//! - Detached signatures. Phase 5 ships attached signatures only
//!   (`RunEvidence.signature = Some(..)`); detached signing is a future
//!   substrate concern.

use crate::run_evidence::RunEvidence;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey, SIGNATURE_LENGTH};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use thiserror::Error;

/// Domain-tag prefix prepended to canonical bytes before [`SigningKey::sign`].
///
/// Including the schema name + version + a trailing NUL byte makes it
/// impossible to take an Ed25519 signature produced for some other tsafe
/// artifact (or a different RunEvidence schema version) and replay it as
/// a valid `tsafe.run_evidence.v1` signature.
pub const DOMAIN_TAG: &[u8] = b"tsafe.run_evidence.v1\0";

/// Algorithm identifier embedded in [`SignaturePayload::algo`].
///
/// Pinned to the only currently-supported value. A future cohort
/// upgrade to e.g. post-quantum signatures will mint a new domain tag
/// + algo value in tandem.
pub const SIG_ALGO_ED25519: &str = "ed25519";

/// Public Ed25519 signature payload carried alongside the signed
/// [`RunEvidence`] artifact.
///
/// All three fields are present on a successfully signed artifact;
/// absence of the parent `signature` slot on the [`RunEvidence`] itself
/// is how unsigned (or opted-out) emissions are represented.
///
/// `pubkey` and `sig` are base64url (no padding) per ec ADR-0003's
/// convention for binary fingerprints on the wire — same encoding the
/// rest of tsafe uses for its hash family.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignaturePayload {
    /// Signature algorithm identifier. Always [`SIG_ALGO_ED25519`] in
    /// Phase 5.
    pub algo: String,
    /// Verifying-key bytes, base64url-encoded, no padding (32 bytes
    /// decoded).
    pub pubkey: String,
    /// Ed25519 signature bytes, base64url-encoded, no padding (64 bytes
    /// decoded).
    pub sig: String,
}

/// Convenience wrapper bundling a `RunEvidence` artifact with its
/// detached-shape [`SignaturePayload`].
///
/// This is just a type-safety convenience for callers that need to pass
/// a guaranteed-signed artifact around in their type system. The
/// underlying wire-format storage location is
/// [`RunEvidence::signature`] — both shapes round-trip cleanly to and
/// from canonical JSON.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedEvidence {
    /// The signed evidence. Its [`RunEvidence::signature`] field is
    /// always `Some` on a value returned from [`sign_evidence`].
    pub evidence: RunEvidence,
    /// The signature payload, duplicated as a sibling field so callers
    /// can borrow the signature without unpacking the option on
    /// `evidence`.
    pub signature: SignaturePayload,
}

/// Errors that can arise while producing a signed `RunEvidence`.
#[derive(Debug, Error)]
pub enum SignError {
    /// The artifact failed JSON serialisation before signing. Should
    /// not happen in practice because `RunEvidence` is `Serialize`-by-
    /// derive and every field is a JSON-native type, but the error
    /// path is preserved for completeness.
    #[error("serialise RunEvidence for signing: {0}")]
    Serialize(#[from] serde_json::Error),
}

/// Errors that can arise while verifying a signed `RunEvidence`.
#[derive(Debug, Error)]
pub enum VerifyError {
    /// The artifact had no `signature` field. Distinguished from a
    /// failed cryptographic verification so callers can choose to
    /// treat absence as "needs operator action" rather than a hard
    /// failure.
    #[error("evidence has no signature field")]
    SignatureAbsent,
    /// The signature payload announced an algorithm tsafe does not
    /// understand. Carries the offending value verbatim so error
    /// surfaces can report it.
    #[error("unsupported signature algorithm: {0}")]
    UnsupportedAlgorithm(String),
    /// The pubkey or signature bytes failed base64url decoding.
    #[error("invalid base64url encoding on signature field: {0}")]
    Base64(#[from] base64::DecodeError),
    /// The decoded pubkey was not 32 bytes long.
    #[error("invalid pubkey length: expected 32 bytes, got {0}")]
    PubkeyLength(usize),
    /// The decoded signature was not 64 bytes long.
    #[error("invalid signature length: expected 64 bytes, got {0}")]
    SignatureLength(usize),
    /// The verifying key bytes were syntactically well-formed but
    /// represent a malformed Ed25519 point.
    #[error("malformed Ed25519 verifying key: {0}")]
    MalformedKey(ed25519_dalek::ed25519::Error),
    /// The signature did not verify against the supplied / embedded
    /// pubkey. This is the canonical "tampered or wrong-key" outcome.
    #[error("signature verification failed: {0}")]
    SignatureMismatch(ed25519_dalek::ed25519::Error),
    /// Serialisation failure while reconstructing the canonical bytes.
    /// Same caveat as [`SignError::Serialize`] — exists only because
    /// the underlying API returns a `Result`.
    #[error("serialise RunEvidence for verification: {0}")]
    Serialize(#[from] serde_json::Error),
}

/// Produce the canonical byte representation of a `RunEvidence` for
/// signing or verification.
///
/// The encoding is a JCS-style canonical JSON:
///
/// - Top-level object keys are sorted lexicographically.
/// - Nested object keys are sorted lexicographically (recursively).
/// - The `signature` field is stripped before encoding so a fresh
///   signature can be computed on the just-signed shape.
/// - No whitespace appears anywhere in the output.
/// - Integers, booleans, nulls, and strings serialise via `serde_json`
///   defaults; `RunEvidence` does not contain floats or duplicate
///   keys, so the JCS edge cases for those are not exercised here.
///
/// The domain-tag prefix [`DOMAIN_TAG`] is **not** included in the
/// return value — callers prepend it inside [`sign_evidence`] /
/// [`verify_evidence`]. Exposing the unprefixed canonical bytes makes
/// the function usable as a regression-test surface and a future
/// substrate hand-off point.
pub fn canonical_bytes(evidence: &RunEvidence) -> Vec<u8> {
    // Re-route through `serde_json::Value` so we can walk the tree and
    // sort object keys. Using `to_value` rather than `to_string` keeps
    // numbers as numbers (no precision conversion) and lets us strip
    // the `signature` field before serialising the canonical form.
    let mut value = serde_json::to_value(evidence)
        .expect("RunEvidence::serialize is infallible (derive-Serialize on JSON-native fields)");
    if let Value::Object(map) = &mut value {
        map.remove("signature");
    }
    let canonical = canonicalise(value);
    // `to_string` on a sorted Value already emits compact JSON
    // (no whitespace between tokens). No second pass needed.
    serde_json::to_string(&canonical)
        .expect("canonical Value is JSON-native by construction")
        .into_bytes()
}

/// Recursively canonicalise object keys.
///
/// `serde_json::Value::Object` is backed by a `Map` which preserves
/// insertion order (when the `preserve_order` feature is off, the
/// default in tsafe, it is backed by `BTreeMap` which already sorts
/// keys — but we re-sort explicitly so the behaviour stays correct
/// regardless of which `serde_json` feature flags downstream consumers
/// enable).
fn canonicalise(value: Value) -> Value {
    match value {
        Value::Object(map) => {
            let mut entries: Vec<(String, Value)> = map.into_iter().collect();
            entries.sort_by(|a, b| a.0.cmp(&b.0));
            let mut sorted = Map::new();
            for (key, child) in entries {
                sorted.insert(key, canonicalise(child));
            }
            Value::Object(sorted)
        }
        Value::Array(items) => Value::Array(items.into_iter().map(canonicalise).collect()),
        other => other,
    }
}

/// Sign a `RunEvidence` with the supplied [`SigningKey`].
///
/// Returns a [`SignedEvidence`] whose embedded `evidence.signature` is
/// `Some(..)` so callers can serialise it directly to a single JSON
/// object.
///
/// The signing pipeline:
///
/// 1. Strip any pre-existing `signature` field via [`canonical_bytes`].
/// 2. Prepend the domain tag [`DOMAIN_TAG`].
/// 3. Sign the resulting byte sequence with Ed25519.
/// 4. Embed the verifying-key bytes + signature in a fresh
///    [`SignaturePayload`], install it on the returned `RunEvidence`.
///
/// The function is total over well-formed input. The `Result` return
/// type is preserved so a future change of canonical encoder (e.g. to a
/// substrate library that can fail on duplicate keys) does not require
/// a breaking API change.
pub fn sign_evidence(
    evidence: &RunEvidence,
    signing_key: &SigningKey,
) -> Result<SignedEvidence, SignError> {
    let canonical = canonical_bytes(evidence);
    let mut to_sign = Vec::with_capacity(DOMAIN_TAG.len() + canonical.len());
    to_sign.extend_from_slice(DOMAIN_TAG);
    to_sign.extend_from_slice(&canonical);
    let signature = signing_key.sign(&to_sign);
    let verifying_key = signing_key.verifying_key();

    let payload = SignaturePayload {
        algo: SIG_ALGO_ED25519.to_string(),
        pubkey: URL_SAFE_NO_PAD.encode(verifying_key.as_bytes()),
        sig: URL_SAFE_NO_PAD.encode(signature.to_bytes()),
    };
    let mut signed_evidence = evidence.clone();
    signed_evidence.signature = Some(payload.clone());
    Ok(SignedEvidence {
        evidence: signed_evidence,
        signature: payload,
    })
}

/// Verify a signed `RunEvidence` against the supplied [`VerifyingKey`].
///
/// Returns `Ok(())` if the signature is valid for the canonical bytes
/// of the evidence (with the `signature` field stripped) under the
/// domain tag [`DOMAIN_TAG`]. Returns a typed [`VerifyError`]
/// otherwise.
///
/// Most callers will use [`verify_signed_evidence`] which derives the
/// verifying key from the artifact itself (TOFU) — this lower-level
/// function exists so an operator-supplied pubkey can be used to
/// short-circuit the embedded pubkey, useful for out-of-band trust
/// pinning.
pub fn verify_evidence(
    signed: &SignedEvidence,
    verifying_key: &VerifyingKey,
) -> Result<(), VerifyError> {
    if signed.signature.algo != SIG_ALGO_ED25519 {
        return Err(VerifyError::UnsupportedAlgorithm(
            signed.signature.algo.clone(),
        ));
    }
    let sig_bytes = URL_SAFE_NO_PAD.decode(&signed.signature.sig)?;
    if sig_bytes.len() != SIGNATURE_LENGTH {
        return Err(VerifyError::SignatureLength(sig_bytes.len()));
    }
    let sig_array: [u8; SIGNATURE_LENGTH] = sig_bytes
        .as_slice()
        .try_into()
        .expect("length-checked above");
    let signature = Signature::from_bytes(&sig_array);

    let canonical = canonical_bytes(&signed.evidence);
    let mut to_verify = Vec::with_capacity(DOMAIN_TAG.len() + canonical.len());
    to_verify.extend_from_slice(DOMAIN_TAG);
    to_verify.extend_from_slice(&canonical);

    verifying_key
        .verify(&to_verify, &signature)
        .map_err(VerifyError::SignatureMismatch)
}

/// Verify a signed `RunEvidence` using the pubkey embedded in the
/// artifact itself (TOFU — Trust-On-First-Use).
///
/// This is the "no operator-supplied pubkey" path: tsafe trusts the
/// artifact's own claim of authorship. Operators MUST pin the pubkey
/// out of band before relying on the signature for any security
/// purpose; this routine guarantees only that the artifact was signed
/// by whoever owns the embedded key, not that the embedded key is
/// trustworthy.
pub fn verify_signed_evidence(signed: &SignedEvidence) -> Result<(), VerifyError> {
    let key = decode_verifying_key(&signed.signature.pubkey)?;
    verify_evidence(signed, &key)
}

/// Decode a base64url verifying-key string into an Ed25519
/// [`VerifyingKey`].
///
/// Exposed for the CLI surface (`tsafe attest verify --pubkey <key>`),
/// which receives the pubkey from the operator as a base64url string.
pub fn decode_verifying_key(pubkey_b64url: &str) -> Result<VerifyingKey, VerifyError> {
    let bytes = URL_SAFE_NO_PAD.decode(pubkey_b64url)?;
    if bytes.len() != ed25519_dalek::PUBLIC_KEY_LENGTH {
        return Err(VerifyError::PubkeyLength(bytes.len()));
    }
    let array: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] =
        bytes.as_slice().try_into().expect("length-checked above");
    VerifyingKey::from_bytes(&array).map_err(VerifyError::MalformedKey)
}

/// Reconstruct a [`SignedEvidence`] from a `RunEvidence` whose
/// `signature` field is `Some(..)`.
///
/// Convenience for callers that have already deserialised a JSON
/// artifact and want the type-safe split.
pub fn signed_from_run_evidence(evidence: RunEvidence) -> Option<SignedEvidence> {
    let signature = evidence.signature.clone()?;
    Some(SignedEvidence {
        evidence,
        signature,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::run_evidence::{
        blake3_hash, ContractRef, DeniedSensitiveEnvEvidence, EnforcementResult,
        EnvironmentEvidence, InjectedSecretEvidence, MachineEvidence, ProcessEvidence, RiskDelta,
        RUN_EVIDENCE_VERSION, RUN_SCHEMA,
    };
    use chrono::Utc;
    use ed25519_dalek::SigningKey;
    use rand::rngs::OsRng;

    fn signing_key() -> SigningKey {
        SigningKey::generate(&mut OsRng)
    }

    fn well_formed_evidence() -> RunEvidence {
        let now = Utc::now();
        RunEvidence {
            schema: RUN_SCHEMA.to_string(),
            tsafe_attest_version: RUN_EVIDENCE_VERSION.to_string(),
            started_at: now,
            finished_at: now,
            repo_path: "/tmp/test".to_string(),
            repo_commit: None,
            command: vec!["true".to_string()],
            contract: ContractRef {
                path: "tsafe.contract.json".to_string(),
                hash: blake3_hash("contract"),
            },
            environment: EnvironmentEvidence {
                parent_env_count: 3,
                child_env_count: 1,
                removed_env_count: 2,
                safe_baseline_injected: vec!["PATH".to_string()],
                secrets_injected: vec![InjectedSecretEvidence {
                    name: "DATABASE_URL".to_string(),
                    source: "literal://demo/DATABASE_URL".to_string(),
                    hash: blake3_hash("db"),
                    redacted_value: "p***".to_string(),
                    required: true,
                }],
                sensitive_env_denied: vec![DeniedSensitiveEnvEvidence {
                    name: "AWS_SECRET_ACCESS_KEY".to_string(),
                    hash: blake3_hash("aws"),
                    reason: "test".to_string(),
                }],
            },
            process: ProcessEvidence {
                pid: 1,
                exit_code: 0,
                duration_ms: 1,
                cwd: "/tmp".to_string(),
            },
            machine: MachineEvidence {
                hostname_hash: blake3_hash("host"),
                username_hash: blake3_hash("user"),
                os: "linux".to_string(),
                arch: "x86_64".to_string(),
            },
            result: EnforcementResult {
                contract_enforced: true,
                violations: Vec::new(),
                risk_delta: RiskDelta {
                    before_score: 10,
                    after_score: 0,
                },
            },
            signature: None,
        }
    }

    #[test]
    fn canonical_bytes_strips_signature_field() {
        let mut signed = well_formed_evidence();
        signed.signature = Some(SignaturePayload {
            algo: "ed25519".into(),
            pubkey: "AAAA".into(),
            sig: "BBBB".into(),
        });
        let unsigned = {
            let mut e = signed.clone();
            e.signature = None;
            e
        };
        assert_eq!(
            canonical_bytes(&signed),
            canonical_bytes(&unsigned),
            "canonical_bytes must be identical regardless of signature presence"
        );
    }

    #[test]
    fn canonical_bytes_object_keys_are_sorted() {
        let bytes = canonical_bytes(&well_formed_evidence());
        let text = String::from_utf8(bytes).unwrap();
        // The top-level object must start with a sorted key.
        // "command" < "contract" < "environment" < ... so the first
        // field after the opening brace is `command`.
        assert!(
            text.starts_with(r#"{"command":"#),
            "canonical encoding should start with sorted keys; got prefix {}",
            &text[..text.len().min(40)]
        );
    }

    #[test]
    fn canonical_bytes_contains_no_whitespace() {
        let bytes = canonical_bytes(&well_formed_evidence());
        for &b in &bytes {
            assert!(
                !matches!(b, b' ' | b'\n' | b'\r' | b'\t'),
                "canonical encoding contained whitespace byte 0x{b:02x}"
            );
        }
    }

    #[test]
    fn sign_then_verify_roundtrips() {
        let evidence = well_formed_evidence();
        let key = signing_key();
        let signed = sign_evidence(&evidence, &key).expect("sign");
        assert!(
            signed.evidence.signature.is_some(),
            "signed evidence must carry a signature payload"
        );
        verify_evidence(&signed, &key.verifying_key()).expect("verify");
    }

    #[test]
    fn verify_signed_evidence_uses_embedded_pubkey() {
        let evidence = well_formed_evidence();
        let key = signing_key();
        let signed = sign_evidence(&evidence, &key).expect("sign");
        verify_signed_evidence(&signed).expect("verify TOFU");
    }

    #[test]
    fn tampered_evidence_fails_verification() {
        let evidence = well_formed_evidence();
        let key = signing_key();
        let mut signed = sign_evidence(&evidence, &key).expect("sign");
        // Mutate a field the canonical encoder includes.
        signed.evidence.process.exit_code = 1;
        let result = verify_evidence(&signed, &key.verifying_key());
        assert!(matches!(result, Err(VerifyError::SignatureMismatch(_))));
    }

    #[test]
    fn wrong_pubkey_fails_verification() {
        let evidence = well_formed_evidence();
        let signed = sign_evidence(&evidence, &signing_key()).expect("sign");
        let wrong = signing_key().verifying_key();
        let result = verify_evidence(&signed, &wrong);
        assert!(matches!(result, Err(VerifyError::SignatureMismatch(_))));
    }

    #[test]
    fn unsupported_algorithm_is_rejected() {
        let evidence = well_formed_evidence();
        let mut signed = sign_evidence(&evidence, &signing_key()).expect("sign");
        signed.signature.algo = "ecdsa-p256".into();
        signed.evidence.signature.as_mut().unwrap().algo = "ecdsa-p256".into();
        let key = signing_key();
        let result = verify_evidence(&signed, &key.verifying_key());
        assert!(matches!(result, Err(VerifyError::UnsupportedAlgorithm(_))));
    }

    #[test]
    fn signed_from_run_evidence_round_trips_through_json() {
        let evidence = well_formed_evidence();
        let key = signing_key();
        let signed = sign_evidence(&evidence, &key).expect("sign");
        let json =
            serde_json::to_string(&signed.evidence).expect("serialise signed RunEvidence to JSON");
        let parsed: RunEvidence = serde_json::from_str(&json).expect("deserialise");
        let reconstituted = signed_from_run_evidence(parsed).expect("signature field present");
        verify_evidence(&reconstituted, &key.verifying_key())
            .expect("signature survives JSON round-trip");
    }

    #[test]
    fn unsigned_evidence_round_trips_through_json_without_signature() {
        // Backward-compat: legacy artifacts that have no `signature` field
        // must continue to parse via `serde(default)`.
        let evidence = well_formed_evidence();
        let json = serde_json::to_string(&evidence).expect("serialise unsigned RunEvidence");
        let parsed: RunEvidence = serde_json::from_str(&json).expect("deserialise unsigned");
        assert!(parsed.signature.is_none());
        assert!(signed_from_run_evidence(parsed).is_none());
    }

    #[test]
    fn signed_payload_pubkey_decodes_to_thirty_two_bytes() {
        let evidence = well_formed_evidence();
        let key = signing_key();
        let signed = sign_evidence(&evidence, &key).expect("sign");
        let bytes = URL_SAFE_NO_PAD.decode(&signed.signature.pubkey).unwrap();
        assert_eq!(bytes.len(), ed25519_dalek::PUBLIC_KEY_LENGTH);
    }

    #[test]
    fn signed_payload_sig_decodes_to_sixty_four_bytes() {
        let evidence = well_formed_evidence();
        let signed = sign_evidence(&evidence, &signing_key()).expect("sign");
        let bytes = URL_SAFE_NO_PAD.decode(&signed.signature.sig).unwrap();
        assert_eq!(bytes.len(), SIGNATURE_LENGTH);
    }

    #[test]
    fn decode_verifying_key_round_trips_with_sign_evidence() {
        let evidence = well_formed_evidence();
        let key = signing_key();
        let signed = sign_evidence(&evidence, &key).expect("sign");
        let decoded = decode_verifying_key(&signed.signature.pubkey).expect("decode pubkey");
        assert_eq!(decoded.as_bytes(), key.verifying_key().as_bytes());
    }
}
