//! User-facing value literals for INSERT / UPDATE / DELETE.
//!
//! The parser produces a small `Value` enum carrying just the
//! shape of the literal as written. Per-column-type validation
//! happens at execute time, where the schema is known and
//! errors can name the offending column.

use std::fmt;

use crate::dsl::shortid;
use crate::dsl::types::Type;

/// A literal value as parsed from DSL input.
///
/// `Number` carries the original string so a literal like
/// `3.14` can be stored as a decimal (TEXT) or a real (f64)
/// depending on the destination column. The conversion happens
/// in `bind_for_column`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
    Number(String),
    Text(String),
    Bool(bool),
    Null,
}

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Number(n) => f.write_str(n),
            Self::Text(s) => write!(f, "'{}'", s.replace('\'', "''")),
            Self::Bool(b) => f.write_str(if *b { "true" } else { "false" }),
            Self::Null => f.write_str("null"),
        }
    }
}

/// Validated value ready to be bound as a parameter to a SQLite
/// statement. Mirrors the storage choices made in ADR-0005.
#[derive(Debug, Clone, PartialEq)]
pub enum Bound {
    Integer(i64),
    Real(f64),
    Text(String),
    Null,
}

/// Per-column value validation error.
///
/// Display flows through the i18n catalog (`value.*`); callers
/// that format this error get the localised wording for free.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValueError {
    TypeMismatch {
        column: String,
        expected_human: String,
        got: String,
    },
    Format {
        column: String,
        message: String,
    },
}

impl std::fmt::Display for ValueError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::TypeMismatch {
                column,
                expected_human,
                got,
            } => f.write_str(&crate::t!(
                "value.type_mismatch",
                column = column,
                expected_human = expected_human,
                got = got,
            )),
            Self::Format { column, message } => f.write_str(&crate::t!(
                "value.format",
                column = column,
                message = message,
            )),
        }
    }
}

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

impl Value {
    /// Validate `self` against `column`'s user-facing type and
    /// produce a value ready for binding.
    pub fn bind_for_column(&self, column: &str, ty: Type) -> Result<Bound, ValueError> {
        if matches!(self, Self::Null) {
            return Ok(Bound::Null);
        }
        match ty {
            Type::Text | Type::ShortId => self.bind_text(column, ty),
            Type::Int | Type::Serial => self.bind_int(column, ty),
            Type::Real => self.bind_real(column),
            Type::Decimal => self.bind_decimal(column),
            Type::Bool => self.bind_bool(column),
            Type::Date => self.bind_date(column),
            Type::DateTime => self.bind_datetime(column),
        }
    }

    fn bind_text(&self, column: &str, ty: Type) -> Result<Bound, ValueError> {
        match self {
            Self::Text(s) => {
                if ty == Type::ShortId {
                    shortid::validate(s).map_err(|message| ValueError::Format {
                        column: column.to_string(),
                        message,
                    })?;
                }
                Ok(Bound::Text(s.clone()))
            }
            other => Err(ValueError::TypeMismatch {
                column: column.to_string(),
                expected_human: format!("a quoted string for `{ty}`"),
                got: other.kind_name().to_string(),
            }),
        }
    }

    fn bind_int(&self, column: &str, ty: Type) -> Result<Bound, ValueError> {
        match self {
            Self::Number(n) => {
                n.parse::<i64>()
                    .map(Bound::Integer)
                    .map_err(|_| ValueError::Format {
                        column: column.to_string(),
                        message: format!("`{n}` is not a valid {ty} (whole number expected)"),
                    })
            }
            other => Err(ValueError::TypeMismatch {
                column: column.to_string(),
                expected_human: format!("a whole number for `{ty}`"),
                got: other.kind_name().to_string(),
            }),
        }
    }

    fn bind_real(&self, column: &str) -> Result<Bound, ValueError> {
        match self {
            Self::Number(n) => n
                .parse::<f64>()
                .map(Bound::Real)
                .map_err(|_| ValueError::Format {
                    column: column.to_string(),
                    message: format!("`{n}` is not a valid real number"),
                }),
            other => Err(ValueError::TypeMismatch {
                column: column.to_string(),
                expected_human: "a real number".to_string(),
                got: other.kind_name().to_string(),
            }),
        }
    }

    fn bind_decimal(&self, column: &str) -> Result<Bound, ValueError> {
        match self {
            Self::Number(n) => {
                // Validate parse-ability so a typo like `3..14` is rejected;
                // we still store the original string to preserve precision.
                if n.parse::<f64>().is_err() {
                    return Err(ValueError::Format {
                        column: column.to_string(),
                        message: format!("`{n}` is not a valid decimal number"),
                    });
                }
                Ok(Bound::Text(n.clone()))
            }
            other => Err(ValueError::TypeMismatch {
                column: column.to_string(),
                expected_human: "a decimal number".to_string(),
                got: other.kind_name().to_string(),
            }),
        }
    }

    fn bind_bool(&self, column: &str) -> Result<Bound, ValueError> {
        match self {
            Self::Bool(b) => Ok(Bound::Integer(i64::from(*b))),
            other => Err(ValueError::TypeMismatch {
                column: column.to_string(),
                expected_human: "`true` or `false`".to_string(),
                got: other.kind_name().to_string(),
            }),
        }
    }

    fn bind_date(&self, column: &str) -> Result<Bound, ValueError> {
        match self {
            Self::Text(s) => {
                validate_date(s).map_err(|message| ValueError::Format {
                    column: column.to_string(),
                    message,
                })?;
                Ok(Bound::Text(s.clone()))
            }
            other => Err(ValueError::TypeMismatch {
                column: column.to_string(),
                expected_human: "a quoted date 'YYYY-MM-DD'".to_string(),
                got: other.kind_name().to_string(),
            }),
        }
    }

    fn bind_datetime(&self, column: &str) -> Result<Bound, ValueError> {
        match self {
            Self::Text(s) => {
                validate_datetime(s).map_err(|message| ValueError::Format {
                    column: column.to_string(),
                    message,
                })?;
                Ok(Bound::Text(s.clone()))
            }
            other => Err(ValueError::TypeMismatch {
                column: column.to_string(),
                expected_human: "a quoted datetime 'YYYY-MM-DDTHH:MM:SS'".to_string(),
                got: other.kind_name().to_string(),
            }),
        }
    }

    const fn kind_name(&self) -> &'static str {
        match self {
            Self::Number(_) => "number",
            Self::Text(_) => "string",
            Self::Bool(_) => "boolean",
            Self::Null => "null",
        }
    }
}

pub(crate) fn validate_date(s: &str) -> Result<(), String> {
    // Expect YYYY-MM-DD: 10 chars, two dashes at fixed positions.
    let bytes = s.as_bytes();
    if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
        return Err(format!("`{s}` is not a date in `YYYY-MM-DD` form"));
    }
    let year = parse_digits(&s[0..4]).ok_or_else(|| format!("`{s}`: invalid year"))?;
    let month = parse_digits(&s[5..7]).ok_or_else(|| format!("`{s}`: invalid month"))?;
    let day = parse_digits(&s[8..10]).ok_or_else(|| format!("`{s}`: invalid day"))?;
    if !(1..=9999).contains(&year) {
        return Err(format!("`{s}`: year {year} out of range 1..=9999"));
    }
    if !(1..=12).contains(&month) {
        return Err(format!("`{s}`: month {month} out of range 1..=12"));
    }
    if !(1..=31).contains(&day) {
        return Err(format!("`{s}`: day {day} out of range 1..=31"));
    }
    Ok(())
}

pub(crate) fn validate_datetime(s: &str) -> Result<(), String> {
    // Minimum: YYYY-MM-DDTHH:MM:SS = 19 chars. Allow optional
    // fractional seconds (.fff) and optional Z or ±HH:MM offset.
    if s.len() < 19 {
        return Err(format!(
            "`{s}` is too short for a datetime in `YYYY-MM-DDTHH:MM:SS` form"
        ));
    }
    let date_part = &s[0..10];
    validate_date(date_part)?;
    let bytes = s.as_bytes();
    if bytes[10] != b'T' {
        return Err(format!(
            "`{s}`: missing `T` separator between date and time"
        ));
    }
    if bytes[13] != b':' || bytes[16] != b':' {
        return Err(format!("`{s}`: time portion must be `HH:MM:SS`"));
    }
    let hour = parse_digits(&s[11..13]).ok_or_else(|| format!("`{s}`: invalid hour"))?;
    let min = parse_digits(&s[14..16]).ok_or_else(|| format!("`{s}`: invalid minute"))?;
    let sec = parse_digits(&s[17..19]).ok_or_else(|| format!("`{s}`: invalid second"))?;
    if hour > 23 {
        return Err(format!("`{s}`: hour {hour} out of range 0..=23"));
    }
    if min > 59 {
        return Err(format!("`{s}`: minute {min} out of range 0..=59"));
    }
    if sec > 60 {
        return Err(format!(
            "`{s}`: second {sec} out of range 0..=60 (60 allowed for leap second)"
        ));
    }
    // Anything after position 19 is optional fractional / timezone
    // suffix; we don't strictly validate it here (a future iteration
    // can tighten this if needed).
    Ok(())
}

fn parse_digits(s: &str) -> Option<u32> {
    if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }
    s.parse::<u32>().ok()
}

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

    fn n(s: &str) -> Value {
        Value::Number(s.to_string())
    }
    fn t(s: &str) -> Value {
        Value::Text(s.to_string())
    }

    #[test]
    fn null_binds_to_null_for_any_type() {
        for ty in Type::all() {
            assert_eq!(Value::Null.bind_for_column("c", *ty).unwrap(), Bound::Null);
        }
    }

    #[test]
    fn integer_for_int_column() {
        assert_eq!(
            n("42").bind_for_column("c", Type::Int).unwrap(),
            Bound::Integer(42)
        );
        assert_eq!(
            n("-7").bind_for_column("c", Type::Int).unwrap(),
            Bound::Integer(-7)
        );
    }

    #[test]
    fn non_integer_for_int_column_is_format_error() {
        let err = n("3.14").bind_for_column("c", Type::Int).unwrap_err();
        match err {
            ValueError::Format { message, .. } => assert!(message.contains("whole number")),
            other => panic!("unexpected: {other:?}"),
        }
    }

    #[test]
    fn string_for_int_column_is_type_mismatch() {
        let err = t("hello").bind_for_column("c", Type::Int).unwrap_err();
        assert!(matches!(err, ValueError::TypeMismatch { .. }));
    }

    #[test]
    fn text_for_text_column() {
        assert_eq!(
            t("hi").bind_for_column("c", Type::Text).unwrap(),
            Bound::Text("hi".to_string())
        );
    }

    #[test]
    fn shortid_validation_runs_on_text_for_shortid_column() {
        let err = t("toolong_xyz_more")
            .bind_for_column("c", Type::ShortId)
            .unwrap_err();
        assert!(matches!(err, ValueError::Format { .. }));

        // Well-formed shortid binds fine.
        assert_eq!(
            t("23456789Ab").bind_for_column("c", Type::ShortId).unwrap(),
            Bound::Text("23456789Ab".to_string())
        );
    }

    #[test]
    fn bool_for_bool_column_maps_to_zero_or_one() {
        assert_eq!(
            Value::Bool(true).bind_for_column("c", Type::Bool).unwrap(),
            Bound::Integer(1)
        );
        assert_eq!(
            Value::Bool(false).bind_for_column("c", Type::Bool).unwrap(),
            Bound::Integer(0)
        );
    }

    #[test]
    fn date_iso_only() {
        assert_eq!(
            t("2025-01-15").bind_for_column("c", Type::Date).unwrap(),
            Bound::Text("2025-01-15".to_string())
        );
        let err = t("2025/01/15")
            .bind_for_column("c", Type::Date)
            .unwrap_err();
        assert!(matches!(err, ValueError::Format { .. }));
    }

    #[test]
    fn date_range_check() {
        let err = t("2025-13-01")
            .bind_for_column("c", Type::Date)
            .unwrap_err();
        assert!(matches!(err, ValueError::Format { message, .. } if message.contains("month")));
    }

    #[test]
    fn datetime_iso_only() {
        assert_eq!(
            t("2025-01-15T14:30:00")
                .bind_for_column("c", Type::DateTime)
                .unwrap(),
            Bound::Text("2025-01-15T14:30:00".to_string())
        );
        let err = t("2025-01-15 14:30:00")
            .bind_for_column("c", Type::DateTime)
            .unwrap_err();
        assert!(matches!(err, ValueError::Format { .. }));
    }

    #[test]
    fn decimal_validates_numeric_string() {
        assert_eq!(
            n("3.14").bind_for_column("c", Type::Decimal).unwrap(),
            Bound::Text("3.14".to_string())
        );
        let err = n("3..14").bind_for_column("c", Type::Decimal).unwrap_err();
        assert!(matches!(err, ValueError::Format { .. }));
    }
}
