"""
Policy Decision Explanation

Provides structured explanations for Cedar policy decisions by matching
determining policies against their structured conditions and the request context.
"""

import re
from dataclasses import dataclass, field
from typing import Any, Optional, Union

from .engine import Decision, DeterminingPolicy
from .rule import PolicyRule, PolicyCondition, PolicyEffect, ConditionOperator, ConditionExpression


# ---------------------------------------------------------------------------
# Evaluated Expression Tree
# ---------------------------------------------------------------------------
# Recursive evaluated condition expression tree.
# Produced by evaluate_expression() by walking ConditionExpression against context.
# Every node has a `matched` boolean; leaf nodes include `actual` values.
#
# Since Python dicts can't have recursive TypedDicts, use a plain dict alias.
#
# Variants (discriminated by "kind"):
#   {"kind": "comparison", "field": str, "operator": ConditionOperator, "expected": ..., "actual": ..., "matched": bool}
#   {"kind": "contains", "field": str, "expected": ..., "actual": ..., "matched": bool}
#   {"kind": "like", "field": str, "pattern": str, "actual": ..., "matched": bool}
#   {"kind": "has", "field": str, "matched": bool}
#   {"kind": "and", "children": list[EvaluatedExpression], "matched": bool}
#   {"kind": "or", "children": list[EvaluatedExpression], "matched": bool}
#   {"kind": "not", "child": EvaluatedExpression, "matched": bool}
#   {"kind": "raw", "text": str, "matched": bool}
EvaluatedExpression = dict[str, Any]


@dataclass
class EvidenceSource:
    """Identifies the detector that produced a context value.
    Used for provenance tracking — linking policy conditions back to their source detector.
    """

    # Detector name (e.g., "injection", "secrets", "tool_validator")
    detector: str
    # Detector execution latency in milliseconds
    latency_ms: int = 0
    # Producer-specific metadata (e.g., "tier", "phase", "priority"). Treated as opaque key-value pairs.
    labels: Optional[dict[str, str]] = None


@dataclass
class ConditionResult:
    """Result of evaluating a single condition against the request context."""

    # Context field name
    field: str
    # Comparison operator
    operator: ConditionOperator
    # Expected value (threshold from the rule)
    expected: Union[str, int, float, bool, list[str]]
    # Actual value from the context (None if field was missing)
    actual: Any = None
    # Whether this condition matched
    matched: bool = False
    # Source detector that produced this context value (when provenance is available)
    source: Optional[EvidenceSource] = None


@dataclass
class PolicyExplanation:
    """Explanation for a single determining policy."""

    # Policy ID
    policy_id: str
    # Policy effect (permit or forbid)
    effect: PolicyEffect
    # Human-readable summary
    summary: str
    # Per-condition match results (flat, from structured conditions[] or leaf extraction)
    condition_results: list[ConditionResult] = field(default_factory=list)
    # Recursive evaluated condition tree with actual values (from conditionExpression)
    evaluated_expression: Optional[EvaluatedExpression] = None
    # Raw Cedar condition text if the policy uses rawCondition instead of structured conditions
    raw_condition: Optional[str] = None


@dataclass
class ExplainedDecision:
    """Result of explaining a policy decision."""

    # The original decision effect ("Allow" or "Deny")
    effect: str
    # Enriched explanations for each determining policy
    explanations: list[PolicyExplanation] = field(default_factory=list)
    # Determining policy IDs that had no matching rule in the provided rules array
    unmatched_policies: list[str] = field(default_factory=list)


# Sentinel for missing context fields (distinguishes from None values)
_MISSING = object()


def explain_decision(
    decision: Decision,
    rules: list[PolicyRule],
    context: dict[str, Any],
    *,
    provenance: Optional[dict[str, EvidenceSource]] = None,
) -> ExplainedDecision:
    """
    Explain a policy decision by matching determining policies against
    their structured conditions and the request context.

    Args:
        decision: The Decision from PolicyEngine.evaluate()
        rules: The PolicyRule list that were loaded (parsed or built)
        context: The context dict that was passed to evaluate()
        provenance: Optional mapping of context field names to their source detectors

    Returns:
        Structured explanation with per-condition match details

    Example:
        >>> decision = engine.evaluate(...)
        >>> explained = explain_decision(decision, rules, request_context)
        >>> for explanation in explained.explanations:
        ...     print(explanation.summary)
        "forbid process_prompt — threat_count (10) > 5"
    """
    # Build lookup map: annotations.id → PolicyRule
    rule_map: dict[str, PolicyRule] = {}
    for rule in rules:
        annotations = rule.get("annotations", {})
        rule_id = annotations.get("id", "")
        if rule_id:
            rule_map[rule_id] = rule

    explanations: list[PolicyExplanation] = []
    unmatched_policies: list[str] = []

    for dp in decision.determining_policies:
        rule = rule_map.get(dp.id)
        if rule is None:
            unmatched_policies.append(dp.id)
            continue

        # Recursive condition evaluation
        evaluated_expr: Optional[EvaluatedExpression] = None
        if rule.get("conditionExpression"):
            evaluated_expr = evaluate_expression(rule["conditionExpression"], context, provenance)

        # Populate condition_results: prefer leaf extraction from expression tree,
        # fall back to flat conditions[] for backward compat with simple policies
        if evaluated_expr:
            condition_results = _collect_leaf_results(evaluated_expr)
        else:
            conditions: list[PolicyCondition] = rule.get("conditions", [])
            condition_results = []
            for cond in conditions:
                actual = context.get(cond["field"], _MISSING)
                actual_value = None if actual is _MISSING else actual
                matched = _evaluate_condition(cond["operator"], actual_value, cond["value"], actual is _MISSING)
                source = provenance.get(cond["field"]) if provenance else None
                condition_results.append(ConditionResult(
                    field=cond["field"],
                    operator=cond["operator"],
                    expected=cond["value"],
                    actual=actual_value,
                    matched=matched,
                    source=source,
                ))

        # Surface rawCondition when there are no structured conditions and no expression tree
        raw_condition = None
        if len(condition_results) == 0 and not evaluated_expr and rule.get("rawCondition"):
            raw_condition = rule["rawCondition"]

        if evaluated_expr:
            summary = _build_tree_summary(rule, evaluated_expr)
        else:
            summary = _build_summary(rule, condition_results, raw_condition)

        explanations.append(PolicyExplanation(
            policy_id=dp.id,
            effect=rule.get("effect", "forbid"),
            summary=summary,
            condition_results=condition_results,
            evaluated_expression=evaluated_expr,
            raw_condition=raw_condition,
        ))

    return ExplainedDecision(
        effect=decision.effect,
        explanations=explanations,
        unmatched_policies=unmatched_policies,
    )


# =============================================================================
# Recursive Expression Evaluation
# =============================================================================


def evaluate_expression(
    expr: ConditionExpression,
    context: dict[str, Any],
    provenance: Optional[dict[str, EvidenceSource]] = None,
) -> EvaluatedExpression:
    """Recursively evaluate a ConditionExpression tree against a context map.

    Returns an EvaluatedExpression tree with ``matched`` booleans and ``actual`` values.
    The optional provenance map links context field names to their source detectors.
    """
    kind = expr.get("kind")

    if kind == "and":
        children = [evaluate_expression(c, context, provenance) for c in expr["children"]]
        return {"kind": "and", "children": children, "matched": all(c["matched"] for c in children)}

    if kind == "or":
        children = [evaluate_expression(c, context, provenance) for c in expr["children"]]
        return {"kind": "or", "children": children, "matched": any(c["matched"] for c in children)}

    if kind == "not":
        child = evaluate_expression(expr["child"], context, provenance)
        return {"kind": "not", "child": child, "matched": not child["matched"]}

    if kind == "has":
        field = expr["field"]
        matched = field in context and context[field] is not None
        return {"kind": "has", "field": field, "matched": matched}

    if kind == "comparison":
        field_name = expr["field"]
        actual = context.get(field_name)
        matched = _evaluate_condition(expr["operator"], actual, expr["value"], actual is None and field_name not in context)
        result: EvaluatedExpression = {
            "kind": "comparison",
            "field": field_name,
            "operator": expr["operator"],
            "expected": expr["value"],
            "actual": actual,
            "matched": matched,
        }
        if provenance and field_name in provenance:
            result["source"] = provenance[field_name]
        return result

    if kind == "contains":
        field_name = expr["field"]
        actual = context.get(field_name)
        matched = _evaluate_contains(actual, expr["value"])
        result = {
            "kind": "contains",
            "field": field_name,
            "expected": expr["value"],
            "actual": actual,
            "matched": matched,
        }
        if provenance and field_name in provenance:
            result["source"] = provenance[field_name]
        return result

    if kind == "like":
        field_name = expr["field"]
        actual = context.get(field_name)
        matched = _evaluate_like(actual, expr["pattern"])
        result = {
            "kind": "like",
            "field": field_name,
            "pattern": expr["pattern"],
            "actual": actual,
            "matched": matched,
        }
        if provenance and field_name in provenance:
            result["source"] = provenance[field_name]
        return result

    if kind == "raw":
        return {"kind": "raw", "text": expr.get("text", ""), "matched": False}

    # Unknown kind -- treat as raw
    return {"kind": "raw", "text": str(expr), "matched": False}


def _collect_leaf_results(expr: EvaluatedExpression) -> list[ConditionResult]:
    """Walk tree, collect comparison/contains/like leaves as flat ConditionResult list.

    Skips 'has' (null-guards) and 'raw' (can't decompose) nodes.
    Recurses into and/or/not to reach all leaves.
    """
    kind = expr.get("kind")

    if kind in ("and", "or"):
        results: list[ConditionResult] = []
        for child in expr.get("children", []):
            results.extend(_collect_leaf_results(child))
        return results

    if kind == "not":
        return _collect_leaf_results(expr["child"])

    if kind in ("has", "raw"):
        return []

    if kind == "comparison":
        return [ConditionResult(
            field=expr["field"],
            operator=expr["operator"],
            expected=expr["expected"],
            actual=expr.get("actual"),
            matched=expr["matched"],
            source=expr.get("source"),
        )]

    if kind == "contains":
        return [ConditionResult(
            field=expr["field"],
            operator="contains",
            expected=expr["expected"],
            actual=expr.get("actual"),
            matched=expr["matched"],
            source=expr.get("source"),
        )]

    if kind == "like":
        return [ConditionResult(
            field=expr["field"],
            operator="like",
            expected=expr["pattern"],
            actual=expr.get("actual"),
            matched=expr["matched"],
            source=expr.get("source"),
        )]

    return []


# =============================================================================
# Condition Evaluation
# =============================================================================

def _evaluate_condition(
    operator: ConditionOperator,
    actual: Any,
    expected: Union[str, int, float, bool, list[str]],
    is_missing: bool,
) -> bool:
    """Evaluate a single condition against an actual context value."""
    if is_missing or actual is None:
        return False

    if operator == "eq":
        return _compare_equal(actual, expected)
    elif operator == "neq":
        return not _compare_equal(actual, expected)
    elif operator == "lt":
        return _compare_numeric(actual, expected, lambda a, b: a < b)
    elif operator == "lte":
        return _compare_numeric(actual, expected, lambda a, b: a <= b)
    elif operator == "gt":
        return _compare_numeric(actual, expected, lambda a, b: a > b)
    elif operator == "gte":
        return _compare_numeric(actual, expected, lambda a, b: a >= b)
    elif operator == "contains":
        return _evaluate_contains(actual, expected)
    elif operator == "in":
        return _evaluate_in(actual, expected)
    elif operator == "like":
        return _evaluate_like(actual, expected)
    return False


def _compare_equal(actual: Any, expected: Any) -> bool:
    """Compare equality with type awareness."""
    # Direct comparison for matching types
    if type(actual) == type(expected):
        return actual == expected

    # Cross-type numeric comparison
    a = _to_number(actual)
    b = _to_number(expected)
    if a is not None and b is not None:
        return a == b

    # String-to-string fallback
    if isinstance(actual, str) and isinstance(expected, str):
        return actual == expected

    return False


def _compare_numeric(actual: Any, expected: Any, comparator) -> bool:
    """Numeric comparison with type coercion."""
    a = _to_number(actual)
    b = _to_number(expected)
    if a is None or b is None:
        return False
    return comparator(a, b)


def _to_number(value: Any) -> Optional[float]:
    """Convert a value to a number, or return None."""
    if isinstance(value, (int, float)):
        return float(value)
    if isinstance(value, str):
        try:
            return float(value)
        except ValueError:
            return None
    return None


def _evaluate_contains(actual: Any, expected: Any) -> bool:
    """Evaluate contains on string or array."""
    # String contains — substring match
    if isinstance(actual, str) and isinstance(expected, str):
        return expected in actual
    # Array contains — type-aware element match
    if isinstance(actual, list):
        return any(_compare_equal(item, expected) for item in actual)
    return False


def _evaluate_in(actual: Any, expected: Any) -> bool:
    """Evaluate whether actual is in the expected array."""
    if isinstance(expected, list):
        return any(_compare_equal(actual, item) for item in expected)
    return False


def _evaluate_like(actual: Any, expected: Any) -> bool:
    """Evaluate Cedar like pattern matching."""
    if not isinstance(actual, str) or not isinstance(expected, str):
        return False
    regex = _like_pattern_to_regex(expected)
    return bool(regex.match(actual))


def _like_pattern_to_regex(pattern: str) -> re.Pattern:
    """
    Convert a Cedar like pattern to a Python regex.
    Cedar like uses * as a wildcard (zero or more characters).
    \\* is a literal asterisk.
    """
    result = "^"
    i = 0
    while i < len(pattern):
        ch = pattern[i]
        if ch == "\\" and i + 1 < len(pattern) and pattern[i + 1] == "*":
            result += "\\*"
            i += 2
        elif ch == "*":
            result += ".*"
            i += 1
        else:
            result += re.escape(ch)
            i += 1
    result += "$"
    return re.compile(result)


# =============================================================================
# Summary Generation
# =============================================================================

_OPERATOR_SYMBOLS: dict[str, str] = {
    "eq": "==",
    "neq": "!=",
    "lt": "<",
    "lte": "<=",
    "gt": ">",
    "gte": ">=",
    "contains": "contains",
    "in": "in",
    "like": "like",
}


def _build_tree_summary(
    rule: PolicyRule,
    evaluated: EvaluatedExpression,
) -> str:
    """Build a human-readable summary from an evaluated expression tree.

    Skips 'has' nodes (they're just null-guards) and focuses on leaf conditions.
    """
    effect = rule.get("effect", "forbid")
    action = _get_action_label(rule)
    parts = _collect_leaf_summaries(evaluated)
    if not parts:
        return f"{effect} {action} — (no conditions)"
    return f"{effect} {action} — {', '.join(parts)}"


def _collect_leaf_summaries(expr: EvaluatedExpression) -> list[str]:
    """Collect human-readable summaries from leaf nodes in the evaluated tree.

    Skips 'has' and 'raw' nodes; recurses into and/or/not.
    """
    kind = expr.get("kind")

    if kind in ("and", "or"):
        results: list[str] = []
        for child in expr.get("children", []):
            results.extend(_collect_leaf_summaries(child))
        return results

    if kind == "not":
        return [f"NOT ({s})" for s in _collect_leaf_summaries(expr["child"])]

    if kind == "has":
        return []  # skip null-guards in summary

    if kind == "comparison":
        op = _OPERATOR_SYMBOLS.get(expr["operator"], expr["operator"])
        return [f"{expr['field']}: {_format_value(expr.get('actual'))} {op} {_format_value(expr['expected'])}"]

    if kind == "contains":
        return [f"{expr['field']}: {_format_value(expr.get('actual'))} contains {_format_value(expr['expected'])}"]

    if kind == "like":
        return [f"{expr['field']}: {_format_value(expr.get('actual'))} like {_format_value(expr['pattern'])}"]

    if kind == "raw":
        return [f"(raw: {expr.get('text', '')})"]

    return []


def _build_summary(
    rule: PolicyRule,
    condition_results: list[ConditionResult],
    raw_condition: Optional[str],
) -> str:
    """Generate a human-readable summary of the policy explanation."""
    effect = rule.get("effect", "forbid")
    action = _get_action_label(rule)

    if raw_condition:
        return f"{effect} {action} — (raw condition: {raw_condition})"

    if not condition_results:
        return f"{effect} {action} — (no conditions)"

    parts = []
    for cr in condition_results:
        op = _OPERATOR_SYMBOLS.get(cr.operator, cr.operator)
        actual_str = _format_value(cr.actual)
        expected_str = _format_value(cr.expected)
        parts.append(f"{cr.field} ({actual_str}) {op} {expected_str}")

    return f"{effect} {action} — {', '.join(parts)}"


def _get_action_label(rule: PolicyRule) -> str:
    """Get action as a display-friendly label."""
    action = rule.get("action")
    if not action:
        return "*"
    if isinstance(action, str):
        # Strip namespace prefix
        if '::"' in action:
            action = action.rsplit('::"', 1)[-1].rstrip('"')
        return action
    if isinstance(action, list) and len(action) > 0:
        labels = []
        for a in action:
            if '::"' in a:
                a = a.rsplit('::"', 1)[-1].rstrip('"')
            labels.append(a)
        return ", ".join(labels)
    return "*"


def _format_value(value: Any) -> str:
    """Format a value for display in summaries."""
    import json
    if value is None:
        return "undefined"
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, str):
        return json.dumps(value)
    if isinstance(value, list):
        items = ", ".join(json.dumps(v) if isinstance(v, str) else str(v) for v in value)
        return f"[{items}]"
    return str(value)
