"""Raw SQL filter compiler — converts FilterExpression to parameterized SQL WHERE clauses.

Usage:
    from pyauthz.filtering.raw_sql import to_raw_sql

    filter_expr = rp.authorized_filter(user, "documents:read")
    where_clause, params = to_raw_sql(filter_expr.expression, table="documents")
    cursor.execute(f"SELECT * FROM documents WHERE {where_clause}", params)

This compiler produces parameterized queries to prevent SQL injection.
Parameters use numbered placeholders (:p0, :p1, ...) suitable for most
database drivers when used with named parameters.
"""

from __future__ import annotations

from typing import Any

from pyauthz.filtering.expressions import (
    AllowAll,
    And,
    Compare,
    DenyAll,
    FilterExpression,
    Not,
    Or,
)

# Mapping from PyAuthz operators to SQL operators
_OP_MAP = {
    "==": "=",
    "!=": "!=",
    ">": ">",
    "<": "<",
    ">=": ">=",
    "<=": "<=",
}


def to_raw_sql(
    expression: FilterExpression,
    table: str | None = None,
    placeholder_style: str = "named",
) -> tuple[str, dict[str, Any]]:
    """Convert a FilterExpression AST into a raw SQL WHERE clause with parameters.

    Args:
        expression: The FilterExpression to compile.
        table: Optional table name prefix for column references.
        placeholder_style: Parameter style. "named" uses :p0, :p1 (default).
            "qmark" uses ? placeholders (params returned as ordered dict).

    Returns:
        A tuple of (where_clause, params) where params is a dict of named parameters.

    Example:
        where, params = to_raw_sql(filter_expr, table="documents")
        # where = "documents.owner_id = :p0"
        # params = {"p0": "alice"}
    """
    ctx = _CompileContext(table=table, placeholder_style=placeholder_style)
    clause = ctx.compile(expression)
    return clause, ctx.params


class _CompileContext:
    """Stateful context for SQL compilation that tracks parameter numbering."""

    def __init__(self, table: str | None = None, placeholder_style: str = "named") -> None:
        self.table = table
        self.placeholder_style = placeholder_style
        self.params: dict[str, Any] = {}
        self._param_counter = 0

    def _next_param(self, value: Any) -> str:
        """Register a parameter and return its placeholder."""
        name = f"p{self._param_counter}"
        self._param_counter += 1
        self.params[name] = value
        if self.placeholder_style == "qmark":
            return "?"
        return f":{name}"

    def _column_ref(self, field: str) -> str:
        """Build a column reference, optionally with table prefix."""
        # Convert dot-notation to a simple column name
        # For nested fields like attributes.classification, we use JSON syntax
        parts = field.split(".")
        col = field if len(parts) == 1 else parts[0] + "".join(f"->>'{p}'" for p in parts[1:])

        if self.table:
            return f"{self.table}.{col}"
        return col

    def compile(self, node: FilterExpression) -> str:
        """Compile a FilterExpression node to a SQL string."""
        if isinstance(node, AllowAll):
            return "1=1"
        if isinstance(node, DenyAll):
            return "1=0"
        if isinstance(node, Compare):
            return self._compile_compare(node)
        if isinstance(node, And):
            if not node.children:
                return "1=1"
            parts = [self.compile(c) for c in node.children]
            return "(" + " AND ".join(parts) + ")"
        if isinstance(node, Or):
            if not node.children:
                return "1=0"
            parts = [self.compile(c) for c in node.children]
            return "(" + " OR ".join(parts) + ")"
        if isinstance(node, Not):
            inner = self.compile(node.child)
            return f"NOT ({inner})"
        raise ValueError(f"Unknown FilterExpression node: {type(node).__name__}")

    def _compile_compare(self, node: Compare) -> str:
        """Compile a Compare node to SQL."""
        col = self._column_ref(node.field)

        if node.op in _OP_MAP:
            placeholder = self._next_param(node.value)
            return f"{col} {_OP_MAP[node.op]} {placeholder}"

        if node.op == "in":
            if not isinstance(node.value, (list, tuple)):
                placeholder = self._next_param(node.value)
                return f"{col} IN ({placeholder})"
            placeholders = [self._next_param(v) for v in node.value]
            return f"{col} IN ({', '.join(placeholders)})"

        if node.op == "not_in":
            if not isinstance(node.value, (list, tuple)):
                placeholder = self._next_param(node.value)
                return f"{col} NOT IN ({placeholder})"
            placeholders = [self._next_param(v) for v in node.value]
            return f"{col} NOT IN ({', '.join(placeholders)})"

        if node.op == "contains":
            placeholder = self._next_param(f"%{node.value}%")
            return f"{col} LIKE {placeholder}"

        if node.op == "matches":
            placeholder = self._next_param(node.value)
            return f"{col} REGEXP {placeholder}"

        raise ValueError(f"Unsupported operator for raw SQL: '{node.op}'")
