"""Report and analysis result data models."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING

from .cloudtrail import CloudTrailEvent
from .scp import SCPPolicy, SCPStatement

if TYPE_CHECKING:
    from .external_context import ExternalContext


@dataclass
class EvaluationContext:
    """
    Context information for evaluating an SCP against a CloudTrail event.

    This contains all the contextual information extracted from a CloudTrail
    event that might be needed for condition evaluation.
    """

    event: CloudTrailEvent
    region: str
    account_id: str | None
    source_ip: str | None
    user_agent: str | None
    request_time: datetime
    principal_arn: str | None
    principal_type: str

    # Identity
    principal_account: str | None = None
    user_id: str | None = None
    username: str | None = None

    # Security
    mfa_authenticated: bool | None = None
    secure_transport: bool | None = None

    # Time
    epoch_time: int | None = None
    token_issue_time: str | None = None

    # Network
    source_vpc: str | None = None
    source_vpce: str | None = None

    # Service context
    called_via: str | None = None
    via_aws_service: bool | None = None

    # Tags
    request_tags: dict[str, str] | None = None
    resource_tags: dict[str, str] | None = None
    tag_keys: list[str] | None = None

    # Organization (from external context)
    principal_org_id: str | None = None
    principal_org_paths: list[str] | None = None

    # Principal tags (from external context, distinct from request_tags)
    principal_tags: dict[str, str] | None = None

    # Resource (set by engine after extraction)
    resource_arn: str | None = None

    @classmethod
    def from_event(cls, event: CloudTrailEvent) -> EvaluationContext:
        """Create an EvaluationContext from a CloudTrailEvent."""
        identity = event.user_identity or {}

        # Extract MFA from session context
        mfa_authenticated: bool | None = None
        session_context = identity.get("sessionContext", {})
        if session_context:
            attrs = session_context.get("attributes", {})
            mfa_val = attrs.get("mfaAuthenticated")
            if mfa_val is not None:
                mfa_authenticated = str(mfa_val).lower() == "true"

        # Extract secure transport from TLS details
        secure_transport: bool | None = None
        if event.tls_details is not None:
            secure_transport = True
        elif event.source_ip_address:
            # If we have a source IP but no TLS details, we can't determine
            secure_transport = None

        # Extract VPC endpoint
        source_vpce = event.vpc_endpoint_id

        # Extract called via from additional event data
        called_via: str | None = None
        via_aws_service: bool | None = None
        if event.additional_event_data:
            called_via = event.additional_event_data.get("calledVia")
        # If invokedBy is present in userIdentity, this was via an AWS service
        invoked_by = identity.get("invokedBy")
        if invoked_by:
            via_aws_service = True
            if called_via is None:
                called_via = invoked_by

        # Extract tags from request parameters
        request_tags: dict[str, str] | None = None
        tag_keys: list[str] | None = None
        if event.request_parameters:
            tags = event.request_parameters.get("tags")
            if isinstance(tags, dict):
                request_tags = {str(k): str(v) for k, v in tags.items()}
                tag_keys = list(request_tags.keys())
            elif isinstance(tags, list):
                request_tags = {}
                for tag in tags:
                    if isinstance(tag, dict) and "key" in tag:
                        request_tags[str(tag["key"])] = str(tag.get("value", ""))
                tag_keys = list(request_tags.keys())

        # Extract token issue time from session context
        token_issue_time: str | None = None
        if session_context:
            attrs = session_context.get("attributes", {})
            creation_date = attrs.get("creationDate")
            if creation_date:
                token_issue_time = str(creation_date)

        return cls(
            event=event,
            region=event.aws_region,
            account_id=event.account_id,
            source_ip=event.source_ip_address,
            user_agent=event.user_agent,
            request_time=event.event_time,
            principal_arn=event.principal_arn,
            principal_type=event.principal_type,
            principal_account=event.account_id,
            user_id=identity.get("principalId"),
            username=identity.get("userName"),
            mfa_authenticated=mfa_authenticated,
            secure_transport=secure_transport,
            epoch_time=int(event.event_time.timestamp()),
            token_issue_time=token_issue_time,
            source_vpc=None,  # Not directly in CloudTrail events
            source_vpce=source_vpce,
            called_via=called_via,
            via_aws_service=via_aws_service,
            request_tags=request_tags,
            resource_tags=None,  # Not directly available from CloudTrail
            tag_keys=tag_keys,
        )

    def enrich(self, external: ExternalContext) -> None:
        """Enrich this context with external data not available in CloudTrail.

        Must be called AFTER resource_arn is set by the engine, since
        resource tag lookup depends on it.

        Args:
            external: External context data supplied by the user.
        """
        # Organization data (always applied)
        if external.org_id:
            self.principal_org_id = external.org_id
        if external.org_paths:
            self.principal_org_paths = external.org_paths

        # Principal tags (matched by principal ARN)
        ptags = external.lookup_principal_tags(self.principal_arn)
        if ptags:
            self.principal_tags = ptags

        # Resource tags (matched by resource ARN)
        rtags = external.lookup_resource_tags(self.resource_arn)
        if rtags:
            self.resource_tags = rtags

        # VPC resolution (vpce -> vpc)
        if self.source_vpce and not self.source_vpc:
            vpc_id = external.lookup_vpc_id(self.source_vpce)
            if vpc_id:
                self.source_vpc = vpc_id


@dataclass
class EvaluationResult:
    """Result of evaluating a CloudTrail event against SCP policies."""

    denied: bool
    reason: str
    matched_statement: SCPStatement | None = None
    policy_name: str | None = None
    unevaluable_condition_keys: list[str] = field(default_factory=list)
    resource_arn_resolved: bool = True

    @property
    def is_allowed(self) -> bool:
        """Check if the action would be allowed."""
        return not self.denied


@dataclass
class ImpactStatistics:
    """Statistical analysis of SCP impact on CloudTrail events."""

    # Breakdown by service (e.g., {"s3": 150, "ec2": 75})
    denied_by_service: dict[str, int] = field(default_factory=dict)

    # Top denied actions (e.g., [("s3:GetObject", 100), ("ec2:RunInstances", 50)])
    top_denied_actions: list[tuple[str, int]] = field(default_factory=list)

    # Affected principals (e.g., {"arn:aws:iam::123:user/john": 50})
    affected_principals: dict[str, int] = field(default_factory=dict)

    # Denials by SCP statement (e.g., {"PreventS3Delete": 200})
    denials_by_statement: dict[str, int] = field(default_factory=dict)

    # Time-based analysis
    denials_over_time: list[tuple[datetime, int]] = field(default_factory=list)

    # Risk assessment
    critical_services_affected: list[str] = field(default_factory=list)
    production_impact_score: float = 0.0  # 0-100 based on frequency/criticality

    # Simulation confidence metrics
    unevaluable_keys_summary: dict[str, int] = field(default_factory=dict)
    unresolved_resource_count: int = 0
    total_evaluated_events: int = 0

    # Filtered event counts
    slr_events_filtered: int = 0
    mgmt_account_events_filtered: int = 0

    @property
    def resource_resolution_rate(self) -> float:
        """Calculate the percentage of events with resolved resource ARNs."""
        if self.total_evaluated_events == 0:
            return 100.0
        resolved = self.total_evaluated_events - self.unresolved_resource_count
        return (resolved / self.total_evaluated_events) * 100

    @property
    def simulation_confidence(self) -> str:
        """
        Determine simulation confidence level based on data quality.

        Returns:
            "HIGH", "MEDIUM", or "LOW"
        """
        # Penalize for unresolved resources
        resource_penalty = 0
        if self.total_evaluated_events > 0:
            unresolved_pct = (self.unresolved_resource_count / self.total_evaluated_events) * 100
            if unresolved_pct > 20:
                resource_penalty = 2
            elif unresolved_pct > 5:
                resource_penalty = 1

        # Penalize for unevaluable condition keys
        key_penalty = 0
        if len(self.unevaluable_keys_summary) > 5:
            key_penalty = 2
        elif len(self.unevaluable_keys_summary) > 0:
            key_penalty = 1

        total_penalty = resource_penalty + key_penalty

        if total_penalty == 0:
            return "HIGH"
        elif total_penalty <= 2:
            return "MEDIUM"
        else:
            return "LOW"

    @property
    def has_unevaluable_conditions(self) -> bool:
        """Check if any condition keys could not be evaluated."""
        return bool(self.unevaluable_keys_summary)

    @property
    def confidence_qualifier(self) -> str:
        """Return qualifier for the denial rate based on evaluation completeness.

        Returns:
            'exact' when all conditions were fully evaluated,
            'lower-bound' when unevaluable keys mean actual rate may be higher.
        """
        if not self.has_unevaluable_conditions:
            return "exact"
        return "lower-bound"

    @property
    def total_denials(self) -> int:
        """Get total number of denials."""
        return sum(self.denied_by_service.values())

    @property
    def affected_services_count(self) -> int:
        """Get count of unique services affected."""
        return len(self.denied_by_service)

    @property
    def affected_principals_count(self) -> int:
        """Get count of unique principals affected."""
        return len(self.affected_principals)


@dataclass
class ImpactReport:
    """Complete impact analysis report."""

    total_events: int
    denied_count: int
    allowed_count: int
    denied_events: list[tuple[CloudTrailEvent, EvaluationResult]]
    statistics: ImpactStatistics
    scp_policies: list[SCPPolicy]
    analysis_period: tuple[datetime, datetime]
    scp_policy_name: str | None = None
    warnings: list[str] = field(default_factory=list)

    @property
    def denial_rate(self) -> float:
        """Calculate the percentage of events that would be denied."""
        if self.total_events == 0:
            return 0.0
        return (self.denied_count / self.total_events) * 100

    @property
    def start_time(self) -> datetime:
        """Get analysis start time."""
        return self.analysis_period[0]

    @property
    def end_time(self) -> datetime:
        """Get analysis end time."""
        return self.analysis_period[1]

    @property
    def has_denials(self) -> bool:
        """Check if any events would be denied."""
        return self.denied_count > 0

    def get_risk_level(self) -> str:
        """
        Determine risk level based on denial rate.

        Returns:
            "LOW", "MEDIUM", "HIGH", or "CRITICAL"
        """
        if self.denial_rate == 0:
            return "NONE"
        elif self.denial_rate < 1:
            return "LOW"
        elif self.denial_rate < 5:
            return "MEDIUM"
        elif self.denial_rate < 20:
            return "HIGH"
        else:
            return "CRITICAL"

    def get_top_denied_actions(self, limit: int = 10) -> list[tuple[str, int]]:
        """Get the top N most frequently denied actions."""
        return self.statistics.top_denied_actions[:limit]

    def get_top_affected_principals(self, limit: int = 10) -> list[tuple[str, int]]:
        """Get the top N most affected principals."""
        items = sorted(
            self.statistics.affected_principals.items(), key=lambda x: x[1], reverse=True
        )
        return items[:limit]
