//! PyO3 bindings for the wirefilter expression parser.
//!
//! Exposes `parse_expression(expr: str, phase: str | None) -> dict` to Python.
//! Returns extracted components on success or `{"error": "..."}` on parse failure.

use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use wirefilter::ParserSettings;

mod scheme;
mod visitor;

use scheme::SCHEME;
use visitor::ExpressionExtractor;

/// Maximum allowed expression length (1 MiB).
const MAX_EXPRESSION_LEN: usize = 1_048_576;

/// Conservative parser settings to catch likely-rejected expressions early.
///
/// - `wildcard_star_limit`: 10 — prevents excessive `*` metacharacters that
///   would cause catastrophic backtracking at evaluation time.
/// - Regex size limits: wirefilter defaults (10 MB DFA, 2 MB compiled).
pub(crate) fn parser_settings() -> ParserSettings {
    ParserSettings {
        wildcard_star_limit: 10,
        ..ParserSettings::default()
    }
}

/// Standard result keys returned by `parse_expression`.
const RESULT_KEYS: &[&str] = &[
    "fields",
    "functions",
    "operators",
    "string_literals",
    "regex_literals",
    "ip_literals",
    "int_literals",
];

/// Populate a Python dict with empty lists for all standard result keys.
fn set_empty_result_keys(py: Python<'_>, dict: &Bound<'_, PyDict>) -> PyResult<()> {
    for key in RESULT_KEYS {
        dict.set_item(*key, PyList::empty(py))?;
    }
    Ok(())
}

/// Parse a Cloudflare wirefilter expression and return extracted components.
///
/// The `phase` parameter is accepted for forward compatibility but currently
/// ignored.  All expressions are validated against a single unified scheme
/// that covers all known Cloudflare fields.  A future version may use `phase`
/// to restrict the field set per ruleset phase.
///
/// Returns a Python dict with:
///   - On success: `{"fields": [...], "functions": [...], "operators": [...], ...}`
///   - On failure: `{"error": "parse error description"}`
///
/// Empty or whitespace-only expressions are valid and return empty lists
/// for all keys (not an error dict).
#[pyfunction]
#[pyo3(signature = (expr, phase=None))]
fn parse_expression(py: Python<'_>, expr: &str, phase: Option<&str>) -> PyResult<Py<PyAny>> {
    // Reject oversized expressions before any processing.
    if expr.len() > MAX_EXPRESSION_LEN {
        let dict = PyDict::new(py);
        dict.set_item(
            "error",
            format!(
                "expression exceeds maximum length ({} bytes, limit {})",
                expr.len(),
                MAX_EXPRESSION_LEN
            ),
        )?;
        set_empty_result_keys(py, &dict)?;
        return Ok(dict.into());
    }

    // Empty expressions are valid — return empty lists (not an error).
    let trimmed = expr.trim();
    if trimmed.is_empty() {
        let dict = PyDict::new(py);
        set_empty_result_keys(py, &dict)?;
        return Ok(dict.into());
    }

    let _ = phase; // accepted for API compat, unused
    let ast = match SCHEME
        .parser_with_settings(parser_settings())
        .parse(trimmed)
    {
        Ok(ast) => ast,
        Err(e) => {
            let dict = PyDict::new(py);
            dict.set_item("error", format!("{e}"))?;
            set_empty_result_keys(py, &dict)?;
            return Ok(dict.into());
        }
    };

    // Walk the AST to extract components.
    let mut extractor = ExpressionExtractor::new();
    extractor.extract(ast.expression());

    // Build the result dict.
    let dict = PyDict::new(py);
    dict.set_item("fields", PyList::new(py, &extractor.fields)?)?;
    dict.set_item("functions", PyList::new(py, &extractor.functions)?)?;
    dict.set_item("operators", PyList::new(py, &extractor.operators)?)?;
    dict.set_item(
        "string_literals",
        PyList::new(py, &extractor.string_literals)?,
    )?;
    dict.set_item(
        "regex_literals",
        PyList::new(py, &extractor.regex_literals)?,
    )?;
    dict.set_item("ip_literals", PyList::new(py, &extractor.ip_literals)?)?;
    dict.set_item("int_literals", PyList::new(py, &extractor.int_literals)?)?;
    if extractor.depth_exceeded() {
        dict.set_item("depth_exceeded", true)?;
    }
    Ok(dict.into())
}

/// Return schema metadata for the wirefilter scheme.
///
/// Returns a Python dict with:
///   - `fields`: list of `{"name": "...", "type": "STRING"}` dicts
///   - `functions`: list of function name strings
#[pyfunction]
fn get_schema_info(py: Python<'_>) -> PyResult<Py<PyAny>> {
    let dict = PyDict::new(py);

    // Fields
    let field_defs = scheme::common_field_defs();
    let fields_list = PyList::empty(py);
    for (name, py_type) in field_defs {
        let entry = PyDict::new(py);
        entry.set_item("name", *name)?;
        entry.set_item("type", *py_type)?;
        fields_list.append(entry)?;
    }
    dict.set_item("fields", fields_list)?;

    // Functions
    let func_names = scheme::common_function_names();
    dict.set_item("functions", PyList::new(py, func_names)?)?;

    Ok(dict.into())
}

/// Python module definition.
#[pymodule]
fn octorules_wirefilter(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(parse_expression, m)?)?;
    m.add_function(wrap_pyfunction!(get_schema_info, m)?)?;
    Ok(())
}
