//! Rule engine — Traefik-style rule parsing and matching
//!
//! Parses expressions like:
//! ```text
//! Host(`api.example.com`) && PathPrefix(`/v1`)
//! ```

use http::HeaderMap;

/// A single matcher condition
#[derive(Debug, Clone, PartialEq)]
pub enum Matcher {
    /// Match by hostname: `Host(`domain`)`
    Host(String),
    /// Match by exact path: `Path(`/exact`)`
    Path(String),
    /// Match by path prefix: `PathPrefix(`/prefix`)`
    PathPrefix(String),
    /// Match by HTTP method: `Method(`GET`)`
    Method(String),
    /// Match by header key-value: `Headers(`key`, `value`)`
    Headers(String, String),
}

impl Matcher {
    /// Check if this matcher matches the given request
    fn matches(&self, host: Option<&str>, path: &str, method: &str, headers: &HeaderMap) -> bool {
        match self {
            Matcher::Host(expected) => host
                .map(|h| strip_host_port(h).eq_ignore_ascii_case(expected))
                .unwrap_or(false),
            Matcher::Path(expected) => path == expected,
            Matcher::PathPrefix(prefix) => path.starts_with(prefix.as_str()),
            Matcher::Method(expected) => method.eq_ignore_ascii_case(expected),
            Matcher::Headers(key, value) => headers
                .get(key.as_str())
                .and_then(|v| v.to_str().ok())
                .map(|v| v == value.as_str())
                .unwrap_or(false),
        }
    }
}

/// Strip an optional `:port` suffix from a request authority before host matching.
///
/// Mirrors standard HTTP / Traefik behavior: the `Host` header / `:authority`
/// may carry a port (e.g. `example.com:8443`) that must be ignored when matching
/// against an Ingress-derived bare hostname. IPv6 literals are kept intact: only
/// a colon that appears after the closing `]` is treated as the port separator.
fn strip_host_port(authority: &str) -> &str {
    if let Some(rest) = authority.strip_prefix('[') {
        // IPv6 literal: `[::1]` or `[::1]:443`. `close` is the `]` index within
        // `rest` (the authority minus the leading `[`), so in `authority` the `]`
        // sits at `close + 1`; slice through `close + 2` to KEEP the closing `]`.
        match rest.find(']') {
            Some(close) => &authority[..close + 2], // keep `[..]`, drop any `:port`
            None => authority,                      // malformed; leave as-is
        }
    } else {
        // Reg-name or IPv4: split off the trailing `:port` if present.
        match authority.rsplit_once(':') {
            Some((host, _port)) => host,
            None => authority,
        }
    }
}

/// A compiled rule — a list of matchers combined with AND
#[derive(Debug, Clone)]
pub struct Rule {
    /// All matchers must match (AND logic)
    matchers: Vec<Matcher>,
}

impl Rule {
    /// Parse a rule expression string
    ///
    /// # Examples
    ///
    /// ```rust,ignore
    /// use a3s_gateway::router::Rule;
    ///
    /// let rule = Rule::parse("Host(`example.com`) && PathPrefix(`/api`)").unwrap();
    /// ```
    pub fn parse(input: &str) -> Result<Self, String> {
        let parts: Vec<&str> = input.split("&&").map(|s| s.trim()).collect();
        let mut matchers = Vec::new();

        for part in parts {
            let matcher = Self::parse_matcher(part)?;
            matchers.push(matcher);
        }

        if matchers.is_empty() {
            return Err("Rule must contain at least one matcher".to_string());
        }

        Ok(Self { matchers })
    }

    /// Parse a single matcher expression
    fn parse_matcher(input: &str) -> Result<Matcher, String> {
        let input = input.trim();

        // Extract function name and arguments: Name(`arg1`, `arg2`)
        let paren_start = input
            .find('(')
            .ok_or_else(|| format!("Invalid matcher syntax, expected '(': {}", input))?;
        let paren_end = input
            .rfind(')')
            .ok_or_else(|| format!("Invalid matcher syntax, expected ')': {}", input))?;

        let name = &input[..paren_start];
        let args_str = &input[paren_start + 1..paren_end];

        // Parse backtick-delimited arguments
        let args = Self::parse_args(args_str)?;

        match name {
            "Host" => {
                if args.len() != 1 {
                    return Err(format!("Host() expects 1 argument, got {}", args.len()));
                }
                Ok(Matcher::Host(args[0].clone()))
            }
            "Path" => {
                if args.len() != 1 {
                    return Err(format!("Path() expects 1 argument, got {}", args.len()));
                }
                Ok(Matcher::Path(args[0].clone()))
            }
            "PathPrefix" => {
                if args.len() != 1 {
                    return Err(format!(
                        "PathPrefix() expects 1 argument, got {}",
                        args.len()
                    ));
                }
                Ok(Matcher::PathPrefix(args[0].clone()))
            }
            "Method" => {
                if args.len() != 1 {
                    return Err(format!("Method() expects 1 argument, got {}", args.len()));
                }
                Ok(Matcher::Method(args[0].clone()))
            }
            "Headers" => {
                if args.len() != 2 {
                    return Err(format!("Headers() expects 2 arguments, got {}", args.len()));
                }
                Ok(Matcher::Headers(args[0].clone(), args[1].clone()))
            }
            _ => Err(format!("Unknown matcher: {}", name)),
        }
    }

    /// Parse backtick-delimited arguments: `arg1`, `arg2`
    fn parse_args(input: &str) -> Result<Vec<String>, String> {
        let mut args = Vec::new();
        let mut chars = input.chars().peekable();

        loop {
            // Skip whitespace and commas
            while chars
                .peek()
                .map(|c| *c == ' ' || *c == ',')
                .unwrap_or(false)
            {
                chars.next();
            }

            if chars.peek().is_none() {
                break;
            }

            // Expect backtick
            match chars.next() {
                Some('`') => {}
                Some(c) => return Err(format!("Expected backtick, got '{}'", c)),
                None => break,
            }

            // Read until closing backtick
            let mut arg = String::new();
            loop {
                match chars.next() {
                    Some('`') => break,
                    Some(c) => arg.push(c),
                    None => return Err("Unterminated backtick argument".to_string()),
                }
            }

            args.push(arg);
        }

        Ok(args)
    }

    /// Check if this rule matches the given request
    pub fn matches(
        &self,
        host: Option<&str>,
        path: &str,
        method: &str,
        headers: &HeaderMap,
    ) -> bool {
        self.matchers
            .iter()
            .all(|m| m.matches(host, path, method, headers))
    }

    /// Number of matchers in this rule
    #[allow(dead_code)]
    pub fn matcher_count(&self) -> usize {
        self.matchers.len()
    }
}

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

    #[test]
    fn test_parse_host() {
        let rule = Rule::parse("Host(`example.com`)").unwrap();
        assert_eq!(rule.matcher_count(), 1);
    }

    #[test]
    fn test_parse_path() {
        let rule = Rule::parse("Path(`/health`)").unwrap();
        assert_eq!(rule.matcher_count(), 1);
    }

    #[test]
    fn test_parse_path_prefix() {
        let rule = Rule::parse("PathPrefix(`/api`)").unwrap();
        assert_eq!(rule.matcher_count(), 1);
    }

    #[test]
    fn test_parse_method() {
        let rule = Rule::parse("Method(`POST`)").unwrap();
        assert_eq!(rule.matcher_count(), 1);
    }

    #[test]
    fn test_parse_headers() {
        let rule = Rule::parse("Headers(`X-Custom`, `value`)").unwrap();
        assert_eq!(rule.matcher_count(), 1);
    }

    #[test]
    fn test_parse_combined_rule() {
        let rule = Rule::parse("Host(`api.example.com`) && PathPrefix(`/v1`)").unwrap();
        assert_eq!(rule.matcher_count(), 2);
    }

    #[test]
    fn test_parse_triple_rule() {
        let rule = Rule::parse("Host(`api.com`) && PathPrefix(`/v1`) && Method(`GET`)").unwrap();
        assert_eq!(rule.matcher_count(), 3);
    }

    #[test]
    fn test_parse_invalid_matcher() {
        let result = Rule::parse("Unknown(`test`)");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Unknown matcher"));
    }

    #[test]
    fn test_parse_missing_backtick() {
        let result = Rule::parse("Host(example.com)");
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_wrong_arg_count_host() {
        let result = Rule::parse("Host(`a`, `b`)");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("expects 1 argument"));
    }

    #[test]
    fn test_parse_wrong_arg_count_headers() {
        let result = Rule::parse("Headers(`key`)");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("expects 2 arguments"));
    }

    #[test]
    fn test_match_host() {
        let rule = Rule::parse("Host(`example.com`)").unwrap();
        let headers = http::HeaderMap::new();
        assert!(rule.matches(Some("example.com"), "/", "GET", &headers));
        assert!(rule.matches(Some("EXAMPLE.COM"), "/", "GET", &headers)); // case insensitive
        assert!(!rule.matches(Some("other.com"), "/", "GET", &headers));
        assert!(!rule.matches(None, "/", "GET", &headers));
    }

    #[test]
    fn test_match_host_strips_port() {
        // A Host header carrying a port (e.g. behind a non-:80 external entry) must
        // still match a port-less Ingress host rule — standard HTTP/Traefik behavior.
        let rule = Rule::parse("Host(`deep-research.10.12.111.133.nip.io`)").unwrap();
        let headers = http::HeaderMap::new();
        assert!(rule.matches(
            Some("deep-research.10.12.111.133.nip.io:49164"),
            "/",
            "GET",
            &headers
        ));
        assert!(rule.matches(
            Some("deep-research.10.12.111.133.nip.io"),
            "/",
            "GET",
            &headers
        ));
        assert!(!rule.matches(Some("other.nip.io:49164"), "/", "GET", &headers));

        // IPv6 literals keep their brackets; only a trailing `:port` is stripped.
        let v6 = Rule::parse("Host(`[::1]`)").unwrap();
        assert!(v6.matches(Some("[::1]:443"), "/", "GET", &headers));
        assert!(v6.matches(Some("[::1]"), "/", "GET", &headers));
    }

    #[test]
    fn test_match_path() {
        let rule = Rule::parse("Path(`/health`)").unwrap();
        let headers = http::HeaderMap::new();
        assert!(rule.matches(None, "/health", "GET", &headers));
        assert!(!rule.matches(None, "/health/check", "GET", &headers));
        assert!(!rule.matches(None, "/", "GET", &headers));
    }

    #[test]
    fn test_match_path_prefix() {
        let rule = Rule::parse("PathPrefix(`/api`)").unwrap();
        let headers = http::HeaderMap::new();
        assert!(rule.matches(None, "/api", "GET", &headers));
        assert!(rule.matches(None, "/api/users", "GET", &headers));
        assert!(rule.matches(None, "/api/users/123", "GET", &headers));
        assert!(!rule.matches(None, "/other", "GET", &headers));
    }

    #[test]
    fn test_match_method() {
        let rule = Rule::parse("Method(`POST`)").unwrap();
        let headers = http::HeaderMap::new();
        assert!(rule.matches(None, "/", "POST", &headers));
        assert!(rule.matches(None, "/", "post", &headers)); // case insensitive
        assert!(!rule.matches(None, "/", "GET", &headers));
    }

    #[test]
    fn test_match_headers() {
        let rule = Rule::parse("Headers(`X-Custom`, `value`)").unwrap();
        let mut headers = http::HeaderMap::new();
        headers.insert(
            "x-custom".parse::<http::header::HeaderName>().unwrap(),
            "value".parse::<http::HeaderValue>().unwrap(),
        );
        assert!(rule.matches(None, "/", "GET", &headers));

        headers.insert(
            "x-custom".parse::<http::header::HeaderName>().unwrap(),
            "other".parse::<http::HeaderValue>().unwrap(),
        );
        assert!(!rule.matches(None, "/", "GET", &headers));

        let empty = http::HeaderMap::new();
        assert!(!rule.matches(None, "/", "GET", &empty));
    }

    #[test]
    fn test_match_combined_and() {
        let rule = Rule::parse("Host(`api.com`) && PathPrefix(`/v1`)").unwrap();
        let headers = http::HeaderMap::new();

        // Both match
        assert!(rule.matches(Some("api.com"), "/v1/users", "GET", &headers));

        // Only host matches
        assert!(!rule.matches(Some("api.com"), "/v2/users", "GET", &headers));

        // Only path matches
        assert!(!rule.matches(Some("other.com"), "/v1/users", "GET", &headers));

        // Neither matches
        assert!(!rule.matches(Some("other.com"), "/v2/users", "GET", &headers));
    }

    #[test]
    fn test_match_triple_and() {
        let rule = Rule::parse("Host(`api.com`) && PathPrefix(`/v1`) && Method(`GET`)").unwrap();
        let headers = http::HeaderMap::new();

        assert!(rule.matches(Some("api.com"), "/v1/users", "GET", &headers));
        assert!(!rule.matches(Some("api.com"), "/v1/users", "POST", &headers));
    }

    #[test]
    fn test_parse_empty_rule() {
        // Empty string after split produces one empty part which fails to parse
        let result = Rule::parse("");
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_missing_closing_paren() {
        // Missing closing paren causes syntax error before backtick processing
        let result = Rule::parse("Host(`unterminated");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("expected ')'"));
    }

    #[test]
    fn test_parse_args_whitespace_and_commas() {
        // Rule::parse handles the full expression, but we can test the parse_args path
        // through parsing with extra whitespace
        let rule = Rule::parse("Host(`example.com`)").unwrap();
        assert_eq!(rule.matcher_count(), 1);
    }

    #[test]
    fn test_parse_args_trailing_content_after_backtick() {
        // Extra content after closing backtick before comma
        let result = Rule::parse("Host(`test`extra`)");
        assert!(result.is_err());
    }
}
