//! Canonical, deterministic, length-prefixed binary encoding for attestation
//! preimages (T-3.D.0, ADR 0010 §1b, ADR 0014 §"Signed preimage").
//!
//! ## Why a custom binary encoder
//!
//! ADR 0010 §1b says: *"Verifiers MUST fail closed on unknown or malformed
//! `schema_version` (no partial verify, no 'best effort' decode)."* That
//! requires the bytes that go into the Ed25519 signature to be **identical**
//! across operating systems, serde versions, and language implementations.
//! `serde_json` does not give that property: object key ordering depends on
//! the `preserve_order` feature, integer-vs-float coercion can drift, and
//! whitespace policy differs.
//!
//! We therefore use the same framing pattern as
//! `cortex-ledger::hash::event_hash` (T-1.B.6): a 1-byte domain tag followed
//! by length-prefixed fields in a **fixed order**. The encoder lives in
//! `cortex-core` so every crate that needs to verify a signature gets the
//! same bytes.
//!
//! ## Framing
//!
//! ```text
//! signing_input = DOMAIN_TAG_ATTESTATION_PREIMAGE          // 1 byte: 0x10
//!              || schema_version (u16, BE)                  // 2 bytes
//!              || lp(event_source_variant_tag)              // 1 + N
//!              || lp(source_field_1) || lp(source_field_2)  // each 8 + N
//!                 ...                                       // (variant-specific)
//!              || lp(event_id)                              // 8 + N
//!              || lp(payload_hash)                          // 8 + N
//!              || lp(session_id)                            // 8 + N
//!              || lp(ledger_id)                             // 8 + N
//!              || lp(chain_position OR previous_hash)       // 8 + N
//!              || lp(signed_at_iso8601)                     // 8 + N
//!              || lp(key_id)                                // 8 + N
//! ```
//!
//! where `lp(x) = (x.len() as u64).to_be_bytes() || x.bytes`. Big-endian is
//! used for both the `schema_version` and length prefixes; this is
//! architecture-independent on every CI target.
//!
//! ## Domain tag allocation
//!
//! `cortex-ledger::hash` reserves 0x01 for `event_hash`. The header comment
//! in that module also reserved 0x02 for "audit" and 0x03 for "trace seal"
//! as future-use slots; **none of those reservations have shipped**. To
//! avoid any chance of collision with that documented reservation block we
//! allocate the attestation preimage tag at **0x10** (decimal 16), opening
//! a fresh domain for cryptographic attestations. Future attestation
//! variants (e.g. rotation envelope) take subsequent values in this block.

use chrono::{DateTime, Utc};

/// Domain tag for attestation preimage framing. Reserved: `0x10`.
///
/// MUST NOT be re-used for any other hash / signature input domain. See
/// module docs for the rationale and reservation table.
pub const DOMAIN_TAG_ATTESTATION_PREIMAGE: u8 = 0x10;

/// Domain tag for identity-rotation envelope framing. Reserved: `0x11`.
pub const DOMAIN_TAG_ROTATION_ENVELOPE: u8 = 0x11;

/// Schema version for the attestation preimage encoding.
///
/// Per ADR 0010 §1b this is **independent** of [`crate::SCHEMA_VERSION`]:
/// it governs the bytes that go into the Ed25519 signature, not the wire
/// shape of the surrounding `Event`. Bump this and write an ADR if any
/// change to the framing or the field set lands.
pub const SCHEMA_VERSION_ATTESTATION: u16 = 1;

/// Source-specific identity material that participates in the signed
/// preimage (ADR 0014 §"Signed preimage": *"`EventSource` variant tag +
/// **all** source-specific fields"*).
///
/// The variant tag string is part of the canonical bytes; the helper
/// [`Self::variant_tag`] returns it. Adding a variant requires a
/// [`SCHEMA_VERSION_ATTESTATION`] bump.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceIdentity {
    /// Operator (human user). No additional source fields.
    User,
    /// Ephemeral child agent. Carries `agent_id`, `parent_session_id`,
    /// `delegation_id`, and `model` per ADR 0014.
    ChildAgent {
        /// Stable identifier for this agent instance.
        agent_id: String,
        /// Session in which the parent delegated to this child.
        parent_session_id: String,
        /// Delegation grant identifier.
        delegation_id: String,
        /// Model identifier (free-form; matches the runtime registry).
        model: String,
    },
    /// Tool invocation. Carries the tool `name`.
    Tool {
        /// Tool name (free-form; matches the runtime registry).
        name: String,
    },
    /// The Cortex runtime itself. No additional source fields.
    Runtime,
    /// Externally-observed outcome. No additional source fields.
    ExternalOutcome,
    /// Explicit operator correction. No additional source fields.
    ManualCorrection,
}

impl SourceIdentity {
    /// Stable wire string for this variant. Part of the canonical preimage
    /// bytes. Renaming requires a [`SCHEMA_VERSION_ATTESTATION`] bump.
    #[must_use]
    pub const fn variant_tag(&self) -> &'static str {
        match self {
            Self::User => "user",
            Self::ChildAgent { .. } => "child_agent",
            Self::Tool { .. } => "tool",
            Self::Runtime => "runtime",
            Self::ExternalOutcome => "external_outcome",
            Self::ManualCorrection => "manual_correction",
        }
    }
}

/// Lineage binding for the preimage (ADR 0014 §Replay: *"preimage MUST
/// include `chain_position` or `previous_hash`"*).
///
/// JSONL appends prefer `PreviousSignature` (Option A from ADR 0010 §2);
/// CLI attestation records that don't have a previous signature on hand
/// at sign time use `ChainPosition` (Option B-style monotonic counter).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LineageBinding {
    /// Monotonic chain position counter (u64).
    ChainPosition(u64),
    /// Hex-encoded previous-row hash (32-byte BLAKE3 → 64 hex chars).
    PreviousHash(String),
}

impl LineageBinding {
    /// Tag byte distinguishing the two variants in the canonical preimage.
    /// `0x01` for `ChainPosition`, `0x02` for `PreviousHash`. The tag is
    /// included so a captured signature for `ChainPosition(10)` cannot be
    /// re-purposed under `PreviousHash("0a…")` even if their canonical
    /// byte forms happened to coincide.
    #[must_use]
    pub const fn tag(&self) -> u8 {
        match self {
            Self::ChainPosition(_) => 0x01,
            Self::PreviousHash(_) => 0x02,
        }
    }
}

/// All material that goes into the Ed25519 signature for an attestation.
///
/// Field order in this struct mirrors the canonical byte order of
/// [`canonical_signing_input`]; do not reorder without bumping
/// [`SCHEMA_VERSION_ATTESTATION`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttestationPreimage {
    /// Attestation schema version (governs the canonical encoding only).
    /// Verifiers MUST fail closed on values they do not understand.
    pub schema_version: u16,
    /// Source identity (variant + source-specific fields, **excluding**
    /// any nested attestation blob — see ADR 0014 §"Signed preimage").
    pub source: SourceIdentity,
    /// Stable event identifier (`evt_…`).
    pub event_id: String,
    /// Hex-encoded BLAKE3 hash of the canonical event payload.
    pub payload_hash: String,
    /// Session identifier (free-form). Stops cross-session replay.
    pub session_id: String,
    /// Ledger identifier / session-scoped ledger namespace. Stops
    /// cross-ledger replay.
    pub ledger_id: String,
    /// Lineage binding (chain position OR previous hash). See
    /// [`LineageBinding`].
    pub lineage: LineageBinding,
    /// When the signature was produced (UTC). Encoded as RFC 3339 /
    /// ISO 8601 — the format is fixed by [`canonical_signing_input`].
    pub signed_at: DateTime<Utc>,
    /// Public-key fingerprint of the signing key. Binds the signature to
    /// the declared key.
    pub key_id: String,
}

/// Encode an [`AttestationPreimage`] as a deterministic byte string.
///
/// **Output is byte-identical across operating systems and serde versions.**
/// See module docs for the framing.
#[must_use]
pub fn canonical_signing_input(p: &AttestationPreimage) -> Vec<u8> {
    let mut out = Vec::with_capacity(256);
    out.push(DOMAIN_TAG_ATTESTATION_PREIMAGE);
    out.extend_from_slice(&p.schema_version.to_be_bytes());

    // Source: variant tag, then variant-specific fields in fixed order.
    write_lp(&mut out, p.source.variant_tag().as_bytes());
    match &p.source {
        SourceIdentity::User
        | SourceIdentity::Runtime
        | SourceIdentity::ExternalOutcome
        | SourceIdentity::ManualCorrection => {}
        SourceIdentity::ChildAgent {
            agent_id,
            parent_session_id,
            delegation_id,
            model,
        } => {
            write_lp(&mut out, agent_id.as_bytes());
            write_lp(&mut out, parent_session_id.as_bytes());
            write_lp(&mut out, delegation_id.as_bytes());
            write_lp(&mut out, model.as_bytes());
        }
        SourceIdentity::Tool { name } => {
            write_lp(&mut out, name.as_bytes());
        }
    }

    write_lp(&mut out, p.event_id.as_bytes());
    write_lp(&mut out, p.payload_hash.as_bytes());
    write_lp(&mut out, p.session_id.as_bytes());
    write_lp(&mut out, p.ledger_id.as_bytes());

    // Lineage: 1-byte tag + length-prefixed body.
    out.push(p.lineage.tag());
    match &p.lineage {
        LineageBinding::ChainPosition(n) => {
            // Length prefix is fixed at 8 (u64 BE) so the encoder is uniform.
            out.extend_from_slice(&8u64.to_be_bytes());
            out.extend_from_slice(&n.to_be_bytes());
        }
        LineageBinding::PreviousHash(hex) => {
            write_lp(&mut out, hex.as_bytes());
        }
    }

    // signed_at: ISO 8601 / RFC 3339 with UTC offset and microsecond precision.
    // chrono's `to_rfc3339_opts` is deterministic across platforms.
    let signed_at_str = p
        .signed_at
        .to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
    write_lp(&mut out, signed_at_str.as_bytes());

    write_lp(&mut out, p.key_id.as_bytes());

    out
}

/// Encode an identity-rotation envelope as a deterministic byte string
/// (separate domain tag — see [`DOMAIN_TAG_ROTATION_ENVELOPE`]).
///
/// ```text
/// rotation_input = DOMAIN_TAG_ROTATION_ENVELOPE              // 1 byte: 0x11
///               || schema_version (u16, BE)                   // 2 bytes
///               || lp(old_pubkey_bytes)                       // 8 + 32
///               || lp(new_pubkey_bytes)                       // 8 + 32
///               || lp(signed_at_iso8601)                      // 8 + N
/// ```
#[must_use]
pub fn canonical_rotation_input(
    schema_version: u16,
    old_pubkey: &[u8; 32],
    new_pubkey: &[u8; 32],
    signed_at: DateTime<Utc>,
) -> Vec<u8> {
    let mut out = Vec::with_capacity(128);
    out.push(DOMAIN_TAG_ROTATION_ENVELOPE);
    out.extend_from_slice(&schema_version.to_be_bytes());
    write_lp(&mut out, old_pubkey);
    write_lp(&mut out, new_pubkey);
    let signed_at_str = signed_at.to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
    write_lp(&mut out, signed_at_str.as_bytes());
    out
}

/// Length-prefix helper. `(bytes.len() as u64).to_be_bytes()` then `bytes`.
fn write_lp(out: &mut Vec<u8>, bytes: &[u8]) {
    out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
    out.extend_from_slice(bytes);
}

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

    fn fixture_preimage() -> AttestationPreimage {
        AttestationPreimage {
            schema_version: SCHEMA_VERSION_ATTESTATION,
            source: SourceIdentity::User,
            event_id: "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".into(),
            payload_hash: "deadbeef".into(),
            session_id: "session-001".into(),
            ledger_id: "ledger-main".into(),
            lineage: LineageBinding::ChainPosition(10),
            signed_at: Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap(),
            key_id: "fp:abc123".into(),
        }
    }

    /// Cross-platform determinism: the canonical bytes for a fixed fixture
    /// MUST equal a hard-coded hex constant. If this test fails, the
    /// encoder drifted; either revert the change or bump
    /// [`SCHEMA_VERSION_ATTESTATION`] **and** an ADR.
    #[test]
    fn canonical_bytes_match_golden_hex_fixture() {
        let bytes = canonical_signing_input(&fixture_preimage());
        let hex = hex_encode(&bytes);

        // Hand-verified golden vector. Generated once on macOS, asserted
        // identical on Linux + Windows in CI. To regenerate (after a
        // deliberate framing change + ADR + schema bump): print `hex` and
        // paste it back here.
        //
        // Note: `chrono::DateTime::to_rfc3339_opts(SecondsFormat::Micros,
        // /* use_z = */ true)` emits `2026-05-02T12:00:00.000000Z` (27
        // bytes), NOT the `+00:00` long form (32 bytes). The `Z` suffix
        // is part of the canonical contract.
        let expected = concat!(
            "10",               // domain tag
            "0001",             // schema_version u16 BE
            "0000000000000004", // lp len 4 (variant tag "user")
            "75736572",         // "user"
            "000000000000001e", // lp len 30 (event_id)
            "6576745f303141525a334e44454b545356345252464651363947354641",
            "56",                     // event_id tail
            "0000000000000008",       // lp len 8 (payload_hash)
            "6465616462656566",       // "deadbeef"
            "000000000000000b",       // lp len 11 (session_id)
            "73657373696f6e2d303031", // "session-001"
            "000000000000000b",       // lp len 11 (ledger_id)
            "6c65646765722d6d61696e", // "ledger-main"
            "01",                     // lineage tag ChainPosition
            "0000000000000008",       // u64 fixed length
            "000000000000000a",       // chain_position = 10
            "000000000000001b",       // lp len 27 (signed_at)
            "323032362d30352d30325431323a30303a30302e3030303030305a",
            "0000000000000009",   // lp len 9 (key_id)
            "66703a616263313233", // "fp:abc123"
        );

        assert_eq!(
            hex, expected,
            "canonical encoder drift — bytes must be byte-identical across platforms"
        );
    }

    /// Two independent code paths produce the same canonical bytes:
    /// (a) the `canonical_signing_input` function;
    /// (b) a hand-built byte vector via the public framing rules.
    #[test]
    fn canonical_byte_identical_across_serdes() {
        let p = fixture_preimage();
        let from_encoder = canonical_signing_input(&p);

        // Hand-built path mirroring the documented framing.
        let mut manual: Vec<u8> = Vec::new();
        manual.push(DOMAIN_TAG_ATTESTATION_PREIMAGE);
        manual.extend_from_slice(&p.schema_version.to_be_bytes());
        push_lp(&mut manual, p.source.variant_tag().as_bytes());
        push_lp(&mut manual, p.event_id.as_bytes());
        push_lp(&mut manual, p.payload_hash.as_bytes());
        push_lp(&mut manual, p.session_id.as_bytes());
        push_lp(&mut manual, p.ledger_id.as_bytes());
        manual.push(p.lineage.tag());
        if let LineageBinding::ChainPosition(n) = p.lineage {
            manual.extend_from_slice(&8u64.to_be_bytes());
            manual.extend_from_slice(&n.to_be_bytes());
        }
        let signed_at_str = p
            .signed_at
            .to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
        push_lp(&mut manual, signed_at_str.as_bytes());
        push_lp(&mut manual, p.key_id.as_bytes());

        assert_eq!(
            from_encoder, manual,
            "encoder output must match the hand-built canonical bytes"
        );
    }

    /// Property: rearranging the Rust struct literal does not change the
    /// signed bytes — only the canonical byte sequence is what is signed.
    /// (This is implicitly proved by the above tests; we add an explicit
    /// case for documentation.)
    #[test]
    fn field_reorder_does_not_change_signed_semantics() {
        let p1 = fixture_preimage();

        // Construct the same logical preimage with the field expressions
        // written in a different source order. Rust struct initialization
        // order does not affect the in-memory layout, but this makes the
        // intent of the test explicit.
        #[allow(clippy::needless_update)]
        let p2 = AttestationPreimage {
            key_id: "fp:abc123".into(),
            signed_at: chrono::Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap(),
            lineage: LineageBinding::ChainPosition(10),
            ledger_id: "ledger-main".into(),
            session_id: "session-001".into(),
            payload_hash: "deadbeef".into(),
            event_id: "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".into(),
            source: SourceIdentity::User,
            schema_version: SCHEMA_VERSION_ATTESTATION,
        };

        assert_eq!(canonical_signing_input(&p1), canonical_signing_input(&p2));
    }

    /// Sanity: distinct logical preimages produce distinct canonical bytes.
    #[test]
    fn distinct_preimages_produce_distinct_bytes() {
        let mut p2 = fixture_preimage();
        p2.event_id = "evt_01ARZ3NDEKTSV4RRFFQ69G5FAW".into();
        assert_ne!(
            canonical_signing_input(&fixture_preimage()),
            canonical_signing_input(&p2)
        );
    }

    /// Lineage tag prevents cross-variant collision: a `ChainPosition(0)`
    /// preimage must not encode to the same bytes as a `PreviousHash("")`
    /// preimage.
    #[test]
    fn lineage_variant_tag_prevents_cross_variant_collision() {
        let mut a = fixture_preimage();
        a.lineage = LineageBinding::ChainPosition(0);
        let mut b = fixture_preimage();
        b.lineage = LineageBinding::PreviousHash(String::new());
        assert_ne!(canonical_signing_input(&a), canonical_signing_input(&b));
    }

    fn push_lp(out: &mut Vec<u8>, bytes: &[u8]) {
        out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
        out.extend_from_slice(bytes);
    }

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