"""Core SCP evaluation engine."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from ..models.cloudtrail import CloudTrailEvent
from ..models.report import EvaluationContext, EvaluationResult
from ..models.scp import SCPPolicy, SCPStatement

if TYPE_CHECKING:
    from ..models.external_context import ExternalContext
from .condition_evaluator import evaluate_condition_block
from .matcher import (
    action_excluded_by_not_action,
    action_matches_any,
    principal_excluded_by_not_principal,
    principal_matches_any,
    resource_excluded_by_not_resource,
    resource_matches_any,
)
from .resource_extractor import ResourceExtractor


class SCPEngine:
    """
    Engine for evaluating whether CloudTrail events would be denied by SCPs.

    Implements AWS SCP evaluation logic:
    1. Explicit Deny in any SCP = DENY (highest priority)
    2. Explicit Allow in all applicable SCPs = ALLOW
    3. No explicit Allow = DENY (implicit deny)

    For MVP, we focus on Deny-based SCPs which are most common.
    """

    def __init__(
        self,
        scp_policies: list[SCPPolicy],
        external_context: ExternalContext | None = None,
        strict_conditions: bool = False,
    ):
        """
        Initialize the SCP engine with policies to evaluate against.

        Args:
            scp_policies: List of SCP policies to evaluate
            external_context: Optional external context for enriching evaluations
            strict_conditions: When True, unevaluable conditions resolve to
                non-matching instead of matching (worst-case analysis)
        """
        self.policies = scp_policies
        self._resource_extractor = ResourceExtractor()
        self._external_context = external_context
        self._strict_conditions = strict_conditions

    def would_deny(
        self,
        event: CloudTrailEvent,
        context: EvaluationContext | None = None,
    ) -> EvaluationResult:
        """
        Determine if a CloudTrail event would be denied by the SCPs.

        Args:
            event: CloudTrail event to evaluate
            context: Optional evaluation context (created from event if not provided)

        Returns:
            EvaluationResult indicating if the action would be denied
        """
        if context is None:
            context = EvaluationContext.from_event(event)

        action = event.iam_action

        # Extract resource ARN from CloudTrail event using multi-layer extractor
        resource_arn = self._resource_extractor.extract(event)
        resource_arn_resolved = resource_arn is not None
        context.resource_arn = resource_arn

        # Enrich with external context data (after resource_arn is set,
        # since resource tag lookup depends on it)
        if self._external_context:
            context.enrich(self._external_context)

        # Track unevaluable condition keys across all statement evaluations
        all_unevaluable_keys: list[str] = []

        # Step 1: Check for explicit Deny statements
        # If any SCP has a matching Deny, the action is denied
        for policy in self.policies:
            for statement in policy.get_deny_statements():
                match, unevaluable_keys = self._statement_matches_with_metadata(
                    statement, action, context, resource_arn
                )
                all_unevaluable_keys.extend(unevaluable_keys)
                if match:
                    return EvaluationResult(
                        denied=True,
                        reason="Explicit Deny in SCP",
                        matched_statement=statement,
                        policy_name=policy.policy_name,
                        unevaluable_condition_keys=all_unevaluable_keys,
                        resource_arn_resolved=resource_arn_resolved,
                    )

        # Step 2: Check for explicit Allow statements
        # For SCPs, if there are Allow statements, at least one must match
        allow_statements_exist = any(policy.has_allow_statements for policy in self.policies)

        if allow_statements_exist:
            has_allow_match = False
            for policy in self.policies:
                for statement in policy.get_allow_statements():
                    match, unevaluable_keys = self._statement_matches_with_metadata(
                        statement, action, context, resource_arn
                    )
                    all_unevaluable_keys.extend(unevaluable_keys)
                    if match:
                        has_allow_match = True
                        break
                if has_allow_match:
                    break

            if not has_allow_match:
                # There are Allow statements but none matched
                return EvaluationResult(
                    denied=True,
                    reason="No matching Allow statement (implicit deny)",
                    matched_statement=None,
                    policy_name=None,
                    unevaluable_condition_keys=all_unevaluable_keys,
                    resource_arn_resolved=resource_arn_resolved,
                )

        # If we get here:
        # - No Deny statements matched
        # - Either no Allow statements exist, or an Allow statement matched
        # Action is allowed
        return EvaluationResult(
            denied=False,
            reason="No deny rules matched",
            unevaluable_condition_keys=all_unevaluable_keys,
            resource_arn_resolved=resource_arn_resolved,
        )

    def _statement_matches_with_metadata(
        self,
        statement: SCPStatement,
        action: str,
        context: EvaluationContext,
        resource_arn: str | None = None,
    ) -> tuple[bool, list[str]]:
        """
        Check if a statement matches and return unevaluable condition keys.

        Returns:
            Tuple of (matches, unevaluable_condition_keys)
        """
        # Reuse the existing match logic but capture condition metadata
        if statement.uses_not_action:
            if action_excluded_by_not_action(statement.not_actions or [], action):
                return False, []
        else:
            if not action_matches_any(statement.actions, action):
                return False, []

        if statement.uses_not_resource:
            if resource_excluded_by_not_resource(statement.not_resources or [], resource_arn):
                return False, []
        else:
            if not resource_matches_any(statement.resources, resource_arn):
                return False, []

        principal_arn = context.principal_arn
        if statement.uses_not_principal:
            if principal_excluded_by_not_principal(statement.not_principals or [], principal_arn):
                return False, []
        else:
            if not principal_matches_any(statement.principals, principal_arn):
                return False, []

        if statement.conditions:
            condition_result = evaluate_condition_block(
                statement.conditions, context, strict_conditions=self._strict_conditions
            )
            return condition_result.matches, condition_result.unevaluable_keys

        return True, []

    def _statement_matches(
        self,
        statement: SCPStatement,
        action: str,
        context: EvaluationContext,
        resource_arn: str | None = None,
    ) -> bool:
        """
        Check if a statement matches the given action, resource, principal, and context.

        Handles:
        - Action vs NotAction matching
        - Resource vs NotResource matching
        - Principal vs NotPrincipal matching
        - Condition evaluation

        Args:
            statement: SCP statement to evaluate
            action: IAM action to check
            context: Evaluation context
            resource_arn: Optional resource ARN from CloudTrail event

        Returns:
            True if the statement matches
        """
        # Check if action matches (handle both Action and NotAction)
        if statement.uses_not_action:
            # NotAction: statement matches if action is NOT in the excluded list
            if action_excluded_by_not_action(statement.not_actions or [], action):
                return False  # Action is excluded, statement doesn't apply
            # Action is not excluded, continue to other checks
        else:
            # Regular Action: must match one of the listed actions
            if not action_matches_any(statement.actions, action):
                return False

        # Check if resource matches (handle both Resource and NotResource)
        if statement.uses_not_resource:
            # NotResource: statement matches if resource is NOT in the excluded list
            if resource_excluded_by_not_resource(statement.not_resources or [], resource_arn):
                return False  # Resource is excluded, statement doesn't apply
            # Resource is not excluded, continue to other checks
        else:
            # Regular Resource: must match one of the listed resources
            if not resource_matches_any(statement.resources, resource_arn):
                return False

        # Check if principal matches (handle both Principal and NotPrincipal)
        principal_arn = context.principal_arn
        if statement.uses_not_principal:
            # NotPrincipal: statement matches if principal is NOT in the excluded list
            if principal_excluded_by_not_principal(statement.not_principals or [], principal_arn):
                return False  # Principal is excluded, statement doesn't apply
            # Principal is not excluded, continue to condition check
        else:
            # Regular Principal: must match one of the listed principals
            if not principal_matches_any(statement.principals, principal_arn):
                return False

        # Evaluate conditions if present
        if statement.conditions:
            condition_result = evaluate_condition_block(statement.conditions, context)
            return condition_result.matches

        # No conditions, so it's a match
        return True

    def evaluate_batch(
        self, events: list[CloudTrailEvent]
    ) -> list[tuple[CloudTrailEvent, EvaluationResult]]:
        """
        Evaluate multiple events in batch.

        Args:
            events: List of CloudTrail events to evaluate

        Returns:
            List of (event, result) tuples
        """
        results = []
        for event in events:
            result = self.would_deny(event)
            results.append((event, result))
        return results

    def get_deny_summary(self) -> dict[str, Any]:
        """
        Get a summary of the deny rules in the loaded SCPs.

        Returns:
            Dictionary with summary information
        """
        total_statements = sum(len(p.statements) for p in self.policies)
        deny_statements = sum(len(p.get_deny_statements()) for p in self.policies)
        allow_statements = sum(len(p.get_allow_statements()) for p in self.policies)

        # Collect all denied actions
        denied_actions = set()
        for policy in self.policies:
            for statement in policy.get_deny_statements():
                if statement.uses_not_action:
                    denied_actions.add("* (NotAction)")
                else:
                    denied_actions.update(statement.actions)

        return {
            "total_policies": len(self.policies),
            "total_statements": total_statements,
            "deny_statements": deny_statements,
            "allow_statements": allow_statements,
            "unique_denied_actions": len(denied_actions),
            "denied_action_patterns": sorted(denied_actions),
        }
