//! User-facing column types and their mapping to SQLite STRICT.
//!
//! Implements the nine-type vocabulary committed to in ADR-0005
//! (ten as originally decided; `blob` dropped by Amendment 2).
//! Storage choices for the text-backed types
//! (`decimal`, `date`, `datetime`) preserve precision and ISO
//! readability; comparisons rely on lexicographic ordering or
//! explicit casts at query time, which is acceptable for a
//! teaching tool and is documented in user-facing help.

use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Type {
    /// UTF-8 text of any length.
    Text,
    /// 64-bit signed integer.
    Int,
    /// IEEE-754 double-precision float.
    Real,
    /// Arbitrary-precision decimal stored as a string. Sorts and
    /// compares lexicographically; numeric ops require casts.
    Decimal,
    /// Boolean stored as 0/1; rendered `true`/`false`.
    Bool,
    /// ISO 8601 date stored as `YYYY-MM-DD` (TEXT).
    Date,
    /// ISO 8601 datetime stored as `YYYY-MM-DDTHH:MM:SS[.fff][Z]` (TEXT).
    DateTime,
    /// Auto-incrementing integer; intended as a default primary key.
    Serial,
    /// 10–12 character base58 random identifier (no ambiguous chars).
    ShortId,
}

impl Type {
    /// The user-facing keyword as it appears in DSL input.
    #[must_use]
    pub const fn keyword(self) -> &'static str {
        match self {
            Self::Text => "text",
            Self::Int => "int",
            Self::Real => "real",
            Self::Decimal => "decimal",
            Self::Bool => "bool",
            Self::Date => "date",
            Self::DateTime => "datetime",
            Self::Serial => "serial",
            Self::ShortId => "shortid",
        }
    }

    /// The SQLite STRICT type clause for this column. The
    /// `serial` type also implies `PRIMARY KEY` semantics in
    /// `sqlite_strict_extra` — see [`Self::sqlite_strict_extra`].
    #[must_use]
    pub const fn sqlite_strict_type(self) -> &'static str {
        match self {
            Self::Text | Self::ShortId | Self::Decimal | Self::Date | Self::DateTime => "TEXT",
            Self::Int | Self::Serial | Self::Bool => "INTEGER",
            Self::Real => "REAL",
        }
    }

    /// Extra clause appended after the type in DDL — e.g.
    /// `PRIMARY KEY` for `serial`. Empty when no extra clause
    /// applies.
    #[must_use]
    pub const fn sqlite_strict_extra(self) -> &'static str {
        match self {
            Self::Serial => " PRIMARY KEY",
            _ => "",
        }
    }

    /// All types known in this iteration, in stable order.
    /// Ordering groups numeric types together, then boolean,
    /// then temporal, then identity-flavoured auto-generated
    /// types.
    #[must_use]
    pub const fn all() -> &'static [Self] {
        &[
            Self::Text,
            Self::Int,
            Self::Real,
            Self::Decimal,
            Self::Bool,
            Self::Date,
            Self::DateTime,
            Self::Serial,
            Self::ShortId,
        ]
    }

    /// True for the numeric types — `int`, `real`, `decimal`,
    /// `serial`. `bool` (stored 0/1) and the text-backed types
    /// are not numeric. Used to flag a `LIKE` text-pattern
    /// match against a numeric column (ADR-0027, Amendment 1).
    #[must_use]
    pub const fn is_numeric(self) -> bool {
        matches!(self, Self::Int | Self::Real | Self::Decimal | Self::Serial)
    }

    /// The user-facing type that an FK column should use to
    /// reference a primary key of *this* type. For most types
    /// the answer is the same type; for `serial` and `shortid`
    /// it differs, because those types carry insert-time
    /// auto-generation semantics that only apply on the PK
    /// side. The FK side stores plain looked-up values, so:
    ///
    /// - `serial` → `int` (FK holds a plain integer value)
    /// - `shortid` → `text` (FK holds a plain text value)
    ///
    /// Consumed by the FK declaration grammar in a later
    /// iteration; defined here so the type system is complete
    /// before that work begins.
    #[must_use]
    pub const fn fk_target_type(self) -> Self {
        match self {
            Self::Serial => Self::Int,
            Self::ShortId => Self::Text,
            other => other,
        }
    }

    /// Resolve a type name from the **advanced-mode SQL** type slot
    /// (ADR-0035 §3). Accepts the nine playground keywords *and* the
    /// standard-SQL aliases mapped onto them. Case-insensitive;
    /// internal whitespace is collapsed so `double precision` resolves
    /// regardless of spacing. Returns `None` for an unrecognised name —
    /// the caller turns that into the friendly "unknown type"
    /// diagnostic.
    ///
    /// Deliberately distinct from [`FromStr`](std::str::FromStr), which
    /// is the *simple-mode* parser and accepts only the nine keywords
    /// (no aliases), so simple mode teaches the playground's own
    /// vocabulary. A length/precision argument (`varchar(255)`) is
    /// stripped by the grammar before the name reaches this resolver.
    #[must_use]
    pub fn from_sql_name(name: &str) -> Option<Self> {
        // Collapse internal whitespace (for the two-word
        // `double precision`) and lowercase for case-insensitive match.
        let normalised = name
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ")
            .to_ascii_lowercase();
        match normalised.as_str() {
            "integer" | "smallint" | "bigint" => Some(Self::Int),
            "varchar" | "char" => Some(Self::Text),
            "boolean" => Some(Self::Bool),
            "timestamp" => Some(Self::DateTime),
            "numeric" => Some(Self::Decimal),
            "float" | "double precision" => Some(Self::Real),
            // Fall through to the canonical nine keywords.
            other => other.parse::<Self>().ok(),
        }
    }
}

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

/// Error returned when parsing a type keyword that isn't recognised.
///
/// Display formatting flows through the i18n catalog
/// (`parse.custom.unknown_type`); call sites that do
/// `err.to_string()` get the localised wording for free.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownType {
    pub found: String,
    pub expected: String,
}

impl fmt::Display for UnknownType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&crate::t!(
            "parse.custom.unknown_type",
            found = self.found,
            expected = self.expected,
        ))
    }
}

impl std::error::Error for UnknownType {}

impl FromStr for Type {
    type Err = UnknownType;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        for &ty in Self::all() {
            if ty.keyword().eq_ignore_ascii_case(s) {
                return Ok(ty);
            }
        }
        Err(UnknownType {
            found: s.to_string(),
            expected: Self::all()
                .iter()
                .map(|t| t.keyword())
                .collect::<Vec<_>>()
                .join(", "),
        })
    }
}

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

    #[test]
    fn keyword_round_trip_for_every_type() {
        for &ty in Type::all() {
            let parsed: Type = ty.keyword().parse().expect("round-trip");
            assert_eq!(parsed, ty);
        }
    }

    #[test]
    fn parsing_is_case_insensitive() {
        assert_eq!("TEXT".parse::<Type>().unwrap(), Type::Text);
        assert_eq!("Int".parse::<Type>().unwrap(), Type::Int);
        assert_eq!("ShortId".parse::<Type>().unwrap(), Type::ShortId);
    }

    #[test]
    fn unknown_type_lists_expected_alternatives() {
        let err = "varchar".parse::<Type>().unwrap_err();
        assert_eq!(err.found, "varchar");
        assert!(err.expected.contains("text"));
        assert!(err.expected.contains("shortid"));
    }

    #[test]
    fn serial_maps_to_integer_with_primary_key() {
        assert_eq!(Type::Serial.sqlite_strict_type(), "INTEGER");
        assert_eq!(Type::Serial.sqlite_strict_extra(), " PRIMARY KEY");
    }

    #[test]
    fn shortid_maps_to_text() {
        assert_eq!(Type::ShortId.sqlite_strict_type(), "TEXT");
        assert_eq!(Type::ShortId.sqlite_strict_extra(), "");
    }

    #[test]
    fn fk_target_type_strips_auto_gen_semantics() {
        // The two non-identity mappings.
        assert_eq!(Type::Serial.fk_target_type(), Type::Int);
        assert_eq!(Type::ShortId.fk_target_type(), Type::Text);
    }

    #[test]
    fn fk_target_type_is_identity_for_plain_value_types() {
        for ty in [
            Type::Text,
            Type::Int,
            Type::Real,
            Type::Decimal,
            Type::Bool,
            Type::Date,
            Type::DateTime,
        ] {
            assert_eq!(ty.fk_target_type(), ty);
        }
    }

    #[test]
    fn all_nine_types_are_present_and_distinct() {
        let kws: Vec<&'static str> = Type::all().iter().map(|t| t.keyword()).collect();
        assert_eq!(kws.len(), 9);
        let mut sorted = kws.clone();
        sorted.sort_unstable();
        sorted.dedup();
        assert_eq!(sorted.len(), 9, "keywords must be unique");
    }

    #[test]
    fn temporal_and_decimal_types_map_to_text_storage() {
        assert_eq!(Type::Decimal.sqlite_strict_type(), "TEXT");
        assert_eq!(Type::Date.sqlite_strict_type(), "TEXT");
        assert_eq!(Type::DateTime.sqlite_strict_type(), "TEXT");
    }

    #[test]
    fn unknown_type_message_lists_all_nine() {
        let err = "varchar".parse::<Type>().unwrap_err();
        for kw in [
            "text", "int", "real", "decimal", "bool", "date", "datetime", "serial", "shortid",
        ] {
            assert!(
                err.expected.contains(kw),
                "expected list should contain `{kw}`: {}",
                err.expected
            );
        }
    }

    // --- ADR-0035 §3: advanced-mode SQL type-name resolution ---
    // `from_sql_name` accepts the nine playground keywords *and* the
    // standard-SQL aliases. Simple-mode `FromStr` is unchanged (it
    // still rejects aliases — see `unknown_type_lists_expected_alternatives`).

    #[test]
    fn sql_resolver_accepts_the_nine_canonical_keywords() {
        for &ty in Type::all() {
            assert_eq!(Type::from_sql_name(ty.keyword()), Some(ty));
        }
    }

    #[test]
    fn sql_resolver_maps_standard_sql_aliases() {
        for (alias, expected) in [
            ("integer", Type::Int),
            ("smallint", Type::Int),
            ("bigint", Type::Int),
            ("varchar", Type::Text),
            ("char", Type::Text),
            ("boolean", Type::Bool),
            ("timestamp", Type::DateTime),
            ("numeric", Type::Decimal),
            ("float", Type::Real),
            ("double precision", Type::Real),
        ] {
            assert_eq!(
                Type::from_sql_name(alias),
                Some(expected),
                "alias `{alias}` should map to {expected:?}"
            );
        }
    }

    #[test]
    fn sql_resolver_is_case_insensitive() {
        assert_eq!(Type::from_sql_name("INTEGER"), Some(Type::Int));
        assert_eq!(Type::from_sql_name("VarChar"), Some(Type::Text));
        assert_eq!(Type::from_sql_name("Double Precision"), Some(Type::Real));
        assert_eq!(Type::from_sql_name("TEXT"), Some(Type::Text));
    }

    #[test]
    fn sql_resolver_rejects_genuinely_unknown_names() {
        assert_eq!(Type::from_sql_name("money"), None);
        assert_eq!(Type::from_sql_name("json"), None);
        assert_eq!(Type::from_sql_name(""), None);
    }

    #[test]
    fn sql_resolver_does_not_accept_serial_aliases() {
        // `serial`/`shortid` are playground-only; there is no standard
        // alias for them, and INTEGER PRIMARY KEY does NOT become serial
        // (ADR-0035 §3 — that is enforced at the PK layer, not here).
        assert_eq!(Type::from_sql_name("serial"), Some(Type::Serial));
        assert_eq!(Type::from_sql_name("autoincrement"), None);
    }
}
