"""Main impact analysis orchestration."""

from __future__ import annotations

from collections.abc import Callable
from datetime import datetime
from typing import TYPE_CHECKING, Any

from ..engine.scp_engine import SCPEngine
from ..ingest.base import CloudTrailIngester
from ..models.cloudtrail import CloudTrailEvent
from ..models.report import EvaluationContext, EvaluationResult, ImpactReport
from ..models.scp import SCPPolicy
from .statistics import calculate_statistics

if TYPE_CHECKING:
    from ..models.external_context import ExternalContext

# Data events that are often not logged (require explicit CloudTrail configuration)
DATA_EVENT_ACTIONS = {
    # S3 data events
    "s3:GetObject",
    "s3:GetObjectVersion",
    "s3:PutObject",
    "s3:DeleteObject",
    "s3:DeleteObjectVersion",
    "s3:HeadObject",
    "s3:GetObjectAcl",
    "s3:GetObjectTagging",
    "s3:PutObjectAcl",
    "s3:PutObjectTagging",
    # Lambda data events
    "lambda:InvokeFunction",
    "lambda:InvokeAsync",
    # DynamoDB data events
    "dynamodb:GetItem",
    "dynamodb:PutItem",
    "dynamodb:DeleteItem",
    "dynamodb:UpdateItem",
    "dynamodb:Query",
    "dynamodb:Scan",
    "dynamodb:BatchGetItem",
    "dynamodb:BatchWriteItem",
    "dynamodb:TransactGetItems",
    "dynamodb:TransactWriteItems",
}


class ImpactAnalyzer:
    """
    Orchestrates the impact analysis process.

    Coordinates between ingester, engine, and statistics calculation
    to produce a comprehensive impact report.
    """

    def __init__(
        self,
        scp_policies: list[SCPPolicy],
        cloudtrail_ingester: CloudTrailIngester,
        external_context: ExternalContext | None = None,
        strict_conditions: bool = False,
    ):
        """
        Initialize the impact analyzer.

        Args:
            scp_policies: List of SCP policies to evaluate
            cloudtrail_ingester: Ingester for CloudTrail events
            external_context: Optional external context for enriching evaluations
            strict_conditions: When True, unevaluable conditions resolve to
                non-matching (worst-case upper-bound denial rate)
        """
        self.policies = scp_policies
        self.ingester = cloudtrail_ingester
        self.external_context = external_context
        self.strict_conditions = strict_conditions
        self.engine = SCPEngine(
            scp_policies,
            external_context=external_context,
            strict_conditions=strict_conditions,
        )

    def analyze(
        self,
        start_time: datetime,
        end_time: datetime,
        progress_callback: Callable[[int, str], None] | None = None,
    ) -> ImpactReport:
        """
        Perform impact analysis on CloudTrail events.

        Main workflow:
        1. Fetch CloudTrail events from ingester
        2. Build evaluation context for each event
        3. Evaluate against SCP policies
        4. Aggregate results
        5. Calculate statistics
        6. Generate impact report with warnings

        Args:
            start_time: Start of analysis period
            end_time: End of analysis period
            progress_callback: Optional callback(count, message) for progress updates

        Returns:
            ImpactReport with complete analysis results
        """
        denied_events: list[tuple[CloudTrailEvent, EvaluationResult]] = []
        allowed_events: list[CloudTrailEvent] = []
        all_results: list[EvaluationResult] = []
        observed_actions: set[str] = set()
        total_events = 0
        slr_events = 0
        mgmt_events = 0

        # Determine management account ID from external context
        mgmt_account_id: str | None = None
        if self.external_context and self.external_context.management_account_id:
            mgmt_account_id = self.external_context.management_account_id

        if progress_callback:
            progress_callback(0, "Starting analysis...")

        # Fetch and evaluate events
        for event in self.ingester.fetch_events(start_time, end_time):
            total_events += 1

            # Filter: SCPs do not apply to service-linked roles
            if event.is_service_linked_role:
                slr_events += 1
                continue

            # Filter: SCPs do not apply to management account principals
            if mgmt_account_id and event.account_id == mgmt_account_id:
                mgmt_events += 1
                continue

            # Track observed actions for data event detection
            observed_actions.add(event.iam_action)

            # Build context and evaluate
            context = EvaluationContext.from_event(event)
            result = self.engine.would_deny(event, context)

            # Collect all results for confidence metrics
            all_results.append(result)

            if result.denied:
                denied_events.append((event, result))
            else:
                allowed_events.append(event)

            # Progress update every 100 events
            if progress_callback and total_events % 100 == 0:
                progress_callback(
                    total_events,
                    f"Analyzed {total_events} events, found {len(denied_events)} denials",
                )

        if progress_callback:
            progress_callback(
                total_events,
                f"Analysis complete: {total_events} events processed",
            )

        # Calculate statistics with all results for confidence metrics
        statistics = calculate_statistics(
            denied_events=denied_events,
            analysis_period=(start_time, end_time),
            all_results=all_results,
        )
        statistics.slr_events_filtered = slr_events
        statistics.mgmt_account_events_filtered = mgmt_events

        # Generate warnings
        warnings = self._generate_warnings(observed_actions)

        # Determine policy name
        policy_name = None
        if self.policies:
            if self.policies[0].policy_name:
                policy_name = self.policies[0].policy_name
            elif len(self.policies) == 1:
                policy_name = "Single Policy"
            else:
                policy_name = f"{len(self.policies)} Policies"

        # Build report -- total_events excludes filtered SLR/mgmt events
        evaluated_events = total_events - slr_events - mgmt_events
        return ImpactReport(
            total_events=evaluated_events,
            denied_count=len(denied_events),
            allowed_count=len(allowed_events),
            denied_events=denied_events,
            statistics=statistics,
            scp_policies=self.policies,
            analysis_period=(start_time, end_time),
            scp_policy_name=policy_name,
            warnings=warnings,
        )

    def _generate_warnings(self, observed_actions: set[str]) -> list[str]:
        """
        Generate warnings about simulation limitations.

        Args:
            observed_actions: Set of IAM actions observed in CloudTrail logs

        Returns:
            List of warning messages
        """
        warnings: list[str] = []

        # Extract actions targeted by SCP policies
        scp_targeted_actions = self._get_scp_targeted_actions()

        # Check for data events that SCP targets but weren't in logs
        missing_data_events = self._detect_missing_data_events(
            scp_targeted_actions, observed_actions
        )
        if missing_data_events:
            services = set()
            for action in missing_data_events:
                service = action.split(":")[0] if ":" in action else action
                services.add(service)

            if "s3" in services:
                warnings.append(
                    "SCP targets S3 data events (GetObject, PutObject, etc.) but none were "
                    "found in CloudTrail logs. Data events require explicit trail configuration. "
                    "The simulation may underestimate actual denials."
                )
            if "lambda" in services:
                warnings.append(
                    "SCP targets Lambda invoke events but none were found in CloudTrail logs. "
                    "Lambda data events require explicit trail configuration."
                )
            if "dynamodb" in services:
                warnings.append(
                    "SCP targets DynamoDB data events (GetItem, PutItem, etc.) "
                    "but none were found in CloudTrail logs. DynamoDB data "
                    "events require explicit trail configuration."
                )

        # Warn when multiple policies are evaluated without OU hierarchy
        if len(self.policies) > 1:
            warnings.append(
                "Multiple SCP policies are being evaluated as a flat set. "
                "In AWS, SCPs are evaluated hierarchically (Root -> OU -> Account) "
                "and an action must be allowed at every level. "
                "Flat evaluation may underestimate or overestimate denials."
            )

        return warnings

    def _get_scp_targeted_actions(self) -> set[str]:
        """Extract all actions targeted by the SCP policies."""
        actions: set[str] = set()
        for policy in self.policies:
            for statement in policy.statements:
                # Direct actions
                for action in statement.actions:
                    actions.add(action.lower())
                # NotAction means all other actions are targeted
                if statement.not_actions:
                    # Can't enumerate all actions, but we can check for wildcards
                    pass
        return actions

    def _detect_missing_data_events(
        self, scp_actions: set[str], observed_actions: set[str]
    ) -> set[str]:
        """
        Detect data event actions that SCP targets but aren't in logs.

        Args:
            scp_actions: Actions targeted by SCPs (may include wildcards)
            observed_actions: Actions observed in CloudTrail logs

        Returns:
            Set of missing data event actions
        """
        missing: set[str] = set()
        observed_lower = {a.lower() for a in observed_actions}

        for data_action in DATA_EVENT_ACTIONS:
            data_action_lower = data_action.lower()
            service = data_action_lower.split(":")[0]

            # Check if this data action is targeted by the SCP
            is_targeted = False
            for scp_action in scp_actions:
                if scp_action == "*":
                    is_targeted = True
                    break
                elif scp_action == f"{service}:*":
                    is_targeted = True
                    break
                elif scp_action == data_action_lower:
                    is_targeted = True
                    break
                elif scp_action.endswith("*"):
                    prefix = scp_action[:-1]
                    if data_action_lower.startswith(prefix):
                        is_targeted = True
                        break

            # If targeted but not observed, it's missing
            if is_targeted and data_action_lower not in observed_lower:
                missing.add(data_action)

        return missing

    def quick_summary(
        self, start_time: datetime, end_time: datetime, sample_size: int = 1000
    ) -> dict[str, Any]:
        """
        Perform a quick analysis on a sample of events.

        Useful for large datasets to get a quick estimate before full analysis.

        Args:
            start_time: Start of analysis period
            end_time: End of analysis period
            sample_size: Maximum number of events to analyze

        Returns:
            Dictionary with summary statistics
        """
        denied_count = 0
        total_count = 0

        for event in self.ingester.fetch_events(start_time, end_time):
            total_count += 1
            result = self.engine.would_deny(event)

            if result.denied:
                denied_count += 1

            if total_count >= sample_size:
                break

        return {
            "sampled_events": total_count,
            "denied_count": denied_count,
            "allowed_count": total_count - denied_count,
            "estimated_denial_rate": (denied_count / total_count * 100) if total_count > 0 else 0,
            "is_sample": total_count >= sample_size,
        }
