"""Filter compiler — converts policies into FilterExpression AST nodes.

This module analyzes applicable policies for a given subject and action,
then produces a FilterExpression AST that can be translated to SQL.

Limitations (documented):
- Conditions referencing environment.* cannot be pushed to SQL. They are
  skipped during filter compilation and noted in the returned metadata.
- Cross-reference conditions with subject.* are resolved to literal values
  at compile time (e.g., resource.owner_id == subject.id becomes
  resource.owner_id == "alice").
- The "matches" (regex) operator is flagged as non-pushable for most SQL
  dialects. Compilers may support it with dialect-specific syntax.
- deny-override combiner produces NOT clauses that may impact query performance.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from pyauthz.filtering.expressions import (
    AllowAll,
    And,
    Compare,
    DenyAll,
    FilterExpression,
    Not,
    Or,
)
from pyauthz.models.condition import Condition
from pyauthz.models.permission import Permission
from pyauthz.models.policy import Policy
from pyauthz.models.role import Role
from pyauthz.models.subject import Subject

# Prefixes that indicate a cross-reference value
_CROSS_REF_PREFIXES = ("subject.", "resource.", "environment.")


@dataclass
class FilterResult:
    """Result of filter compilation.

    Attributes:
        expression: The compiled FilterExpression AST.
        unpushable_conditions: Conditions that could not be pushed to SQL
            (e.g., environment-dependent or regex conditions).
        metadata: Additional information about the compilation.
    """

    expression: FilterExpression
    unpushable_conditions: list[str] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)


def compile_filter(
    subject: Subject,
    action: str,
    policies: list[Policy],
    roles: dict[str, Role],
    effective_roles: set[str],
    combiner: str,
) -> FilterResult:
    """Compile applicable policies into a FilterExpression.

    Analyzes all policies that could apply to the subject and action,
    and produces a filter expression representing the resource-level
    constraints.

    Args:
        subject: The subject requesting access.
        action: The resolved action string (e.g., "documents:read").
        policies: All policies in the system.
        roles: All roles in the system.
        effective_roles: The subject's effective roles (including inherited).
        combiner: The combiner name being used.

    Returns:
        A FilterResult with the expression and any unpushable conditions.
    """
    if ":" in action:
        req_resource_type, req_action_verb = action.split(":", 1)
    else:
        req_resource_type = "*"
        req_action_verb = action

    unpushable: list[str] = []
    allow_exprs: list[FilterExpression] = []
    deny_exprs: list[FilterExpression] = []

    for policy in policies:
        if not policy.enabled:
            continue

        # Check role membership
        if policy.roles and not any(r in effective_roles for r in policy.roles):
            continue

        # Check permission match
        if policy.permissions:
            matched = False
            for perm_str in policy.permissions:
                try:
                    perm = Permission.from_string(perm_str)
                except ValueError:
                    continue
                if perm.matches(req_resource_type, req_action_verb):
                    matched = True
                    break
            if not matched:
                continue

        # This policy is applicable — compile its conditions into a filter
        condition_expr, policy_unpushable = _compile_conditions(policy.conditions, subject)
        unpushable.extend(policy_unpushable)

        if policy.effect.value == "allow":
            allow_exprs.append(condition_expr)
        else:
            deny_exprs.append(condition_expr)

    # No applicable policies at all
    if not allow_exprs and not deny_exprs:
        return FilterResult(expression=DenyAll())

    # Build the combined expression based on the combiner
    result_expr = _combine_expressions(allow_exprs, deny_exprs, combiner)

    return FilterResult(
        expression=result_expr,
        unpushable_conditions=unpushable,
        metadata={
            "combiner": combiner,
            "allow_policy_count": len(allow_exprs),
            "deny_policy_count": len(deny_exprs),
        },
    )


def compile_filter_auto_default(
    subject: Subject,
    action: str,
    roles: dict[str, Role],
    effective_roles: set[str],
) -> FilterResult:
    """Compile a filter for auto-default mode (no explicit policies).

    When no policies exist, allows if the subject has a matching permission.
    Returns AllowAll or DenyAll since there are no conditions to push.
    """
    if ":" in action:
        req_resource_type, req_action_verb = action.split(":", 1)
    else:
        req_resource_type = "*"
        req_action_verb = action

    for role_name in effective_roles:
        role = roles.get(role_name)
        if role is None:
            continue
        for perm_str in role.permissions:
            try:
                perm = Permission.from_string(perm_str)
            except ValueError:
                continue
            if perm.matches(req_resource_type, req_action_verb):
                return FilterResult(expression=AllowAll())

    return FilterResult(expression=DenyAll())


def _compile_conditions(
    conditions: list[Condition],
    subject: Subject,
) -> tuple[FilterExpression, list[str]]:
    """Convert policy conditions into a FilterExpression.

    Cross-references to subject.* are resolved to literal values.
    Environment-dependent and regex conditions are flagged as unpushable.

    Returns:
        A tuple of (FilterExpression, list of unpushable condition descriptions).
    """
    if not conditions:
        return AllowAll(), []

    parts: list[FilterExpression] = []
    unpushable: list[str] = []

    for cond in conditions:
        # Skip environment-dependent conditions (can't push to SQL)
        if cond.field.startswith("environment."):
            unpushable.append(
                f"Environment condition not pushable to SQL: {cond.field} {cond.op} {cond.value!r}"
            )
            continue

        # Only resource.* fields can be pushed to SQL
        if not cond.field.startswith("resource."):
            unpushable.append(
                f"Non-resource condition not pushable to SQL: {cond.field} {cond.op} {cond.value!r}"
            )
            continue

        # Strip "resource." prefix for the SQL field name
        sql_field = cond.field[len("resource.") :]

        # Resolve the value
        value = cond.value
        if isinstance(value, str) and value.startswith(_CROSS_REF_PREFIXES):
            resolved = _resolve_subject_value(value, subject)
            if resolved is None:
                unpushable.append(
                    f"Cross-reference not resolvable: {cond.field} {cond.op} {value}"
                )
                continue
            value = resolved

        # Flag regex as potentially unpushable
        if cond.op == "matches":
            unpushable.append(
                f"Regex condition may not be supported by all SQL dialects: "
                f"{cond.field} matches {value!r}"
            )

        parts.append(Compare(field=sql_field, op=cond.op, value=value))

    if not parts:
        # All conditions were unpushable — we can't filter at the SQL level.
        # Return AllowAll and rely on app-layer filtering for the unpushable ones.
        return AllowAll(), unpushable

    if len(parts) == 1:
        return parts[0], unpushable
    return And(children=parts), unpushable


def _resolve_subject_value(path: str, subject: Subject) -> Any:
    """Resolve a subject.* cross-reference to a literal value.

    For example, "subject.id" -> "alice", "subject.attributes.department" -> "engineering".
    """
    parts = path.split(".")
    if len(parts) < 2 or parts[0] != "subject":
        return None

    obj: Any = subject
    for segment in parts[1:]:
        if isinstance(obj, dict):
            if segment not in obj:
                return None
            obj = obj[segment]
        elif hasattr(obj, segment):
            obj = getattr(obj, segment)
        else:
            return None

    return obj


def _combine_expressions(
    allow_exprs: list[FilterExpression],
    deny_exprs: list[FilterExpression],
    combiner: str,
) -> FilterExpression:
    """Combine allow and deny expressions based on the combiner strategy."""
    # Build the allow side — OR of all allow policy conditions
    if not allow_exprs:
        allow_combined = DenyAll()
    elif len(allow_exprs) == 1:
        allow_combined = allow_exprs[0]
    else:
        allow_combined = Or(children=allow_exprs)

    # If there are no deny policies, just return the allow side
    if not deny_exprs:
        return allow_combined

    # Build the deny side — OR of all deny policy conditions, then NOT
    deny_combined = deny_exprs[0] if len(deny_exprs) == 1 else Or(children=deny_exprs)

    # deny-override: resources must match ALLOW conditions AND NOT match DENY conditions
    if combiner in ("deny-override", "priority-first"):
        return And(children=[allow_combined, Not(child=deny_combined)])

    # allow-override: if any allow matches, allow (deny is secondary)
    if combiner == "allow-override":
        return allow_combined

    # first-match: order-dependent, complex to push to SQL — use allow side
    return allow_combined
