/* search_query.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use chrono::{Months, NaiveDate, Utc};

const PREFIXES: &[&str] = &["file:", "author:", "since:", "until:"];

#[derive(Debug, Clone, Default)]
pub struct SearchQuery {
    pub files: Vec<String>,
    pub authors: Vec<String>,
    pub since: Option<i64>,
    pub until: Option<i64>,
    pub message_words: Vec<String>,
}

impl SearchQuery {
    pub fn is_empty(&self) -> bool {
        self.files.is_empty()
            && self.authors.is_empty()
            && self.since.is_none()
            && self.until.is_none()
            && self.message_words.is_empty()
    }

    pub fn parse(input: &str) -> Self {
        let mut query = SearchQuery::default();

        for (prefix, value) in tokenize(input) {
            match prefix {
                Some("file:") if !value.is_empty() => query.files.push(value),
                Some("author:") if !value.is_empty() => query.authors.push(value),

                Some("since:") => {
                    if let Some(ts) = parse_date(&value) {
                        query.since = Some(ts);
                    }
                }

                Some("until:") => {
                    if let Some(ts) = parse_date(&value) {
                        query.until = Some(ts);
                    }
                }

                _ if !value.is_empty() => query.message_words.push(value),
                _ => {}
            }
        }

        query
    }
}

fn tokenize(input: &str) -> Vec<(Option<&'static str>, String)> {
    let mut tokens = Vec::new();
    let mut rest = input.trim();

    while !rest.is_empty() {
        // Try to match a known prefix
        if let Some(&prefix) = PREFIXES.iter().find(|p| rest.starts_with(**p)) {
            rest = &rest[prefix.len()..];
            let value = consume_value(&mut rest);
            tokens.push((Some(prefix), value));
        } else if rest.starts_with('"') {
            rest = &rest[1..];
            let value = consume_quoted(&mut rest);
            tokens.push((None, value));
        } else {
            let value = consume_word(&mut rest);
            tokens.push((None, value));
        }

        rest = rest.trim_start();
    }

    tokens
}

fn consume_quoted(rest: &mut &str) -> String {
    if let Some(end) = rest.find('"') {
        let value = rest[..end].to_string();
        *rest = &rest[end + 1..];
        value
    } else {
        let value = rest.to_string();
        *rest = "";
        value
    }
}

fn consume_word(rest: &mut &str) -> String {
    let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
    let value = rest[..end].to_string();

    *rest = &rest[end..];

    value
}

fn consume_value(rest: &mut &str) -> String {
    if rest.starts_with('"') {
        *rest = &rest[1..];
        consume_quoted(rest)
    } else {
        consume_word(rest)
    }
}

fn parse_date(value: &str) -> Option<i64> {
    parse_absolute_date(value).or_else(|| parse_relative_date(value))
}

fn parse_absolute_date(value: &str) -> Option<i64> {
    let date = NaiveDate::parse_from_str(value, "%Y-%m-%d").ok()?;
    Some(date.and_hms_opt(0, 0, 0)?.and_utc().timestamp())
}

fn parse_relative_date(value: &str) -> Option<i64> {
    let (num_str, unit) = split_number_unit(value)?;
    let num: u32 = num_str.parse().ok()?;
    let now = Utc::now();

    let past = match unit {
        "d" | "day" | "days" => now - chrono::Duration::days(num as i64),
        "w" | "week" | "weeks" => now - chrono::Duration::weeks(num as i64),
        "m" | "month" | "months" => now.checked_sub_months(Months::new(num))?,
        "y" | "year" | "years" => now.checked_sub_months(Months::new(num * 12))?,
        _ => return None,
    };

    Some(past.timestamp())
}

fn split_number_unit(value: &str) -> Option<(&str, &str)> {
    let pos = value.find(|c: char| !c.is_ascii_digit())?;

    if pos == 0 {
        return None;
    }

    Some((&value[..pos], &value[pos..]))
}

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

    #[test]
    fn test_empty_query() {
        let q = SearchQuery::parse("");

        assert!(q.is_empty());
    }

    #[test]
    fn test_message_only() {
        let q = SearchQuery::parse("fix crash");

        assert_eq!(q.message_words, vec!["fix", "crash"]);
        assert!(q.files.is_empty());
        assert!(q.authors.is_empty());
    }

    #[test]
    fn test_single_file() {
        let q = SearchQuery::parse("file:src/main.rs");

        assert_eq!(q.files, vec!["src/main.rs"]);
        assert!(q.message_words.is_empty());
    }

    #[test]
    fn test_multiple_files() {
        let q = SearchQuery::parse("file:main.rs file:lib.rs");

        assert_eq!(q.files, vec!["main.rs", "lib.rs"]);
    }

    #[test]
    fn test_single_author() {
        let q = SearchQuery::parse("author:Christian");

        assert_eq!(q.authors, vec!["Christian"]);
    }

    #[test]
    fn test_quoted_author() {
        let q = SearchQuery::parse("author:\"Christian Kruse\"");

        assert_eq!(q.authors, vec!["Christian Kruse"]);
    }

    #[test]
    fn test_multiple_authors() {
        let q = SearchQuery::parse("author:Christian author:Max");

        assert_eq!(q.authors, vec!["Christian", "Max"]);
    }

    #[test]
    fn test_combined_query() {
        let q = SearchQuery::parse("file:main.rs author:Christian fix bug");

        assert_eq!(q.files, vec!["main.rs"]);
        assert_eq!(q.authors, vec!["Christian"]);
        assert_eq!(q.message_words, vec!["fix", "bug"]);
    }

    #[test]
    fn test_combined_or_query() {
        let q = SearchQuery::parse("file:main.rs file:lib.rs author:Christian author:Max fix");

        assert_eq!(q.files, vec!["main.rs", "lib.rs"]);
        assert_eq!(q.authors, vec!["Christian", "Max"]);
        assert_eq!(q.message_words, vec!["fix"]);
    }

    #[test]
    fn test_absolute_date() {
        let q = SearchQuery::parse("since:2024-01-15");
        assert!(q.since.is_some());

        let expected = NaiveDate::from_ymd_opt(2024, 1, 15)
            .unwrap()
            .and_hms_opt(0, 0, 0)
            .unwrap()
            .and_utc()
            .timestamp();

        assert_eq!(q.since.unwrap(), expected);
    }

    #[test]
    fn test_date_range() {
        let q = SearchQuery::parse("since:2024-01-01 until:2024-06-01");

        assert!(q.since.is_some());
        assert!(q.until.is_some());
        assert!(q.since.unwrap() < q.until.unwrap());
    }

    #[test]
    fn test_relative_date() {
        let q = SearchQuery::parse("since:2w");

        assert!(q.since.is_some());

        let expected = (Utc::now() - chrono::Duration::weeks(2)).timestamp();
        let diff = (expected - q.since.unwrap()).abs();

        assert!(diff < 5);
    }

    #[test]
    fn test_relative_date_months() {
        let q = SearchQuery::parse("since:3months");

        assert!(q.since.is_some());

        let expected = Utc::now().checked_sub_months(Months::new(3)).unwrap().timestamp();
        let diff = (expected - q.since.unwrap()).abs();

        assert!(diff < 5);
    }

    #[test]
    fn test_relative_date_year() {
        let q = SearchQuery::parse("since:1year");

        assert!(q.since.is_some());

        let expected = Utc::now().checked_sub_months(Months::new(12)).unwrap().timestamp();
        let diff = (expected - q.since.unwrap()).abs();

        assert!(diff < 5);
    }

    #[test]
    fn test_quoted_message() {
        let q = SearchQuery::parse("\"fix the crash\"");

        assert_eq!(q.message_words, vec!["fix the crash"]);
    }

    #[test]
    fn test_invalid_date_ignored() {
        let q = SearchQuery::parse("since:notadate");

        assert!(q.since.is_none());
    }

    #[test]
    fn test_empty_prefix_ignored() {
        let q = SearchQuery::parse("file: author:");

        assert!(q.files.is_empty());
        assert!(q.authors.is_empty());
    }
}
