//! Shared identifiers, receipts, status, and the error taxonomy.
//!
//! Lead-owned (scope 2.1). Agents do not edit this file; an interface change
//! request goes to the lead.
//!
//! The whole file is the Phase 1 realization of plan §5.1's frozen public API.

use std::fmt;
use std::sync::Arc;

use levcs_core::ObjectId;

/// A storage namespace. Equal to the repository's `repo_id`; the two are the
/// same 32 bytes and never diverge. Index keys include this, which is what
/// makes namespace isolation a property of the key rather than of a check
/// (plan §4 resource invariants, §5.3).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct NamespaceId(pub [u8; 32]);

impl NamespaceId {
    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
        Self(bytes)
    }

    pub const fn as_bytes(&self) -> &[u8; 32] {
        &self.0
    }

    pub fn to_hex(&self) -> String {
        hex::encode(self.0)
    }
}

impl From<ObjectId> for NamespaceId {
    fn from(id: ObjectId) -> Self {
        Self(id.0)
    }
}

impl From<NamespaceId> for ObjectId {
    fn from(id: NamespaceId) -> Self {
        ObjectId(id.0)
    }
}

impl fmt::Debug for NamespaceId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "NamespaceId({})", self.to_hex())
    }
}

/// Client-generated operation identifier. Window-scoped, not lifetime-unique;
/// see plan §4 transaction invariant 7 for exactly what uniqueness is and is
/// not claimed.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct OperationId(pub [u8; 16]);

impl OperationId {
    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
        Self(bytes)
    }

    pub const fn as_bytes(&self) -> &[u8; 16] {
        &self.0
    }

    pub fn to_hex(&self) -> String {
        hex::encode(self.0)
    }
}

impl fmt::Debug for OperationId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "OperationId({})", self.to_hex())
    }
}

/// The applied-ref element of a receipt.
///
/// This is the frozen protocol type, re-exported rather than mirrored. The
/// receipt crosses the wire as `CommitReceiptV1`; a parallel store-local type
/// would need a conversion that could silently disagree with a frozen
/// contract, which is the failure mode contract review 2026-07-24-A was about
/// (scope 2.2).
pub use levcs_protocol::v2::AppliedRefV1 as AppliedRef;

/// Durable outcome of one committed transaction.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommitReceipt {
    pub operation_id: OperationId,
    /// Logical per-repository sequence. Never the physical `shard_sequence`;
    /// plan §4 transaction invariant 8 forbids interchanging the two domains.
    pub repo_sequence: u64,
    pub current_authority: ObjectId,
    pub refs: Vec<AppliedRef>,
    /// Per-transaction count, not a lifetime counter (plan §5.1). `u64`
    /// because an atomic staged-projection install reports its complete
    /// adopted membership here, bounded by `max_projection_objects` rather
    /// than by `max_objects_per_transaction`.
    pub objects_new: u64,
}

/// Which stage a not-yet-appended operation is in.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PendingPhase {
    Receiving,
    Validating,
    Queued,
    Sequenced,
}

/// Normal transaction lifecycle. Never an error channel: plan §5.1 reserves
/// `StoreError` for inability to answer, and this enum for every state the
/// store can actually report. The D0 contract test asserts the split.
///
/// HTTP mapping (plan §5.1): `Committed` -> 200, `Pending` -> 202
/// `OperationPending`, `Resolving` -> 202 `OperationResolving`, `Expired` ->
/// 410 `ReceiptExpired`, `Unknown` -> 404.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TransactionStatus {
    Committed(CommitReceipt),
    Pending {
        operation_digest: ObjectId,
        retry_until_micros: i64,
        phase: PendingPhase,
    },
    /// From append start until committed-root publication, inclusive of
    /// poisoned-shard recovery. Dominates `retry_until` (plan §7 stage 10).
    Resolving {
        operation_digest: ObjectId,
        retry_until_micros: i64,
        shard_sequence: Option<u64>,
    },
    Expired {
        operation_digest: ObjectId,
        retry_until_micros: i64,
        tombstone_until_micros: i64,
    },
    Unknown,
}

/// Inability to answer. Corruption, unavailable recovery state, refusal to
/// open, or a definitive rejection — never a lifecycle state.
#[derive(Clone, Debug, thiserror::Error)]
pub enum StoreError {
    #[error("not implemented: {0}")]
    NotImplemented(&'static str),

    #[error("store corruption: {0}")]
    Corruption(String),

    #[error("recovery required before this operation")]
    RecoveryRequired,

    #[error("shard {shard} is poisoned and admits no mutation until recovery: {cause}")]
    ShardPoisoned { shard: u16, cause: String },

    #[error("store is read-only")]
    ReadOnly,

    #[error("format mismatch: {0}")]
    FormatMismatch(String),

    /// A recognized non-empty legacy layout with no `FORMAT` marker. Carries
    /// the exact command plan §5.2 requires be printed, so the caller never
    /// has to reconstruct it.
    #[error("legacy instance layout; run: {migrate_command}")]
    LegacyLayout { migrate_command: String },

    /// Non-empty and unrecognized. Refused without modification or probing.
    #[error("unrecognized non-empty layout at {0}; refusing to modify")]
    UnrecognizedLayout(String),

    #[error("store root is locked by another process")]
    AlreadyLocked,

    #[error("io: {0}")]
    Io(#[source] Arc<std::io::Error>),

    #[error("no space left on the store device")]
    NoSpace,

    #[error("limit exceeded: {limit} ({observed} > {allowed})")]
    LimitExceeded {
        limit: &'static str,
        observed: u64,
        allowed: u64,
    },

    #[error("store overloaded at {limit}; retry after {retry_after_micros} microseconds")]
    Overloaded {
        limit: &'static str,
        retry_after_micros: u64,
    },

    #[error("mutable-state conflict: {0}")]
    Conflict(String),

    /// No repository is bound for this namespace in the captured committed
    /// root. Contract review 2026-08-07-A, requested by B1 for scope 6-B1
    /// deliverable 8.
    ///
    /// This is an inability to answer and not a lifecycle state, which is the
    /// distinction the taxonomy above turns on. A namespace that was never
    /// bound has no `RepoState`, so there is no `genesis_authority` to report
    /// and no sequence to report it at; `StoreEngine::snapshot` cannot
    /// manufacture either without fabricating a trust root. A namespace that
    /// *is* bound and has been retired is the opposite case — `Deleted` is a
    /// lifecycle, it has a `RepoState`, and it is reported through
    /// `RepoSnapshot::lifecycle` rather than through this error.
    ///
    /// It carries the typed `NamespaceId` rather than a rendered string so a
    /// caller can match on the identity it asked for instead of parsing it
    /// back out of a message.
    #[error("no repository bound for namespace {}", namespace.to_hex())]
    NoSuchRepository { namespace: NamespaceId },

    #[error("store is not ready")]
    NotReady,

    #[error("evidence signer: {0}")]
    Signer(#[from] SignerError),

    #[error("invalid configuration: {0}")]
    InvalidConfiguration(String),
}

#[derive(Clone, Debug, thiserror::Error)]
pub enum SignerError {
    #[error("signer unavailable")]
    Unavailable,
    #[error("signer rejected the request: {0}")]
    Rejected(String),
}

impl From<std::io::Error> for StoreError {
    fn from(error: std::io::Error) -> Self {
        Self::Io(Arc::new(error))
    }
}

/// Signs committed-transaction evidence on behalf of the instance.
///
/// Registered by instance composition (plan §5.2). The store calls it before
/// append and never decides who may sign — that is a federation trust
/// decision and this crate makes none.
pub trait CommitEvidenceSigner: Send + Sync {
    fn key_epoch(&self) -> u64;
    fn public_key(&self) -> [u8; 32];

    /// Signs `SignedCommittedTransactionV1::signing_digest`, **not** the bare
    /// event digest.
    ///
    /// The parameter is named for the value a correct caller must pass because
    /// nothing downstream can detect the substitution until verification
    /// fails. The frozen `SignedCommittedTransactionV1::verify` recomputes
    /// `signing_digest(transaction, source_key_epoch, durability_result)` and
    /// checks `source_signature` against it, so a signature over the event
    /// digest alone would omit the key epoch and the durability result and
    /// would be rejected by every verifier — including the mirror path in
    /// another instance, after the transaction is already durable. The event
    /// digest is the chain identity that `previous_event_digest` links; it is
    /// not the signature message.
    fn sign_event(&self, signing_digest: &ObjectId) -> Result<[u8; 64], SignerError>;
}

/// Capability token for constructing a `ValidatedTransaction`.
///
/// Deliberately not obtainable from a `&StoreEngine`: a
/// `StoreEngine::privileged()` accessor would make the seal decorative, since
/// everything able to call `submit` holds an engine. Construction is instead
/// gated on the `store-privileged` cargo feature, enabled only by validated
/// ingest, this crate's recovery path, and the Phase 2 migrator (scope 2.2).
pub struct PrivilegedConstruction(());

impl PrivilegedConstruction {
    #[cfg(feature = "store-privileged")]
    pub fn assert_validated() -> Self {
        Self(())
    }

    /// In-crate privileged construction for `recovery.rs`, which rebuilds
    /// transactions from frames that were already validated before their
    /// original append.
    // D0 scaffolding: `recovery.rs` lands in Wave A.
    #[allow(dead_code)]
    pub(crate) fn internal() -> Self {
        Self(())
    }
}

/// Counts of durability-relevant syscalls, per shard.
///
/// The point of these is that "one fence per group" and "no per-object fsync"
/// become assertions against observed counters rather than claims about the
/// code (scope 2.3, Phase 1 exit criteria).
#[derive(Debug, Default)]
pub struct DurabilityCounters {
    pub fdatasync: std::sync::atomic::AtomicU64,
    pub fsync_dir: std::sync::atomic::AtomicU64,
    pub write_vectored: std::sync::atomic::AtomicU64,
    pub short_writes: std::sync::atomic::AtomicU64,
    pub bytes_written: std::sync::atomic::AtomicU64,
}

impl DurabilityCounters {
    pub fn snapshot(&self) -> DurabilityCounterSnapshot {
        use std::sync::atomic::Ordering::Relaxed;
        DurabilityCounterSnapshot {
            fdatasync: self.fdatasync.load(Relaxed),
            fsync_dir: self.fsync_dir.load(Relaxed),
            write_vectored: self.write_vectored.load(Relaxed),
            short_writes: self.short_writes.load(Relaxed),
            bytes_written: self.bytes_written.load(Relaxed),
        }
    }
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct DurabilityCounterSnapshot {
    pub fdatasync: u64,
    pub fsync_dir: u64,
    pub write_vectored: u64,
    pub short_writes: u64,
    pub bytes_written: u64,
}

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

    #[test]
    fn cloned_io_error_shares_the_original_error_losslessly() {
        use std::error::Error;

        let error = StoreError::from(std::io::Error::from_raw_os_error(libc::EIO));
        let clone = error.clone();

        let (StoreError::Io(original), StoreError::Io(cloned)) = (&error, &clone) else {
            panic!("expected two shared I/O errors");
        };
        assert!(Arc::ptr_eq(original, cloned));
        assert_eq!(cloned.raw_os_error(), Some(libc::EIO));
        let source = clone.source().expect("I/O source remains available");
        let shared = source
            .downcast_ref::<Arc<std::io::Error>>()
            .expect("shared I/O source remains downcastable without reconstruction");
        assert!(Arc::ptr_eq(original, shared));
    }

    #[test]
    fn overloaded_error_is_cloneable_and_preserves_retry_guidance() {
        let error = StoreError::Overloaded {
            limit: "max_status_entries",
            retry_after_micros: 1_000,
        };
        let StoreError::Overloaded {
            limit,
            retry_after_micros,
        } = error.clone()
        else {
            panic!("clone changed the error variant");
        };
        assert_eq!(limit, "max_status_entries");
        assert_eq!(retry_after_micros, 1_000);
    }
}
