use anyhow::{Context, Result, bail};
use chrono::{DateTime, Duration, Local, TimeZone};

use crate::model::Item;

#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
    True,
    Predicate(Predicate),
    Not(Box<Expr>),
    And(Box<Expr>, Box<Expr>),
    Or(Box<Expr>, Box<Expr>),
}

#[derive(Debug, Clone, PartialEq)]
pub enum Predicate {
    TextContains(String),
    NoteContains(String),
    Category(String),
    Priority(Compare, i64),
    Value(Compare, f64),
    NumericValue(String, Compare, f64),
    NumericRange(String, f64, f64, bool),
    DateValue(String, Compare, String),
    DateRange(String, String, String, bool),
    Done(bool),
    HasNote,
    HasDate(bool),
    DueWithin(i64),
    Overdue,
    Alarm,
    Recurring(bool),
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Compare {
    Eq,
    Ne,
    Lt,
    Le,
    Gt,
    Ge,
}

pub fn parse(source: &str) -> Result<Expr> {
    if source.trim().is_empty() {
        return Ok(Expr::True);
    }
    let tokens = tokenize(source)?;
    let mut parser = Parser { tokens, at: 0 };
    let expr = parser.parse_or()?;
    if parser.at != parser.tokens.len() {
        bail!("unexpected token {:?}", parser.tokens[parser.at]);
    }
    Ok(expr)
}

pub fn matches(source: &str, item: &Item) -> Result<bool> {
    Ok(evaluate(&parse(source)?, item))
}

pub fn evaluate(expr: &Expr, item: &Item) -> bool {
    match expr {
        Expr::True => true,
        Expr::Predicate(p) => predicate(p, item),
        Expr::Not(inner) => !evaluate(inner, item),
        Expr::And(a, b) => evaluate(a, item) && evaluate(b, item),
        Expr::Or(a, b) => evaluate(a, item) || evaluate(b, item),
    }
}

fn predicate(p: &Predicate, item: &Item) -> bool {
    match p {
        Predicate::TextContains(s) => item.text.to_lowercase().contains(&s.to_lowercase()),
        Predicate::NoteContains(s) => item.note.to_lowercase().contains(&s.to_lowercase()),
        Predicate::Category(s) => item
            .categories
            .iter()
            .any(|c| c.name.eq_ignore_ascii_case(s)),
        Predicate::Priority(op, n) => compare_i64(item.priority, *op, *n),
        Predicate::Value(op, n) => item.numeric_value.is_some_and(|v| compare_f64(v, *op, *n)),
        Predicate::NumericValue(category, op, number) => item
            .numeric_value_for(category)
            .is_some_and(|value| compare_f64(value, *op, *number)),
        Predicate::NumericRange(category, minimum, maximum, inside) => item
            .numeric_value_for(category)
            .is_some_and(|value| ((value >= *minimum) && (value <= *maximum)) == *inside),
        Predicate::DateValue(category, op, expected) => item
            .date_value_for(category)
            .is_some_and(|value| compare_str(value, *op, expected)),
        Predicate::DateRange(category, minimum, maximum, inside) => {
            item.date_value_for(category).is_some_and(|value| {
                ((value >= minimum.as_str()) && (value <= maximum.as_str())) == *inside
            })
        }
        Predicate::Done(expected) => item.done_at.is_some() == *expected,
        Predicate::HasNote => !item.note.trim().is_empty(),
        Predicate::HasDate(expected) => item.when_at.is_some() == *expected,
        Predicate::DueWithin(days) => item
            .when_at
            .as_deref()
            .and_then(parse_date)
            .is_some_and(|d| d >= Local::now() && d <= Local::now() + Duration::days(*days)),
        Predicate::Overdue => {
            item.done_at.is_none()
                && item
                    .when_at
                    .as_deref()
                    .and_then(parse_date)
                    .is_some_and(|d| d < Local::now())
        }
        Predicate::Alarm => item.alarm_at.is_some(),
        Predicate::Recurring(expected) => !item.recurrence.is_empty() == *expected,
    }
}

fn parse_date(value: &str) -> Option<DateTime<Local>> {
    DateTime::parse_from_rfc3339(value)
        .ok()
        .and_then(|date_time| {
            Local
                .from_local_datetime(&date_time.naive_local())
                .earliest()
        })
}
fn compare_i64(a: i64, op: Compare, b: i64) -> bool {
    match op {
        Compare::Eq => a == b,
        Compare::Ne => a != b,
        Compare::Lt => a < b,
        Compare::Le => a <= b,
        Compare::Gt => a > b,
        Compare::Ge => a >= b,
    }
}
fn compare_f64(a: f64, op: Compare, b: f64) -> bool {
    match op {
        Compare::Eq => (a - b).abs() < f64::EPSILON,
        Compare::Ne => (a - b).abs() >= f64::EPSILON,
        Compare::Lt => a < b,
        Compare::Le => a <= b,
        Compare::Gt => a > b,
        Compare::Ge => a >= b,
    }
}

fn compare_str(a: &str, op: Compare, b: &str) -> bool {
    match op {
        Compare::Eq => a == b,
        Compare::Ne => a != b,
        Compare::Lt => a < b,
        Compare::Le => a <= b,
        Compare::Gt => a > b,
        Compare::Ge => a >= b,
    }
}

#[derive(Debug, Clone, PartialEq)]
enum Token {
    Word(String),
    String(String),
    And,
    Or,
    Not,
    LParen,
    RParen,
    Op(Compare),
}

fn tokenize(source: &str) -> Result<Vec<Token>> {
    let chars: Vec<char> = source.chars().collect();
    let mut out = vec![];
    let mut i = 0;
    while i < chars.len() {
        if chars[i].is_whitespace() {
            i += 1;
            continue;
        }
        match chars[i] {
            '(' => {
                out.push(Token::LParen);
                i += 1
            }
            ')' => {
                out.push(Token::RParen);
                i += 1
            }
            '"' | '\'' => {
                let quote = chars[i];
                i += 1;
                let mut s = String::new();
                while i < chars.len() && chars[i] != quote {
                    if chars[i] == '\\' && i + 1 < chars.len() {
                        i += 1;
                    }
                    s.push(chars[i]);
                    i += 1;
                }
                if i == chars.len() {
                    bail!("unterminated quoted string")
                }
                i += 1;
                out.push(Token::String(s));
            }
            '&' if chars.get(i + 1) == Some(&'&') => {
                out.push(Token::And);
                i += 2
            }
            '|' if chars.get(i + 1) == Some(&'|') => {
                out.push(Token::Or);
                i += 2
            }
            '!' if chars.get(i + 1) == Some(&'=') => {
                out.push(Token::Op(Compare::Ne));
                i += 2
            }
            '!' => {
                out.push(Token::Not);
                i += 1
            }
            '<' if chars.get(i + 1) == Some(&'=') => {
                out.push(Token::Op(Compare::Le));
                i += 2
            }
            '>' if chars.get(i + 1) == Some(&'=') => {
                out.push(Token::Op(Compare::Ge));
                i += 2
            }
            '<' => {
                out.push(Token::Op(Compare::Lt));
                i += 1
            }
            '>' => {
                out.push(Token::Op(Compare::Gt));
                i += 1
            }
            '=' => {
                out.push(Token::Op(Compare::Eq));
                i += 1
            }
            '~' => {
                out.push(Token::Op(Compare::Eq));
                i += 1
            }
            _ => {
                let start = i;
                while i < chars.len()
                    && !chars[i].is_whitespace()
                    && !"()=!<>~&|".contains(chars[i])
                {
                    i += 1;
                }
                let word: String = chars[start..i].iter().collect();
                match word.to_lowercase().as_str() {
                    "and" => out.push(Token::And),
                    "or" => out.push(Token::Or),
                    "not" => out.push(Token::Not),
                    _ => out.push(Token::Word(word)),
                }
            }
        }
    }
    Ok(out)
}

struct Parser {
    tokens: Vec<Token>,
    at: usize,
}
impl Parser {
    fn parse_or(&mut self) -> Result<Expr> {
        let mut left = self.parse_and()?;
        while self.take(&Token::Or) {
            left = Expr::Or(Box::new(left), Box::new(self.parse_and()?));
        }
        Ok(left)
    }
    fn parse_and(&mut self) -> Result<Expr> {
        let mut left = self.parse_not()?;
        while self.take(&Token::And) {
            left = Expr::And(Box::new(left), Box::new(self.parse_not()?));
        }
        Ok(left)
    }
    fn parse_not(&mut self) -> Result<Expr> {
        if self.take(&Token::Not) {
            Ok(Expr::Not(Box::new(self.parse_not()?)))
        } else {
            self.parse_primary()
        }
    }
    fn parse_primary(&mut self) -> Result<Expr> {
        if self.take(&Token::LParen) {
            let e = self.parse_or()?;
            if !self.take(&Token::RParen) {
                bail!("missing closing parenthesis")
            }
            return Ok(e);
        }
        self.parse_predicate().map(Expr::Predicate)
    }
    fn parse_predicate(&mut self) -> Result<Predicate> {
        let field = self.word()?.to_lowercase();
        match field.as_str() {
            "done" => Ok(Predicate::Done(true)),
            "open" => Ok(Predicate::Done(false)),
            "has-note" => Ok(Predicate::HasNote),
            "dated" => Ok(Predicate::HasDate(true)),
            "undated" => Ok(Predicate::HasDate(false)),
            "overdue" => Ok(Predicate::Overdue),
            "alarm" => Ok(Predicate::Alarm),
            "recurring" => Ok(Predicate::Recurring(true)),
            "nonrecurring" => Ok(Predicate::Recurring(false)),
            "text" | "note" | "category" => {
                if self.optional_op() == Some(Compare::Ne) {
                    bail!("use 'not {field}=value' for a negative predicate")
                }
                let value = self.word()?;
                Ok(match field.as_str() {
                    "text" => Predicate::TextContains(value),
                    "note" => Predicate::NoteContains(value),
                    _ => Predicate::Category(value),
                })
            }
            "priority" => {
                let op = self.required_op()?;
                Ok(Predicate::Priority(op, self.word()?.parse()?))
            }
            "value" => {
                let op = self.required_op()?;
                Ok(Predicate::Value(op, self.word()?.parse()?))
            }
            field if field.starts_with("numeric:") => {
                let category = if field.len() == "numeric:".len() {
                    self.word()?
                } else {
                    field["numeric:".len()..].to_owned()
                };
                if let Some(inside) = self.range_operator() {
                    let (minimum, maximum) = parse_range::<f64>(&self.word()?)?;
                    Ok(Predicate::NumericRange(category, minimum, maximum, inside))
                } else {
                    let op = self.required_op()?;
                    Ok(Predicate::NumericValue(category, op, self.word()?.parse()?))
                }
            }
            field if field.starts_with("date:") => {
                let category = if field.len() == "date:".len() {
                    self.word()?
                } else {
                    field["date:".len()..].to_owned()
                };
                if let Some(inside) = self.range_operator() {
                    let (minimum, maximum) = parse_range::<String>(&self.word()?)?;
                    Ok(Predicate::DateRange(category, minimum, maximum, inside))
                } else {
                    let op = self.required_op()?;
                    Ok(Predicate::DateValue(category, op, self.word()?))
                }
            }
            "due" => {
                let _ = self.optional_op();
                let raw = self.word()?.to_lowercase();
                let days = raw.strip_suffix('d').unwrap_or(&raw).parse()?;
                Ok(Predicate::DueWithin(days))
            }
            _ => bail!("unknown filter predicate {field}"),
        }
    }
    fn word(&mut self) -> Result<String> {
        let token = self.tokens.get(self.at).cloned();
        match token {
            Some(Token::Word(s)) | Some(Token::String(s)) => {
                self.at += 1;
                Ok(s)
            }
            _ => bail!("expected a value"),
        }
    }
    fn required_op(&mut self) -> Result<Compare> {
        match self.tokens.get(self.at).cloned() {
            Some(Token::Op(op)) => {
                self.at += 1;
                Ok(op)
            }
            _ => bail!("expected a comparison operator"),
        }
    }

    fn range_operator(&mut self) -> Option<bool> {
        let Some(Token::Word(operator)) = self.tokens.get(self.at) else {
            return None;
        };
        let inside = match operator.to_lowercase().as_str() {
            "inside" => true,
            "outside" => false,
            _ => return None,
        };
        self.at += 1;
        Some(inside)
    }
    fn optional_op(&mut self) -> Option<Compare> {
        if let Some(Token::Op(op)) = self.tokens.get(self.at).cloned() {
            self.at += 1;
            Some(op)
        } else {
            None
        }
    }
    fn take(&mut self, expected: &Token) -> bool {
        if self.tokens.get(self.at) == Some(expected) {
            self.at += 1;
            true
        } else {
            false
        }
    }
}

fn parse_range<T>(source: &str) -> Result<(T, T)>
where
    T: std::str::FromStr,
    T::Err: std::error::Error + Send + Sync + 'static,
{
    let (minimum, maximum) = source
        .split_once("..")
        .context("ranges must use minimum..maximum")?;
    Ok((minimum.parse()?, maximum.parse()?))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{Category, CategoryKind};
    fn item() -> Item {
        Item {
            id: 1,
            text: "Call Ada about Rust".into(),
            note: "budget is 42".into(),
            priority: 2,
            when_at: None,
            done_at: None,
            alarm_at: None,
            numeric_value: Some(42.0),
            recurrence: String::new(),
            created_at: String::new(),
            updated_at: String::new(),
            discarded: false,
            values: vec![],
            categories: vec![Category {
                id: 1,
                name: "Work".into(),
                parent_id: None,
                kind: CategoryKind::Standard,
                match_text: String::new(),
                exclusive: false,
            }],
        }
    }
    #[test]
    fn evaluates_boolean_expressions() {
        let i = item();
        assert!(matches("category=Work and (priority<=2 or done)", &i).unwrap());
        assert!(!matches("done or text='missing'", &i).unwrap());
    }
    #[test]
    fn supports_text_and_numeric_predicates() {
        let i = item();
        assert!(matches("text~rust and note~budget and value>=40", &i).unwrap());
    }
    #[test]
    fn rejects_invalid_expressions() {
        assert!(parse("priority nope 3").is_err());
        assert!(parse("(done or open").is_err());
    }

    #[test]
    fn dates_are_interpreted_as_local_wall_time() {
        let date = parse_date("2026-08-14T09:00:00+00:00").unwrap();
        assert_eq!(
            date.format("%Y-%m-%d %H:%M").to_string(),
            "2026-08-14 09:00"
        );
    }
}
