"""
SCP Condition Evaluator.

Evaluates AWS IAM/SCP policy conditions against CloudTrail event context.
Supports common condition operators like StringEquals, StringLike, ArnLike, etc.

Operator implementations live in condition_operators.py.
Key extraction logic lives in condition_key_extractors.py.
Both are re-exported here for backward compatibility.
"""

from __future__ import annotations

from typing import Any

from ..models.report import EvaluationContext

# Re-export key extractors for backward compatibility
from .condition_key_extractors import (  # noqa: F401
    CONTEXT_KEY_MAP,
    GLOBAL_KEY_EXTRACTORS,
    KNOWN_UNOBTAINABLE_KEYS,
    NULL,
    RESOLVED,
    SERVICE_KEY_EXTRACTORS,
    SERVICE_SPECIFIC,
    UNKNOWN,
    UNOBTAINABLE,
    _get_all_service_extractors,
    get_context_value,
    get_context_value_with_status,
)

# Re-export operators for backward compatibility
from .condition_operators import (  # noqa: F401
    OPERATORS,
    arn_equals,
    arn_like,
    arn_not_equals,
    arn_not_like,
    binary_equals,
    bool_equals,
    date_equals,
    date_greater_than,
    date_greater_than_equals,
    date_less_than,
    date_less_than_equals,
    date_not_equals,
    ip_address_matches,
    not_ip_address,
    null_check,
    numeric_equals,
    numeric_greater_than,
    numeric_greater_than_equals,
    numeric_less_than,
    numeric_less_than_equals,
    numeric_not_equals,
    string_equals,
    string_equals_ignore_case,
    string_like,
    string_not_equals,
    string_not_equals_ignore_case,
    string_not_like,
)


class ConditionEvaluationResult:
    """Result of condition evaluation."""

    def __init__(
        self,
        matches: bool,
        reason: str = "",
        unevaluable_keys: list[str] | None = None,
    ):
        self.matches = matches
        self.reason = reason
        self.unevaluable_keys = unevaluable_keys or []

    def __bool__(self) -> bool:
        return self.matches


def _normalize_to_list(value: Any) -> list[str]:
    """Normalize a condition value to a list of strings."""
    if isinstance(value, list):
        return [str(v) for v in value]
    return [str(value)]


def _parse_operator(operator: str) -> tuple[str, bool, str | None]:
    """
    Parse an operator to extract the base operator and any modifiers.

    Returns:
        Tuple of (base_operator, is_if_exists, set_operator)
        set_operator is "ForAllValues", "ForAnyValue", or None
    """
    is_if_exists = False
    set_operator: str | None = None

    # Check for IfExists suffix
    if operator.endswith("IfExists"):
        is_if_exists = True
        operator = operator[:-8]  # Remove "IfExists"

    # Check for ForAllValues/ForAnyValue prefix
    if operator.startswith("ForAllValues:"):
        set_operator = "ForAllValues"
        operator = operator[13:]
    elif operator.startswith("ForAnyValue:"):
        set_operator = "ForAnyValue"
        operator = operator[12:]

    return operator, is_if_exists, set_operator


def evaluate_single_condition(
    operator: str,
    condition_key: str,
    condition_values: Any,
    context: EvaluationContext,
    strict_conditions: bool = False,
) -> ConditionEvaluationResult:
    """
    Evaluate a single condition against the context.

    Args:
        operator: The condition operator (e.g., "StringEquals")
        condition_key: The context key (e.g., "aws:SourceIp")
        condition_values: The expected value(s)
        context: The evaluation context
        strict_conditions: When True, unevaluable conditions resolve to
            non-matching (False) instead of matching (True)

    Returns:
        ConditionEvaluationResult with match status
    """
    # Parse operator modifiers
    base_operator, is_if_exists, set_operator = _parse_operator(operator)

    # Get the operator function
    op_func = OPERATORS.get(base_operator)
    if op_func is None:
        # Unknown operator - can't evaluate, treat as non-matching for safety
        return ConditionEvaluationResult(
            matches=False,
            reason=f"Unknown operator: {operator}",
            unevaluable_keys=[condition_key],
        )

    # Get context value with resolution status
    context_value, status = get_context_value_with_status(context, condition_key)

    # Handle IfExists: if key doesn't exist, condition is satisfied
    if is_if_exists and context_value is None:
        return ConditionEvaluationResult(
            matches=True,
            reason=f"IfExists: key {condition_key} not present",
        )

    # Handle based on resolution status when value is None
    if context_value is None:
        if status == UNOBTAINABLE:
            # Normal: assume matches (conservative). Strict: assume no match (worst-case).
            return ConditionEvaluationResult(
                matches=not strict_conditions,
                reason=f"Cannot evaluate unobtainable context key: {condition_key}",
                unevaluable_keys=[condition_key],
            )
        if status == SERVICE_SPECIFIC:
            # Normal: assume matches (conservative). Strict: assume no match (worst-case).
            return ConditionEvaluationResult(
                matches=not strict_conditions,
                reason=f"Cannot evaluate service-specific context key: {condition_key}",
                unevaluable_keys=[condition_key],
            )
        if status == UNKNOWN:
            # Truly unknown key: don't assume match in either mode
            return ConditionEvaluationResult(
                matches=False,
                reason=f"Unknown context key: {condition_key}",
                unevaluable_keys=[condition_key],
            )
        # status == NULL or RESOLVED with None: key is known, value absent
        # For Null operator, we need to proceed to let it evaluate
        if base_operator == "Null":
            pass  # Fall through to normal evaluation
        else:
            return ConditionEvaluationResult(
                matches=False,
                reason=f"Context key {condition_key} has no value",
            )

    # Normalize condition values to list
    values_list = _normalize_to_list(condition_values)

    # Handle set operators
    if set_operator:
        # For set operators, context_value should be a list
        if not isinstance(context_value, list):
            context_values = [context_value] if context_value is not None else []
        else:
            context_values = context_value

        if set_operator == "ForAllValues":
            # All context values must satisfy the condition
            if not context_values:
                return ConditionEvaluationResult(matches=True)
            for cv in context_values:
                if not op_func(cv, values_list):
                    return ConditionEvaluationResult(
                        matches=False,
                        reason=f"ForAllValues: {cv} did not match",
                    )
            return ConditionEvaluationResult(matches=True)

        elif set_operator == "ForAnyValue":
            # At least one context value must satisfy the condition
            for cv in context_values:
                if op_func(cv, values_list):
                    return ConditionEvaluationResult(matches=True)
            return ConditionEvaluationResult(
                matches=False,
                reason="ForAnyValue: no values matched",
            )

    # Standard evaluation
    result = op_func(context_value, values_list)
    return ConditionEvaluationResult(
        matches=result,
        reason="" if result else f"{condition_key} did not match {base_operator}",
    )


def evaluate_condition_block(
    conditions: dict[str, Any],
    context: EvaluationContext,
    strict_conditions: bool = False,
) -> ConditionEvaluationResult:
    """
    Evaluate a complete condition block from an SCP statement.

    In AWS, all operators in a condition block must be satisfied (AND logic).
    Within each operator, any key match is sufficient (OR logic within operator).

    Args:
        conditions: The Condition block from an SCP statement
        context: The evaluation context
        strict_conditions: When True, unevaluable conditions resolve to
            non-matching (worst-case analysis)

    Returns:
        ConditionEvaluationResult with overall match status
    """
    if not conditions:
        return ConditionEvaluationResult(matches=True, reason="No conditions")

    all_unevaluable_keys: list[str] = []

    # Each operator must be satisfied (AND)
    for operator, key_values in conditions.items():
        if not isinstance(key_values, dict):
            continue

        # All keys within an operator must match (AND)
        for condition_key, condition_values in key_values.items():
            result = evaluate_single_condition(
                operator, condition_key, condition_values, context,
                strict_conditions=strict_conditions,
            )

            all_unevaluable_keys.extend(result.unevaluable_keys)

            if not result.matches:
                return ConditionEvaluationResult(
                    matches=False,
                    reason=result.reason,
                    unevaluable_keys=all_unevaluable_keys,
                )

    return ConditionEvaluationResult(
        matches=True,
        reason="All conditions satisfied",
        unevaluable_keys=all_unevaluable_keys,
    )
