"""Common IAM models, risk classification, framework mapping, and patch generation.

Contract:
- IAMFinding: 11-field dataclass for cloud-native IAM over-provisioning findings.
- classify_risk_level(): deterministic risk classification per spec.
- iam_finding_to_findings(): converts IAMFinding to Finding objects per framework.
- generate_iam_patches(): generates PatchFile objects from IAMFindings.
"""

from __future__ import annotations

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

from complyform.core.assessor.control_matcher import Finding
from complyform.core.cloud_intelligence import CloudIntelligenceResult
from complyform.core.remediator.patch_generator import PatchFile

__all__ = [
    "IAM_FRAMEWORK_CONTROL_MAP",
    "IAMAnalysisResult",
    "IAMFinding",
    "classify_risk_level",
    "generate_iam_patches",
    "iam_finding_to_findings",
]

_CRITICAL_ROLES: frozenset[str] = frozenset(
    {
        "roles/owner",
        "AdministratorAccess",
        "Owner",
    }
)

_HIGH_ROLES: frozenset[str] = frozenset(
    {
        "roles/editor",
        "Contributor",
    }
)

# Broad write policies for AWS detection.
_AWS_BROAD_WRITE_PATTERNS: tuple[str, ...] = (
    "PowerUserAccess",
    "AmazonS3FullAccess",
    "AmazonEC2FullAccess",
    "IAMFullAccess",
)

IAM_FRAMEWORK_CONTROL_MAP: dict[str, list[dict[str, str]]] = {
    "soc2": [
        {"framework": "soc2", "control_id": "CC6.1"},
        {"framework": "soc2", "control_id": "CC6.2"},
        {"framework": "soc2", "control_id": "CC6.3"},
    ],
    "hipaa": [
        {"framework": "hipaa", "control_id": "164.312(d)"},
    ],
    "iso27001": [
        {"framework": "iso27001", "control_id": "A.5.15"},
        {"framework": "iso27001", "control_id": "A.5.18"},
        {"framework": "iso27001", "control_id": "A.8.2"},
    ],
    "nist_800_53": [
        {"framework": "nist_800_53", "control_id": "AC-2"},
        {"framework": "nist_800_53", "control_id": "AC-6"},
    ],
    "pci_dss": [
        {"framework": "pci_dss", "control_id": "7.1"},
        {"framework": "pci_dss", "control_id": "7.2"},
        {"framework": "pci_dss", "control_id": "8.1"},
    ],
}


@dataclass
class IAMFinding:
    """Single IAM over-provisioning finding from cloud-native IAM intelligence."""

    cloud: str  # "gcp" | "aws" | "azure"
    principal: str  # Full principal identifier
    principal_type: str  # "service_account" | "user" | "group" | "role"
    current_role: str  # Current over-broad role
    recommended_role: str | None  # Narrowed role, or None = remove entirely
    unused_permissions: list[str]  # Specific permissions never exercised
    last_activity: str | None  # ISO datetime, or None if never used
    risk_level: str  # "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"
    framework_controls: list[
        dict[str, str]
    ]  # [{"framework": "soc2", "control_id": "CC6.1"}]
    remediation_terraform: str | None  # Terraform patch snippet
    source_api: str  # "iam_recommender" | "access_analyzer" | "activity_log"


@dataclass
class IAMAnalysisResult(CloudIntelligenceResult):
    """Result of IAM analysis for a single cloud."""

    findings: list[IAMFinding] = field(default_factory=list)
    principals_scanned: int = 0
    over_provisioned_count: int = 0
    unused_role_count: int = 0


def classify_risk_level(
    current_role: str,
    last_activity: str | None,
    unused_permissions: list[str],
) -> str:
    """Classify risk level for an IAM finding.

    Checked in order — first match wins.
    """
    if current_role in _CRITICAL_ROLES:
        return "CRITICAL"
    if current_role in _HIGH_ROLES or current_role in _AWS_BROAD_WRITE_PATTERNS:
        return "HIGH"
    if last_activity is None:
        return "HIGH"
    if len(unused_permissions) >= 5:  # noqa: PLR2004
        return "MEDIUM"
    return "LOW"


def iam_finding_to_findings(
    iam: IAMFinding,
    frameworks: list[str],
) -> list[Finding]:
    """Convert a single IAMFinding to Finding objects per framework control."""
    results: list[Finding] = []
    description = (
        f"Over-provisioned {iam.principal_type} '{iam.principal}': "
        f"current role '{iam.current_role}'"
    )
    if iam.recommended_role:
        remediation_text = f"Narrow role to '{iam.recommended_role}'"
    else:
        remediation_text = f"Remove role '{iam.current_role}' — principal is unused"

    for fw in frameworks:
        controls = IAM_FRAMEWORK_CONTROL_MAP.get(fw, [])
        for ctrl in controls:
            results.append(
                Finding(
                    control_id=ctrl["control_id"],
                    framework=ctrl["framework"],
                    resource_address=iam.principal,
                    resource_type="iam_binding",
                    status="FAIL",
                    severity=iam.risk_level,
                    finding=description,
                    remediation=remediation_text,
                    checkov_id=None,
                    cross_mappings=None,
                    native_ids=None,
                    confidence="verified",
                )
            )
    return results


def _sanitize_principal(principal: str) -> str:
    """Sanitize principal for use in filenames."""
    return re.sub(r"[^a-zA-Z0-9_-]", "_", principal)[:100]


def _generate_header_comment(iam: IAMFinding) -> str:
    """Generate header comment for IAM patch file."""
    controls = ", ".join(
        f"{c['framework']}:{c['control_id']}" for c in iam.framework_controls
    )
    return (
        f"# IAM Remediation — {iam.cloud.upper()}\n"
        f"# Principal: {iam.principal}\n"
        f"# Current role: {iam.current_role}\n"
        f"# Recommended role: {iam.recommended_role or 'REMOVE'}\n"
        f"# Risk level: {iam.risk_level}\n"
        f"# Framework controls: {controls}\n"
    )


def _generate_gcp_patch(iam: IAMFinding) -> str:
    """Generate GCP Terraform resource block for narrowed role."""
    header = _generate_header_comment(iam)
    p = iam.principal
    r = iam.current_role
    if iam.recommended_role is None:
        return f"{header}\n# REMOVE: Role binding for {p} with role {r}\n"
    name = _sanitize_principal(p)
    return (
        f"{header}\n"
        f'resource "google_project_iam_member" "narrowed_{name}" {{\n'
        f"  project = var.project_id\n"
        f'  role    = "{iam.recommended_role}"\n'
        f'  member  = "{p}"\n'
        f"}}\n"
    )


def _generate_aws_patch(iam: IAMFinding) -> str:
    """Generate AWS Terraform resource block for scoped policy."""
    header = _generate_header_comment(iam)
    p = iam.principal
    r = iam.current_role
    if iam.recommended_role is None:
        return f"{header}\n# REMOVE: Policy attachment for {p} with policy {r}\n"
    name = _sanitize_principal(p)
    return (
        f"{header}\n"
        f'resource "aws_iam_role_policy_attachment"'
        f' "narrowed_{name}" {{\n'
        f'  role       = "{p}"\n'
        f'  policy_arn = "{iam.recommended_role}"\n'
        f"}}\n"
    )


def _generate_azure_patch(iam: IAMFinding) -> str:
    """Generate Azure Terraform resource block for narrowed role."""
    header = _generate_header_comment(iam)
    p = iam.principal
    r = iam.current_role
    if iam.recommended_role is None:
        return f"{header}\n# REMOVE: Role assignment for {p} with role {r}\n"
    name = _sanitize_principal(p)
    return (
        f"{header}\n"
        f'resource "azurerm_role_assignment"'
        f' "narrowed_{name}" {{\n'
        f'  principal_id         = "{p}"\n'
        f'  role_definition_name = "{iam.recommended_role}"\n'
        f"  scope                = var.subscription_scope\n"
        f"}}\n"
    )


_PATCH_GENERATORS: dict[str, Any] = {
    "gcp": _generate_gcp_patch,
    "aws": _generate_aws_patch,
    "azure": _generate_azure_patch,
}


def generate_iam_patches(
    iam_findings: list[IAMFinding],
    cloud: str,
) -> list[PatchFile]:
    """Generate PatchFile objects from IAM findings."""
    generator = _PATCH_GENERATORS[cloud]
    patches: list[PatchFile] = []
    for iam in iam_findings:
        content = generator(iam)
        sanitized = _sanitize_principal(iam.principal)
        filename = f"iam_{sanitized}.tf"
        controls = ", ".join(
            f"{c['framework']}:{c['control_id']}" for c in iam.framework_controls
        )
        patches.append(
            PatchFile(
                resource_address=iam.principal,
                control_id=controls or "iam_least_privilege",
                requirement_id=None,
                resource_type="iam_binding",
                filename=filename,
                content=content,
                diff=content,
                strategy="source_modify",
                dependencies=[],
                risk_level=iam.risk_level.lower(),
            )
        )
    return patches
