//! Storage-local typed error.
//!
//! Backend code that wants to branch on storage failure modes pattern-matches
//! on [`Error`] directly. The crate boundary into [`lumotia_core::error::Error`]
//! flattens this into a [`Error::Storage`] variant carrying a serde-friendly
//! `kind`, an `operation` label, and the Display output as `detail` — so the
//! frontend (which still receives stringified errors from Tauri commands today)
//! keeps working, and Area E can later expose the typed `kind` to the FE without
//! touching call sites.
//!
//! The `sqlx::Error` source is deliberately not serialized. Sources stay sources;
//! only the boundary shape is wire-friendly.

use std::borrow::Cow;
use std::path::PathBuf;

use lumotia_core::error::{Error as CoreError, StorageKind};

/// Kinds of database-open operation that can fail before the pool is ready.
#[derive(Debug, Clone, Copy)]
pub enum OpenOp {
    Connect,
    ReadOnlyConnect,
    ForeignKeysPragma,
}

impl OpenOp {
    fn label(self) -> &'static str {
        match self {
            OpenOp::Connect => "connect",
            OpenOp::ReadOnlyConnect => "read_only_connect",
            OpenOp::ForeignKeysPragma => "foreign_keys_pragma",
        }
    }
}

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

/// Per-step phase inside the migration framework.
#[derive(Debug, Clone, Copy)]
pub enum MigrationStep {
    SchemaVersionTableCreate,
    SchemaVersionQuery,
    TxBegin,
    Apply,
    RecordVersion,
    Commit,
}

impl MigrationStep {
    fn label(self) -> &'static str {
        match self {
            MigrationStep::SchemaVersionTableCreate => "schema_version_table_create",
            MigrationStep::SchemaVersionQuery => "schema_version_query",
            MigrationStep::TxBegin => "tx_begin",
            MigrationStep::Apply => "apply",
            MigrationStep::RecordVersion => "record_version",
            MigrationStep::Commit => "commit",
        }
    }
}

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

/// Entities the storage layer can fail to find or validate.
///
/// Seeded with only the three real entities (Transcript, Task, Profile) — add
/// more here only when a typed `NotFound` / `InvalidReference` case actually
/// requires them.
#[derive(Debug, Clone, Copy)]
pub enum Entity {
    Transcript,
    Task,
    Profile,
    ImplementationRule,
    Feedback,
}

impl Entity {
    fn label(self) -> &'static str {
        match self {
            Entity::Transcript => "transcript",
            Entity::Task => "task",
            Entity::Profile => "profile",
            Entity::ImplementationRule => "implementation_rule",
            Entity::Feedback => "feedback",
        }
    }
}

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

/// Storage-crate-local typed error.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("database open failed during {operation}: {source}")]
    DatabaseOpen {
        operation: OpenOp,
        #[source]
        source: sqlx::Error,
    },

    #[error("migration step {step} (version {version:?}) failed: {source}")]
    Migration {
        version: Option<i64>,
        step: MigrationStep,
        #[source]
        source: sqlx::Error,
    },

    #[error("query failed during {operation}: {source}")]
    Query {
        operation: Cow<'static, str>,
        #[source]
        source: sqlx::Error,
    },

    #[error("{entity} not found: '{key}'")]
    NotFound { entity: Entity, key: String },

    #[error("invalid {entity} reference: {reason}")]
    InvalidReference {
        entity: Entity,
        reason: Cow<'static, str>,
    },

    #[error("filesystem operation failed at '{}': {source}", path.display())]
    Filesystem {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
}

impl Error {
    /// Discriminator for the boundary conversion into [`Error`].
    pub fn kind(&self) -> StorageKind {
        match self {
            Error::DatabaseOpen { .. } => StorageKind::DatabaseOpen,
            Error::Migration { .. } => StorageKind::Migration,
            Error::Query { .. } => StorageKind::Query,
            Error::NotFound { .. } => StorageKind::NotFound,
            Error::InvalidReference { .. } => StorageKind::InvalidReference,
            Error::Filesystem { .. } => StorageKind::Filesystem,
        }
    }

    /// Operation label that flows into the [`Error::Storage`] boundary
    /// shape. For per-query failures this is the caller-provided label;
    /// for the other variants it's the variant's intrinsic label.
    pub fn operation_label(&self) -> Cow<'static, str> {
        match self {
            Error::DatabaseOpen { operation, .. } => Cow::Borrowed(operation.label()),
            Error::Migration { step, .. } => Cow::Borrowed(step.label()),
            Error::Query { operation, .. } => operation.clone(),
            Error::NotFound { entity, .. } => Cow::Owned(format!("not_found:{}", entity.label())),
            Error::InvalidReference { entity, .. } => {
                Cow::Owned(format!("invalid_reference:{}", entity.label()))
            }
            Error::Filesystem { .. } => Cow::Borrowed("filesystem"),
        }
    }
}

/// Boundary conversion — flattens the typed error into the wire-friendly
/// [`CoreError::Storage`] variant. Lives in the storage crate (not in core)
/// to avoid a `core -> storage` dependency cycle.
impl From<Error> for CoreError {
    fn from(error: Error) -> Self {
        let kind = error.kind();
        let operation = error.operation_label().into_owned();
        let detail = error.to_string();
        CoreError::Storage {
            kind,
            operation,
            detail,
        }
    }
}

/// `Result` alias for storage-crate functions.
pub type Result<T> = std::result::Result<T, Error>;
