use std::io;

use armour_core::persist::PersistError;

#[derive(thiserror::Error, Debug)]
pub enum DbError {
    /// 400
    #[error("client error: {0}")]
    Client(&'static str),
    /// 404
    #[error("key not found")]
    KeyNotFound,
    /// 501
    #[error("not implemented")]
    NotImplemented,
    #[error("storage error: {0}")]
    Internal(&'static str),
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error("CRC32 mismatch: expected {expected:#x}, got {actual:#x}")]
    CrcMismatch { expected: u32, actual: u32 },
    #[error("corrupted entry at offset {offset}")]
    CorruptedEntry { offset: u64 },
    #[error("invalid configuration: {0}")]
    Config(&'static str),
    #[error("CAS value mismatch")]
    CasMismatch,
    #[error("key already exists")]
    KeyExists,
    /// A copied DiskLoc points to a file that has just been removed from
    /// inner.immutable by compaction. The data is logically still live;
    /// the caller should retry with a fresh DiskLoc from the index.
    #[error("stale DiskLoc - file removed by compaction")]
    StaleDiskLoc,
    #[error("all slots are occupied")]
    SlotsFull,
    #[error("key routes to a different shard")]
    ShardMismatch,
    /// A multi-collection transaction (`atomic2`/`atomic3`/`atomic4`) was given the
    /// same collection in two argument positions; re-locking its shards from the
    /// same thread would self-deadlock.
    #[error("the same collection appears twice in a cross-collection transaction")]
    DuplicateCollectionInTx,
    /// Optimistic-concurrency retry signal: a value read in phase 1 changed before
    /// the narrow transaction in phase 2 could commit. The caller should retry.
    #[error("transaction conflict — retry")]
    TxConflict,
    #[error("disk format mismatch: {0}")]
    FormatMismatch(String),
    #[cfg(feature = "encryption")]
    #[error("encryption error: {0}")]
    EncryptionError(String),
    #[cfg(feature = "replication")]
    #[error("replication error: {0}")]
    Replication(String),
    #[cfg(feature = "replication")]
    #[error("shard count mismatch: leader has {leader}, follower has {follower}")]
    ShardCountMismatch { leader: usize, follower: usize },
    /// Schema/version mismatch detected while opening a collection.
    /// See `SchemaMismatchKind` for the specific violation.
    #[error("schema mismatch on '{name}': {kind}")]
    SchemaMismatch {
        name: String,
        kind: SchemaMismatchKind,
    },
    #[error("recovery thread panicked")]
    RecoveryPanic,
}

/// Specific kind of schema mismatch (see `DbError::SchemaMismatch`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaMismatchKind {
    /// Stored and expected `typ_hash` differ at the same `version`.
    /// Indicates the schema changed without bumping `T::VERSION`.
    TypHash { stored: u64, expected: u64 },
    /// Stored `version` is newer than the requested `meta.version`
    /// (rollback to an older binary).
    Downgrade { stored: u16, requested: u16 },
    /// No migration step is registered for the source version `from`.
    MissingStep { from: u16 },
}

impl core::fmt::Display for SchemaMismatchKind {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::TypHash { stored, expected } => write!(
                f,
                "type hash drift: stored={stored:#x} expected={expected:#x}"
            ),
            Self::Downgrade { stored, requested } => write!(
                f,
                "version downgrade: stored={stored} requested={requested}"
            ),
            Self::MissingStep { from } => write!(f, "missing migration from version {from}"),
        }
    }
}

impl DbError {
    pub fn status_code(&self) -> u16 {
        match self {
            DbError::Client(_) => 400,
            DbError::KeyNotFound => 404,
            DbError::NotImplemented => 501,
            DbError::SchemaMismatch { .. } => 422,
            DbError::KeyExists => 409,
            DbError::CasMismatch => 412,
            _ => 500,
        }
    }
}

pub type DbResult<T> = Result<T, DbError>;

impl From<PersistError> for DbError {
    fn from(err: PersistError) -> Self {
        match err {
            PersistError::Io(e) => Self::Io(e),
            PersistError::Json(e) => Self::FormatMismatch(e.to_string()),
        }
    }
}

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

    #[test]
    fn schema_mismatch_typ_hash_maps_to_422() {
        let e = DbError::SchemaMismatch {
            name: "users".into(),
            kind: SchemaMismatchKind::TypHash {
                stored: 0xDEAD,
                expected: 0xBEEF,
            },
        };
        assert_eq!(e.status_code(), 422);
    }

    #[test]
    fn schema_mismatch_downgrade_maps_to_422() {
        let e = DbError::SchemaMismatch {
            name: "users".into(),
            kind: SchemaMismatchKind::Downgrade {
                stored: 3,
                requested: 2,
            },
        };
        assert_eq!(e.status_code(), 422);
    }

    #[test]
    fn schema_mismatch_missing_step_maps_to_422() {
        let e = DbError::SchemaMismatch {
            name: "users".into(),
            kind: SchemaMismatchKind::MissingStep { from: 1 },
        };
        assert_eq!(e.status_code(), 422);
    }

    #[test]
    fn key_exists_maps_to_409() {
        assert_eq!(DbError::KeyExists.status_code(), 409);
    }

    #[test]
    fn cas_mismatch_maps_to_412() {
        assert_eq!(DbError::CasMismatch.status_code(), 412);
    }
}
