#![allow(dead_code)] // Phase 2: consumed by filter_bar widget

//! Filter DSL parser for entity list screens.
//!
//! Parses a compact filter string into structured tokens:
//! - `field:value` — typed field filter (e.g., `state:opened`, `author:taylor`)
//! - `-field:value` — negation filter (exclude matches)
//! - `"quoted value"` — preserved as a single free-text token
//! - bare words — free-text search terms
//!
//! The DSL is intentionally simple: no boolean operators, no nesting.
//! Filters are AND-combined at the query layer.

// ---------------------------------------------------------------------------
// Token types
// ---------------------------------------------------------------------------

/// A single parsed filter token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FilterToken {
    /// `field:value` — match entities where `field` equals `value`.
    FieldValue { field: String, value: String },
    /// `-field:value` — exclude entities where `field` equals `value`.
    Negation { field: String, value: String },
    /// Bare word(s) used as free-text search.
    FreeText(String),
    /// `"quoted value"` — preserved as a single search term.
    QuotedValue(String),
}

// ---------------------------------------------------------------------------
// Known fields per entity type
// ---------------------------------------------------------------------------

/// Known filter fields for issues.
pub const ISSUE_FIELDS: &[&str] = &[
    "state",
    "author",
    "assignee",
    "label",
    "milestone",
    "status",
];

/// Known filter fields for merge requests.
pub const MR_FIELDS: &[&str] = &[
    "state",
    "author",
    "reviewer",
    "target_branch",
    "source_branch",
    "label",
    "draft",
];

// ---------------------------------------------------------------------------
// Parser
// ---------------------------------------------------------------------------

/// Parse a filter input string into a sequence of tokens.
///
/// Empty input returns an empty vec (no-op filter = show all).
pub fn parse_filter_tokens(input: &str) -> Vec<FilterToken> {
    let input = input.trim();
    if input.is_empty() {
        return Vec::new();
    }

    let mut tokens = Vec::new();
    let mut chars = input.chars().peekable();

    while chars.peek().is_some() {
        // Skip whitespace between tokens.
        while chars.peek().is_some_and(|c| c.is_whitespace()) {
            chars.next();
        }

        match chars.peek() {
            None => break,
            Some('"') => {
                // Quoted value — consume until closing quote or end.
                chars.next(); // consume opening "
                let value: String = consume_until(&mut chars, '"');
                if chars.peek() == Some(&'"') {
                    chars.next(); // consume closing "
                }
                if !value.is_empty() {
                    tokens.push(FilterToken::QuotedValue(value));
                }
            }
            Some('-') => {
                // Could be negation prefix or just a free-text word starting with -.
                chars.next(); // consume -
                let word = consume_word(&mut chars);
                if let Some((field, value)) = word.split_once(':') {
                    tokens.push(FilterToken::Negation {
                        field: field.to_string(),
                        value: value.to_string(),
                    });
                } else if !word.is_empty() {
                    // Bare negation without field:value — treat as free text with -.
                    tokens.push(FilterToken::FreeText(format!("-{word}")));
                }
            }
            Some(_) => {
                let word = consume_word(&mut chars);
                if let Some((field, value)) = word.split_once(':') {
                    tokens.push(FilterToken::FieldValue {
                        field: field.to_string(),
                        value: value.to_string(),
                    });
                } else if !word.is_empty() {
                    tokens.push(FilterToken::FreeText(word));
                }
            }
        }
    }

    tokens
}

/// Validate that a field name is known for the given entity type.
///
/// Returns `true` if the field is in the known set, `false` otherwise.
pub fn is_known_field(field: &str, known_fields: &[&str]) -> bool {
    known_fields.contains(&field)
}

/// Extract all unknown fields from a token list.
pub fn unknown_fields<'a>(tokens: &'a [FilterToken], known_fields: &[&str]) -> Vec<&'a str> {
    tokens
        .iter()
        .filter_map(|t| match t {
            FilterToken::FieldValue { field, .. } | FilterToken::Negation { field, .. } => {
                if is_known_field(field, known_fields) {
                    None
                } else {
                    Some(field.as_str())
                }
            }
            _ => None,
        })
        .collect()
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Consume characters until `delim` is found (exclusive) or end of input.
fn consume_until(chars: &mut std::iter::Peekable<std::str::Chars<'_>>, delim: char) -> String {
    let mut buf = String::new();
    while let Some(&c) = chars.peek() {
        if c == delim {
            break;
        }
        buf.push(c);
        chars.next();
    }
    buf
}

/// Consume a non-whitespace word.
fn consume_word(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
    let mut buf = String::new();
    while let Some(&c) = chars.peek() {
        if c.is_whitespace() {
            break;
        }
        // Stop at quote boundaries so they're handled separately.
        if c == '"' && !buf.is_empty() {
            break;
        }
        buf.push(c);
        chars.next();
    }
    buf
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    // -- TDD Anchor: basic field:value parsing --

    #[test]
    fn test_parse_filter_basic() {
        let tokens = parse_filter_tokens("state:opened author:taylor");
        assert_eq!(tokens.len(), 2);
        assert_eq!(
            tokens[0],
            FilterToken::FieldValue {
                field: "state".into(),
                value: "opened".into()
            }
        );
        assert_eq!(
            tokens[1],
            FilterToken::FieldValue {
                field: "author".into(),
                value: "taylor".into()
            }
        );
    }

    #[test]
    fn test_parse_quoted_value() {
        let tokens = parse_filter_tokens("\"in progress\"");
        assert_eq!(tokens.len(), 1);
        assert_eq!(tokens[0], FilterToken::QuotedValue("in progress".into()));
    }

    #[test]
    fn test_parse_negation() {
        let tokens = parse_filter_tokens("-state:closed");
        assert_eq!(tokens.len(), 1);
        assert_eq!(
            tokens[0],
            FilterToken::Negation {
                field: "state".into(),
                value: "closed".into()
            }
        );
    }

    #[test]
    fn test_parse_mixed() {
        let tokens = parse_filter_tokens("state:opened \"bug fix\" -label:wontfix");
        assert_eq!(tokens.len(), 3);
        assert_eq!(
            tokens[0],
            FilterToken::FieldValue {
                field: "state".into(),
                value: "opened".into()
            }
        );
        assert_eq!(tokens[1], FilterToken::QuotedValue("bug fix".into()));
        assert_eq!(
            tokens[2],
            FilterToken::Negation {
                field: "label".into(),
                value: "wontfix".into()
            }
        );
    }

    #[test]
    fn test_parse_empty_returns_empty() {
        assert!(parse_filter_tokens("").is_empty());
        assert!(parse_filter_tokens("   ").is_empty());
    }

    #[test]
    fn test_parse_free_text() {
        let tokens = parse_filter_tokens("authentication bug");
        assert_eq!(tokens.len(), 2);
        assert_eq!(tokens[0], FilterToken::FreeText("authentication".into()));
        assert_eq!(tokens[1], FilterToken::FreeText("bug".into()));
    }

    #[test]
    fn test_parse_bare_negation_as_free_text() {
        let tokens = parse_filter_tokens("-wontfix");
        assert_eq!(tokens.len(), 1);
        assert_eq!(tokens[0], FilterToken::FreeText("-wontfix".into()));
    }

    #[test]
    fn test_parse_unicode() {
        let tokens = parse_filter_tokens("author:田中 \"認証バグ\"");
        assert_eq!(tokens.len(), 2);
        assert_eq!(
            tokens[0],
            FilterToken::FieldValue {
                field: "author".into(),
                value: "田中".into()
            }
        );
        assert_eq!(tokens[1], FilterToken::QuotedValue("認証バグ".into()));
    }

    #[test]
    fn test_parse_unclosed_quote() {
        let tokens = parse_filter_tokens("\"open ended");
        assert_eq!(tokens.len(), 1);
        assert_eq!(tokens[0], FilterToken::QuotedValue("open ended".into()));
    }

    // -- Field validation --

    #[test]
    fn test_known_field_issues() {
        assert!(is_known_field("state", ISSUE_FIELDS));
        assert!(is_known_field("author", ISSUE_FIELDS));
        assert!(!is_known_field("reviewer", ISSUE_FIELDS));
        assert!(!is_known_field("bogus", ISSUE_FIELDS));
    }

    #[test]
    fn test_known_field_mrs() {
        assert!(is_known_field("draft", MR_FIELDS));
        assert!(is_known_field("reviewer", MR_FIELDS));
        assert!(!is_known_field("assignee", MR_FIELDS));
    }

    #[test]
    fn test_unknown_fields_detection() {
        let tokens = parse_filter_tokens("state:opened bogus:val author:taylor unknown:x");
        let unknown = unknown_fields(&tokens, ISSUE_FIELDS);
        assert_eq!(unknown, vec!["bogus", "unknown"]);
    }
}
