//! SP 800-57 Key Lifecycle Management
//!
//! This module implements formal key lifecycle management per NIST SP 800-57
//! requirements, including state transitions, custodianship, and audit trails.
//!
//! # Key States (SP 800-57 Section 3)
//!
//! - **Generation**: Key material is being generated
//! - **Active**: Key is ready for use
//! - **Rotating**: Key rotation in progress (overlap period)
//! - **Retired**: Key scheduled for retirement
//! - **Destroyed**: Key material zeroized
//!
//! # Example
//!
//! ```
//! use latticearc::types::key_lifecycle::{KeyLifecycleRecord, KeyLifecycleState};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut record = KeyLifecycleRecord::new(
//!     "key-123".to_string(),
//!     "ML-KEM-768".to_string(),
//!     3,   // security level (1-5, NIST PQC categories)
//!     365, // rotation interval (days, > 0)
//!     30,  // overlap period (days)
//! )?;
//!
//! // Activate the key
//! record.transition(
//!     KeyLifecycleState::Active,
//!     "alice".to_string(),
//!     "Key generation complete".to_string(),
//!     Some("approval-123".to_string()),
//! )?;
//!
//! assert!(record.is_valid_for_use());
//! # Ok(())
//! # }
//! ```

use crate::types::error::{Result, TypeError};
use serde::{Deserialize, Serialize};

/// Maximum length (in bytes) for a single audit-trail string field.
/// Keeps state_history bounded and JSONL audit consumers happy.
const MAX_AUDIT_FIELD_LEN: usize = 512;

/// Cap on the number of `StateTransition` entries in
/// `KeyLifecycleRecord::state_history`. The state machine has 5
/// terminal states; legitimate records have under a dozen entries.
/// 1024 is generous slack for Active⇄Rotating cycling, while
/// preventing a tampered or pathologically-cycled record from driving
/// unbounded growth (and making the linear scans in
/// `KeyLifecycleRecordRaw::try_from` O(n²) — see L8).
const MAX_STATE_HISTORY: usize = 1024;

/// Cap on the number of approver IDs in
/// `KeyLifecycleRecord::approvers`. Real-world quorum schemes top out
/// at a few dozen approvers; 256 is generous slack with the same
/// motivation as `MAX_STATE_HISTORY`.
const MAX_APPROVERS: usize = 256;

/// Validate an audit-trail string. Rejects:
/// - empty
/// - control characters (including newlines, which break JSONL)
/// - more than `MAX_AUDIT_FIELD_LEN` bytes
///
/// `Option<String>` callers should validate inside the `Some(_)` arm:
/// presence is decided at the caller; content is validated here.
fn validate_audit_field(name: &str, value: &str) -> Result<()> {
    if value.is_empty() {
        return Err(TypeError::InvalidAuditInput(format!("{name} must not be empty")));
    }
    if value.len() > MAX_AUDIT_FIELD_LEN {
        return Err(TypeError::InvalidAuditInput(format!(
            "{} exceeds {} bytes",
            name, MAX_AUDIT_FIELD_LEN
        )));
    }
    if value.chars().any(char::is_control) {
        return Err(TypeError::InvalidAuditInput(format!("{name} contains control characters")));
    }
    Ok(())
}

/// SP 800-57 Section 3: Key Lifecycle States
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(kani, derive(kani::Arbitrary))]
pub enum KeyLifecycleState {
    /// Key material generation
    Generation,
    /// Key activation - ready for use
    Active,
    /// Key rotation in progress (overlap period)
    Rotating,
    /// Key scheduled for retirement
    Retired,
    /// Key destruction - material zeroized
    Destroyed,
}

/// SP 800-57 Key State Transitions (formally verifiable)
pub struct KeyStateMachine;

impl KeyStateMachine {
    /// Verify state transition is valid per SP 800-57 Part 1 Rev. 5.
    ///
    /// Valid transitions:
    /// - None -> Generation (initial state)
    /// - Generation -> Active (initialization complete)
    /// - Generation -> Destroyed (compromised pre-activation key,
    ///   per SP 800-57 Part 1 Rev. 5 §8.3.1 — pre-activation
    ///   compromise does NOT require passing through Activation)
    /// - Active -> Rotating (rotation initiated)
    /// - Active -> Retired (direct retirement)
    /// - Rotating -> Retired (rotation complete)
    /// - Retired -> Destroyed (cleanup)
    ///
    /// The `Generation -> Destroyed` transition is the "emergency
    /// destruction" path for keys discovered compromised between
    /// keygen and activation. Forcing such a key through
    /// `Generation -> Active -> Retired -> Destroyed` would write
    /// three semantically false transitions to `state_history`
    /// (the key was never actually activated), corrupting the
    /// audit trail. Direct destruction preserves audit-trail truth.
    #[must_use]
    pub fn is_valid_transition(from: Option<KeyLifecycleState>, to: KeyLifecycleState) -> bool {
        match (from, to) {
            // Generation is always valid initial state
            (None, KeyLifecycleState::Generation) => true,

            // Generation -> Active (initialization complete)
            (Some(KeyLifecycleState::Generation), KeyLifecycleState::Active) => true,

            // Generation -> Destroyed (compromised pre-activation key,
            // SP 800-57 Part 1 Rev. 5 §8.3.1)
            (Some(KeyLifecycleState::Generation), KeyLifecycleState::Destroyed) => true,

            // Active -> Rotating (rotation initiated)
            (Some(KeyLifecycleState::Active), KeyLifecycleState::Rotating) => true,

            // Rotating -> Retired (rotation complete)
            (Some(KeyLifecycleState::Rotating), KeyLifecycleState::Retired) => true,

            // Active -> Retired (direct retirement)
            (Some(KeyLifecycleState::Active), KeyLifecycleState::Retired) => true,

            // Retired -> Destroyed (cleanup)
            (Some(KeyLifecycleState::Retired), KeyLifecycleState::Destroyed) => true,

            // All other transitions are invalid
            _ => false,
        }
    }

    /// Get allowed next states from current state.
    ///
    /// Mirrors [`Self::is_valid_transition`]; keep the two in sync.
    /// The `Generation -> Destroyed` entry covers the pre-activation
    /// emergency-destruction path (see `is_valid_transition` doc).
    #[must_use]
    pub fn allowed_next_states(current: KeyLifecycleState) -> Vec<KeyLifecycleState> {
        match current {
            KeyLifecycleState::Generation => {
                vec![KeyLifecycleState::Active, KeyLifecycleState::Destroyed]
            }
            KeyLifecycleState::Active => {
                vec![KeyLifecycleState::Rotating, KeyLifecycleState::Retired]
            }
            KeyLifecycleState::Rotating => vec![KeyLifecycleState::Retired],
            KeyLifecycleState::Retired => vec![KeyLifecycleState::Destroyed],
            KeyLifecycleState::Destroyed => vec![],
        }
    }
}

/// SP 800-57 Custodianship (Section 5)
///
/// Fields are private so a future `is_currently_authorized()` predicate
/// reading `approved_until` cannot be silently bypassed by external
/// post-construction mutation. Callers construct via [`Self::new`] and
/// read via the typed accessors below.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyCustodian {
    custodian_id: String,
    name: String,
    role: CustodianRole,
    responsibilities: Vec<String>,
    approved_until: chrono::DateTime<chrono::Utc>,
}

impl KeyCustodian {
    /// Construct a new custodian record. `approved_until` is auth-relevant —
    /// future predicates (e.g., `is_currently_authorized()`) must read it
    /// rather than allowing external mutation.
    #[must_use]
    pub fn new(
        custodian_id: String,
        name: String,
        role: CustodianRole,
        responsibilities: Vec<String>,
        approved_until: chrono::DateTime<chrono::Utc>,
    ) -> Self {
        Self { custodian_id, name, role, responsibilities, approved_until }
    }

    /// Unique identifier for the custodian.
    #[must_use]
    pub fn custodian_id(&self) -> &str {
        &self.custodian_id
    }

    /// Human-readable name.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Role in key management.
    #[must_use]
    pub fn role(&self) -> CustodianRole {
        self.role
    }

    /// List of responsibilities.
    #[must_use]
    pub fn responsibilities(&self) -> &[String] {
        &self.responsibilities
    }

    /// Approval expiration date — auth-relevant. A predicate that
    /// validates "currently authorized" must read this through the
    /// accessor (mutation is closed by the privatized field).
    #[must_use]
    pub fn approved_until(&self) -> chrono::DateTime<chrono::Utc> {
        self.approved_until
    }
}

/// Roles for key custodians
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CustodianRole {
    /// Authorized to generate keys
    KeyGenerator,
    /// Authorized to approve key operations
    KeyApprover,
    /// Authorized to destroy keys
    KeyDestroyer,
    /// Authorized to audit key operations
    KeyAuditor,
}

/// Key lifecycle record with audit trail.
///
/// All fields are private — read access is via getters. Construction is
/// via [`Self::new`] (validated) or via `serde::Deserialize` through the
/// [`KeyLifecycleRecordRaw`] try_from path (also validated). The
/// construction-time fields (`key_id`, `key_type`, `security_level`,
/// `generated_at`, `rotation_interval_days`, `overlap_period_days`)
/// are private specifically so the numeric-bound validators
/// ([`validate_security_level`], [`validate_rotation_interval`])
/// cannot be bypassed by direct field assignment after construction.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "KeyLifecycleRecordRaw")]
pub struct KeyLifecycleRecord {
    // Construction-time identity & policy (set in `new`, never
    // reassigned). Private so the numeric-bound validators cannot be
    // bypassed by direct field assignment.
    key_id: String,
    key_type: String,
    security_level: u32,

    // State management — private so transitions only happen via `transition()`
    current_state: KeyLifecycleState,
    state_history: Vec<StateTransition>,

    // Custodianship — private so updates only happen via `transition()` /
    // `add_approver()`
    generator: Option<String>,
    approvers: Vec<String>,
    destroyer: Option<String>,

    // When the key was generated.
    generated_at: chrono::DateTime<chrono::Utc>,

    // Per-state timestamps — private so they can only be set by
    // `transition()`
    activated_at: Option<chrono::DateTime<chrono::Utc>>,
    rotated_at: Option<chrono::DateTime<chrono::Utc>>,
    retired_at: Option<chrono::DateTime<chrono::Utc>>,
    destroyed_at: Option<chrono::DateTime<chrono::Utc>>,

    // SP 800-57 rotation policy (private — the numeric-bound
    // validators would otherwise be bypassable via direct field
    // assignment).
    rotation_interval_days: u32,
    overlap_period_days: u32,
}

/// Wire-shape mirror of `KeyLifecycleRecord` used by the serde
/// `try_from` hook. Loading a persisted record goes
/// JSON → `KeyLifecycleRecordRaw` → `validate()` → `KeyLifecycleRecord`,
/// so a tampered or corrupted file with state-machine inconsistencies
/// (e.g. `Destroyed` with empty `state_history`, or `Generation` with
/// a populated `activated_at`) is rejected at deserialize time rather
/// than producing a silently-broken record.
#[derive(Debug, Clone, Deserialize)]
#[doc(hidden)]
struct KeyLifecycleRecordRaw {
    key_id: String,
    key_type: String,
    security_level: u32,
    current_state: KeyLifecycleState,
    state_history: Vec<StateTransition>,
    generator: Option<String>,
    approvers: Vec<String>,
    destroyer: Option<String>,
    generated_at: chrono::DateTime<chrono::Utc>,
    activated_at: Option<chrono::DateTime<chrono::Utc>>,
    rotated_at: Option<chrono::DateTime<chrono::Utc>>,
    retired_at: Option<chrono::DateTime<chrono::Utc>>,
    destroyed_at: Option<chrono::DateTime<chrono::Utc>>,
    rotation_interval_days: u32,
    overlap_period_days: u32,
}

impl TryFrom<KeyLifecycleRecordRaw> for KeyLifecycleRecord {
    type Error = TypeError;

    fn try_from(raw: KeyLifecycleRecordRaw) -> Result<Self> {
        // Re-validate state-machine invariants on load so a corrupted
        // or tampered persisted record cannot bypass the rules
        // `transition()` enforces in memory.

        // (0) Vector caps: same bounds the in-memory mutators
        //     enforce. Without these, the linear scans below
        //     (`entered`, last-history check) become O(n²) on a
        //     pathologically-large persisted record, and the
        //     in-memory record can grow past `transition()`'s cap
        //     after a roundtrip.
        if raw.state_history.len() > MAX_STATE_HISTORY {
            return Err(TypeError::InvalidAuditInput(format!(
                "state_history length {} exceeds cap of {}",
                raw.state_history.len(),
                MAX_STATE_HISTORY
            )));
        }
        if raw.approvers.len() > MAX_APPROVERS {
            return Err(TypeError::InvalidAuditInput(format!(
                "approvers length {} exceeds cap of {}",
                raw.approvers.len(),
                MAX_APPROVERS
            )));
        }

        // (1) `current_state` must be reachable from the persisted
        //     `activated_at` value. Rotating and Retired both require
        //     having passed through Active, so `activated_at` MUST be
        //     `Some` for those — and Generation MUST NOT have one.
        match (raw.current_state, raw.activated_at) {
            (KeyLifecycleState::Generation, Some(_)) => {
                return Err(TypeError::InvalidAuditInput(
                    "current_state=Generation with activated_at set".to_string(),
                ));
            }
            (
                KeyLifecycleState::Active
                | KeyLifecycleState::Rotating
                | KeyLifecycleState::Retired,
                None,
            ) => {
                return Err(TypeError::InvalidAuditInput(format!(
                    "current_state={:?} requires activated_at",
                    raw.current_state
                )));
            }
            _ => {}
        }

        // (2) For non-Generation states, the last state_history entry
        //     must agree with `current_state`. Otherwise a tampered
        //     file with `current_state = Active` and a `state_history`
        //     ending in `Rotating` (or empty) would deserialize cleanly.
        match (raw.current_state, raw.state_history.last()) {
            (KeyLifecycleState::Generation, _) => {}
            (_, None) => {
                return Err(TypeError::InvalidAuditInput(format!(
                    "current_state={:?} with empty state_history",
                    raw.current_state
                )));
            }
            (current, Some(t)) if t.to_state != current => {
                return Err(TypeError::InvalidAuditInput(format!(
                    "current_state={:?} but last state_history entry is {:?}",
                    current, t.to_state
                )));
            }
            _ => {}
        }

        // (3) Per-state timestamp consistency: the timestamp for a
        //     state must be present iff that state has been entered
        //     (i.e. appears in state_history).
        let entered = |st: KeyLifecycleState| raw.state_history.iter().any(|t| t.to_state == st);
        // `activated_at` parallels `rotated_at` / `retired_at` /
        // `destroyed_at` below. (1)'s match arm only covers four of the
        // five (state, timestamp) combinations — current_state=Destroyed
        // with a fabricated `activated_at` slips through, even though
        // state_history may not include Active. Closing the gap here
        // makes the validator's tamper-evidence guarantee uniform.
        if entered(KeyLifecycleState::Active) != raw.activated_at.is_some() {
            return Err(TypeError::InvalidAuditInput(
                "activated_at presence disagrees with state_history".to_string(),
            ));
        }
        if entered(KeyLifecycleState::Rotating) != raw.rotated_at.is_some() {
            return Err(TypeError::InvalidAuditInput(
                "rotated_at presence disagrees with state_history".to_string(),
            ));
        }
        if entered(KeyLifecycleState::Retired) != raw.retired_at.is_some() {
            return Err(TypeError::InvalidAuditInput(
                "retired_at presence disagrees with state_history".to_string(),
            ));
        }
        if entered(KeyLifecycleState::Destroyed) != raw.destroyed_at.is_some() {
            return Err(TypeError::InvalidAuditInput(
                "destroyed_at presence disagrees with state_history".to_string(),
            ));
        }

        // (4) state_history timestamps must be non-decreasing. A
        //     persisted record with `state_history[i].timestamp >
        //     state_history[i+1].timestamp` is a tamper signal — the
        //     in-memory `transition()` path always appends with
        //     `Utc::now()`, which is monotone in practice and forbids
        //     out-of-order entries by construction.
        for pair in raw.state_history.windows(2) {
            // `windows(2)` always yields 2-element slices; pattern-
            // bind so clippy's indexing-may-panic gate is satisfied
            // without an `[allow]` waiver.
            let [prev, curr] = pair else { continue };
            if curr.timestamp < prev.timestamp {
                return Err(TypeError::InvalidAuditInput(format!(
                    "state_history timestamps decrease between {:?} ({}) and {:?} ({})",
                    prev.to_state, prev.timestamp, curr.to_state, curr.timestamp
                )));
            }
        }

        // (5) Approver identifiers must be unique. The in-memory
        //     `add_approver` path explicitly deduplicates; a persisted
        //     record with `approvers = ["alice", "alice"]` would
        //     otherwise inflate threshold-quorum counts.
        let mut seen: std::collections::HashSet<&String> =
            std::collections::HashSet::with_capacity(raw.approvers.len());
        for approver in &raw.approvers {
            if !seen.insert(approver) {
                return Err(TypeError::InvalidAuditInput(format!(
                    "approvers contains duplicate id {:?}",
                    approver
                )));
            }
        }

        // (6) Each `StateTransition` entry's audit-trail strings
        //     (`custodian_id`, `justification`, `approval_id`) must
        //     pass the same sanitization as the in-memory
        //     `transition()` path — empty / control-char / oversized
        //     strings are rejected. Without this check a tampered
        //     persisted record could load a `StateTransition` with
        //     embedded `\n` (corrupting JSONL audit consumers),
        //     empty attribution (defeating the audit trail), or
        //     a 1 MiB `justification` (memory amplification on the
        //     in-memory side after deserialization).
        for (idx, t) in raw.state_history.iter().enumerate() {
            validate_audit_field(&format!("state_history[{idx}].custodian_id"), &t.custodian_id)?;
            validate_audit_field(&format!("state_history[{idx}].justification"), &t.justification)?;
            if let Some(ref approval_id) = t.approval_id {
                validate_audit_field(&format!("state_history[{idx}].approval_id"), approval_id)?;
            }
        }

        // (7) Top-level per-state timestamps must respect the
        //     state-machine's temporal ordering:
        //         generated_at ≤ activated_at ≤ rotated_at ≤ retired_at ≤ destroyed_at
        //     The state_history monotonicity check (step 4) only
        //     covers entries within the history vector — the top-
        //     level timestamps are populated from independent JSON
        //     fields and could otherwise carry impossible orderings
        //     (e.g. `retired_at < activated_at`). The in-memory
        //     `transition()` path always assigns these via
        //     `Utc::now()` after the monotonicity-enforced state
        //     change, so the relationship holds by construction
        //     there; deserialization needs an explicit gate.
        type TsPair = (
            &'static str,
            Option<chrono::DateTime<chrono::Utc>>,
            &'static str,
            Option<chrono::DateTime<chrono::Utc>>,
        );
        let pairs: [TsPair; 5] = [
            ("generated_at", Some(raw.generated_at), "activated_at", raw.activated_at),
            ("activated_at", raw.activated_at, "rotated_at", raw.rotated_at),
            ("activated_at", raw.activated_at, "retired_at", raw.retired_at),
            ("rotated_at", raw.rotated_at, "retired_at", raw.retired_at),
            ("retired_at", raw.retired_at, "destroyed_at", raw.destroyed_at),
        ];
        for (earlier_name, earlier, later_name, later) in pairs {
            if let (Some(e), Some(l)) = (earlier, later)
                && l < e
            {
                return Err(TypeError::InvalidAuditInput(format!(
                    "{later_name} ({l}) precedes {earlier_name} ({e}) — state-machine ordering violated"
                )));
            }
        }

        // Re-validate the numeric-bound invariants that
        // `KeyLifecycleRecord::new` enforces on construction. The
        // state-machine-invariant pass above does not cover these,
        // so without this gate a tampered persisted record could
        // carry security_level = 999 or rotation_interval_days = 0
        // past the round-trip.
        validate_security_level(raw.security_level)?;
        validate_rotation_interval(raw.rotation_interval_days)?;

        Ok(KeyLifecycleRecord {
            key_id: raw.key_id,
            key_type: raw.key_type,
            security_level: raw.security_level,
            current_state: raw.current_state,
            state_history: raw.state_history,
            generator: raw.generator,
            approvers: raw.approvers,
            destroyer: raw.destroyer,
            generated_at: raw.generated_at,
            activated_at: raw.activated_at,
            rotated_at: raw.rotated_at,
            retired_at: raw.retired_at,
            destroyed_at: raw.destroyed_at,
            rotation_interval_days: raw.rotation_interval_days,
            overlap_period_days: raw.overlap_period_days,
        })
    }
}

/// Reject `security_level` outside the documented 1-5 range. Used by
/// both `KeyLifecycleRecord::new` and the deserializer so the in-memory
/// and on-disk paths share one gate.
fn validate_security_level(security_level: u32) -> Result<()> {
    if !(1..=5).contains(&security_level) {
        return Err(TypeError::InvalidAuditInput(format!(
            "security_level {} outside documented range 1-5",
            security_level
        )));
    }
    Ok(())
}

/// Reject `rotation_interval_days == 0`. A zero interval makes
/// `requires_rotation()` return `true` immediately on activation, so
/// every key would be flagged for rotation at the moment it goes live —
/// almost certainly a configuration bug, not a real lifecycle policy.
fn validate_rotation_interval(rotation_interval_days: u32) -> Result<()> {
    if rotation_interval_days == 0 {
        return Err(TypeError::InvalidAuditInput(format!(
            "rotation_interval_days {} is zero; would force immediate rotation on activation",
            rotation_interval_days
        )));
    }
    Ok(())
}

/// Record of a state transition.
///
/// Fields are private — once a transition is recorded into
/// `KeyLifecycleRecord::state_history`, mutation of any field would
/// invalidate the audit trail. Construction goes through
/// [`Self::new`]; external code reads via typed accessors.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateTransition {
    from_state: Option<KeyLifecycleState>,
    to_state: KeyLifecycleState,
    timestamp: chrono::DateTime<chrono::Utc>,
    custodian_id: String,
    justification: String,
    approval_id: Option<String>,
}

impl StateTransition {
    /// Construct a new state-transition record. Internal callers (the
    /// `transition_to` driver) capture `now` once and pass it in so the
    /// transition timestamp matches sibling per-state timestamps.
    #[must_use]
    pub fn new(
        from_state: Option<KeyLifecycleState>,
        to_state: KeyLifecycleState,
        timestamp: chrono::DateTime<chrono::Utc>,
        custodian_id: String,
        justification: String,
        approval_id: Option<String>,
    ) -> Self {
        Self { from_state, to_state, timestamp, custodian_id, justification, approval_id }
    }

    /// Previous state (None if initial).
    #[must_use]
    pub fn from_state(&self) -> Option<KeyLifecycleState> {
        self.from_state
    }

    /// New state.
    #[must_use]
    pub fn to_state(&self) -> KeyLifecycleState {
        self.to_state
    }

    /// When the transition occurred.
    #[must_use]
    pub fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
        self.timestamp
    }

    /// ID of the custodian who performed the transition.
    #[must_use]
    pub fn custodian_id(&self) -> &str {
        &self.custodian_id
    }

    /// Reason for the transition.
    #[must_use]
    pub fn justification(&self) -> &str {
        &self.justification
    }

    /// Approval reference, if applicable.
    #[must_use]
    pub fn approval_id(&self) -> Option<&str> {
        self.approval_id.as_deref()
    }
}

impl KeyLifecycleRecord {
    /// Create new key lifecycle record
    ///
    /// # Arguments
    ///
    /// * `key_id` - Unique identifier for the key
    /// * `key_type` - Algorithm/key type (e.g., "ML-KEM-768")
    /// * `security_level` - Security level (1-5, NIST PQC categories)
    /// * `rotation_interval_days` - How often to rotate the key (must be > 0)
    /// * `overlap_period_days` - Overlap period during rotation
    ///
    /// # Errors
    ///
    /// Returns [`TypeError::InvalidAuditInput`] if `security_level` is
    /// outside the documented 1-5 range, or if `rotation_interval_days`
    /// is zero (forcing immediate rotation on activation). The
    /// deserializer applies the same checks so in-memory and on-disk
    /// records share one validation gate.
    pub fn new(
        key_id: String,
        key_type: String,
        security_level: u32,
        rotation_interval_days: u32,
        overlap_period_days: u32,
    ) -> Result<Self> {
        validate_security_level(security_level)?;
        validate_rotation_interval(rotation_interval_days)?;
        Ok(Self {
            key_id,
            key_type,
            security_level,
            current_state: KeyLifecycleState::Generation,
            state_history: Vec::new(),
            generator: None,
            approvers: Vec::new(),
            destroyer: None,
            generated_at: chrono::Utc::now(),
            activated_at: None,
            rotated_at: None,
            retired_at: None,
            destroyed_at: None,
            rotation_interval_days,
            overlap_period_days,
        })
    }

    /// Transition key to new state with custodianship tracking
    ///
    /// # Arguments
    ///
    /// * `to_state` - Target state
    /// * `custodian_id` - ID of the custodian performing the transition
    /// * `justification` - Reason for the transition
    /// * `approval_id` - Optional approval reference
    ///
    /// # Errors
    ///
    /// Returns `TypeError::InvalidStateTransition` if the transition is invalid
    pub fn transition(
        &mut self,
        to_state: KeyLifecycleState,
        custodian_id: String,
        justification: String,
        approval_id: Option<String>,
    ) -> Result<()> {
        // Audit-trail input gates: empty / control-chars / oversized
        // strings break JSONL audit consumers and produce attribution-
        // free entries. Reject up front rather than persisting them.
        validate_audit_field("custodian_id", &custodian_id)?;
        validate_audit_field("justification", &justification)?;
        // Inside `Some(_)` the caller has explicitly chosen to send a
        // value; empty-string is a sentinel that confuses
        // presence-vs-content matching downstream. Reject it here.
        if let Some(ref approval_id) = approval_id {
            validate_audit_field("approval_id", approval_id)?;
        }

        if !KeyStateMachine::is_valid_transition(Some(self.current_state), to_state) {
            return Err(TypeError::InvalidStateTransition {
                from: self.current_state,
                to: to_state,
            });
        }
        // Capture `now` once so the state_history timestamp and the
        // per-state timestamp below match exactly. An auditor comparing
        // `state_history[i].timestamp` against `activated_at` (etc.)
        // would otherwise see sub-tick skew on every transition.
        let now = chrono::Utc::now();
        let transition = StateTransition::new(
            Some(self.current_state),
            to_state,
            now,
            custodian_id.clone(),
            justification,
            approval_id,
        );

        if self.state_history.len() >= MAX_STATE_HISTORY {
            return Err(TypeError::InvalidAuditInput(format!(
                "state_history at cap of {} entries; cannot record further transitions",
                MAX_STATE_HISTORY
            )));
        }
        self.state_history.push(transition);
        self.current_state = to_state;

        // Update timestamps (use the captured `now`).
        match to_state {
            KeyLifecycleState::Active => self.activated_at = Some(now),
            KeyLifecycleState::Rotating => self.rotated_at = Some(now),
            KeyLifecycleState::Retired => self.retired_at = Some(now),
            KeyLifecycleState::Destroyed => self.destroyed_at = Some(now),
            _ => {}
        }

        // Update custodianship
        // Generator is the custodian who completes key generation (moves to Active)
        // Check the last transition we just added to see if it was from Generation
        if let Some(last_transition) = self.state_history.last()
            && last_transition.from_state == Some(KeyLifecycleState::Generation)
            && to_state == KeyLifecycleState::Active
        {
            self.generator = Some(custodian_id.clone());
        }
        if to_state == KeyLifecycleState::Destroyed {
            self.destroyer = Some(custodian_id);
        }

        Ok(())
    }

    /// Days elapsed since `activated`, signed (negative for future
    /// activation due to clock skew). Internal helper backing
    /// [`Self::requires_rotation`] and [`Self::age_days`] so the
    /// `Utc::now().signed_duration_since(...).num_days()` chain lives
    /// in one place.
    fn days_since_activation(activated: chrono::DateTime<chrono::Utc>) -> i64 {
        chrono::Utc::now().signed_duration_since(activated).num_days()
    }

    /// Check if key is due for rotation per SP 800-57
    #[must_use]
    pub fn requires_rotation(&self) -> bool {
        let Some(activated_at) = self.activated_at else { return false };
        let age_days_i64 = Self::days_since_activation(activated_at);
        if age_days_i64 < 0 {
            // Future activation date — clock skew or misconfiguration
            tracing::warn!(
                "Key has negative age ({age_days_i64} days); activation_at in the future"
            );
            return false;
        }
        // Ages larger than u32::MAX → always require rotation
        let age_days = u32::try_from(age_days_i64).unwrap_or(u32::MAX);
        age_days >= self.rotation_interval_days
    }

    /// Get key age in days since activation
    #[must_use]
    pub fn age_days(&self) -> Option<u32> {
        self.activated_at.map(|activated| {
            // Negative ages (future activation) treated as 0.
            u32::try_from(Self::days_since_activation(activated)).unwrap_or(0)
        })
    }

    /// Check if key is in valid state for use
    #[must_use]
    pub fn is_valid_for_use(&self) -> bool {
        matches!(self.current_state, KeyLifecycleState::Active | KeyLifecycleState::Rotating)
    }

    /// Add an approver to the key. Silently ignored if the approver
    /// is already in the list, or if the cap (`MAX_APPROVERS`) has
    /// been reached. Returns `false` on cap rejection so callers can
    /// surface a warning if needed — the `#[must_use]` here forces
    /// every caller to acknowledge the bool, since
    /// changed the return type from `()` precisely to expose this
    /// case and a caller that just `.;` it would silently lose the
    /// signal.
    #[must_use = "add_approver returns false when the cap is reached; surface that to the caller"]
    pub fn add_approver(&mut self, approver_id: impl Into<String>) -> bool {
        let approver_id = approver_id.into();
        if self.approvers.contains(&approver_id) {
            return true;
        }
        if self.approvers.len() >= MAX_APPROVERS {
            return false;
        }
        self.approvers.push(approver_id);
        true
    }

    // ----------------------------------------------------------------
    // Read-only accessors for the privatized state-machine fields.
    // External callers go through these so the lifecycle invariants
    // stay enforced by `transition()`.
    // ----------------------------------------------------------------

    /// Unique key identifier.
    #[must_use]
    pub fn key_id(&self) -> &str {
        &self.key_id
    }

    /// Algorithm / key-type identifier (e.g. `"ML-KEM-768"`).
    #[must_use]
    pub fn key_type(&self) -> &str {
        &self.key_type
    }

    /// NIST PQC security level (1-5).
    #[must_use]
    pub fn security_level(&self) -> u32 {
        self.security_level
    }

    /// Timestamp the record was generated.
    #[must_use]
    pub fn generated_at(&self) -> chrono::DateTime<chrono::Utc> {
        self.generated_at
    }

    /// Configured rotation interval (days, > 0).
    #[must_use]
    pub fn rotation_interval_days(&self) -> u32 {
        self.rotation_interval_days
    }

    /// Configured overlap period during rotation (days).
    #[must_use]
    pub fn overlap_period_days(&self) -> u32 {
        self.overlap_period_days
    }

    /// Current lifecycle state.
    #[must_use]
    pub fn current_state(&self) -> KeyLifecycleState {
        self.current_state
    }

    /// History of state transitions (read-only).
    #[must_use]
    pub fn state_history(&self) -> &[StateTransition] {
        &self.state_history
    }

    /// ID of the key generator (the custodian who completed key generation).
    #[must_use]
    pub fn generator(&self) -> Option<&str> {
        self.generator.as_deref()
    }

    /// IDs of approvers (read-only).
    #[must_use]
    pub fn approvers(&self) -> &[String] {
        &self.approvers
    }

    /// ID of the destroyer (the custodian who performed destruction).
    #[must_use]
    pub fn destroyer(&self) -> Option<&str> {
        self.destroyer.as_deref()
    }

    /// When the key was activated.
    #[must_use]
    pub fn activated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
        self.activated_at
    }

    /// When key rotation was initiated.
    #[must_use]
    pub fn rotated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
        self.rotated_at
    }

    /// When the key was retired.
    #[must_use]
    pub fn retired_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
        self.retired_at
    }

    /// When the key was destroyed.
    #[must_use]
    pub fn destroyed_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
        self.destroyed_at
    }
}

// Formal verification with Kani (requires kani toolchain)
#[cfg(kani)]
mod kani_proofs {
    use super::*;

    // --- Existing proofs ---

    #[kani::proof]
    fn key_state_machine_destroyed_cannot_transition() {
        let to: KeyLifecycleState = kani::any();

        // Property: Destroyed keys cannot transition to any state
        let is_valid = KeyStateMachine::is_valid_transition(Some(KeyLifecycleState::Destroyed), to);
        kani::assert(!is_valid, "Destroyed keys should not transition");
    }

    #[kani::proof]
    fn key_state_machine_no_backward_to_generation() {
        let from: KeyLifecycleState = kani::any();

        // Property: Cannot go back to Generation from any state
        kani::assume(from != KeyLifecycleState::Generation);
        let is_valid =
            KeyStateMachine::is_valid_transition(Some(from), KeyLifecycleState::Generation);
        kani::assert(!is_valid, "Cannot transition back to Generation");
    }

    // --- New proofs ---

    /// Proves that the only valid initial state is Generation.
    /// Security property: keys must begin in the Generation state.
    #[kani::proof]
    fn key_state_machine_only_generation_from_none() {
        let to: KeyLifecycleState = kani::any();
        kani::assume(to != KeyLifecycleState::Generation);

        let is_valid = KeyStateMachine::is_valid_transition(None, to);
        kani::assert(!is_valid, "Only Generation is valid from initial state (None)");
    }

    /// Proves that `is_valid_transition` matches the SP 800-57 transition
    /// specification for all state pairs. This is an independent encoding of
    /// the allowed transitions — if `is_valid_transition` has a bug, this
    /// proof will catch the discrepancy.
    ///
    /// Note: `allowed_next_states()` returns `Vec`, which Kani cannot
    /// efficiently verify (unbounded loop unwinding on heap iteration).
    /// Unit tests verify `allowed_next_states()` separately.
    #[kani::proof]
    fn key_state_machine_transitions_match_spec() {
        let from: KeyLifecycleState = kani::any();
        let to: KeyLifecycleState = kani::any();

        let transition_valid = KeyStateMachine::is_valid_transition(Some(from), to);

        // Independent specification of SP 800-57 allowed transitions.
        // Generation has two valid forward edges: Active (normal
        // activation) and Destroyed (pre-activation compromise,
        // SP 800-57 Part 1 Rev. 5 §8.3.1).
        let spec_allows = match from {
            KeyLifecycleState::Generation => {
                to == KeyLifecycleState::Active || to == KeyLifecycleState::Destroyed
            }
            KeyLifecycleState::Active => {
                to == KeyLifecycleState::Rotating || to == KeyLifecycleState::Retired
            }
            KeyLifecycleState::Rotating => to == KeyLifecycleState::Retired,
            KeyLifecycleState::Retired => to == KeyLifecycleState::Destroyed,
            KeyLifecycleState::Destroyed => false,
        };

        kani::assert(
            transition_valid == spec_allows,
            "is_valid_transition must match SP 800-57 specification",
        );
    }

    /// Proves that retired keys can only transition to Destroyed.
    /// Security property: once retired, the only path is secure destruction.
    #[kani::proof]
    fn key_state_machine_retired_only_to_destroyed() {
        let to: KeyLifecycleState = kani::any();
        kani::assume(to != KeyLifecycleState::Destroyed);

        let is_valid = KeyStateMachine::is_valid_transition(Some(KeyLifecycleState::Retired), to);
        kani::assert(!is_valid, "Retired keys can only transition to Destroyed");
    }
}

#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "test/bench scaffolding: lints suppressed for this module")]
mod tests {
    use super::*;

    #[test]
    fn test_valid_state_transitions_succeeds() {
        // Generation -> Active (normal activation)
        assert!(KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Generation),
            KeyLifecycleState::Active
        ));

        // Generation -> Destroyed (pre-activation compromise; SP 800-57
        // Part 1 Rev. 5 §8.3.1 — pre-activation compromised keys may
        // be destroyed without passing through Activation).
        assert!(KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Generation),
            KeyLifecycleState::Destroyed
        ));

        // Active -> Rotating
        assert!(KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Active),
            KeyLifecycleState::Rotating
        ));

        // Rotating -> Retired
        assert!(KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Rotating),
            KeyLifecycleState::Retired
        ));

        // Retired -> Destroyed
        assert!(KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Retired),
            KeyLifecycleState::Destroyed
        ));

        // Active -> Retired (direct)
        assert!(KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Active),
            KeyLifecycleState::Retired
        ));
    }

    #[test]
    fn test_invalid_state_transitions_fails() {
        // Cannot go backwards
        assert!(!KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Active),
            KeyLifecycleState::Generation
        ));

        // Cannot skip from Active straight to Destroyed (would have to
        // pass through Retired — the NIST "Active compromise destroys
        // active key" path goes Active -> Retired -> Destroyed). The
        // emergency-destruction shortcut only applies to pre-activation
        // (Generation) keys.
        assert!(!KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Active),
            KeyLifecycleState::Destroyed
        ));

        // Destroyed cannot transition
        assert!(!KeyStateMachine::is_valid_transition(
            Some(KeyLifecycleState::Destroyed),
            KeyLifecycleState::Active
        ));
    }

    #[test]
    fn test_allowed_next_states_succeeds() {
        // Generation has TWO valid forward edges: Active (normal
        // activation) and Destroyed (pre-activation compromise).
        // Vec ordering is the implementation's responsibility; assert
        // set-equality via `contains` to keep the test robust to
        // reordering.
        let from_generation = KeyStateMachine::allowed_next_states(KeyLifecycleState::Generation);
        assert_eq!(from_generation.len(), 2);
        assert!(from_generation.contains(&KeyLifecycleState::Active));
        assert!(from_generation.contains(&KeyLifecycleState::Destroyed));

        assert_eq!(
            KeyStateMachine::allowed_next_states(KeyLifecycleState::Active),
            vec![KeyLifecycleState::Rotating, KeyLifecycleState::Retired]
        );

        assert_eq!(KeyStateMachine::allowed_next_states(KeyLifecycleState::Destroyed), vec![]);
    }

    #[test]
    fn test_generation_to_destroyed_audit_trail_preserves_truth() {
        // End-to-end check that the audit trail for a pre-activation
        // destroyed key shows EXACTLY [Generation -> Destroyed],
        // without spurious Active/Retired entries that would falsify
        // the record. SP 800-57 §8.3.1 alignment.
        let mut record = KeyLifecycleRecord::new(
            "compromised-pre-activation".to_string(),
            "ML-KEM-768".to_string(),
            3,
            365,
            30,
        )
        .unwrap();
        assert_eq!(record.current_state(), KeyLifecycleState::Generation);

        record
            .transition(
                KeyLifecycleState::Destroyed,
                "incident-responder".to_string(),
                "Pre-activation key compromised; emergency destruction per SP 800-57 §8.3.1"
                    .to_string(),
                Some("incident-2026-05-11".to_string()),
            )
            .unwrap();

        assert_eq!(record.current_state(), KeyLifecycleState::Destroyed);
        // Exactly one transition in the history.
        let history = record.state_history();
        assert_eq!(history.len(), 1);
        let transition = history.first().unwrap();
        assert_eq!(transition.from_state(), Some(KeyLifecycleState::Generation));
        assert_eq!(transition.to_state(), KeyLifecycleState::Destroyed);
        // Crucially: the audit trail does NOT contain Active or
        // Retired entries for a key that was never activated.
        assert!(history.iter().all(|t| t.to_state() != KeyLifecycleState::Active));
        assert!(history.iter().all(|t| t.to_state() != KeyLifecycleState::Retired));
    }

    #[test]
    fn test_new_rejects_security_level_below_range_fails() {
        let err = KeyLifecycleRecord::new("k".to_string(), "ML-KEM-768".to_string(), 0, 365, 30)
            .unwrap_err();
        assert!(matches!(err, TypeError::InvalidAuditInput(_)), "got {:?}", err);
        assert!(err.to_string().contains("security_level 0"));
    }

    #[test]
    fn test_new_rejects_security_level_above_range_fails() {
        let err = KeyLifecycleRecord::new("k".to_string(), "ML-KEM-768".to_string(), 6, 365, 30)
            .unwrap_err();
        assert!(matches!(err, TypeError::InvalidAuditInput(_)), "got {:?}", err);
        assert!(err.to_string().contains("security_level 6"));
    }

    #[test]
    fn test_new_rejects_zero_rotation_interval_fails() {
        let err = KeyLifecycleRecord::new("k".to_string(), "ML-KEM-768".to_string(), 3, 0, 30)
            .unwrap_err();
        assert!(matches!(err, TypeError::InvalidAuditInput(_)), "got {:?}", err);
        assert!(err.to_string().contains("rotation_interval_days 0"));
    }

    /// Build a JSON document that bypasses `KeyLifecycleRecord::new`'s
    /// numeric-bounds gate (the constructor's `Result` arm is never
    /// reached) so the deserializer's parity-with-`new` validation is
    /// the only thing that can catch the invalid value.
    fn build_record_json(security_level: u32, rotation_interval_days: u32) -> String {
        format!(
            r#"{{"key_id":"k","key_type":"ML-KEM-768","security_level":{security_level},"current_state":"Generation","state_history":[],"generator":null,"approvers":[],"destroyer":null,"generated_at":"2026-01-01T00:00:00Z","activated_at":null,"rotated_at":null,"retired_at":null,"destroyed_at":null,"rotation_interval_days":{rotation_interval_days},"overlap_period_days":30}}"#
        )
    }

    #[test]
    fn test_deserialize_rejects_security_level_below_range_fails() {
        let json = build_record_json(0, 365);
        let err = serde_json::from_str::<KeyLifecycleRecord>(&json).unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("security_level 0"), "deserializer should echo rejection: got {msg}");
    }

    #[test]
    fn test_deserialize_rejects_security_level_above_range_fails() {
        let json = build_record_json(6, 365);
        let err = serde_json::from_str::<KeyLifecycleRecord>(&json).unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("security_level 6"), "deserializer should echo rejection: got {msg}");
    }

    #[test]
    fn test_deserialize_rejects_zero_rotation_interval_fails() {
        let json = build_record_json(3, 0);
        let err = serde_json::from_str::<KeyLifecycleRecord>(&json).unwrap_err();
        let msg = err.to_string();
        assert!(
            msg.contains("rotation_interval_days 0"),
            "deserializer should echo rejection: got {msg}"
        );
    }

    #[test]
    fn test_deserialize_rejects_destroyed_with_fabricated_activated_at() {
        // Tamper scenario: state_history skips Active entirely
        // (Generation → Destroyed), but the persisted record carries a
        // fabricated `activated_at`. Without the Active/activated_at
        // parity check, this passed deserialization unchallenged because
        // `current_state=Destroyed` does not appear in the (state,
        // timestamp) match arm. The parity check in (3) closes that gap.
        let json = r#"{
            "key_id":"k","key_type":"ML-KEM-768","security_level":3,
            "current_state":"Destroyed",
            "state_history":[
                {"from_state":null,"to_state":"Generation","timestamp":"2026-01-01T00:00:00Z","custodian_id":"alice","justification":"init","approval_id":null},
                {"from_state":"Generation","to_state":"Destroyed","timestamp":"2026-01-02T00:00:00Z","custodian_id":"alice","justification":"abort","approval_id":null}
            ],
            "generator":null,"approvers":[],"destroyer":null,
            "generated_at":"2026-01-01T00:00:00Z",
            "activated_at":"2026-01-01T12:00:00Z",
            "rotated_at":null,"retired_at":null,
            "destroyed_at":"2026-01-02T00:00:00Z",
            "rotation_interval_days":365,"overlap_period_days":30
        }"#;
        let err = serde_json::from_str::<KeyLifecycleRecord>(json).unwrap_err();
        let msg = err.to_string();
        assert!(
            msg.contains("activated_at presence disagrees with state_history"),
            "deserializer should reject fabricated activated_at: got {msg}"
        );
    }

    #[test]
    fn test_key_lifecycle_record_succeeds() {
        let mut record = KeyLifecycleRecord::new(
            "test-key-123".to_string(),
            "ML-KEM-768".to_string(),
            3,
            365,
            30,
        )
        .unwrap();

        assert_eq!(record.current_state, KeyLifecycleState::Generation);
        assert!(!record.is_valid_for_use());

        // Transition to Active
        record
            .transition(
                KeyLifecycleState::Active,
                "alice".to_string(),
                "Key generation complete".to_string(),
                Some("approval-123".to_string()),
            )
            .unwrap();

        assert_eq!(record.current_state, KeyLifecycleState::Active);
        assert!(record.is_valid_for_use());
        assert_eq!(record.generator, Some("alice".to_string()));
        assert!(record.activated_at.is_some());

        // Check rotation requirement (new key shouldn't need rotation)
        assert!(!record.requires_rotation());
    }

    #[test]
    fn test_rotation_requirement_succeeds() {
        let mut record = KeyLifecycleRecord::new(
            "test-key-123".to_string(),
            "AES-256".to_string(),
            3,
            90, // 90 day rotation
            7,
        )
        .unwrap();

        // Drive through the state machine before back-dating, so that
        // `state_history` and `generator` are populated. A future
        // rotation policy that consults history (not just the cached
        // timestamp) cannot then be silently masked by this test.
        record
            .transition(
                KeyLifecycleState::Active,
                "alice".to_string(),
                "test activation".to_string(),
                None,
            )
            .unwrap();
        record.activated_at = Some(chrono::Utc::now() - chrono::Duration::days(100));

        assert!(record.requires_rotation());
        assert_eq!(record.age_days(), Some(100));
    }

    #[test]
    fn test_transition_validation_succeeds() {
        let mut record = KeyLifecycleRecord::new(
            "test-key-123".to_string(),
            "ML-DSA-65".to_string(),
            3,
            365,
            30,
        )
        .unwrap();

        // Active -> Generation is NOT a valid transition (no backward
        // edge from any state to Generation). We need an Active state
        // first; activate the key, then try to roll back.
        record
            .transition(
                KeyLifecycleState::Active,
                "alice".to_string(),
                "activate".to_string(),
                None,
            )
            .unwrap();

        // Active -> Generation must fail (no backward transition).
        let result = record.transition(
            KeyLifecycleState::Generation,
            "alice".to_string(),
            "Invalid transition: cannot go backwards".to_string(),
            None,
        );
        assert!(matches!(result, Err(TypeError::InvalidStateTransition { .. })));
    }

    #[test]
    fn test_add_approver_succeeds() {
        let mut record = KeyLifecycleRecord::new(
            "test-key-123".to_string(),
            "ML-KEM-768".to_string(),
            3,
            365,
            30,
        )
        .unwrap();

        assert!(record.add_approver("alice".to_string()));
        assert!(record.add_approver("bob".to_string()));
        // Duplicate: returns true (idempotent acceptance) but
        // doesn't grow the list.
        assert!(record.add_approver("alice".to_string()));

        assert_eq!(record.approvers.len(), 2);
        assert!(record.approvers.contains(&"alice".to_string()));
        assert!(record.approvers.contains(&"bob".to_string()));
    }
}
