"""SCP Linter - Validates and analyzes SCP policies for issues."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from ..data.iam_reference import AWSIAMReference, get_iam_reference
from ..models.lint import LintReport, LintResult, LintSeverity
from .lint_rules import (
    check_best_practices,
    validate_actions,
    validate_condition_block,
    validate_resource_level_permissions,
    validate_resources,
)


class SCPLinter:
    """
    Linter for AWS Service Control Policies.

    Validates JSON syntax, structure, and best practices.
    """

    # AWS SCP limits
    MAX_POLICY_SIZE = 5120  # characters
    SIZE_WARNING_THRESHOLD = 0.75  # warn at 75% capacity
    MAX_STATEMENTS = 20  # soft limit, warn if exceeded

    # Valid values
    VALID_EFFECTS = {"Allow", "Deny"}
    VALID_VERSIONS = {"2012-10-17", "2008-10-17"}

    def __init__(self, iam_reference: AWSIAMReference | None = None) -> None:
        self.report: LintReport = LintReport()
        self._iam_ref = iam_reference

    @property
    def iam_reference(self) -> AWSIAMReference:
        """Get the IAM reference, loading lazily if needed."""
        if self._iam_ref is None:
            self._iam_ref = get_iam_reference()
        return self._iam_ref

    def lint(self, policy: str | Path | dict) -> LintReport:
        """
        Lint an SCP policy.

        Args:
            policy: JSON string, file path, or parsed dict

        Returns:
            LintReport with all issues found
        """
        self.report = LintReport()

        # Step 1: Parse/load the policy
        policy_dict, policy_str = self._load_policy(policy)
        if policy_dict is None:
            return self.report

        # Step 2: Check size limits
        self._check_size(policy_str)

        # Step 3: Validate structure
        self._validate_structure(policy_dict)

        # Step 4: Validate statements
        if "Statement" in policy_dict:
            self._validate_statements(policy_dict["Statement"])

        # Step 5: Check for best practices
        check_best_practices(policy_dict, self.report)

        return self.report

    def lint_file(self, file_path: str | Path) -> LintReport:
        """Lint an SCP from a file."""
        return self.lint(Path(file_path))

    def lint_string(self, json_str: str) -> LintReport:
        """Lint an SCP from a JSON string."""
        return self.lint(json_str)

    def _load_policy(self, policy: str | Path | dict) -> tuple[dict | None, str]:
        """Load and parse policy, returning (dict, original_string)."""
        policy_str = ""
        policy_dict = None

        if isinstance(policy, dict):
            policy_dict = policy
            policy_str = json.dumps(policy)
            return policy_dict, policy_str

        if isinstance(policy, Path):
            if not policy.exists():
                self.report.add(
                    LintResult(
                        severity=LintSeverity.ERROR,
                        code="E001",
                        message=f"File not found: {policy}",
                    )
                )
                return None, ""

            try:
                policy_str = policy.read_text(encoding="utf-8")
            except Exception as e:
                self.report.add(
                    LintResult(
                        severity=LintSeverity.ERROR,
                        code="E002",
                        message=f"Cannot read file: {e}",
                    )
                )
                return None, ""
        else:
            policy_str = policy

        # Parse JSON
        try:
            policy_dict = json.loads(policy_str)
        except json.JSONDecodeError as e:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E003",
                    message=f"Invalid JSON syntax: {e.msg}",
                    location=f"line {e.lineno}, column {e.colno}",
                    suggestion="Check for missing commas, brackets, or quotes",
                )
            )
            return None, policy_str

        return policy_dict, policy_str

    def _check_size(self, policy_str: str) -> None:
        """Check policy size against AWS limits."""
        # Get size without whitespace (AWS counts minified)
        minified = json.dumps(json.loads(policy_str), separators=(",", ":"))
        size = len(minified)
        self.report.policy_size = size

        if size > self.MAX_POLICY_SIZE:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E010",
                    message=f"Policy exceeds max size: {size}/{self.MAX_POLICY_SIZE} chars",
                    suggestion="Split into multiple SCPs or remove unused statements",
                )
            )
        elif size > self.MAX_POLICY_SIZE * self.SIZE_WARNING_THRESHOLD:
            percent = (size / self.MAX_POLICY_SIZE) * 100
            self.report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W010",
                    message=f"Policy is {percent:.0f}% of max size ({size}/{self.MAX_POLICY_SIZE})",
                    suggestion="Consider splitting before adding more statements",
                )
            )

    def _validate_structure(self, policy_dict: dict) -> None:
        """Validate top-level policy structure."""
        # Check Version
        if "Version" not in policy_dict:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E020",
                    message="Missing required field: Version",
                    suggestion='Add "Version": "2012-10-17"',
                )
            )
        elif policy_dict["Version"] not in self.VALID_VERSIONS:
            self.report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W020",
                    message=f"Unusual Version value: {policy_dict['Version']}",
                    location="Version",
                    suggestion='Use "2012-10-17" (recommended) or "2008-10-17"',
                )
            )

        # Check Statement
        if "Statement" not in policy_dict:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E021",
                    message="Missing required field: Statement",
                    suggestion="Add a Statement array with at least one statement",
                )
            )
        elif not isinstance(policy_dict["Statement"], (list, dict)):
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E022",
                    message="Statement must be an object or array",
                    location="Statement",
                )
            )

        # Check for unknown top-level fields
        valid_fields = {"Version", "Statement", "Id"}
        unknown = set(policy_dict.keys()) - valid_fields
        for field_name in unknown:
            self.report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W021",
                    message=f"Unknown top-level field: {field_name}",
                    location=field_name,
                    suggestion="Only Version, Statement, and Id are valid at top level",
                )
            )

    def _validate_statements(self, statements: list | dict) -> None:
        """Validate all statements in the policy."""
        if isinstance(statements, dict):
            statements = [statements]

        self.report.statement_count = len(statements)

        if len(statements) == 0:
            self.report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W030",
                    message="Policy has no statements",
                    suggestion="Add at least one statement to the policy",
                )
            )
            return

        if len(statements) > self.MAX_STATEMENTS:
            self.report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W031",
                    message=f"Policy has {len(statements)} statements (max: {self.MAX_STATEMENTS})",
                    suggestion="Consider splitting into multiple SCPs for maintainability",
                )
            )

        for i, stmt in enumerate(statements):
            self._validate_statement(stmt, i)

        # W060: Duplicate Sid detection
        sids_seen: dict[str, list[int]] = {}
        for i, stmt in enumerate(statements):
            if isinstance(stmt, dict) and "Sid" in stmt:
                sid = stmt["Sid"]
                if sid not in sids_seen:
                    sids_seen[sid] = []
                sids_seen[sid].append(i)

        for sid, indices in sids_seen.items():
            if len(indices) > 1:
                locations = ", ".join(f"Statement[{i}]" for i in indices)
                self.report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W060",
                        message=f"Duplicate Sid '{sid}' found in multiple statements",
                        location=locations,
                        suggestion="Each statement should have a unique Sid for clarity",
                    )
                )

        # W061: Duplicate statement detection
        stmt_signatures: dict[tuple, list[int]] = {}
        for i, stmt in enumerate(statements):
            if isinstance(stmt, dict):
                sig = self._normalize_statement_for_comparison(stmt)
                if sig not in stmt_signatures:
                    stmt_signatures[sig] = []
                stmt_signatures[sig].append(i)

        for _sig, indices in stmt_signatures.items():
            if len(indices) > 1:
                locations = ", ".join(f"Statement[{i}]" for i in indices)
                self.report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W061",
                        message=(
                            "Duplicate statements found"
                            " (identical Effect, Action, Resource, Condition)"
                        ),
                        location=locations,
                        suggestion="Remove duplicate statements to improve policy clarity",
                    )
                )

    def _normalize_statement_for_comparison(self, stmt: dict) -> tuple:
        """Normalize a statement for comparison, excluding Sid."""

        def normalize_value(val: Any) -> Any:
            if isinstance(val, list):
                return tuple(sorted(str(v) for v in val))
            elif isinstance(val, dict):
                return tuple(sorted((k, normalize_value(v)) for k, v in val.items()))
            else:
                return str(val)

        compare_fields = ["Effect", "Action", "NotAction", "Resource", "NotResource", "Condition"]
        normalized: list[tuple[str, Any]] = []
        for fld in compare_fields:
            if fld in stmt:
                normalized.append((fld, normalize_value(stmt[fld])))
        return tuple(normalized)

    def _validate_statement(self, stmt: dict, index: int) -> None:
        """Validate a single statement."""
        loc_prefix = f"Statement[{index}]"

        if not isinstance(stmt, dict):
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E030",
                    message="Statement must be an object",
                    location=loc_prefix,
                )
            )
            return

        # Check Effect (required)
        if "Effect" not in stmt:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E031",
                    message="Missing required field: Effect",
                    location=loc_prefix,
                    suggestion='Add "Effect": "Deny" or "Effect": "Allow"',
                )
            )
        elif stmt["Effect"] not in self.VALID_EFFECTS:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E032",
                    message=f"Invalid Effect value: {stmt['Effect']}",
                    location=f"{loc_prefix}.Effect",
                    suggestion='Must be "Allow" or "Deny"',
                )
            )

        # Check Action (required for SCPs)
        if "Action" not in stmt and "NotAction" not in stmt:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E033",
                    message="Missing required field: Action or NotAction",
                    location=loc_prefix,
                    suggestion='Add "Action": ["service:Action"] or "Action": "*"',
                )
            )
        else:
            action_field = "Action" if "Action" in stmt else "NotAction"
            validate_actions(
                stmt[action_field], f"{loc_prefix}.{action_field}",
                self.report, self.iam_reference,
            )

        # Check Resource
        if "Resource" not in stmt and "NotResource" not in stmt:
            self.report.add(
                LintResult(
                    severity=LintSeverity.INFO,
                    code="I030",
                    message="No Resource specified, defaults to '*'",
                    location=loc_prefix,
                    suggestion='Explicitly add "Resource": "*" for clarity',
                )
            )

        # Validate Resource ARN format (W080)
        if "Resource" in stmt:
            validate_resources(stmt["Resource"], f"{loc_prefix}.Resource", self.report)
        elif "NotResource" in stmt:
            validate_resources(stmt["NotResource"], f"{loc_prefix}.NotResource", self.report)

        # W090: NotAction warning (inverse logic)
        if "NotAction" in stmt:
            self.report.add(
                LintResult(
                    severity=LintSeverity.INFO,
                    code="W090",
                    message="Statement uses NotAction (inverse logic)",
                    location=f"{loc_prefix}.NotAction",
                    suggestion=(
                        "NotAction can be error-prone; consider using Action with explicit list"
                    ),
                )
            )

        # W091: NotResource warning (inverse logic)
        if "NotResource" in stmt:
            self.report.add(
                LintResult(
                    severity=LintSeverity.INFO,
                    code="W091",
                    message="Statement uses NotResource (inverse logic)",
                    location=f"{loc_prefix}.NotResource",
                    suggestion=(
                        "NotResource can be error-prone; consider using Resource with explicit ARNs"
                    ),
                )
            )

        # Check for Sid
        if "Sid" not in stmt:
            self.report.add(
                LintResult(
                    severity=LintSeverity.INFO,
                    code="I031",
                    message="Statement has no Sid (identifier)",
                    location=loc_prefix,
                    suggestion="Add a Sid for easier debugging and management",
                )
            )

        # Check for Principal in SCP (should not be present)
        if "Principal" in stmt or "NotPrincipal" in stmt:
            self.report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E034",
                    message="SCPs cannot have Principal or NotPrincipal",
                    location=loc_prefix,
                    suggestion="Remove Principal - SCPs apply to all principals in the account",
                )
            )

        # Check valid statement fields
        valid_stmt_fields = {
            "Sid",
            "Effect",
            "Action",
            "NotAction",
            "Resource",
            "NotResource",
            "Condition",
        }
        unknown = set(stmt.keys()) - valid_stmt_fields
        for field_name in unknown:
            self.report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W032",
                    message=f"Unknown statement field: {field_name}",
                    location=f"{loc_prefix}.{field_name}",
                )
            )

        # W052: Resource-level permissions check (cross-references Action + Resource)
        validate_resource_level_permissions(stmt, loc_prefix, self.report)

        # Validate Condition block
        if "Condition" in stmt:
            validate_condition_block(
                stmt["Condition"], f"{loc_prefix}.Condition",
                self.report, self.iam_reference,
            )
