use super::{EntityId, EntityKind};
use crate::db::ConfigError;
use crate::query::retrieve::QueryError;

/// Structured context attached to an internal error for diagnostics.
///
/// Provides operation name, optional entity identification, and optional
/// signal type. Replaces bare `String` in `TidalError::Internal`.
#[derive(Debug, Clone)]
pub struct ErrorContext {
    /// The operation that failed (e.g., "signal", "retrieve", "write\_item").
    pub operation: &'static str,
    /// The entity involved, if any.
    pub entity_id: Option<u64>,
    /// The entity kind involved, if any.
    pub entity_kind: Option<EntityKind>,
    /// The signal type involved, if any.
    pub signal_type: Option<String>,
    /// Additional detail (replaces bare String messages).
    pub detail: String,
}

impl ErrorContext {
    /// Create a context with just an operation and detail message.
    #[must_use]
    pub fn new(operation: &'static str, detail: impl Into<String>) -> Self {
        Self {
            operation,
            entity_id: None,
            entity_kind: None,
            signal_type: None,
            detail: detail.into(),
        }
    }

    /// Attach an entity ID to the context.
    #[must_use]
    pub const fn with_entity(mut self, id: u64) -> Self {
        self.entity_id = Some(id);
        self
    }

    /// Attach an entity kind to the context.
    #[must_use]
    pub const fn with_kind(mut self, kind: EntityKind) -> Self {
        self.entity_kind = Some(kind);
        self
    }

    /// Attach a signal type to the context.
    #[must_use]
    pub fn with_signal(mut self, signal_type: impl Into<String>) -> Self {
        self.signal_type = Some(signal_type.into());
        self
    }
}

impl std::fmt::Display for ErrorContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[op={}]", self.operation)?;
        if let Some(id) = self.entity_id {
            write!(f, "[entity={id}]")?;
        }
        if let Some(kind) = &self.entity_kind {
            write!(f, "[kind={kind}]")?;
        }
        if let Some(sig) = &self.signal_type {
            write!(f, "[signal={sig}]")?;
        }
        write!(f, " {}", self.detail)
    }
}

/// Top-level error type. Every public API method returns `Result<T, TidalError>`.
///
/// # Examples
///
/// ```
/// use tidaldb::TidalError;
///
/// let err = TidalError::internal("test", "something unexpected");
/// assert!(err.to_string().contains("internal error"));
/// ```
#[derive(Debug, thiserror::Error)]
pub enum TidalError {
    /// Storage engine failure. Retry may succeed.
    #[error("storage error: {0}")]
    Storage(#[from] StorageError),
    /// Entity not found. Caller should handle.
    #[error("{kind} {id} not found")]
    NotFound { kind: EntityKind, id: EntityId },
    /// Schema violation. Caller's fault — fix the input.
    #[error("{0}")]
    Schema(#[from] SchemaError),
    /// Signal write failed durability check. Retry required.
    #[error("durability error: {0}")]
    Durability(#[from] DurabilityError),
    /// Query malformed. Parse error with details.
    #[error("query error: {0}")]
    Query(#[from] QueryError),
    /// Configuration error. Caller supplied invalid config.
    #[error("config error: {0}")]
    Config(#[from] ConfigError),
    /// Internal invariant violated. This is a bug in tidalDB.
    #[error("internal error: {0}")]
    Internal(ErrorContext),
    /// A session policy rejected the signal write.
    #[error(
        "policy violation: signal '{signal_type}' rejected by policy '{policy_name}': {reason}"
    )]
    PolicyViolation {
        signal_type: String,
        policy_name: String,
        reason: String,
    },
    /// A session has exceeded its `max_session_duration` policy limit.
    #[error("session {session_id} expired (max duration: {max_duration_secs:.1}s)")]
    SessionExpired {
        session_id: u64,
        max_duration_secs: f64,
    },
    /// The WAL write queue is saturated. The caller should retry after the
    /// suggested delay. This is NOT a data loss event — the signal was
    /// never enqueued, so it can be safely retried.
    #[error("backpressure: WAL queue full, retry after {retry_after_ms}ms")]
    Backpressure {
        /// Suggested delay before retrying, in milliseconds.
        retry_after_ms: u64,
    },
    /// A session signal write was rejected because the agent exceeded its
    /// rate limit. The signal was NOT written. The caller should back off
    /// and retry after the suggested delay.
    #[error(
        "rate limited: agent '{agent_id}' at {limit} signals/sec, retry after {retry_after_ms}ms"
    )]
    RateLimited {
        /// The agent that exceeded its limit.
        agent_id: String,
        /// The configured rate limit (signals/sec).
        limit: f64,
        /// Suggested delay before retrying, in milliseconds.
        retry_after_ms: u64,
    },
    /// This node is a read-only follower. Redirect writes to the leader.
    #[error("node is read-only: {0}")]
    ReadOnly(String),
    /// The caller supplied invalid input (e.g., NaN weight, oversized metadata).
    #[error("invalid input: {0}")]
    InvalidInput(String),
    /// The schema fingerprint stored on disk does not match the schema provided
    /// at open time. Signal decay parameters, signal names, or entity kinds
    /// have changed since the data directory was created.
    #[error("schema mismatch: {0}")]
    SchemaMismatch(String),
    /// Per-tenant resource quota exceeded.
    #[error("quota exceeded: {0}")]
    QuotaExceeded(String),
    /// Invalid state transition.
    #[error("invalid state: {0}")]
    InvalidState(String),
    /// The operation cannot proceed because a prerequisite has not been met yet.
    ///
    /// Unlike `InvalidState` (wrong transition), `NotReady` means the system
    /// needs more time to reach the required condition (e.g., a follower catching
    /// up to a cutover sequence number before migration can finalize).
    #[error("not ready: {0}")]
    NotReady(String),
}

impl TidalError {
    /// Create an `Internal` error with minimal context (operation + detail only).
    pub fn internal(operation: &'static str, detail: impl Into<String>) -> Self {
        Self::Internal(ErrorContext::new(operation, detail))
    }

    /// Create a `ReadOnly` error when a write operation is attempted on a follower.
    #[must_use]
    pub fn read_only(operation: &str) -> Self {
        Self::ReadOnly(format!("operation '{operation}' rejected on follower node"))
    }

    /// Create an `InvalidInput` error for bad user-supplied values.
    pub fn invalid_input(detail: impl Into<String>) -> Self {
        Self::InvalidInput(detail.into())
    }

    /// Create a `SchemaMismatch` error when the stored fingerprint differs.
    pub fn schema_mismatch(detail: impl Into<String>) -> Self {
        Self::SchemaMismatch(detail.into())
    }
}

/// Schema validation errors.
///
/// `Eq` is manually implemented because f64 fields (from `Duration::as_secs_f64()`)
/// are always non-NaN, making equality reflexive.
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum SchemaError {
    #[error("duplicate signal name: '{0}'")]
    DuplicateSignalName(String),
    #[error("invalid signal name: '{0}'")]
    InvalidSignalName(String),
    #[error("signal '{signal_name}': invalid half-life: {half_life_secs}s")]
    InvalidHalfLife {
        signal_name: String,
        half_life_secs: f64,
    },
    #[error("signal '{signal_name}': invalid lifetime: {lifetime_secs}s")]
    InvalidLifetime {
        signal_name: String,
        lifetime_secs: f64,
    },
    #[error("signal '{signal_name}': non-permanent signal requires at least one window")]
    EmptyWindows { signal_name: String },
    #[error("signal '{signal_name}': velocity requires at least one window")]
    VelocityWithoutWindows { signal_name: String },
    #[error("schema must define at least one signal")]
    NoSignalsDefined,
    /// Signal type name not found in schema at runtime.
    #[error("unknown signal type: '{0}'")]
    UnknownSignalType(String),
    /// Two session policies share the same name.
    #[error("duplicate policy name: '{0}'")]
    DuplicatePolicyName(String),
    /// Session policy name is not a valid identifier.
    #[error("invalid policy name: '{0}'")]
    InvalidPolicyName(String),
    /// A signal referenced in a policy does not exist in the schema.
    #[error("policy '{policy}': signal '{signal}' not defined in schema")]
    PolicySignalNotInSchema { policy: String, signal: String },
    /// A signal appears in both `allowed_signals` and `denied_signals`.
    #[error("policy '{policy}': signal '{signal}' in both allowed_signals and denied_signals")]
    PolicySignalConflict { policy: String, signal: String },
}

impl Eq for SchemaError {}

/// Re-exported from `crate::storage::StorageError`.
pub use crate::storage::StorageError;

/// Stub for Phase 1.2+.
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct DurabilityError {
    pub message: String,
}

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

    #[test]
    fn tidal_error_display_not_found() {
        let e = TidalError::NotFound {
            kind: EntityKind::Item,
            id: EntityId::new(42),
        };
        assert_eq!(e.to_string(), "item 42 not found");
    }

    #[test]
    fn tidal_error_display_schema() {
        let e = TidalError::Schema(SchemaError::DuplicateSignalName("view".into()));
        assert!(e.to_string().contains("duplicate signal name"));
    }

    #[test]
    fn tidal_error_display_internal() {
        let e = TidalError::internal("test", "something broke");
        assert!(e.to_string().contains("internal error"));
    }

    #[test]
    fn tidal_error_display_storage() {
        let e = TidalError::Storage(StorageError::Corruption {
            message: "disk full".into(),
        });
        assert!(e.to_string().contains("disk full"));
    }

    #[test]
    fn tidal_error_display_durability() {
        let e = TidalError::Durability(DurabilityError {
            message: "fsync failed".into(),
        });
        assert!(e.to_string().contains("fsync failed"));
    }

    #[test]
    fn tidal_error_display_query() {
        let e = TidalError::Query(QueryError::ProfileNotFound("parse error".into()));
        assert!(e.to_string().contains("parse error"));
    }

    #[test]
    fn tidal_error_source_schema() {
        use std::error::Error;
        let e = TidalError::Schema(SchemaError::NoSignalsDefined);
        assert!(e.source().is_some());
    }

    #[test]
    fn tidal_error_source_internal_is_none() {
        use std::error::Error;
        let e = TidalError::internal("test", "bug");
        assert!(e.source().is_none());
    }

    #[test]
    fn tidal_error_source_not_found_is_none() {
        use std::error::Error;
        let e = TidalError::NotFound {
            kind: EntityKind::User,
            id: EntityId::new(1),
        };
        assert!(e.source().is_none());
    }

    #[test]
    fn schema_error_converts_to_tidal_error() {
        let schema_err = SchemaError::NoSignalsDefined;
        let tidal_err: TidalError = schema_err.into();
        assert!(matches!(
            tidal_err,
            TidalError::Schema(SchemaError::NoSignalsDefined)
        ));
    }

    #[test]
    fn storage_error_converts_to_tidal_error() {
        let e = StorageError::Closed;
        let tidal_err: TidalError = e.into();
        assert!(matches!(tidal_err, TidalError::Storage(_)));
    }

    #[test]
    fn durability_error_converts_to_tidal_error() {
        let e = DurabilityError {
            message: "test".into(),
        };
        let tidal_err: TidalError = e.into();
        assert!(matches!(tidal_err, TidalError::Durability(_)));
    }

    #[test]
    fn query_error_converts_to_tidal_error() {
        let e = QueryError::ProfileNotFound("test".into());
        let tidal_err: TidalError = e.into();
        assert!(matches!(tidal_err, TidalError::Query(_)));
    }

    #[test]
    fn schema_error_display_all_variants() {
        assert_eq!(
            SchemaError::DuplicateSignalName("view".into()).to_string(),
            "duplicate signal name: 'view'"
        );
        assert_eq!(
            SchemaError::InvalidSignalName("BAD".into()).to_string(),
            "invalid signal name: 'BAD'"
        );
        assert!(
            SchemaError::InvalidHalfLife {
                signal_name: "s".into(),
                half_life_secs: 0.0,
            }
            .to_string()
            .contains("invalid half-life")
        );
        assert!(
            SchemaError::InvalidLifetime {
                signal_name: "s".into(),
                lifetime_secs: -1.0,
            }
            .to_string()
            .contains("invalid lifetime")
        );
        assert!(
            SchemaError::EmptyWindows {
                signal_name: "s".into()
            }
            .to_string()
            .contains("requires at least one window")
        );
        assert!(
            SchemaError::VelocityWithoutWindows {
                signal_name: "s".into()
            }
            .to_string()
            .contains("velocity requires")
        );
        assert_eq!(
            SchemaError::NoSignalsDefined.to_string(),
            "schema must define at least one signal"
        );
    }

    #[test]
    fn schema_error_eq() {
        assert_eq!(
            SchemaError::DuplicateSignalName("a".into()),
            SchemaError::DuplicateSignalName("a".into())
        );
        assert_ne!(
            SchemaError::DuplicateSignalName("a".into()),
            SchemaError::DuplicateSignalName("b".into())
        );
        assert_ne!(
            SchemaError::NoSignalsDefined,
            SchemaError::DuplicateSignalName("a".into())
        );
    }

    #[test]
    fn error_context_display_minimal() {
        let ctx = ErrorContext::new("signal", "WAL append failed");
        assert_eq!(ctx.to_string(), "[op=signal] WAL append failed");
    }

    #[test]
    fn error_context_display_with_entity() {
        let ctx = ErrorContext::new("write_item", "storage full")
            .with_entity(42)
            .with_kind(EntityKind::Item);
        let s = ctx.to_string();
        assert!(s.contains("[op=write_item]"));
        assert!(s.contains("[entity=42]"));
        assert!(s.contains("storage full"));
    }

    #[test]
    fn error_context_display_with_signal() {
        let ctx = ErrorContext::new("signal", "unknown type").with_signal("view");
        let s = ctx.to_string();
        assert!(s.contains("[signal=view]"));
    }

    #[test]
    fn tidal_error_internal_convenience() {
        let err = TidalError::internal("open", "lock file exists");
        let s = err.to_string();
        assert!(s.contains("internal error"));
        assert!(s.contains("[op=open]"));
        assert!(s.contains("lock file exists"));
    }

    #[test]
    fn tidal_error_display_schema_mismatch() {
        let err = TidalError::schema_mismatch("decay changed");
        let s = err.to_string();
        assert!(s.contains("schema mismatch"), "got: {s}");
        assert!(s.contains("decay changed"), "got: {s}");
    }

    #[test]
    fn tidal_error_source_schema_mismatch_is_none() {
        use std::error::Error;
        let err = TidalError::schema_mismatch("test");
        assert!(err.source().is_none());
    }
}
