use crate::{AuthorityKind, FieldDefinitionId, LocalizedLabel, VocabularyId};

/// The type of a flexible field, carrying its binding where applicable.
///
/// Type-driven: a `Term` always names its vocabulary; a non-term never carries one.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldType {
    Text,
    LocalizedText,
    Integer,
    Date,
    Boolean,
    Term {
        vocabulary_id: VocabularyId,
    },
    /// An authority reference. `kind: None` accepts any authority kind;
    /// `Some(kind)` constrains to that kind.
    Authority {
        kind: Option<AuthorityKind>,
    },
}

impl FieldType {
    /// The stored discriminant string.
    pub fn kind_str(&self) -> &'static str {
        match self {
            FieldType::Text => "text",
            FieldType::LocalizedText => "localized_text",
            FieldType::Integer => "integer",
            FieldType::Date => "date",
            FieldType::Boolean => "boolean",
            FieldType::Term { .. } => "term",
            FieldType::Authority { .. } => "authority",
        }
    }

    /// Decompose into the three stored columns: `(data_type, vocabulary_id, authority_kind)`.
    pub fn to_parts(&self) -> (&'static str, Option<VocabularyId>, Option<AuthorityKind>) {
        match self {
            FieldType::Term { vocabulary_id } => ("term", Some(*vocabulary_id), None),
            FieldType::Authority { kind } => ("authority", None, *kind),
            other => (other.kind_str(), None, None),
        }
    }

    /// Reconstruct from the stored columns. Returns `None` for an unknown
    /// discriminant or an inconsistent combination — a scalar type carrying any
    /// binding, a `term` without a vocabulary (or with an authority kind), or an
    /// `authority` carrying a vocabulary.
    pub fn from_parts(
        data_type: &str,
        vocabulary_id: Option<VocabularyId>,
        authority_kind: Option<AuthorityKind>,
    ) -> Option<Self> {
        let scalar = vocabulary_id.is_none() && authority_kind.is_none();

        match data_type {
            "text" if scalar => Some(FieldType::Text),
            "localized_text" if scalar => Some(FieldType::LocalizedText),
            "integer" if scalar => Some(FieldType::Integer),
            "date" if scalar => Some(FieldType::Date),
            "boolean" if scalar => Some(FieldType::Boolean),
            "term" => match vocabulary_id {
                Some(vocabulary_id) if authority_kind.is_none() => {
                    Some(FieldType::Term { vocabulary_id })
                }
                _ => None,
            },
            "authority" if vocabulary_id.is_none() => Some(FieldType::Authority {
                kind: authority_kind,
            }),
            _ => None,
        }
    }
}

/// The stored `data_type` discriminant of a field definition — mirrors the strings from
/// [`FieldType::kind_str`]. Exists so the OpenAPI schema can describe `data_type` as a
/// closed string enum (consumed by the typed web client). Keep in sync with `kind_str`.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, utoipa::ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum DataType {
    Text,
    LocalizedText,
    Integer,
    Date,
    Boolean,
    Term,
    Authority,
}

/// A registered flexible field, with its multilingual display labels.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldDefinition {
    pub id: FieldDefinitionId,
    pub key: String,
    pub field_type: FieldType,
    pub required: bool,
    pub group_key: Option<String>,
    pub labels: Vec<LocalizedLabel>,
}

/// A field definition to be created.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewFieldDefinition {
    pub key: String,
    pub field_type: FieldType,
    pub required: bool,
    pub group_key: Option<String>,
    pub labels: Vec<LocalizedLabel>,
}

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

    #[test]
    fn field_type_round_trips_through_parts() {
        let v = VocabularyId::new();
        let cases = [
            FieldType::Text,
            FieldType::LocalizedText,
            FieldType::Integer,
            FieldType::Date,
            FieldType::Boolean,
            FieldType::Term { vocabulary_id: v },
            FieldType::Authority {
                kind: Some(AuthorityKind::Person),
            },
            FieldType::Authority { kind: None },
        ];
        for ft in cases {
            let (data_type, vocabulary_id, authority_kind) = ft.to_parts();
            assert_eq!(
                FieldType::from_parts(data_type, vocabulary_id, authority_kind),
                Some(ft)
            );
        }
    }

    #[test]
    fn term_without_vocabulary_is_invalid() {
        assert_eq!(FieldType::from_parts("term", None, None), None);
    }

    #[test]
    fn unknown_type_is_none() {
        assert_eq!(FieldType::from_parts("blob", None, None), None);
    }

    #[test]
    fn stray_binding_on_scalar_type_is_rejected() {
        let v = VocabularyId::new();
        assert_eq!(FieldType::from_parts("text", Some(v), None), None);
        assert_eq!(
            FieldType::from_parts("boolean", None, Some(AuthorityKind::Person)),
            None
        );
    }

    #[test]
    fn term_or_authority_with_wrong_binding_is_rejected() {
        let v = VocabularyId::new();
        assert_eq!(
            FieldType::from_parts("term", Some(v), Some(AuthorityKind::Person)),
            None
        );
        assert_eq!(FieldType::from_parts("authority", Some(v), None), None);
    }

    #[test]
    fn data_type_serde_matches_kind_str() {
        use serde_json::json;
        assert_eq!(
            serde_json::to_value(DataType::LocalizedText).unwrap(),
            json!("localized_text")
        );
        assert_eq!(serde_json::to_value(DataType::Text).unwrap(), json!("text"));
        assert_eq!(
            serde_json::to_value(DataType::Authority).unwrap(),
            json!("authority")
        );
    }
}
