//! Mini query language parser for structured post queries.
//!
//! Grammar:
//!
//! ```text
//! query     := condition ( " AND " condition )*
//! condition := field operator value
//! field     := "text" | "is_reply" | "author_did" | "created_at"
//!            | "reply_parent_uri" | "reply_root_uri" | "embed_type" | "lang"
//! operator  := "~" | "=" | "!=" | ">=" | "<=" | ">" | "<"
//! value     := quoted_string | bare_word
//! ```
//!
//! The `~` operator is only valid on `text` and maps to FTS5 `MATCH`.

use std::fmt;

/// A parsed query consisting of one or more conditions joined by AND.
#[derive(Debug, PartialEq, Eq)]
pub struct Query {
    /// The individual filter conditions.
    pub conditions: Vec<Condition>,
}

/// A single filter condition: `field op value`.
#[derive(Debug, PartialEq, Eq)]
pub struct Condition {
    /// The column to filter on.
    pub field: Field,
    /// The comparison operator.
    pub op: Operator,
    /// The literal value to compare against.
    pub value: String,
}

/// Queryable fields corresponding to `posts` table columns.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Field {
    /// Full-text search on `text` (FTS5).
    Text,
    /// Boolean: whether the post is a reply (`0` or `1`, `true` or `false`).
    IsReply,
    /// The DID of the post author.
    AuthorDid,
    /// ISO 8601 creation timestamp.
    CreatedAt,
    /// AT URI of the parent post (replies only).
    ReplyParentUri,
    /// AT URI of the thread root post (replies only).
    ReplyRootUri,
    /// Embed type suffix (e.g. `images`, `external`, `record`, `video`).
    EmbedType,
    /// Primary language (BCP-47 tag).
    Lang,
}

impl Field {
    /// The SQL column name for this field.
    #[must_use]
    pub fn column_name(self) -> &'static str {
        match self {
            Self::Text => "text",
            Self::IsReply => "is_reply",
            Self::AuthorDid => "author_did",
            Self::CreatedAt => "created_at",
            Self::ReplyParentUri => "reply_parent_uri",
            Self::ReplyRootUri => "reply_root_uri",
            Self::EmbedType => "embed_type",
            Self::Lang => "lang",
        }
    }
}

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

/// Comparison operators supported by the query language.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operator {
    /// FTS5 `MATCH` (only valid for `text`).
    Match,
    /// Equality (`=`).
    Eq,
    /// Inequality (`!=`).
    Ne,
    /// Greater than (`>`).
    Gt,
    /// Less than (`<`).
    Lt,
    /// Greater than or equal (`>=`).
    Gte,
    /// Less than or equal (`<=`).
    Lte,
}

impl Operator {
    /// The SQL operator string.
    #[must_use]
    pub fn sql_op(self) -> &'static str {
        match self {
            Self::Match => "MATCH",
            Self::Eq => "=",
            Self::Ne => "!=",
            Self::Gt => ">",
            Self::Lt => "<",
            Self::Gte => ">=",
            Self::Lte => "<=",
        }
    }
}

/// Parse a query string into a structured [`Query`].
///
/// # Errors
///
/// Returns an error message if the input is empty, contains an unknown field,
/// uses an invalid operator, or uses `~` on a non-text field.
pub fn parse(input: &str) -> Result<Query, String> {
    let input = input.trim();
    if input.is_empty() {
        return Err("empty query".to_string());
    }

    let parts = split_on_and(input);
    let mut conditions = Vec::with_capacity(parts.len());

    for part in parts {
        conditions.push(parse_condition(part.trim())?);
    }

    Ok(Query { conditions })
}

/// Split the input on ` AND ` (case-sensitive), respecting quoted strings.
fn split_on_and(input: &str) -> Vec<&str> {
    let mut parts = Vec::new();
    let mut start = 0;
    let bytes = input.as_bytes();
    let pattern = b" AND ";
    let mut in_quote = false;
    let mut i = 0;

    while i < bytes.len() {
        if bytes[i] == b'"' || bytes[i] == b'\'' {
            in_quote = !in_quote;
            i += 1;
            continue;
        }
        if !in_quote && i + pattern.len() <= bytes.len() && &bytes[i..i + pattern.len()] == pattern
        {
            parts.push(&input[start..i]);
            start = i + pattern.len();
            i = start;
            continue;
        }
        i += 1;
    }
    parts.push(&input[start..]);
    parts
}

/// Parse a single `field op value` condition.
fn parse_condition(s: &str) -> Result<Condition, String> {
    // Try multi-char operators first, then single-char.
    let (field_str, op, value_str) = try_split_condition(s)?;

    let field = parse_field(field_str.trim())?;
    let value = parse_value(value_str.trim());

    // Validate: ~ only valid for text.
    if op == Operator::Match && field != Field::Text {
        return Err(format!(
            "operator ~ (MATCH) is only valid for 'text', not '{field}'"
        ));
    }

    // Normalize boolean values for is_reply.
    let value = if field == Field::IsReply {
        normalize_bool(&value, op)?
    } else {
        value
    };

    Ok(Condition { field, op, value })
}

/// Try to find the operator in a condition string, returning (field, op, value).
fn try_split_condition(s: &str) -> Result<(&str, Operator, &str), String> {
    // Order matters: try >= and <= before > and <, try != before =.
    for (pattern, op) in &[
        (">=", Operator::Gte),
        ("<=", Operator::Lte),
        ("!=", Operator::Ne),
        ("~", Operator::Match),
        (">", Operator::Gt),
        ("<", Operator::Lt),
        ("=", Operator::Eq),
    ] {
        if let Some(pos) = s.find(pattern) {
            let field = &s[..pos];
            let value = &s[pos + pattern.len()..];
            if field.trim().is_empty() || value.trim().is_empty() {
                return Err(format!("incomplete condition: '{s}'"));
            }
            return Ok((field, *op, value));
        }
    }
    Err(format!(
        "no operator found in condition: '{s}' (expected one of: ~ = != > < >= <=)"
    ))
}

/// Parse a field name.
fn parse_field(s: &str) -> Result<Field, String> {
    match s {
        "text" => Ok(Field::Text),
        "is_reply" => Ok(Field::IsReply),
        "author_did" => Ok(Field::AuthorDid),
        "created_at" => Ok(Field::CreatedAt),
        "reply_parent_uri" => Ok(Field::ReplyParentUri),
        "reply_root_uri" => Ok(Field::ReplyRootUri),
        "embed_type" => Ok(Field::EmbedType),
        "lang" => Ok(Field::Lang),
        _ => Err(format!(
            "unknown field: '{s}' (valid: text, is_reply, author_did, created_at, \
             reply_parent_uri, reply_root_uri, embed_type, lang)"
        )),
    }
}

/// Strip surrounding quotes from a value if present.
fn parse_value(s: &str) -> String {
    let s = s.trim();
    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
        s[1..s.len() - 1].to_string()
    } else {
        s.to_string()
    }
}

/// Normalize boolean-like values for `is_reply` to `0`/`1`.
fn normalize_bool(value: &str, op: Operator) -> Result<String, String> {
    match op {
        Operator::Eq | Operator::Ne => match value.to_lowercase().as_str() {
            "true" | "1" => Ok("1".to_string()),
            "false" | "0" => Ok("0".to_string()),
            _ => Err(format!("is_reply expects true/false or 0/1, got '{value}'")),
        },
        _ => Err(format!(
            "is_reply only supports = and != operators, not '{}'",
            op.sql_op()
        )),
    }
}

impl Query {
    /// Generate a SQL WHERE clause and parameter values from this query.
    ///
    /// Returns `(where_clause, params)` where `where_clause` is a string like
    /// `"WHERE is_reply = ?1 AND rowid IN (SELECT rowid FROM posts_fts WHERE posts_fts MATCH ?2)"`
    /// and `params` is the ordered list of bind values.
    #[must_use]
    pub fn to_where_clause(&self) -> (String, Vec<String>) {
        let mut fragments = Vec::with_capacity(self.conditions.len());
        let mut params = Vec::with_capacity(self.conditions.len());

        for cond in &self.conditions {
            let idx = params.len() + 1;
            if cond.op == Operator::Match {
                fragments.push(format!(
                    "rowid IN (SELECT rowid FROM posts_fts WHERE posts_fts MATCH ?{idx})"
                ));
            } else {
                fragments.push(format!(
                    "{} {} ?{idx}",
                    cond.field.column_name(),
                    cond.op.sql_op()
                ));
            }
            params.push(cond.value.clone());
        }

        let clause = format!("WHERE {}", fragments.join(" AND "));
        (clause, params)
    }
}

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

    #[test]
    fn test_parse_single_eq() {
        let q = parse("is_reply = false").unwrap();
        assert_eq!(q.conditions.len(), 1);
        assert_eq!(q.conditions[0].field, Field::IsReply);
        assert_eq!(q.conditions[0].op, Operator::Eq);
        assert_eq!(q.conditions[0].value, "0");
    }

    #[test]
    fn test_parse_fts_match() {
        let q = parse("text ~ rust").unwrap();
        assert_eq!(q.conditions.len(), 1);
        assert_eq!(q.conditions[0].field, Field::Text);
        assert_eq!(q.conditions[0].op, Operator::Match);
        assert_eq!(q.conditions[0].value, "rust");
    }

    #[test]
    fn test_parse_fts_match_phrase() {
        let q = parse("text ~ \"hello world\"").unwrap();
        assert_eq!(q.conditions[0].value, "hello world");
    }

    #[test]
    fn test_parse_multiple_and() {
        let q = parse("text ~ rust AND is_reply = false AND created_at > 2024-01-01").unwrap();
        assert_eq!(q.conditions.len(), 3);
        assert_eq!(q.conditions[0].field, Field::Text);
        assert_eq!(q.conditions[1].field, Field::IsReply);
        assert_eq!(q.conditions[2].field, Field::CreatedAt);
        assert_eq!(q.conditions[2].op, Operator::Gt);
    }

    #[test]
    fn test_parse_comparison_operators() {
        for (input, expected_op) in [
            ("created_at > 2024-01-01", Operator::Gt),
            ("created_at < 2024-12-31", Operator::Lt),
            ("created_at >= 2024-01-01", Operator::Gte),
            ("created_at <= 2024-12-31", Operator::Lte),
            ("lang != en", Operator::Ne),
        ] {
            let q = parse(input).unwrap();
            assert_eq!(q.conditions[0].op, expected_op, "failed on: {input}");
        }
    }

    #[test]
    fn test_parse_quoted_value() {
        let q = parse("author_did = \"did:plc:abc\"").unwrap();
        assert_eq!(q.conditions[0].value, "did:plc:abc");
    }

    #[test]
    fn test_parse_single_quoted_value() {
        let q = parse("lang = 'en'").unwrap();
        assert_eq!(q.conditions[0].value, "en");
    }

    #[test]
    fn test_parse_invalid_field_rejected() {
        let err = parse("foo = bar").unwrap_err();
        assert!(err.contains("unknown field"), "error was: {err}");
    }

    #[test]
    fn test_parse_match_only_valid_for_text() {
        let err = parse("lang ~ en").unwrap_err();
        assert!(err.contains("only valid for 'text'"), "error was: {err}");
    }

    #[test]
    fn test_parse_empty_query() {
        assert!(parse("").is_err());
        assert!(parse("  ").is_err());
    }

    #[test]
    fn test_parse_no_operator() {
        let err = parse("text hello").unwrap_err();
        assert!(err.contains("no operator found"), "error was: {err}");
    }

    #[test]
    fn test_parse_is_reply_rejects_non_bool() {
        let err = parse("is_reply = maybe").unwrap_err();
        assert!(err.contains("true/false"), "error was: {err}");
    }

    #[test]
    fn test_parse_is_reply_true_normalizes() {
        let q = parse("is_reply = true").unwrap();
        assert_eq!(q.conditions[0].value, "1");
    }

    #[test]
    fn test_to_where_clause_simple() {
        let q = parse("lang = en").unwrap();
        let (clause, params) = q.to_where_clause();
        assert_eq!(clause, "WHERE lang = ?1");
        assert_eq!(params, vec!["en"]);
    }

    #[test]
    fn test_to_where_clause_fts() {
        let q = parse("text ~ rust").unwrap();
        let (clause, params) = q.to_where_clause();
        assert_eq!(
            clause,
            "WHERE rowid IN (SELECT rowid FROM posts_fts WHERE posts_fts MATCH ?1)"
        );
        assert_eq!(params, vec!["rust"]);
    }

    #[test]
    fn test_to_where_clause_combined() {
        let q = parse("text ~ rust AND is_reply = false").unwrap();
        let (clause, params) = q.to_where_clause();
        assert!(clause.contains("MATCH ?1"));
        assert!(clause.contains("is_reply = ?2"));
        assert_eq!(params, vec!["rust", "0"]);
    }

    #[test]
    fn test_all_fields_parse() {
        for field in [
            "text",
            "is_reply",
            "author_did",
            "created_at",
            "reply_parent_uri",
            "reply_root_uri",
            "embed_type",
            "lang",
        ] {
            let input = if field == "text" {
                format!("{field} ~ test")
            } else if field == "is_reply" {
                format!("{field} = true")
            } else {
                format!("{field} = value")
            };
            assert!(parse(&input).is_ok(), "failed to parse field: {field}");
        }
    }
}
