use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryIntegrityKind {
    ManifestWalSegmentMissing,
    ManifestWalSegmentMissingSizeMetadata,
    ManifestWalSegmentMissingChecksumMetadata,
    ManifestWalSegmentChecksumMismatch,
    ManifestWalSegmentTruncated,
    ManifestWalSegmentSizeMismatch,
    ManifestWalSegmentInvalidChecksumMetadata,
}

impl RecoveryIntegrityKind {
    pub fn as_str(&self) -> &'static str {
        match self {
            RecoveryIntegrityKind::ManifestWalSegmentMissing => "manifest_wal_segment_missing",
            RecoveryIntegrityKind::ManifestWalSegmentMissingSizeMetadata => {
                "manifest_wal_segment_missing_size_metadata"
            }
            RecoveryIntegrityKind::ManifestWalSegmentMissingChecksumMetadata => {
                "manifest_wal_segment_missing_checksum_metadata"
            }
            RecoveryIntegrityKind::ManifestWalSegmentChecksumMismatch => {
                "manifest_wal_segment_checksum_mismatch"
            }
            RecoveryIntegrityKind::ManifestWalSegmentTruncated => "manifest_wal_segment_truncated",
            RecoveryIntegrityKind::ManifestWalSegmentSizeMismatch => {
                "manifest_wal_segment_size_mismatch"
            }
            RecoveryIntegrityKind::ManifestWalSegmentInvalidChecksumMetadata => {
                "manifest_wal_segment_invalid_checksum_metadata"
            }
        }
    }
}

impl std::fmt::Display for RecoveryIntegrityKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RecoveryIntegrityDiagnostic {
    pub kind: RecoveryIntegrityKind,
    pub segment_filename: String,
    pub manifest_path: std::path::PathBuf,
    pub wal_dir: std::path::PathBuf,
    pub recovery_mode: crate::config::RecoveryMode,
    pub durable_seq: u64,
    pub visible_seq: u64,
    pub active_segment_seq: u64,
    pub segment_seq: u64,
    pub latest_checkpoint_seq: Option<u64>,
    pub expected_size_bytes: Option<u64>,
    pub actual_size_bytes: Option<u64>,
    pub expected_sha256_hex: Option<String>,
    pub actual_sha256_hex: Option<String>,
}

impl RecoveryIntegrityDiagnostic {
    pub fn operator_message(&self) -> &'static str {
        match self.recovery_mode {
            crate::config::RecoveryMode::Strict => {
                "strict recovery stopped because the manifest and WAL files are inconsistent; local dev data may be corrupt, and persisted data requires restore/repair from a known-good backup"
            }
            crate::config::RecoveryMode::Permissive => {
                "permissive recovery skipped manifest-referenced WAL data; local dev data may be corrupt, and persisted data requires restore/repair from a known-good backup before relying on it"
            }
        }
    }
}

impl std::fmt::Display for RecoveryIntegrityDiagnostic {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}: segment={} manifest={} wal_dir={} mode={:?} durable_seq={} visible_seq={} active_segment_seq={} segment_seq={}",
            self.kind,
            self.segment_filename,
            self.manifest_path.display(),
            self.wal_dir.display(),
            self.recovery_mode,
            self.durable_seq,
            self.visible_seq,
            self.active_segment_seq,
            self.segment_seq
        )?;
        if let Some(seq) = self.latest_checkpoint_seq {
            write!(f, " latest_checkpoint_seq={seq}")?;
        }
        if let Some(size) = self.expected_size_bytes {
            write!(f, " expected_size_bytes={size}")?;
        }
        if let Some(size) = self.actual_size_bytes {
            write!(f, " actual_size_bytes={size}")?;
        }
        if let Some(ref sha) = self.expected_sha256_hex {
            write!(f, " expected_sha256_hex={sha}")?;
        }
        if let Some(ref sha) = self.actual_sha256_hex {
            write!(f, " actual_sha256_hex={sha}")?;
        }
        write!(f, "; {}", self.operator_message())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceType {
    Project,
    Scope,
    Table,
    Index,
}

impl std::fmt::Display for ResourceType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ResourceType::Project => write!(f, "project"),
            ResourceType::Scope => write!(f, "scope"),
            ResourceType::Table => write!(f, "table"),
            ResourceType::Index => write!(f, "index"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AedbErrorCode {
    Io,
    Encode,
    Decode,
    Validation,
    InvalidConfig,
    IntegrityError,
    RecoveryIntegrity,
    Unavailable,
    CheckpointInProgress,
    ProjectAlreadyExists,
    ScopeAlreadyExists,
    TableAlreadyExists,
    IndexAlreadyExists,
    ProjectNotFound,
    ScopeNotFound,
    TableNotFound,
    IndexNotFound,
    DuplicatePrimaryKey,
    UniqueViolation,
    ForeignKeyViolation,
    CheckConstraintFailed,
    NotNullViolation,
    TypeMismatch,
    UnknownColumn,
    Conflict,
    Underflow,
    Overflow,
    QueueFull,
    PermissionDenied,
    Timeout,
    PartitionLockTimeout,
    EpochApplyTimeout,
    ParallelApplyCancelled,
    ParallelApplyWorkerPanicked,
    SnapshotExpired,
    AssertionFailed,
}

/// Broad caller-action class for [`AedbError`].
///
/// Match on [`AedbError::code`] for stable machine-readable detail, then use
/// this class to choose the caller response without parsing error text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AedbErrorClass {
    /// Transient capacity or timing failure. Retrying with backoff is expected.
    Retryable,
    /// Optimistic concurrency or assertion failure. Refresh the read state and
    /// retry only if the business operation is still valid.
    Conflict,
    /// Caller is not authenticated or lacks permission. Do not retry until the
    /// caller identity or grants change.
    Permission,
    /// Request shape, schema, type, arithmetic, or missing resource problem.
    /// Fix the request before retrying.
    Validation,
    /// Durability, recovery, decode, or integrity failure. Treat as operator
    /// action required before continuing writes.
    Integrity,
    /// Service or subsystem is not currently available. Retry after readiness
    /// or the competing operation changes.
    Unavailable,
}

impl AedbErrorCode {
    pub fn as_str(self) -> &'static str {
        match self {
            AedbErrorCode::Io => "io",
            AedbErrorCode::Encode => "encode",
            AedbErrorCode::Decode => "decode",
            AedbErrorCode::Validation => "validation",
            AedbErrorCode::InvalidConfig => "invalid_config",
            AedbErrorCode::IntegrityError => "integrity_error",
            AedbErrorCode::RecoveryIntegrity => "recovery_integrity",
            AedbErrorCode::Unavailable => "unavailable",
            AedbErrorCode::CheckpointInProgress => "checkpoint_in_progress",
            AedbErrorCode::ProjectAlreadyExists => "project_already_exists",
            AedbErrorCode::ScopeAlreadyExists => "scope_already_exists",
            AedbErrorCode::TableAlreadyExists => "table_already_exists",
            AedbErrorCode::IndexAlreadyExists => "index_already_exists",
            AedbErrorCode::ProjectNotFound => "project_not_found",
            AedbErrorCode::ScopeNotFound => "scope_not_found",
            AedbErrorCode::TableNotFound => "table_not_found",
            AedbErrorCode::IndexNotFound => "index_not_found",
            AedbErrorCode::DuplicatePrimaryKey => "duplicate_primary_key",
            AedbErrorCode::UniqueViolation => "unique_violation",
            AedbErrorCode::ForeignKeyViolation => "foreign_key_violation",
            AedbErrorCode::CheckConstraintFailed => "check_constraint_failed",
            AedbErrorCode::NotNullViolation => "not_null_violation",
            AedbErrorCode::TypeMismatch => "type_mismatch",
            AedbErrorCode::UnknownColumn => "unknown_column",
            AedbErrorCode::Conflict => "conflict",
            AedbErrorCode::Underflow => "underflow",
            AedbErrorCode::Overflow => "overflow",
            AedbErrorCode::QueueFull => "queue_full",
            AedbErrorCode::PermissionDenied => "permission_denied",
            AedbErrorCode::Timeout => "timeout",
            AedbErrorCode::PartitionLockTimeout => "partition_lock_timeout",
            AedbErrorCode::EpochApplyTimeout => "epoch_apply_timeout",
            AedbErrorCode::ParallelApplyCancelled => "parallel_apply_cancelled",
            AedbErrorCode::ParallelApplyWorkerPanicked => "parallel_apply_worker_panicked",
            AedbErrorCode::SnapshotExpired => "snapshot_expired",
            AedbErrorCode::AssertionFailed => "assertion_failed",
        }
    }

    pub fn class(self) -> AedbErrorClass {
        match self {
            AedbErrorCode::QueueFull
            | AedbErrorCode::Timeout
            | AedbErrorCode::PartitionLockTimeout
            | AedbErrorCode::EpochApplyTimeout
            | AedbErrorCode::ParallelApplyCancelled
            | AedbErrorCode::ParallelApplyWorkerPanicked
            | AedbErrorCode::SnapshotExpired => AedbErrorClass::Retryable,
            AedbErrorCode::Conflict | AedbErrorCode::AssertionFailed => AedbErrorClass::Conflict,
            AedbErrorCode::PermissionDenied => AedbErrorClass::Permission,
            AedbErrorCode::Io
            | AedbErrorCode::Encode
            | AedbErrorCode::Decode
            | AedbErrorCode::IntegrityError
            | AedbErrorCode::RecoveryIntegrity => AedbErrorClass::Integrity,
            AedbErrorCode::Unavailable | AedbErrorCode::CheckpointInProgress => {
                AedbErrorClass::Unavailable
            }
            AedbErrorCode::Validation
            | AedbErrorCode::InvalidConfig
            | AedbErrorCode::ProjectAlreadyExists
            | AedbErrorCode::ScopeAlreadyExists
            | AedbErrorCode::TableAlreadyExists
            | AedbErrorCode::IndexAlreadyExists
            | AedbErrorCode::ProjectNotFound
            | AedbErrorCode::ScopeNotFound
            | AedbErrorCode::TableNotFound
            | AedbErrorCode::IndexNotFound
            | AedbErrorCode::DuplicatePrimaryKey
            | AedbErrorCode::UniqueViolation
            | AedbErrorCode::ForeignKeyViolation
            | AedbErrorCode::CheckConstraintFailed
            | AedbErrorCode::NotNullViolation
            | AedbErrorCode::TypeMismatch
            | AedbErrorCode::UnknownColumn
            | AedbErrorCode::Underflow
            | AedbErrorCode::Overflow => AedbErrorClass::Validation,
        }
    }

    pub fn is_retryable(self) -> bool {
        matches!(self.class(), AedbErrorClass::Retryable)
    }
}

#[derive(Debug, Error)]
pub enum AedbError {
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("encode error: {0}")]
    Encode(String),
    #[error("decode error: {0}")]
    Decode(String),
    #[error("validation error: {0}")]
    Validation(String),
    #[error("invalid config: {message}")]
    InvalidConfig { message: String },
    #[error("integrity error: {message}")]
    IntegrityError { message: String },
    #[error("recovery integrity error: {diagnostic}")]
    RecoveryIntegrity {
        diagnostic: Box<RecoveryIntegrityDiagnostic>,
    },
    #[error("resource unavailable: {message}")]
    Unavailable { message: String },
    #[error("checkpoint in progress")]
    CheckpointInProgress,
    #[error("{resource_type} '{resource_id}' already exists")]
    AlreadyExists {
        resource_type: ResourceType,
        resource_id: String,
    },
    #[error("{resource_type} '{resource_id}' not found")]
    NotFound {
        resource_type: ResourceType,
        resource_id: String,
    },
    #[error("duplicate primary key in table '{table}': {key}")]
    DuplicatePK { table: String, key: String },
    #[error("unique constraint violation on index '{index}' in table '{table}'")]
    UniqueViolation {
        table: String,
        index: String,
        key: String,
    },
    #[error("foreign key violation: {fk_name} references {ref_table}({ref_key})")]
    ForeignKeyViolation {
        fk_name: String,
        table: String,
        ref_table: String,
        ref_key: String,
    },
    #[error("check constraint '{constraint}' failed on table '{table}'")]
    CheckConstraintFailed { table: String, constraint: String },
    #[error("NOT NULL violation: column '{column}' in table '{table}'")]
    NotNullViolation { table: String, column: String },
    #[error(
        "type mismatch: column '{column}' in table '{table}' expected {expected}, got {actual}"
    )]
    TypeMismatch {
        table: String,
        column: String,
        expected: String,
        actual: String,
    },
    #[error("unknown column '{column}' in table '{table}'")]
    UnknownColumn { table: String, column: String },
    #[error("conflict error: {0}")]
    Conflict(String),
    #[error("underflow")]
    Underflow,
    #[error("overflow")]
    Overflow,
    #[error("queue full")]
    QueueFull,
    #[error("permission denied: {0}")]
    PermissionDenied(String),
    #[error("timeout")]
    Timeout,
    #[error("partition lock timeout")]
    PartitionLockTimeout,
    #[error("epoch apply timeout exceeded")]
    EpochApplyTimeout,
    #[error("parallel apply cancelled")]
    ParallelApplyCancelled,
    #[error("parallel apply worker panicked")]
    ParallelApplyWorkerPanicked,
    #[error("snapshot expired")]
    SnapshotExpired,
    #[error("assertion failed at index {index}")]
    AssertionFailed {
        index: usize,
        assertion: Box<crate::commit::tx::ReadAssertion>,
        actual: Box<crate::commit::tx::AssertionActual>,
    },
}

impl AedbError {
    pub fn code(&self) -> AedbErrorCode {
        match self {
            AedbError::Io(_) => AedbErrorCode::Io,
            AedbError::Encode(_) => AedbErrorCode::Encode,
            AedbError::Decode(_) => AedbErrorCode::Decode,
            AedbError::Validation(_) => AedbErrorCode::Validation,
            AedbError::InvalidConfig { .. } => AedbErrorCode::InvalidConfig,
            AedbError::IntegrityError { .. } => AedbErrorCode::IntegrityError,
            AedbError::RecoveryIntegrity { .. } => AedbErrorCode::RecoveryIntegrity,
            AedbError::Unavailable { .. } => AedbErrorCode::Unavailable,
            AedbError::CheckpointInProgress => AedbErrorCode::CheckpointInProgress,
            AedbError::AlreadyExists { resource_type, .. } => match resource_type {
                ResourceType::Project => AedbErrorCode::ProjectAlreadyExists,
                ResourceType::Scope => AedbErrorCode::ScopeAlreadyExists,
                ResourceType::Table => AedbErrorCode::TableAlreadyExists,
                ResourceType::Index => AedbErrorCode::IndexAlreadyExists,
            },
            AedbError::NotFound { resource_type, .. } => match resource_type {
                ResourceType::Project => AedbErrorCode::ProjectNotFound,
                ResourceType::Scope => AedbErrorCode::ScopeNotFound,
                ResourceType::Table => AedbErrorCode::TableNotFound,
                ResourceType::Index => AedbErrorCode::IndexNotFound,
            },
            AedbError::DuplicatePK { .. } => AedbErrorCode::DuplicatePrimaryKey,
            AedbError::UniqueViolation { .. } => AedbErrorCode::UniqueViolation,
            AedbError::ForeignKeyViolation { .. } => AedbErrorCode::ForeignKeyViolation,
            AedbError::CheckConstraintFailed { .. } => AedbErrorCode::CheckConstraintFailed,
            AedbError::NotNullViolation { .. } => AedbErrorCode::NotNullViolation,
            AedbError::TypeMismatch { .. } => AedbErrorCode::TypeMismatch,
            AedbError::UnknownColumn { .. } => AedbErrorCode::UnknownColumn,
            AedbError::Conflict(_) => AedbErrorCode::Conflict,
            AedbError::Underflow => AedbErrorCode::Underflow,
            AedbError::Overflow => AedbErrorCode::Overflow,
            AedbError::QueueFull => AedbErrorCode::QueueFull,
            AedbError::PermissionDenied(_) => AedbErrorCode::PermissionDenied,
            AedbError::Timeout => AedbErrorCode::Timeout,
            AedbError::PartitionLockTimeout => AedbErrorCode::PartitionLockTimeout,
            AedbError::EpochApplyTimeout => AedbErrorCode::EpochApplyTimeout,
            AedbError::ParallelApplyCancelled => AedbErrorCode::ParallelApplyCancelled,
            AedbError::ParallelApplyWorkerPanicked => AedbErrorCode::ParallelApplyWorkerPanicked,
            AedbError::SnapshotExpired => AedbErrorCode::SnapshotExpired,
            AedbError::AssertionFailed { .. } => AedbErrorCode::AssertionFailed,
        }
    }

    pub fn code_str(&self) -> &'static str {
        self.code().as_str()
    }

    pub fn class(&self) -> AedbErrorClass {
        self.code().class()
    }

    pub fn is_retryable(&self) -> bool {
        self.code().is_retryable()
    }
}

#[cfg(test)]
mod tests;
