//! Graph-layer error types and GQLSTATUS mappings.

use selene_core::{CoreError, DbString, EdgeId, NodeId};
use selene_persist::PersistError;
use smallvec::SmallVec;

use crate::index_provider::ProviderError;
use crate::type_validator::TypeViolation;
use crate::typed_index::TypedIndexKind;

/// Result alias for graph operations.
pub type GraphResult<T> = Result<T, GraphError>;

/// Store-assignment data-exception family.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum StoreAssignmentException {
    /// String or byte-string assignment would truncate non-padding data.
    StringDataRightTruncation,
    /// Numeric assignment cannot be represented by the target type.
    NumericValueOutOfRange,
}

impl StoreAssignmentException {
    /// Map this store-assignment exception to its ISO GQLSTATUS code.
    #[must_use]
    pub const fn gqlstatus(self) -> &'static str {
        match self {
            Self::StringDataRightTruncation => "22001",
            Self::NumericValueOutOfRange => "22003",
        }
    }
}

/// Error raised while applying ISO store-assignment conversion rules.
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error("store assignment to property {property} failed: {reason}")]
#[diagnostic(code(SLENE_G_027))]
pub struct StoreAssignmentError {
    /// Property being assigned.
    pub property: DbString,
    /// Data-exception family.
    pub exception: StoreAssignmentException,
    /// Human-readable reason.
    pub reason: String,
}

/// Error type for graph storage and mutation operations.
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[non_exhaustive]
pub enum GraphError {
    /// The requested node row does not exist.
    #[error("node not found: {id}")]
    #[diagnostic(code(SLENE_G_001))]
    NodeNotFound {
        /// Missing node ID.
        id: NodeId,
    },

    /// The requested edge row does not exist.
    #[error("edge not found: {id}")]
    #[diagnostic(code(SLENE_G_002))]
    EdgeNotFound {
        /// Missing edge ID.
        id: EdgeId,
    },

    /// The requested node row exists but is not alive.
    #[error("node {id} is not alive")]
    #[diagnostic(code(SLENE_G_003))]
    NodeNotAlive {
        /// Dead node ID.
        id: NodeId,
    },

    /// The requested edge row exists but is not alive.
    #[error("edge {id} is not alive")]
    #[diagnostic(code(SLENE_G_004))]
    EdgeNotAlive {
        /// Dead edge ID.
        id: EdgeId,
    },

    /// The dense row store filled the v1 row-addressable range (max 2^32 rows).
    ///
    /// Post-4c the cap is a *row count*, not an id value: rows append at the
    /// dense end and `u32::MAX` is reserved as `RowIndex::TOMBSTONE`, so the last
    /// addressable row is `u32::MAX - 1`.
    #[error("{kind} row store is full ({rows} rows; max {max_rows})")]
    #[diagnostic(code(SLENE_G_005))]
    RowSpaceExhausted {
        /// `"node"` or `"edge"`.
        kind: &'static str,
        /// The current row count that hit the cap.
        rows: u64,
        /// The maximum addressable row count.
        max_rows: u64,
    },

    /// The graph snapshot violates a structural invariant (e.g., row count
    /// exceeds the addressable u32 range).
    #[error("graph snapshot is inconsistent: {reason}")]
    #[diagnostic(code(SLENE_G_006))]
    Inconsistent {
        /// Free-form description of the inconsistency.
        reason: String,
    },

    /// A property index already exists for this `(label, property)`.
    #[error("property index already exists for ({label}, {property})")]
    #[diagnostic(code(SLENE_G_007))]
    PropertyIndexAlreadyExists {
        /// Indexed node label.
        label: DbString,
        /// Indexed property key.
        property: DbString,
    },

    /// The named property index does not exist.
    #[error("property index does not exist for ({label}, {property})")]
    #[diagnostic(code(SLENE_G_008))]
    PropertyIndexNotFound {
        /// Indexed node label.
        label: DbString,
        /// Indexed property key.
        property: DbString,
    },

    /// A value cannot be admitted to the declared property index kind.
    #[error(
        "property index ({label}, {property}) expected {expected_kind:?} but observed {observed}"
    )]
    #[diagnostic(code(SLENE_G_009))]
    IndexValueRejected {
        /// Indexed node label.
        label: DbString,
        /// Indexed property key.
        property: DbString,
        /// Registered index kind.
        expected_kind: TypedIndexKind,
        /// Observed value kind or `"NaN"`.
        observed: &'static str,
    },

    /// A composite property index already exists for this `(label, properties...)`.
    #[error("composite property index already exists for ({label}, {properties:?})")]
    #[diagnostic(code(SLENE_G_020))]
    CompositePropertyIndexAlreadyExists {
        /// Indexed node label.
        label: DbString,
        /// Indexed property keys in declaration order.
        ///
        /// Boxed so this variant does not inflate `GraphError` past clippy's
        /// `result_large_err` byte threshold: an inline `SmallVec<[DbString; 4]>`
        /// is ~104 B (four owned string values plus header), and the variant
        /// otherwise drives every `GraphResult<T>` stack slot over the limit.
        /// The `Box` pushes the allocation onto the cold error-construction
        /// path.
        properties: Box<SmallVec<[DbString; 4]>>,
    },

    /// A vector property index already exists for this `(label, property)`.
    #[error("vector index already exists for ({label}, {property})")]
    #[diagnostic(code(SLENE_G_021))]
    VectorIndexAlreadyExists {
        /// Indexed node label.
        label: DbString,
        /// Indexed vector property key.
        property: DbString,
    },

    /// A vector index was declared with an invalid dimensionality.
    #[error("vector index dimension must be non-zero, observed {dimension}")]
    #[diagnostic(code(SLENE_G_022))]
    VectorIndexInvalidDimension {
        /// Declared vector dimensionality.
        dimension: u32,
    },

    /// A vector index was declared with invalid HNSW construction parameters.
    #[error(
        "invalid HNSW vector index config max_neighbors={max_neighbors}, ef_construction={ef_construction}: {reason}"
    )]
    #[diagnostic(code(SLENE_G_024))]
    VectorIndexInvalidHnswConfig {
        /// Declared HNSW `M` fanout.
        max_neighbors: u16,
        /// Declared HNSW construction beam width.
        ef_construction: u16,
        /// Reason the configuration is rejected.
        reason: &'static str,
    },

    /// A vector index was declared with invalid IVF construction parameters.
    #[error("invalid IVF vector index config target_centroids={target_centroids}: {reason}")]
    #[diagnostic(code(SLENE_G_025))]
    VectorIndexInvalidIvfConfig {
        /// Declared IVF target centroid count.
        target_centroids: u16,
        /// Reason the configuration is rejected.
        reason: &'static str,
    },

    /// A value cannot be admitted to a vector index.
    #[error(
        "vector index ({label}, {property}) expected VECTOR<{expected_dimension}> but observed {observed}"
    )]
    #[diagnostic(code(SLENE_G_023))]
    VectorIndexValueRejected {
        /// Indexed node label.
        label: DbString,
        /// Indexed vector property key.
        property: DbString,
        /// Registered vector dimensionality.
        expected_dimension: u32,
        /// Observed value kind or dimensionality.
        observed: String,
    },

    /// A text index already exists for this `(label, property)`.
    #[error("text index already exists for ({label}, {property})")]
    #[diagnostic(code(SLENE_G_026))]
    TextIndexAlreadyExists {
        /// Indexed node label.
        label: DbString,
        /// Indexed string property key.
        property: DbString,
    },

    /// A closed graph mutation violates its bound graph type.
    #[error(transparent)]
    #[diagnostic(transparent)]
    TypeViolation(#[from] TypeViolation),

    /// A store assignment failed before the graph mutation was applied.
    #[error(transparent)]
    #[diagnostic(transparent)]
    StoreAssignment(Box<StoreAssignmentError>),

    /// A commit-critical durable provider rejected or failed a write.
    #[error("durable provider failed: {reason}")]
    #[diagnostic(code(SLENE_G_015))]
    Durable {
        /// Human-readable durable provider failure reason.
        reason: String,
    },

    /// The commit was cancelled at the pre-WAL cut-line (BRIEF-117): the
    /// committer observed the cancellation token set before it appended the
    /// commit to the WAL, so nothing was persisted or published. Past the WAL
    /// append a commit is irrevocable and this is never returned.
    #[error("commit cancelled before durable append")]
    #[diagnostic(code(SLENE_G_019))]
    Cancelled,

    /// Error propagated from selene-core.
    #[error(transparent)]
    #[diagnostic(transparent)]
    Core(#[from] CoreError),

    /// Error propagated from an index provider.
    #[error(transparent)]
    #[diagnostic(transparent)]
    Provider(#[from] ProviderError),

    /// Error propagated from persistence recovery.
    #[error(transparent)]
    #[diagnostic(transparent)]
    Persist(#[from] PersistError),
}

impl GraphError {
    /// Map this error to its 5-character ISO GQLSTATUS code.
    #[must_use]
    pub const fn gqlstatus(&self) -> &'static str {
        match self {
            Self::NodeNotFound { .. }
            | Self::EdgeNotFound { .. }
            | Self::NodeNotAlive { .. }
            | Self::EdgeNotAlive { .. } => "22G03",
            Self::RowSpaceExhausted { .. } => "53000",
            Self::Inconsistent { .. } => "5GQL0",
            Self::PropertyIndexAlreadyExists { .. }
            | Self::PropertyIndexNotFound { .. }
            | Self::IndexValueRejected { .. }
            | Self::CompositePropertyIndexAlreadyExists { .. }
            | Self::VectorIndexAlreadyExists { .. }
            | Self::VectorIndexInvalidDimension { .. }
            | Self::VectorIndexInvalidHnswConfig { .. }
            | Self::VectorIndexInvalidIvfConfig { .. }
            | Self::VectorIndexValueRejected { .. }
            | Self::TextIndexAlreadyExists { .. } => "22G03",
            Self::TypeViolation(_) => "G2000",
            Self::StoreAssignment(source) => source.exception.gqlstatus(),
            Self::Core(source) => source.gqlstatus(),
            Self::Durable { .. } => "5GQL0",
            Self::Cancelled => "5GQL2",
            Self::Provider(_) | Self::Persist(_) => "5GQL0",
        }
    }
}

#[cfg(test)]
mod tests {
    use rstest::rstest;
    use selene_core::db_string;

    use super::*;
    use crate::ProviderError;

    #[rstest]
    #[case(GraphError::NodeNotFound { id: NodeId::new(1) }, "22G03")]
    #[case(GraphError::EdgeNotFound { id: EdgeId::new(1) }, "22G03")]
    #[case(GraphError::NodeNotAlive { id: NodeId::new(1) }, "22G03")]
    #[case(GraphError::EdgeNotAlive { id: EdgeId::new(1) }, "22G03")]
    #[case(
        GraphError::RowSpaceExhausted { kind: "node", rows: 4_294_967_295, max_rows: 4_294_967_295 },
        "53000"
    )]
    #[case(
        GraphError::Inconsistent { reason: "row index exceeds u32::MAX".to_owned() },
        "5GQL0"
    )]
    #[case(
        GraphError::PropertyIndexAlreadyExists {
            label: db_string("err.label").unwrap(),
            property: db_string("err.property").unwrap(),
        },
        "22G03"
    )]
    #[case(
        GraphError::PropertyIndexNotFound {
            label: db_string("err.label.missing").unwrap(),
            property: db_string("err.property.missing").unwrap(),
        },
        "22G03"
    )]
    #[case(
        GraphError::IndexValueRejected {
            label: db_string("err.label.rejected").unwrap(),
            property: db_string("err.property.rejected").unwrap(),
            expected_kind: TypedIndexKind::I64,
            observed: "String",
        },
        "22G03"
    )]
    #[case(
        GraphError::VectorIndexAlreadyExists {
            label: db_string("err.label.vector.exists").unwrap(),
            property: db_string("err.property.vector.exists").unwrap(),
        },
        "22G03"
    )]
    #[case(GraphError::VectorIndexInvalidDimension { dimension: 0 }, "22G03")]
    #[case(
        GraphError::VectorIndexInvalidHnswConfig {
            max_neighbors: 24,
            ef_construction: 8,
            reason: "ef_construction must be at least max_neighbors",
        },
        "22G03"
    )]
    #[case(
        GraphError::VectorIndexInvalidIvfConfig {
            target_centroids: 0,
            reason: "target_centroids must be greater than zero",
        },
        "22G03"
    )]
    #[case(
        GraphError::VectorIndexValueRejected {
            label: db_string("err.label.vector.rejected").unwrap(),
            property: db_string("err.property.vector.rejected").unwrap(),
            expected_dimension: 3,
            observed: "VECTOR<4>".to_owned(),
        },
        "22G03"
    )]
    #[case(
        GraphError::TextIndexAlreadyExists {
            label: db_string("err.label.text.exists").unwrap(),
            property: db_string("err.property.text.exists").unwrap(),
        },
        "22G03"
    )]
    #[case(
        GraphError::TypeViolation(TypeViolation::UnknownEdgeLabel {
            id: EdgeId::new(1),
            label: db_string("err.edge.label").unwrap(),
        }),
        "G2000"
    )]
    #[case(
        GraphError::StoreAssignment(Box::new(StoreAssignmentError {
            property: db_string("err.assignment.property").unwrap(),
            exception: StoreAssignmentException::StringDataRightTruncation,
            reason: "right truncation".to_owned(),
        })),
        "22001"
    )]
    #[case(GraphError::Core(CoreError::ZeroIdentifier), "0G003")]
    #[case(GraphError::Durable { reason: "wal unavailable".to_owned() }, "5GQL0")]
    #[case(GraphError::Cancelled, "5GQL2")]
    #[case(
        GraphError::Provider(ProviderError::Inconsistent { reason: "duplicate provider tag DEMO".to_owned() }),
        "5GQL0"
    )]
    #[case(GraphError::Persist(PersistError::MalformedSnapshotFilename), "5GQL0")]
    fn gqlstatus_for_each_variant(#[case] error: GraphError, #[case] status: &str) {
        assert_eq!(error.gqlstatus(), status);
        assert!(
            selene_core::gqlstatus_name(status).is_some(),
            "GQLSTATUS code {status} emitted by GraphError but not in ALL_GQLSTATUS_NAMES"
        );
    }

    #[test]
    fn core_error_variant_propagates() {
        fn inner() -> Result<(), CoreError> {
            Err(CoreError::ZeroIdentifier)
        }
        fn outer() -> GraphResult<()> {
            inner()?;
            Ok(())
        }
        assert!(matches!(
            outer(),
            Err(GraphError::Core(CoreError::ZeroIdentifier))
        ));
    }

    /// Internal miette `SLENE_G_*` diagnostic codes must be unique across the
    /// graph-crate diagnostic enums (`GraphError`, `TypeViolation`,
    /// `ProviderError`). A reused code makes two semantically different errors
    /// indistinguishable in diagnostics. (GRAPH-42 regression guard.)
    #[test]
    fn internal_diagnostic_codes_are_unique() {
        use miette::Diagnostic;
        use selene_core::{LabelSet, PropertyValueType};

        use crate::graph_types::EdgeEndpointDef;
        use crate::type_validator::EntityId;

        let lbl = db_string("codes.label").unwrap();
        let prop = db_string("codes.property").unwrap();

        // One representative of every code-carrying GraphError variant (the
        // transparent `TypeViolation`/`Core`/`Persist`/`Provider` wrappers carry
        // no own code).
        let graph_errors: Vec<GraphError> = vec![
            GraphError::NodeNotFound { id: NodeId::new(1) },
            GraphError::EdgeNotFound { id: EdgeId::new(1) },
            GraphError::NodeNotAlive { id: NodeId::new(1) },
            GraphError::EdgeNotAlive { id: EdgeId::new(1) },
            GraphError::RowSpaceExhausted {
                kind: "node",
                rows: 1,
                max_rows: 1,
            },
            GraphError::Inconsistent {
                reason: "x".to_owned(),
            },
            GraphError::PropertyIndexAlreadyExists {
                label: lbl.clone(),
                property: prop.clone(),
            },
            GraphError::PropertyIndexNotFound {
                label: lbl.clone(),
                property: prop.clone(),
            },
            GraphError::IndexValueRejected {
                label: lbl.clone(),
                property: prop.clone(),
                expected_kind: TypedIndexKind::I64,
                observed: "String",
            },
            GraphError::CompositePropertyIndexAlreadyExists {
                label: lbl.clone(),
                properties: Box::default(),
            },
            GraphError::VectorIndexAlreadyExists {
                label: lbl.clone(),
                property: prop.clone(),
            },
            GraphError::VectorIndexInvalidDimension { dimension: 0 },
            GraphError::VectorIndexInvalidHnswConfig {
                max_neighbors: 24,
                ef_construction: 8,
                reason: "ef_construction must be at least max_neighbors",
            },
            GraphError::VectorIndexInvalidIvfConfig {
                target_centroids: 0,
                reason: "target_centroids must be greater than zero",
            },
            GraphError::VectorIndexValueRejected {
                label: lbl.clone(),
                property: prop.clone(),
                expected_dimension: 3,
                observed: "VECTOR<4>".to_owned(),
            },
            GraphError::TextIndexAlreadyExists {
                label: lbl.clone(),
                property: prop.clone(),
            },
            GraphError::StoreAssignment(Box::new(StoreAssignmentError {
                property: prop.clone(),
                exception: StoreAssignmentException::StringDataRightTruncation,
                reason: "right truncation".to_owned(),
            })),
            GraphError::Durable {
                reason: "x".to_owned(),
            },
            GraphError::Cancelled,
        ];

        let type_violations: Vec<TypeViolation> = vec![
            TypeViolation::UnknownNodeLabel {
                id: NodeId::new(1),
                labels: LabelSet::new(),
            },
            TypeViolation::UnknownEdgeLabel {
                id: EdgeId::new(1),
                label: lbl.clone(),
            },
            TypeViolation::EdgeEndpointTypeMismatch {
                id: EdgeId::new(1),
                label: lbl.clone(),
                expected_source_type: EdgeEndpointDef::Any,
                observed_source_type: 0,
                expected_target_type: EdgeEndpointDef::Any,
                observed_target_type: 0,
            },
            TypeViolation::MissingRequiredProperty {
                entity_id: EntityId::Node(NodeId::new(1)),
                property: prop.clone(),
                declared_in: lbl.clone(),
            },
            TypeViolation::PropertyTypeMismatch {
                entity_id: EntityId::Node(NodeId::new(1)),
                property: prop.clone(),
                expected: PropertyValueType::Int,
                observed: "String",
            },
            TypeViolation::ExtensionValueRejected {
                entity_id: EntityId::Node(NodeId::new(1)),
                property: prop.clone(),
            },
            TypeViolation::UndeclaredProperty {
                entity_id: EntityId::Node(NodeId::new(1)),
                property: prop.clone(),
            },
            TypeViolation::ImmutablePropertyUpdate {
                entity_id: EntityId::Node(NodeId::new(1)),
                property: prop,
                declared_in: lbl,
            },
        ];

        let provider_errors: Vec<ProviderError> = vec![
            ProviderError::InvalidPayload {
                reason: "x".to_owned(),
            },
            ProviderError::SerializationFailed {
                reason: "x".to_owned(),
            },
            ProviderError::Inconsistent {
                reason: "x".to_owned(),
            },
        ];

        let mut codes: Vec<String> = Vec::new();
        codes.extend(
            graph_errors
                .iter()
                .filter_map(|e| e.code().map(|c| c.to_string())),
        );
        codes.extend(
            type_violations
                .iter()
                .filter_map(|e| e.code().map(|c| c.to_string())),
        );
        codes.extend(
            provider_errors
                .iter()
                .filter_map(|e| e.code().map(|c| c.to_string())),
        );

        let mut seen = std::collections::HashSet::new();
        for code in &codes {
            assert!(
                seen.insert(code.clone()),
                "duplicate internal diagnostic code {code} across graph-crate error enums"
            );
        }
    }
}
