"""Condition evaluation engine for ABAC attribute-based policy checks.

Resolves dot-notation field paths against request context (subject, resource,
environment) and evaluates conditions using the full operator set. Supports
cross-reference conditions where the value is also a field path.

Missing attributes produce a fail-safe result (condition fails) with a
descriptive reason — never a crash.
"""

from __future__ import annotations

import re
from typing import Any

from pyauthz.models.condition import Condition
from pyauthz.models.context import AccessRequest

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


def evaluate_conditions(
    conditions: list[Condition],
    request: AccessRequest,
) -> tuple[bool, list[str]]:
    """Evaluate all conditions against the request context.

    All conditions must pass for the result to be True (AND logic).

    Args:
        conditions: The list of conditions to evaluate.
        request: The full access request with subject, resource, environment.

    Returns:
        A tuple of (all_passed, reasons) where reasons explain any failures.
    """
    reasons: list[str] = []

    for condition in conditions:
        passed, reason = _evaluate_single(condition, request)
        if not passed:
            reasons.append(reason)
            return False, reasons

    return True, reasons


def _evaluate_single(
    condition: Condition,
    request: AccessRequest,
) -> tuple[bool, str]:
    """Evaluate a single condition against the request context."""
    # Resolve the field (left-hand side)
    field_val, field_err = _resolve_field(condition.field, request)
    if field_err is not None:
        return False, field_err

    # Resolve the value (right-hand side) — may be a literal or cross-reference
    value = condition.value
    if isinstance(value, str) and value.startswith(_CROSS_REF_PREFIXES):
        value, val_err = _resolve_field(value, request)
        if val_err is not None:
            return False, val_err

    # Evaluate the operator
    return _apply_operator(condition.op, field_val, value, condition.field)


def _resolve_field(path: str, request: AccessRequest) -> tuple[Any, str | None]:
    """Resolve a dot-notation field path against the request context.

    Supports paths like:
    - subject.id, subject.roles, subject.attributes.department
    - resource.id, resource.type, resource.owner_id, resource.attributes.classification
    - environment.hour, environment.ip

    Returns:
        A tuple of (value, error_message). error_message is None on success.
    """
    parts = path.split(".")
    if len(parts) < 2:
        return (
            None,
            f"Condition failed: invalid field path '{path}' (must be dotted, e.g. 'subject.id')",
        )

    root = parts[0]

    if root == "subject":
        obj: Any = request.subject
    elif root == "resource":
        obj = request.resource
    elif root == "environment":
        obj = request.environment
    else:
        return None, f"Condition failed: unknown root '{root}' in field path '{path}'"

    # Walk the remaining path segments
    for i, segment in enumerate(parts[1:], start=1):
        current_path = ".".join(parts[: i + 1])

        if isinstance(obj, dict):
            if segment not in obj:
                return None, f"Condition failed: attribute '{current_path}' not found"
            obj = obj[segment]
        elif hasattr(obj, segment):
            obj = getattr(obj, segment)
        else:
            return None, f"Condition failed: attribute '{current_path}' not found"

    return obj, None


def _apply_operator(op: str, field_val: Any, value: Any, field_path: str) -> tuple[bool, str]:
    """Apply a comparison operator to field_val and value.

    Returns (passed, reason). On failure, reason describes why.
    """
    try:
        if op == "==":
            result = field_val == value
        elif op == "!=":
            result = field_val != value
        elif op == ">":
            result = field_val > value
        elif op == "<":
            result = field_val < value
        elif op == ">=":
            result = field_val >= value
        elif op == "<=":
            result = field_val <= value
        elif op == "in":
            result = field_val in value
        elif op == "not_in":
            result = field_val not in value
        elif op == "contains":
            result = value in field_val
        elif op == "matches":
            result = bool(re.search(str(value), str(field_val)))
        else:
            return False, f"Condition failed: unknown operator '{op}'"
    except (TypeError, ValueError):
        return False, (
            f"Condition failed: cannot apply '{op}' to "
            f"'{field_path}' ({type(field_val).__name__}) and {value!r}"
        )

    if result:
        return True, f"Condition passed: {field_path} {op} {value!r}"
    return False, f"Condition failed: {field_path} {op} {value!r} (got {field_val!r})"
