use std::{fmt, io};

/// Alias for `Result`s in Luci.
pub type Result<T> = std::result::Result<T, LuciError>;

/// Top-level error type for all Luci operations.
///
/// Designed to map cleanly across the FFI boundary to Python exceptions.
/// See [[architecture-api-surface#Error Handling]].
#[derive(Debug)]
pub enum LuciError {
    /// Underlying I/O error.
    Io(io::Error),

    /// Index file does not exist at the given path.
    IndexNotFound(String),

    /// Index file failed checksum validation — data is corrupt.
    /// See [[architecture-storage-format#Crash Recovery]].
    IndexCorrupted(String),

    /// Another `IndexWriter` holds the file lock.
    /// See [[architecture-concurrency-model#Single-Writer Model]].
    WriterLocked,

    /// Document field type conflicts with the schema mapping.
    SchemaConflict {
        field: String,
        expected: String,
        actual: String,
    },

    /// Query type is recognized but not yet implemented.
    UnsupportedQuery(String),

    /// Query JSON is malformed or fails validation.
    InvalidQuery(String),
}

impl fmt::Display for LuciError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(e) => write!(f, "I/O error: {e}"),
            Self::IndexNotFound(path) => write!(f, "index not found: {path}"),
            Self::IndexCorrupted(msg) => write!(f, "index corrupted: {msg}"),
            Self::WriterLocked => write!(f, "another writer holds the lock"),
            Self::SchemaConflict {
                field,
                expected,
                actual,
            } => write!(
                f,
                "schema conflict on field '{field}': expected {expected}, got {actual}"
            ),
            Self::UnsupportedQuery(q) => write!(f, "unsupported query type: {q}"),
            Self::InvalidQuery(msg) => write!(f, "invalid query: {msg}"),
        }
    }
}

impl std::error::Error for LuciError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<io::Error> for LuciError {
    fn from(e: io::Error) -> Self {
        Self::Io(e)
    }
}

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

    #[test]
    fn io_error_conversion() {
        let io_err = io::Error::new(io::ErrorKind::NotFound, "file gone");
        let luci_err = LuciError::from(io_err);
        assert!(matches!(luci_err, LuciError::Io(_)));
        assert!(luci_err.source().is_some());
    }

    #[test]
    fn display_index_not_found() {
        let err = LuciError::IndexNotFound("/tmp/test.luci".into());
        assert!(format!("{err}").contains("/tmp/test.luci"));
    }

    #[test]
    fn display_index_corrupted() {
        let err = LuciError::IndexCorrupted("checksum mismatch".into());
        assert!(format!("{err}").contains("checksum mismatch"));
    }

    #[test]
    fn display_writer_locked() {
        let err = LuciError::WriterLocked;
        assert!(format!("{err}").contains("lock"));
    }

    #[test]
    fn display_schema_conflict() {
        let err = LuciError::SchemaConflict {
            field: "price".into(),
            expected: "float".into(),
            actual: "text".into(),
        };
        let msg = format!("{err}");
        assert!(msg.contains("price"));
        assert!(msg.contains("float"));
        assert!(msg.contains("text"));
    }

    #[test]
    fn display_unsupported_query() {
        let err = LuciError::UnsupportedQuery("span_near".into());
        assert!(format!("{err}").contains("span_near"));
    }

    #[test]
    fn display_invalid_query() {
        let err = LuciError::InvalidQuery("unexpected token".into());
        assert!(format!("{err}").contains("unexpected token"));
    }

    #[test]
    fn non_io_errors_have_no_source() {
        let err = LuciError::WriterLocked;
        assert!(err.source().is_none());
    }
}
