//! Bounded, redacted observability, journal, metrics, and deterministic replay.
//!
//! Events describe externally observable inputs, decisions, actions, and outcomes.
//! They deliberately contain no prompt bodies, private message bodies, credentials,
//! capability URLs, inventory payloads, or hidden model reasoning. Ordering is total
//! inside one recorder; concurrently produced events reflect recorder admission order.

#![allow(clippy::missing_errors_doc)]

use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, VecDeque};
use std::error::Error;
use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, Weak};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle;

use crate::policy::{
    MemoryPolicyAudit, PolicyAuditError, PolicyAuditRecord, PolicyAuditSink, PolicyDisposition,
    PolicyFinalOutcome,
};

pub const OBSERVABILITY_SCHEMA_VERSION: u16 = 1;
const MAX_EVENT_BYTES_HARD: usize = 16 * 1024;
const MAX_RING_EVENTS_HARD: usize = 65_536;
const MAX_SUBSCRIBERS_HARD: usize = 1_024;
const MAX_FIELD_COUNT: usize = 32;
const MAX_CODE_BYTES: usize = 96;
const MAX_ID_BYTES: usize = 128;
const MAX_DIAGNOSTIC_ENTRIES_HARD: usize = 4_096;
const JOURNAL_EVENT_ID_RESERVATION: u64 = 1 << 32;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventSeverity {
    Trace,
    Info,
    Warning,
    Error,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventOrigin {
    Grid,
    Model,
    Policy,
    Scheduler,
    Control,
    Operator,
    Service,
}

/// Stable event families. The wire schema stores their snake-case code so
/// unknown future families remain readable by older replay implementations.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EventFamily {
    LifecycleTransition,
    InboundMessage,
    OutboundMessage,
    SessionCreated,
    SessionExpired,
    InferenceRequest,
    InferenceResult,
    ModelActionSummary,
    ToolProposed,
    PolicyDecision,
    ApprovalDecision,
    ToolStarted,
    ToolProgress,
    ToolResult,
    BehaviorTransition,
    ScheduledJob,
    ControlCommand,
    Shutdown,
    DiagnosticEnvelope,
}

impl EventFamily {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::LifecycleTransition => "lifecycle_transition",
            Self::InboundMessage => "inbound_message",
            Self::OutboundMessage => "outbound_message",
            Self::SessionCreated => "session_created",
            Self::SessionExpired => "session_expired",
            Self::InferenceRequest => "inference_request",
            Self::InferenceResult => "inference_result",
            Self::ModelActionSummary => "model_action_summary",
            Self::ToolProposed => "tool_proposed",
            Self::PolicyDecision => "policy_decision",
            Self::ApprovalDecision => "approval_decision",
            Self::ToolStarted => "tool_started",
            Self::ToolProgress => "tool_progress",
            Self::ToolResult => "tool_result",
            Self::BehaviorTransition => "behavior_transition",
            Self::ScheduledJob => "scheduled_job",
            Self::ControlCommand => "control_command",
            Self::Shutdown => "shutdown",
            Self::DiagnosticEnvelope => "diagnostic_envelope",
        }
    }
}

#[derive(Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CorrelationIds {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub avatar_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub action_id: Option<String>,
}

impl CorrelationIds {
    fn validate(&self) -> bool {
        [
            self.avatar_id.as_deref(),
            self.session_id.as_deref(),
            self.request_id.as_deref(),
            self.action_id.as_deref(),
        ]
        .into_iter()
        .flatten()
        .all(valid_identifier)
    }

    fn pseudonymized(mut self) -> Self {
        self.avatar_id = self.avatar_id.map(|value| pseudonymous_identifier(&value));
        self.session_id = self.session_id.map(|value| pseudonymous_identifier(&value));
        self.request_id = self.request_id.map(|value| pseudonymous_identifier(&value));
        self.action_id = self.action_id.map(|value| pseudonymous_identifier(&value));
        self
    }
}

impl fmt::Debug for CorrelationIds {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("CorrelationIds")
            .field("avatar", &self.avatar_id.is_some())
            .field("session", &self.session_id.is_some())
            .field("request", &self.request_id.is_some())
            .field("action", &self.action_id.is_some())
            .finish()
    }
}

/// One forward-compatible JSONL event. Unknown keys inside `fields` survive a
/// deserialize/serialize cycle and unknown `family` values remain replayable.
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct StructuredEvent {
    pub schema_version: u16,
    pub event_id: u64,
    pub unix_millis: u64,
    pub severity: EventSeverity,
    pub component: String,
    pub family: String,
    pub correlation: CorrelationIds,
    pub origin: EventOrigin,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_millis: Option<u64>,
    pub retry_count: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result_code: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason_code: Option<String>,
    pub redaction_flags: Vec<String>,
    #[serde(default)]
    pub fields: BTreeMap<String, Value>,
    /// Unknown top-level properties retained for schema evolution.
    #[serde(flatten)]
    pub extensions: BTreeMap<String, Value>,
}

impl fmt::Debug for StructuredEvent {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("StructuredEvent")
            .field("schema_version", &self.schema_version)
            .field("event_id", &self.event_id)
            .field("unix_millis", &self.unix_millis)
            .field("severity", &self.severity)
            .field("component_bytes", &self.component.len())
            .field("family_bytes", &self.family.len())
            .field("correlation", &self.correlation)
            .field("origin", &self.origin)
            .field("duration_millis", &self.duration_millis)
            .field("retry_count", &self.retry_count)
            .field("has_result_code", &self.result_code.is_some())
            .field("has_reason_code", &self.reason_code.is_some())
            .field("redaction_count", &self.redaction_flags.len())
            .field("field_count", &self.fields.len())
            .field("extension_count", &self.extensions.len())
            .finish_non_exhaustive()
    }
}

/// Validated event before the recorder assigns identity and time.
#[derive(Clone, Debug)]
pub struct EventDraft {
    severity: EventSeverity,
    component: String,
    family: EventFamily,
    correlation: CorrelationIds,
    origin: EventOrigin,
    duration_millis: Option<u64>,
    retry_count: u32,
    result_code: Option<String>,
    reason_code: Option<String>,
    redaction_flags: Vec<String>,
    fields: BTreeMap<String, Value>,
}

impl EventDraft {
    pub fn new(
        family: EventFamily,
        severity: EventSeverity,
        component: &'static str,
        origin: EventOrigin,
    ) -> Result<Self, ObservabilityError> {
        if !valid_code(component) {
            return Err(ObservabilityError::InvalidEvent("invalid component"));
        }
        Ok(Self {
            severity,
            component: component.to_owned(),
            family,
            correlation: CorrelationIds::default(),
            origin,
            duration_millis: None,
            retry_count: 0,
            result_code: None,
            reason_code: None,
            redaction_flags: Vec::new(),
            fields: BTreeMap::new(),
        })
    }

    pub fn correlation(mut self, correlation: CorrelationIds) -> Result<Self, ObservabilityError> {
        if !correlation.validate() {
            return Err(ObservabilityError::InvalidEvent(
                "invalid correlation identifier",
            ));
        }
        self.correlation = correlation.pseudonymized();
        Ok(self)
    }

    #[must_use]
    pub const fn duration_millis(mut self, duration_millis: u64) -> Self {
        self.duration_millis = Some(duration_millis);
        self
    }

    #[must_use]
    pub const fn retry_count(mut self, retry_count: u32) -> Self {
        self.retry_count = retry_count;
        self
    }

    pub fn result_code(mut self, code: &'static str) -> Result<Self, ObservabilityError> {
        self.result_code = Some(checked_code(code.to_owned())?);
        Ok(self)
    }

    pub fn reason_code(mut self, code: &'static str) -> Result<Self, ObservabilityError> {
        self.reason_code = Some(checked_code(code.to_owned())?);
        Ok(self)
    }

    pub fn redacted(mut self, flag: &'static str) -> Result<Self, ObservabilityError> {
        let flag = checked_code(flag.to_owned())?;
        if !self.redaction_flags.contains(&flag) {
            self.redaction_flags.push(flag);
        }
        Ok(self)
    }

    /// Adds a non-sensitive bounded scalar. Free-form text and nested payloads
    /// are intentionally rejected at this boundary.
    pub fn field(mut self, name: &'static str, value: Value) -> Result<Self, ObservabilityError> {
        let name = checked_code(name.to_owned())?;
        if self.fields.len() == MAX_FIELD_COUNT
            || !matches!(value, Value::Null | Value::Bool(_) | Value::Number(_))
        {
            return Err(ObservabilityError::InvalidEvent("unsafe event field"));
        }
        self.fields.insert(name, value);
        Ok(self)
    }

    /// Adds a readable, compile-time stable enum/state code.
    pub fn code_field(
        mut self,
        name: &'static str,
        value: &'static str,
    ) -> Result<Self, ObservabilityError> {
        let name = checked_code(name.to_owned())?;
        let value = checked_code(value.to_owned())?;
        if self.fields.len() == MAX_FIELD_COUNT {
            return Err(ObservabilityError::InvalidEvent("too many event fields"));
        }
        self.fields.insert(name, Value::String(value));
        Ok(self)
    }

    /// Adds a stable SHA-256 pseudonym for a runtime identifier.
    pub fn identifier_field(
        mut self,
        name: &'static str,
        value: &str,
    ) -> Result<Self, ObservabilityError> {
        let name = checked_code(name.to_owned())?;
        if value.is_empty() || value.len() > 16 * 1024 || self.fields.len() == MAX_FIELD_COUNT {
            return Err(ObservabilityError::InvalidEvent("invalid event identifier"));
        }
        self.fields
            .insert(name, Value::String(pseudonymous_identifier(value)));
        Ok(self)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ObservabilityLimits {
    pub ring_events: usize,
    pub subscriber_queue: usize,
    pub max_subscribers: usize,
    pub journal_queue: usize,
    pub max_event_bytes: usize,
    pub max_diagnostic_entries: usize,
}

impl Default for ObservabilityLimits {
    fn default() -> Self {
        Self {
            ring_events: 2_048,
            subscriber_queue: 256,
            max_subscribers: 64,
            journal_queue: 1_024,
            max_event_bytes: 8 * 1024,
            max_diagnostic_entries: 128,
        }
    }
}

impl ObservabilityLimits {
    #[must_use]
    pub const fn is_valid(self) -> bool {
        self.ring_events > 0
            && self.ring_events <= MAX_RING_EVENTS_HARD
            && self.subscriber_queue > 0
            && self.subscriber_queue <= 8_192
            && self.max_subscribers > 0
            && self.max_subscribers <= MAX_SUBSCRIBERS_HARD
            && self.journal_queue > 0
            && self.journal_queue <= 16_384
            && self.max_event_bytes >= 512
            && self.max_event_bytes <= MAX_EVENT_BYTES_HARD
            && self.max_diagnostic_entries <= MAX_DIAGNOSTIC_ENTRIES_HARD
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JournalConfig {
    pub directory: PathBuf,
    pub max_segment_bytes: u64,
    pub max_segments: usize,
    pub max_total_bytes: u64,
    pub sync_each_record: bool,
}

impl JournalConfig {
    fn validate(&self, max_event_bytes: usize) -> Result<(), ObservabilityError> {
        let event = u64::try_from(max_event_bytes).unwrap_or(u64::MAX);
        if self.max_segment_bytes < event
            || self.max_segment_bytes > 256 * 1024 * 1024
            || self.max_segments == 0
            || self.max_segments > 1_024
            || self.max_total_bytes < self.max_segment_bytes
            || self.max_total_bytes > 16 * 1024 * 1024 * 1024
        {
            return Err(ObservabilityError::UnsafeLimits);
        }
        Ok(())
    }
}

#[derive(Debug)]
pub enum ObservabilityError {
    UnsafeLimits,
    InvalidEvent(&'static str),
    Io(std::io::Error),
    Json(serde_json::Error),
    JournalClosed,
    CorruptJournal { path: PathBuf, line: usize },
}

impl fmt::Display for ObservabilityError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnsafeLimits => formatter.write_str("unsafe observability limits"),
            Self::InvalidEvent(reason) => write!(formatter, "invalid observable event: {reason}"),
            Self::Io(error) => write!(formatter, "observability I/O failed: {error}"),
            Self::Json(error) => write!(formatter, "observability JSON failed: {error}"),
            Self::JournalClosed => formatter.write_str("observability journal is closed"),
            Self::CorruptJournal { path, line } => {
                write!(
                    formatter,
                    "corrupt journal {} at line {line}",
                    path.display()
                )
            }
        }
    }
}

impl Error for ObservabilityError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Io(error) => Some(error),
            Self::Json(error) => Some(error),
            _ => None,
        }
    }
}

impl From<std::io::Error> for ObservabilityError {
    fn from(value: std::io::Error) -> Self {
        Self::Io(value)
    }
}

impl From<serde_json::Error> for ObservabilityError {
    fn from(value: serde_json::Error) -> Self {
        Self::Json(value)
    }
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct OutcomeMetrics {
    pub allowed: u64,
    pub denied: u64,
    pub approval_required: u64,
    pub completed: u64,
    pub failed: u64,
    pub cancelled: u64,
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct LatencyMetrics {
    pub samples: u64,
    pub total_millis: u64,
    pub maximum_millis: u64,
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct MetricsSnapshot {
    pub ready: bool,
    pub reconnects: u64,
    pub active_sessions: u64,
    pub active_tasks: u64,
    pub queue_depth: u64,
    pub queue_capacity: u64,
    pub inference_latency: LatencyMetrics,
    pub tool_latency: LatencyMetrics,
    pub inference_outcomes: OutcomeMetrics,
    pub tool_outcomes: OutcomeMetrics,
    pub policy_outcomes: OutcomeMetrics,
    pub rate_limit_used: u64,
    pub rate_limit_capacity: u64,
    pub recorded_events: u64,
    pub dropped_ring_events: u64,
    pub dropped_subscriber_events: u64,
    pub dropped_journal_events: u64,
    pub diagnostic_entries: u64,
}

#[derive(Debug, Default)]
struct AtomicLatency {
    samples: AtomicU64,
    total: AtomicU64,
    maximum: AtomicU64,
}

impl AtomicLatency {
    fn record(&self, millis: u64) {
        self.samples.fetch_add(1, Ordering::Relaxed);
        self.total.fetch_add(millis, Ordering::Relaxed);
        self.maximum.fetch_max(millis, Ordering::Relaxed);
    }

    fn snapshot(&self) -> LatencyMetrics {
        LatencyMetrics {
            samples: self.samples.load(Ordering::Relaxed),
            total_millis: self.total.load(Ordering::Relaxed),
            maximum_millis: self.maximum.load(Ordering::Relaxed),
        }
    }
}

#[derive(Debug, Default)]
struct AtomicOutcomes {
    allowed: AtomicU64,
    denied: AtomicU64,
    approval_required: AtomicU64,
    completed: AtomicU64,
    failed: AtomicU64,
    cancelled: AtomicU64,
}

impl AtomicOutcomes {
    fn record(&self, code: OutcomeCode) {
        let counter = match code {
            OutcomeCode::Allowed => &self.allowed,
            OutcomeCode::Denied => &self.denied,
            OutcomeCode::ApprovalRequired => &self.approval_required,
            OutcomeCode::Completed => &self.completed,
            OutcomeCode::Failed => &self.failed,
            OutcomeCode::Cancelled => &self.cancelled,
        };
        counter.fetch_add(1, Ordering::Relaxed);
    }

    fn snapshot(&self) -> OutcomeMetrics {
        OutcomeMetrics {
            allowed: self.allowed.load(Ordering::Relaxed),
            denied: self.denied.load(Ordering::Relaxed),
            approval_required: self.approval_required.load(Ordering::Relaxed),
            completed: self.completed.load(Ordering::Relaxed),
            failed: self.failed.load(Ordering::Relaxed),
            cancelled: self.cancelled.load(Ordering::Relaxed),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OutcomeCode {
    Allowed,
    Denied,
    ApprovalRequired,
    Completed,
    Failed,
    Cancelled,
}

/// Fixed-cardinality, lock-free metrics. There are no caller-defined labels.
#[derive(Debug, Default)]
pub struct AgentMetrics {
    ready: AtomicBool,
    reconnects: AtomicU64,
    active_sessions: AtomicU64,
    active_tasks: AtomicU64,
    queue_depth: AtomicU64,
    queue_capacity: AtomicU64,
    inference_latency: AtomicLatency,
    tool_latency: AtomicLatency,
    inference_outcomes: AtomicOutcomes,
    tool_outcomes: AtomicOutcomes,
    policy_outcomes: AtomicOutcomes,
    rate_limit_used: AtomicU64,
    rate_limit_capacity: AtomicU64,
    recorded_events: AtomicU64,
    dropped_ring_events: AtomicU64,
    dropped_subscriber_events: AtomicU64,
    dropped_journal_events: AtomicU64,
    diagnostic_entries: AtomicU64,
}

impl AgentMetrics {
    pub fn set_health(&self, ready: bool, reconnects: u64) {
        self.ready.store(ready, Ordering::Relaxed);
        self.reconnects.store(reconnects, Ordering::Relaxed);
    }

    pub fn set_ready(&self, ready: bool) {
        self.ready.store(ready, Ordering::Relaxed);
    }

    pub fn record_reconnect(&self) {
        self.reconnects.fetch_add(1, Ordering::Relaxed);
    }

    pub fn set_load(
        &self,
        active_sessions: u64,
        active_tasks: u64,
        queue_depth: u64,
        queue_capacity: u64,
    ) {
        self.active_sessions
            .store(active_sessions, Ordering::Relaxed);
        self.active_tasks.store(active_tasks, Ordering::Relaxed);
        self.queue_depth.store(queue_depth, Ordering::Relaxed);
        self.queue_capacity.store(queue_capacity, Ordering::Relaxed);
    }

    pub fn set_rate_limit(&self, used: u64, capacity: u64) {
        self.rate_limit_used.store(used, Ordering::Relaxed);
        self.rate_limit_capacity.store(capacity, Ordering::Relaxed);
    }

    pub fn record_inference_latency(&self, duration: Duration) {
        self.inference_latency.record(millis(duration));
    }

    pub fn record_tool_latency(&self, duration: Duration) {
        self.tool_latency.record(millis(duration));
    }

    pub fn record_tool_outcome(&self, outcome: OutcomeCode) {
        self.tool_outcomes.record(outcome);
    }

    pub fn record_inference_outcome(&self, outcome: OutcomeCode) {
        self.inference_outcomes.record(outcome);
    }

    pub fn record_policy_outcome(&self, outcome: OutcomeCode) {
        self.policy_outcomes.record(outcome);
    }

    #[must_use]
    pub fn snapshot(&self) -> MetricsSnapshot {
        MetricsSnapshot {
            ready: self.ready.load(Ordering::Relaxed),
            reconnects: self.reconnects.load(Ordering::Relaxed),
            active_sessions: self.active_sessions.load(Ordering::Relaxed),
            active_tasks: self.active_tasks.load(Ordering::Relaxed),
            queue_depth: self.queue_depth.load(Ordering::Relaxed),
            queue_capacity: self.queue_capacity.load(Ordering::Relaxed),
            inference_latency: self.inference_latency.snapshot(),
            tool_latency: self.tool_latency.snapshot(),
            inference_outcomes: self.inference_outcomes.snapshot(),
            tool_outcomes: self.tool_outcomes.snapshot(),
            policy_outcomes: self.policy_outcomes.snapshot(),
            rate_limit_used: self.rate_limit_used.load(Ordering::Relaxed),
            rate_limit_capacity: self.rate_limit_capacity.load(Ordering::Relaxed),
            recorded_events: self.recorded_events.load(Ordering::Relaxed),
            dropped_ring_events: self.dropped_ring_events.load(Ordering::Relaxed),
            dropped_subscriber_events: self.dropped_subscriber_events.load(Ordering::Relaxed),
            dropped_journal_events: self.dropped_journal_events.load(Ordering::Relaxed),
            diagnostic_entries: self.diagnostic_entries.load(Ordering::Relaxed),
        }
    }
}

#[derive(Debug)]
struct RecorderState {
    ring: VecDeque<StructuredEvent>,
    subscribers: BTreeMap<u64, mpsc::Sender<StructuredEvent>>,
    next_subscriber: u64,
}

#[derive(Debug)]
enum JournalCommand {
    Event(Vec<u8>),
    Shutdown(oneshot::Sender<Result<(), ObservabilityError>>),
}

/// Nonblocking recorder shared by service components and control clients.
pub struct Observability {
    limits: ObservabilityLimits,
    next_event: AtomicU64,
    state: Mutex<RecorderState>,
    metrics: AgentMetrics,
    journal: Option<mpsc::Sender<JournalCommand>>,
    event_id_limit: u64,
}

/// Policy audit sink that preserves the existing fail-closed bounded audit and
/// mirrors content-free decisions into the unified event stream.
#[derive(Debug)]
pub struct UnifiedPolicyAudit {
    memory: Arc<MemoryPolicyAudit>,
    observability: Arc<Observability>,
}

impl UnifiedPolicyAudit {
    #[must_use]
    pub const fn new(memory: Arc<MemoryPolicyAudit>, observability: Arc<Observability>) -> Self {
        Self {
            memory,
            observability,
        }
    }

    #[must_use]
    pub fn memory(&self) -> Arc<MemoryPolicyAudit> {
        Arc::clone(&self.memory)
    }
}

impl PolicyAuditSink for UnifiedPolicyAudit {
    fn emit(&self, record: PolicyAuditRecord) -> Result<(), PolicyAuditError> {
        self.memory.emit(record.clone())?;
        let correlation = CorrelationIds {
            avatar_id: record.origin_avatar_id.map(|id| id.to_string()),
            session_id: Some(record.session_id.as_str().to_owned()),
            request_id: Some(record.correlation_id.as_str().to_owned()),
            action_id: record
                .authorization_id
                .map(|id| format!("authorization-{id}")),
        };
        let disposition = policy_disposition_code(record.disposition);
        let reason = policy_reason_code(record.reason);
        let outcome = policy_outcome_code(record.final_outcome);
        let proposed = EventDraft::new(
            EventFamily::ToolProposed,
            EventSeverity::Info,
            "policy",
            EventOrigin::Model,
        )
        .and_then(|event| event.correlation(correlation.clone()))
        .and_then(|event| event.identifier_field("tool", record.tool.as_str()))
        .and_then(|event| {
            event.identifier_field("arguments_digest", record.arguments_hash.as_str())
        })
        .and_then(|event| event.redacted("tool_arguments"));
        if let Ok(proposed) = proposed {
            let _ = self.observability.record(proposed);
        }
        let family = match record.final_outcome {
            PolicyFinalOutcome::ApprovalGranted => EventFamily::ApprovalDecision,
            PolicyFinalOutcome::Completed
            | PolicyFinalOutcome::Rejected
            | PolicyFinalOutcome::Failed
            | PolicyFinalOutcome::AmbiguousMutation => EventFamily::ToolResult,
            _ => EventFamily::PolicyDecision,
        };
        let severity = if matches!(
            record.final_outcome,
            PolicyFinalOutcome::Failed | PolicyFinalOutcome::AmbiguousMutation
        ) {
            EventSeverity::Error
        } else if matches!(record.disposition, PolicyDisposition::Denied) {
            EventSeverity::Warning
        } else {
            EventSeverity::Info
        };
        let event = EventDraft::new(family, severity, "policy", EventOrigin::Policy)
            .and_then(|event| event.correlation(correlation))
            .and_then(|event| event.result_code(outcome))
            .and_then(|event| event.reason_code(reason))
            .and_then(|event| event.identifier_field("tool", record.tool.as_str()))
            .and_then(|event| event.code_field("disposition", disposition))
            .and_then(|event| {
                event.field("tool_calls", Value::from(record.applied_budget.tool_calls))
            })
            .and_then(|event| {
                event.field(
                    "movement_millimeters",
                    Value::from(record.applied_budget.movement_millimeters),
                )
            })
            .and_then(|event| event.redacted("tool_arguments"));
        if let Ok(event) = event {
            let _ = self.observability.record(event);
        }
        let metric = match record.final_outcome {
            PolicyFinalOutcome::Denied | PolicyFinalOutcome::Rejected => OutcomeCode::Denied,
            PolicyFinalOutcome::ApprovalRequired => OutcomeCode::ApprovalRequired,
            PolicyFinalOutcome::Completed => OutcomeCode::Completed,
            PolicyFinalOutcome::Failed | PolicyFinalOutcome::AmbiguousMutation => {
                OutcomeCode::Failed
            }
            PolicyFinalOutcome::ApprovalGranted | PolicyFinalOutcome::Authorized => {
                OutcomeCode::Allowed
            }
        };
        self.observability.metrics.record_policy_outcome(metric);
        if family == EventFamily::ToolResult {
            self.observability.metrics.record_tool_outcome(metric);
        }
        Ok(())
    }
}

impl fmt::Debug for Observability {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Observability")
            .field("limits", &self.limits)
            .field("metrics", &self.metrics.snapshot())
            .finish_non_exhaustive()
    }
}

impl Observability {
    pub fn memory(limits: ObservabilityLimits) -> Result<Arc<Self>, ObservabilityError> {
        if !limits.is_valid() {
            return Err(ObservabilityError::UnsafeLimits);
        }
        Ok(Arc::new(Self::create(limits, None, 1, u64::MAX)))
    }

    /// Creates a recorder with an owned asynchronous journal worker.
    pub fn journaled(
        limits: ObservabilityLimits,
        config: JournalConfig,
    ) -> Result<(Arc<Self>, ObservabilityRuntime), ObservabilityError> {
        if !limits.is_valid() {
            return Err(ObservabilityError::UnsafeLimits);
        }
        config.validate(limits.max_event_bytes)?;
        fs::create_dir_all(&config.directory)?;
        let active = config.directory.join("events-current.jsonl.tmp");
        recover_active(&active, config.max_segment_bytes)?;
        let scanned_next = scan_last_event_id(&config.directory, limits.max_event_bytes)?
            .checked_add(1)
            .ok_or(ObservabilityError::InvalidEvent("event ID space exhausted"))?;
        let (next_event, event_id_limit) = reserve_event_ids(&config.directory, scanned_next)?;
        let (sender, receiver) = mpsc::channel(limits.journal_queue);
        let observer = Arc::new(Self::create(
            limits,
            Some(sender),
            next_event,
            event_id_limit,
        ));
        let metrics = Arc::downgrade(&observer);
        let stop = Arc::new(AtomicBool::new(false));
        let worker_stop = Arc::clone(&stop);
        let task = tokio::task::spawn_blocking(move || {
            journal_worker(config, receiver, metrics, &worker_stop)
        });
        Ok((
            observer,
            ObservabilityRuntime {
                task: Some(task),
                stop,
            },
        ))
    }

    fn create(
        limits: ObservabilityLimits,
        journal: Option<mpsc::Sender<JournalCommand>>,
        next_event: u64,
        event_id_limit: u64,
    ) -> Self {
        Self {
            limits,
            next_event: AtomicU64::new(next_event),
            state: Mutex::new(RecorderState {
                ring: VecDeque::with_capacity(limits.ring_events),
                subscribers: BTreeMap::new(),
                next_subscriber: 1,
            }),
            metrics: AgentMetrics::default(),
            journal,
            event_id_limit,
        }
    }

    #[must_use]
    pub const fn metrics(&self) -> &AgentMetrics {
        &self.metrics
    }

    /// Records without awaiting disk or subscribers. Overflow is counted and
    /// old ring entries are evicted; slow subscribers are disconnected.
    pub fn record(&self, draft: EventDraft) -> Result<u64, ObservabilityError> {
        let event_id = self
            .next_event
            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
                (current < self.event_id_limit)
                    .then(|| current.checked_add(1))
                    .flatten()
            })
            .map_err(|_| ObservabilityError::InvalidEvent("event ID space exhausted"))?;
        let event = StructuredEvent {
            schema_version: OBSERVABILITY_SCHEMA_VERSION,
            event_id,
            unix_millis: unix_millis(),
            severity: draft.severity,
            component: draft.component,
            family: draft.family.as_str().to_owned(),
            correlation: draft.correlation,
            origin: draft.origin,
            duration_millis: draft.duration_millis,
            retry_count: draft.retry_count,
            result_code: draft.result_code,
            reason_code: draft.reason_code,
            redaction_flags: draft.redaction_flags,
            fields: draft.fields,
            extensions: BTreeMap::new(),
        };
        let bytes = serde_json::to_vec(&event)?;
        if bytes.len().saturating_add(1) > self.limits.max_event_bytes {
            return Err(ObservabilityError::InvalidEvent(
                "serialized event exceeds bound",
            ));
        }
        self.metrics.recorded_events.fetch_add(1, Ordering::Relaxed);
        {
            let mut state = lock(&self.state);
            if state.ring.len() == self.limits.ring_events {
                state.ring.pop_front();
                self.metrics
                    .dropped_ring_events
                    .fetch_add(1, Ordering::Relaxed);
            }
            state.ring.push_back(event.clone());
            state.subscribers.retain(|_, subscriber| {
                if subscriber.try_send(event.clone()).is_ok() {
                    true
                } else {
                    self.metrics
                        .dropped_subscriber_events
                        .fetch_add(1, Ordering::Relaxed);
                    false
                }
            });
        }
        if let Some(journal) = &self.journal
            && journal.try_send(JournalCommand::Event(bytes)).is_err()
        {
            self.metrics
                .dropped_journal_events
                .fetch_add(1, Ordering::Relaxed);
        }
        Ok(event_id)
    }

    #[must_use]
    pub fn snapshot(&self) -> Vec<StructuredEvent> {
        lock(&self.state).ring.iter().cloned().collect()
    }

    pub fn subscribe(
        self: &Arc<Self>,
        after_event_id: Option<u64>,
    ) -> Result<EventSubscription, ObservabilityError> {
        let mut state = lock(&self.state);
        if state.subscribers.len() == self.limits.max_subscribers {
            return Err(ObservabilityError::InvalidEvent("subscriber limit reached"));
        }
        let retained_first = state
            .ring
            .front()
            .map_or(self.next_event.load(Ordering::Relaxed), |event| {
                event.event_id
            });
        let requested = after_event_id.map_or(retained_first, |id| id.saturating_add(1));
        let gap = (requested < retained_first).then_some((requested, retained_first));
        let initial = state
            .ring
            .iter()
            .filter(|event| event.event_id >= requested.max(retained_first))
            .cloned()
            .collect();
        let (sender, receiver) = mpsc::channel(self.limits.subscriber_queue);
        let id = state.next_subscriber;
        state.next_subscriber = state.next_subscriber.saturating_add(1);
        state.subscribers.insert(id, sender);
        Ok(EventSubscription {
            id,
            owner: Arc::downgrade(self),
            gap,
            initial,
            receiver,
        })
    }

    /// Records an explicit diagnostic envelope containing only sizes and
    /// SHA-256 digests. Content is never retained, even when capture is enabled.
    pub fn capture_diagnostic(
        &self,
        direction: DiagnosticDirection,
        request_id: impl Into<String>,
        parts: &[SensitiveDiagnosticPart<'_>],
    ) -> Result<u64, ObservabilityError> {
        let current = self.metrics.diagnostic_entries.load(Ordering::Relaxed);
        if current >= u64::try_from(self.limits.max_diagnostic_entries).unwrap_or(u64::MAX) {
            return Err(ObservabilityError::InvalidEvent(
                "diagnostic retention limit reached",
            ));
        }
        let request_id = request_id.into();
        if !valid_identifier(&request_id) || parts.len() > 64 {
            return Err(ObservabilityError::InvalidEvent(
                "invalid diagnostic envelope",
            ));
        }
        let total_bytes = parts
            .iter()
            .fold(0usize, |sum, part| sum.saturating_add(part.contents.len()));
        let mut hasher = Sha256::new();
        for part in parts {
            hasher.update(part.role.as_bytes());
            hasher.update([0]);
            hasher.update(part.contents.as_bytes());
            hasher.update([0xff]);
        }
        let digest = format!("sha256:{}", hex_bytes(&hasher.finalize()));
        let draft = EventDraft::new(
            EventFamily::DiagnosticEnvelope,
            EventSeverity::Warning,
            "llm",
            EventOrigin::Model,
        )?
        .correlation(CorrelationIds {
            request_id: Some(request_id),
            ..CorrelationIds::default()
        })?
        .redacted("content_omitted")?
        .redacted("diagnostic_capture")?
        .code_field("direction", direction.as_str())?
        .field("part_count", Value::from(parts.len()))?
        .field("content_bytes", Value::from(total_bytes))?
        .identifier_field("content_digest", &digest)?;
        self.metrics
            .diagnostic_entries
            .fetch_add(1, Ordering::Relaxed);
        self.record(draft)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiagnosticDirection {
    Prompt,
    Response,
}

impl DiagnosticDirection {
    const fn as_str(self) -> &'static str {
        match self {
            Self::Prompt => "prompt",
            Self::Response => "response",
        }
    }
}

pub struct SensitiveDiagnosticPart<'a> {
    pub role: &'a str,
    pub contents: &'a str,
}

impl fmt::Debug for SensitiveDiagnosticPart<'_> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("SensitiveDiagnosticPart")
            .field("role_bytes", &self.role.len())
            .field("content_bytes", &self.contents.len())
            .field("contents", &"[REDACTED]")
            .finish()
    }
}

pub struct EventSubscription {
    id: u64,
    owner: Weak<Observability>,
    gap: Option<(u64, u64)>,
    initial: VecDeque<StructuredEvent>,
    receiver: mpsc::Receiver<StructuredEvent>,
}

impl EventSubscription {
    #[must_use]
    pub fn take_gap(&mut self) -> Option<(u64, u64)> {
        self.gap.take()
    }

    pub async fn recv(&mut self) -> Option<StructuredEvent> {
        if let Some(event) = self.initial.pop_front() {
            Some(event)
        } else {
            self.receiver.recv().await
        }
    }
}

impl Drop for EventSubscription {
    fn drop(&mut self) {
        if let Some(owner) = self.owner.upgrade() {
            lock(&owner.state).subscribers.remove(&self.id);
        }
    }
}

pub struct ObservabilityRuntime {
    task: Option<JoinHandle<Result<(), ObservabilityError>>>,
    stop: Arc<AtomicBool>,
}

impl ObservabilityRuntime {
    pub async fn shutdown(mut self, observer: &Observability) -> Result<(), ObservabilityError> {
        if let Some(sender) = &observer.journal {
            let (reply, receive) = oneshot::channel();
            sender
                .send(JournalCommand::Shutdown(reply))
                .await
                .map_err(|_| ObservabilityError::JournalClosed)?;
            receive
                .await
                .map_err(|_| ObservabilityError::JournalClosed)??;
        }
        if let Some(task) = self.task.take() {
            task.await
                .map_err(|_| ObservabilityError::JournalClosed)??;
        }
        Ok(())
    }
}

impl Drop for ObservabilityRuntime {
    fn drop(&mut self) {
        // `spawn_blocking` tasks cannot be cancelled once running. Signal the
        // polling worker so dropping this guard cannot leave an orphan thread.
        self.stop.store(true, Ordering::Release);
    }
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct ReplayAction {
    pub action_id: String,
    pub events: Vec<u64>,
    pub final_result: Option<String>,
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ReplayState {
    pub last_event_id: u64,
    pub lifecycle_state: Option<String>,
    pub behavior_state: Option<String>,
    pub sessions_created: u64,
    pub sessions_expired: u64,
    pub policy_decisions: u64,
    pub tool_outcomes: u64,
    pub unavailable_private_content: bool,
    pub actions: BTreeMap<String, ReplayAction>,
    pub timeline: Vec<StructuredEvent>,
}

/// Reads journal files in segment order and reconstructs observable state only.
/// It never invokes a backend, model, policy gateway, or mutation executor.
pub fn replay_journal(
    directory: impl AsRef<Path>,
    max_event_bytes: usize,
    max_events: usize,
) -> Result<ReplayState, ObservabilityError> {
    if max_event_bytes == 0 || max_event_bytes > MAX_EVENT_BYTES_HARD || max_events == 0 {
        return Err(ObservabilityError::UnsafeLimits);
    }
    let mut paths = journal_paths(directory.as_ref())?;
    paths.sort();
    let mut replay = ReplayState::default();
    for path in paths {
        let active = path
            .file_name()
            .is_some_and(|name| name == "events-current.jsonl.tmp");
        read_journal_file(&path, active, max_event_bytes, max_events, &mut replay)?;
    }
    replay.timeline.sort_by_key(|event| event.event_id);
    replay.last_event_id = replay.timeline.last().map_or(0, |event| event.event_id);
    Ok(replay)
}

fn read_journal_file(
    path: &Path,
    allow_truncated_final: bool,
    max_event_bytes: usize,
    max_events: usize,
    replay: &mut ReplayState,
) -> Result<(), ObservabilityError> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);
    let mut line = Vec::new();
    let mut line_number = 0usize;
    loop {
        line.clear();
        let bytes = reader.read_until(b'\n', &mut line)?;
        if bytes == 0 {
            break;
        }
        line_number = line_number.saturating_add(1);
        if line.len() > max_event_bytes {
            return Err(ObservabilityError::CorruptJournal {
                path: path.to_owned(),
                line: line_number,
            });
        }
        let terminated = line.last() == Some(&b'\n');
        if !terminated && allow_truncated_final {
            break;
        }
        let event: StructuredEvent =
            serde_json::from_slice(&line).map_err(|_| ObservabilityError::CorruptJournal {
                path: path.to_owned(),
                line: line_number,
            })?;
        if replay.timeline.len() == max_events {
            return Err(ObservabilityError::UnsafeLimits);
        }
        apply_replay(replay, &event);
        replay.timeline.push(event);
    }
    Ok(())
}

fn apply_replay(replay: &mut ReplayState, event: &StructuredEvent) {
    replay.unavailable_private_content |= event.redaction_flags.iter().any(|flag| {
        matches!(
            flag.as_str(),
            "content_omitted" | "private_content" | "inventory_payload"
        )
    });
    match event.family.as_str() {
        "lifecycle_transition" => {
            replay.lifecycle_state = event
                .fields
                .get("to")
                .and_then(Value::as_str)
                .map(str::to_owned);
        }
        "behavior_transition" => {
            replay.behavior_state = event
                .fields
                .get("to")
                .and_then(Value::as_str)
                .map(str::to_owned);
        }
        "session_created" => replay.sessions_created = replay.sessions_created.saturating_add(1),
        "session_expired" => replay.sessions_expired = replay.sessions_expired.saturating_add(1),
        "policy_decision" | "approval_decision" => {
            replay.policy_decisions = replay.policy_decisions.saturating_add(1);
        }
        "tool_result" => replay.tool_outcomes = replay.tool_outcomes.saturating_add(1),
        _ => {}
    }
    if let Some(action_id) = &event.correlation.action_id {
        let action = replay
            .actions
            .entry(action_id.clone())
            .or_insert_with(|| ReplayAction {
                action_id: action_id.clone(),
                ..ReplayAction::default()
            });
        action.events.push(event.event_id);
        if event.family == "tool_result" {
            action.final_result.clone_from(&event.result_code);
        }
    }
}

#[allow(clippy::needless_pass_by_value)] // The blocking worker owns the weak lifecycle handle.
fn journal_worker(
    config: JournalConfig,
    mut receiver: mpsc::Receiver<JournalCommand>,
    observer: Weak<Observability>,
    stop: &AtomicBool,
) -> Result<(), ObservabilityError> {
    let mut writer = JournalWriter::open(config)?;
    loop {
        if stop.load(Ordering::Acquire) {
            return writer.finish();
        }
        let command = match receiver.try_recv() {
            Ok(command) => command,
            Err(mpsc::error::TryRecvError::Empty) => {
                std::thread::sleep(Duration::from_millis(10));
                continue;
            }
            Err(mpsc::error::TryRecvError::Disconnected) => return writer.finish(),
        };
        match command {
            JournalCommand::Event(bytes) => {
                if let Err(error) = writer.append(&bytes) {
                    if let Some(observer) = observer.upgrade() {
                        observer
                            .metrics
                            .dropped_journal_events
                            .fetch_add(1, Ordering::Relaxed);
                    }
                    return Err(error);
                }
            }
            JournalCommand::Shutdown(reply) => {
                let result = writer.finish();
                let reply_result = result
                    .as_ref()
                    .map_err(|error| {
                        ObservabilityError::InvalidEvent(match error {
                            ObservabilityError::Io(_) => "journal flush failed",
                            _ => "journal shutdown failed",
                        })
                    })
                    .copied();
                let _ = reply.send(reply_result);
                return result;
            }
        }
    }
}

struct JournalWriter {
    config: JournalConfig,
    active_path: PathBuf,
    file: File,
    bytes: u64,
    next_segment: u64,
}

impl JournalWriter {
    fn open(config: JournalConfig) -> Result<Self, ObservabilityError> {
        fs::create_dir_all(&config.directory)?;
        let active_path = config.directory.join("events-current.jsonl.tmp");
        recover_active(&active_path, config.max_segment_bytes)?;
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .read(true)
            .open(&active_path)?;
        let bytes = file.metadata()?.len();
        let next_segment = next_segment_id(&config.directory)?;
        Ok(Self {
            config,
            active_path,
            file,
            bytes,
            next_segment,
        })
    }

    fn append(&mut self, event: &[u8]) -> Result<(), ObservabilityError> {
        let required = u64::try_from(event.len().saturating_add(1)).unwrap_or(u64::MAX);
        if self.bytes > 0 && self.bytes.saturating_add(required) > self.config.max_segment_bytes {
            self.rotate()?;
        }
        self.file.write_all(event)?;
        self.file.write_all(b"\n")?;
        self.file.flush()?;
        if self.config.sync_each_record {
            self.file.sync_data()?;
        }
        self.bytes = self.bytes.saturating_add(required);
        enforce_retention(&self.config)
    }

    fn rotate(&mut self) -> Result<(), ObservabilityError> {
        self.file.sync_all()?;
        let archived = self
            .config
            .directory
            .join(format!("events-{:020}.jsonl", self.next_segment));
        self.next_segment = self.next_segment.saturating_add(1);
        fs::rename(&self.active_path, archived)?;
        self.file = OpenOptions::new()
            .create_new(true)
            .append(true)
            .read(true)
            .open(&self.active_path)?;
        self.bytes = 0;
        enforce_retention(&self.config)?;
        Ok(())
    }

    fn finish(mut self) -> Result<(), ObservabilityError> {
        self.file.flush()?;
        self.file.sync_all()?;
        enforce_retention(&self.config)
    }
}

fn recover_active(path: &Path, max_segment_bytes: u64) -> Result<(), ObservabilityError> {
    let Some(length) = fs::metadata(path).ok().map(|metadata| metadata.len()) else {
        return Ok(());
    };
    if length > max_segment_bytes {
        return Err(ObservabilityError::UnsafeLimits);
    }
    let mut file = OpenOptions::new().read(true).write(true).open(path)?;
    if length == 0 {
        return Ok(());
    }
    file.seek(SeekFrom::End(-1))?;
    let mut byte = [0u8; 1];
    file.read_exact(&mut byte)?;
    if byte[0] == b'\n' {
        return Ok(());
    }
    file.seek(SeekFrom::Start(0))?;
    let mut bytes = Vec::with_capacity(usize::try_from(length).unwrap_or(0));
    file.read_to_end(&mut bytes)?;
    let valid = bytes
        .iter()
        .rposition(|byte| *byte == b'\n')
        .map_or(0, |index| index.saturating_add(1));
    file.set_len(u64::try_from(valid).unwrap_or(0))?;
    file.sync_all()?;
    Ok(())
}

fn enforce_retention(config: &JournalConfig) -> Result<(), ObservabilityError> {
    let mut segments = journal_paths(&config.directory)?
        .into_iter()
        .filter(|path| {
            path.extension()
                .is_some_and(|extension| extension == "jsonl")
        })
        .collect::<Vec<_>>();
    segments.sort();
    let active_bytes = fs::metadata(config.directory.join("events-current.jsonl.tmp"))
        .map_or(0, |metadata| metadata.len());
    let mut total = segments.iter().try_fold(active_bytes, |sum, path| {
        Ok::<_, std::io::Error>(sum.saturating_add(fs::metadata(path)?.len()))
    })?;
    while segments.len() > config.max_segments || total > config.max_total_bytes {
        let oldest = segments.remove(0);
        let length = fs::metadata(&oldest)?.len();
        fs::remove_file(oldest)?;
        total = total.saturating_sub(length);
    }
    Ok(())
}

/// Reserves a large, durable ID range before admitting journaled events. This
/// prevents IDs that were observed in memory but dropped by a saturated disk
/// queue from being reused after a restart. Immutable reservation markers keep
/// the update atomic on Unix and Windows without replacing an open file.
fn reserve_event_ids(
    directory: &Path,
    scanned_next: u64,
) -> Result<(u64, u64), ObservabilityError> {
    loop {
        let mut reservations = reservation_paths(directory)?;
        reservations.sort_by_key(|(limit, _)| *limit);
        let reserved_next = reservations.last().map_or(1, |(limit, _)| *limit);
        let next = scanned_next.max(reserved_next);
        if next == u64::MAX {
            return Err(ObservabilityError::InvalidEvent("event ID space exhausted"));
        }
        let limit = next.saturating_add(JOURNAL_EVENT_ID_RESERVATION);
        let marker = directory.join(format!("events-id-reservation-{limit:020}"));
        match OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&marker)
        {
            Ok(mut file) => {
                writeln!(file, "{next}..{limit}")?;
                file.sync_all()?;
                reservations.push((limit, marker));
                reservations.sort_by_key(|(reserved_limit, _)| *reserved_limit);
                while reservations.len() > 1 {
                    let (_, oldest) = reservations.remove(0);
                    fs::remove_file(oldest)?;
                }
                return Ok((next, limit));
            }
            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
            Err(error) => return Err(error.into()),
        }
    }
}

fn reservation_paths(directory: &Path) -> Result<Vec<(u64, PathBuf)>, ObservabilityError> {
    let mut paths = Vec::new();
    for entry in fs::read_dir(directory)? {
        let entry = entry?;
        if !entry.file_type()?.is_file() {
            continue;
        }
        let name = entry.file_name();
        let name = name.to_string_lossy();
        let Some(limit) = name
            .strip_prefix("events-id-reservation-")
            .and_then(|value| value.parse::<u64>().ok())
        else {
            continue;
        };
        paths.push((limit, entry.path()));
    }
    Ok(paths)
}

fn next_segment_id(directory: &Path) -> Result<u64, ObservabilityError> {
    let retained = journal_paths(directory)?
        .into_iter()
        .filter_map(|path| {
            let name = path.file_name()?.to_str()?;
            name.strip_prefix("events-")?
                .strip_suffix(".jsonl")?
                .parse::<u64>()
                .ok()
        })
        .max();
    let retained_next = retained.map_or(Ok(0), |identifier| {
        identifier
            .checked_add(1)
            .ok_or(ObservabilityError::InvalidEvent(
                "journal segment ID space exhausted",
            ))
    })?;
    Ok(unix_millis().max(retained_next))
}

fn scan_last_event_id(directory: &Path, max_event_bytes: usize) -> Result<u64, ObservabilityError> {
    let mut maximum = 0u64;
    for path in journal_paths(directory)? {
        let active = path
            .file_name()
            .is_some_and(|name| name == "events-current.jsonl.tmp");
        let mut reader = BufReader::new(File::open(&path)?);
        let mut line = Vec::new();
        let mut line_number = 0usize;
        loop {
            line.clear();
            let bytes = reader.read_until(b'\n', &mut line)?;
            if bytes == 0 {
                break;
            }
            line_number = line_number.saturating_add(1);
            if line.len() > max_event_bytes {
                return Err(ObservabilityError::CorruptJournal {
                    path: path.clone(),
                    line: line_number,
                });
            }
            if line.last() != Some(&b'\n') && active {
                break;
            }
            let event: StructuredEvent =
                serde_json::from_slice(&line).map_err(|_| ObservabilityError::CorruptJournal {
                    path: path.clone(),
                    line: line_number,
                })?;
            maximum = maximum.max(event.event_id);
        }
    }
    Ok(maximum)
}

fn journal_paths(directory: &Path) -> Result<Vec<PathBuf>, ObservabilityError> {
    if !directory.exists() {
        return Ok(Vec::new());
    }
    let mut paths = Vec::new();
    for entry in fs::read_dir(directory)? {
        let entry = entry?;
        if !entry.file_type()?.is_file() {
            continue;
        }
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if (name.starts_with("events-") && name.ends_with(".jsonl"))
            || name == "events-current.jsonl.tmp"
        {
            paths.push(entry.path());
        }
    }
    Ok(paths)
}

fn checked_code(value: String) -> Result<String, ObservabilityError> {
    if valid_code(&value) {
        Ok(value)
    } else {
        Err(ObservabilityError::InvalidEvent("invalid stable code"))
    }
}

const fn policy_disposition_code(value: PolicyDisposition) -> &'static str {
    match value {
        PolicyDisposition::Allowed => "allowed",
        PolicyDisposition::Denied => "denied",
        PolicyDisposition::ApprovalRequired => "approval_required",
    }
}

const fn policy_outcome_code(value: PolicyFinalOutcome) -> &'static str {
    match value {
        PolicyFinalOutcome::Denied => "denied",
        PolicyFinalOutcome::ApprovalRequired => "approval_required",
        PolicyFinalOutcome::ApprovalGranted => "approval_granted",
        PolicyFinalOutcome::Authorized => "authorized",
        PolicyFinalOutcome::Completed => "completed",
        PolicyFinalOutcome::Rejected => "rejected",
        PolicyFinalOutcome::Failed => "failed",
        PolicyFinalOutcome::AmbiguousMutation => "ambiguous_mutation",
    }
}

const fn policy_reason_code(value: crate::policy::PolicyReasonCode) -> &'static str {
    use crate::policy::PolicyReasonCode;
    match value {
        PolicyReasonCode::Allowed => "allowed",
        PolicyReasonCode::UnknownTool => "unknown_tool",
        PolicyReasonCode::InvalidArguments => "invalid_arguments",
        PolicyReasonCode::OriginDenied => "origin_denied",
        PolicyReasonCode::CapabilityDenied => "capability_denied",
        PolicyReasonCode::ForbiddenMilestoneOperation => "forbidden_milestone_operation",
        PolicyReasonCode::ToolCostExceeded => "tool_cost_exceeded",
        PolicyReasonCode::PrincipalBudgetExceeded => "principal_budget_exceeded",
        PolicyReasonCode::GlobalBudgetExceeded => "global_budget_exceeded",
        PolicyReasonCode::ApprovalRequired => "approval_required",
        PolicyReasonCode::ApprovalUnknown => "approval_unknown",
        PolicyReasonCode::ApprovalNotGranted => "approval_not_granted",
        PolicyReasonCode::ApprovalExpired => "approval_expired",
        PolicyReasonCode::ApprovalReplayed => "approval_replayed",
        PolicyReasonCode::ApprovalMismatch => "approval_mismatch",
        PolicyReasonCode::ApprovalUnexpected => "approval_unexpected",
        PolicyReasonCode::SchedulerGrantUnknown => "scheduler_grant_unknown",
        PolicyReasonCode::SchedulerGrantExpired => "scheduler_grant_expired",
        PolicyReasonCode::SchedulerGrantExhausted => "scheduler_grant_exhausted",
        PolicyReasonCode::SchedulerPrivilegeEscalation => "scheduler_privilege_escalation",
        PolicyReasonCode::AuditUnavailable => "audit_unavailable",
    }
}

fn valid_code(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= MAX_CODE_BYTES
        && value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
}

fn valid_identifier(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= MAX_ID_BYTES
        && value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':'))
}

/// Returns a stable, non-reversible correlation value for runtime identifiers.
#[must_use]
pub fn pseudonymous_identifier(value: &str) -> String {
    let digest = Sha256::digest(value.as_bytes());
    format!("id:{}", &hex_bytes(&digest)[..24])
}

fn millis(duration: Duration) -> u64 {
    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}

fn hex_bytes(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut result = String::with_capacity(bytes.len().saturating_mul(2));
    for byte in bytes {
        result.push(char::from(HEX[usize::from(byte >> 4)]));
        result.push(char::from(HEX[usize::from(byte & 0x0f)]));
    }
    result
}

fn unix_millis() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, millis)
}

fn lock<T>(value: &Mutex<T>) -> MutexGuard<'_, T> {
    value
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

/// Converts a structured event to a generic JSON object for clients that want
/// to preserve fields introduced by a newer schema.
pub fn event_json(event: &StructuredEvent) -> Result<Map<String, Value>, ObservabilityError> {
    match serde_json::to_value(event)? {
        Value::Object(object) => Ok(object),
        _ => Err(ObservabilityError::InvalidEvent(
            "event did not encode as an object",
        )),
    }
}
