"""JSON parsing utilities for loading SCP policies."""

import json
from pathlib import Path

from ..models.scp import SCPPolicy, SCPStatement


class SCPParseError(Exception):
    """Raised when a policy file cannot be parsed."""

    pass


def load_policy(path: str | Path) -> SCPPolicy:
    """
    Load an SCP policy from a JSON file.

    Handles both raw policy format and wrapped format (with PolicyName, PolicyDocument).

    Args:
        path: Path to the JSON policy file

    Returns:
        SCPPolicy object

    Raises:
        SCPParseError: If the file cannot be parsed as a valid SCP
        FileNotFoundError: If the file doesn't exist
    """
    path = Path(path)

    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        raise SCPParseError(f"Invalid JSON in {path}: {e}") from e

    # Use filename as default policy name
    default_name = path.stem

    return parse_policy_dict(data, default_name)


def parse_policy_dict(data: dict, default_name: str | None = None) -> SCPPolicy:
    """
    Parse a policy dictionary into an SCPPolicy object.

    Handles both formats:
    - Raw: {"Version": "...", "Statement": [...]}
    - Wrapped: {"PolicyName": "...", "PolicyDocument": {...}}

    Args:
        data: Dictionary containing policy data
        default_name: Default name if not specified in the data

    Returns:
        SCPPolicy object

    Raises:
        SCPParseError: If the data cannot be parsed as a valid SCP
    """
    # Check for wrapped format
    if "PolicyDocument" in data:
        policy_name = data.get("PolicyName", default_name)
        policy_doc = data["PolicyDocument"]
    elif "Content" in data:
        # Another common wrapper format
        policy_name = data.get("Name", default_name)
        policy_doc = data["Content"]
        if isinstance(policy_doc, str):
            try:
                policy_doc = json.loads(policy_doc)
            except json.JSONDecodeError as e:
                raise SCPParseError(f"Invalid JSON in Content field: {e}") from e
    else:
        # Raw format
        policy_name = default_name
        policy_doc = data

    # Validate required fields
    if "Statement" not in policy_doc:
        raise SCPParseError("Policy must contain a 'Statement' field")

    # Parse version
    version = policy_doc.get("Version", "2012-10-17")

    # Parse statements
    statements_data = policy_doc["Statement"]
    if not isinstance(statements_data, list):
        statements_data = [statements_data]

    statements = []
    for idx, stmt_data in enumerate(statements_data):
        try:
            stmt = _parse_statement(stmt_data)
            statements.append(stmt)
        except (KeyError, ValueError) as e:
            raise SCPParseError(f"Invalid statement at index {idx}: {e}") from e

    return SCPPolicy(
        statements=statements,
        name=policy_name,
        version=version,
    )


def _parse_statement(data: dict) -> SCPStatement:
    """Parse a single statement dictionary into an SCPStatement."""
    # Effect is required
    if "Effect" not in data:
        raise ValueError("Statement must have 'Effect' field")

    effect = data["Effect"]

    # Handle Action vs NotAction
    actions: list[str] = []
    not_actions: list[str] | None = None

    if "Action" in data:
        actions = data["Action"]
        if isinstance(actions, str):
            actions = [actions]
    elif "NotAction" in data:
        not_actions = data["NotAction"]
        if isinstance(not_actions, str):
            not_actions = [not_actions]
    else:
        raise ValueError("Statement must have 'Action' or 'NotAction' field")

    # Handle Resource vs NotResource
    resources: list[str] = ["*"]
    not_resources: list[str] | None = None

    if "Resource" in data:
        resources = data["Resource"]
        if isinstance(resources, str):
            resources = [resources]
    elif "NotResource" in data:
        not_resources = data["NotResource"]
        if isinstance(not_resources, str):
            not_resources = [not_resources]
        resources = []  # Clear default when using NotResource

    # Handle Principal vs NotPrincipal
    # Note: SCPs typically don't use Principal (implicitly "*"), but we support it
    principals: list[str] = ["*"]
    not_principals: list[str] | None = None

    if "Principal" in data:
        principal_data = data["Principal"]
        if isinstance(principal_data, str):
            principals = [principal_data]
        elif isinstance(principal_data, dict):
            # Handle {"AWS": "arn:..."} or {"AWS": ["arn:...", "arn:..."]}
            principals = []
            for key, value in principal_data.items():
                if isinstance(value, str):
                    principals.append(value)
                elif isinstance(value, list):
                    principals.extend(value)
        else:
            principals = list(principal_data) if principal_data else ["*"]
    elif "NotPrincipal" in data:
        not_principal_data = data["NotPrincipal"]
        if isinstance(not_principal_data, str):
            not_principals = [not_principal_data]
        elif isinstance(not_principal_data, dict):
            not_principals = []
            for key, value in not_principal_data.items():
                if isinstance(value, str):
                    not_principals.append(value)
                elif isinstance(value, list):
                    not_principals.extend(value)
        else:
            not_principals = list(not_principal_data) if not_principal_data else None
        principals = []  # Clear default when using NotPrincipal

    # Optional fields
    sid = data.get("Sid")
    conditions = data.get("Condition")

    return SCPStatement(
        effect=effect,
        actions=actions,
        resources=resources,
        not_actions=not_actions,
        not_resources=not_resources,
        principals=principals,
        not_principals=not_principals,
        sid=sid,
        conditions=conditions,
    )


def load_policies_from_dir(directory: str | Path) -> list[SCPPolicy]:
    """
    Load all SCP policies from a directory.

    Loads all .json files in the directory (non-recursive).

    Args:
        directory: Path to directory containing policy JSON files

    Returns:
        List of SCPPolicy objects

    Raises:
        FileNotFoundError: If directory doesn't exist
        SCPParseError: If any policy file cannot be parsed
    """
    directory = Path(directory)

    if not directory.is_dir():
        raise FileNotFoundError(f"Directory not found: {directory}")

    policies: list[SCPPolicy] = []
    json_files = sorted(directory.glob("*.json"))

    if not json_files:
        return policies

    for json_file in json_files:
        policy = load_policy(json_file)
        policies.append(policy)

    return policies


def parse_policy(data: dict, policy_name: str | None = None) -> SCPPolicy:
    """
    Parse a policy dictionary into an SCPPolicy object.

    This is an alias for parse_policy_dict for backward compatibility.

    Args:
        data: Dictionary containing policy data
        policy_name: Optional name for the policy

    Returns:
        SCPPolicy object
    """
    return parse_policy_dict(data, policy_name)


def validate_scp(policy: SCPPolicy) -> list[str]:
    """
    Validate an SCP policy and return warnings.

    Args:
        policy: SCPPolicy to validate

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

    # Check for blanket deny without conditions
    for stmt in policy.statements:
        if stmt.effect == "Deny":
            has_condition = stmt.conditions is not None and len(stmt.conditions) > 0

            # Check for wildcard actions
            if "*" in stmt.actions and not has_condition:
                warnings.append(
                    f"Statement {stmt.sid or 'unnamed'} denies all actions (*) without conditions"
                )
            # Check for service-wide denies
            for action in stmt.actions:
                if action.endswith(":*") and not has_condition:
                    service = action.split(":")[0]
                    warnings.append(
                        f"Statement {stmt.sid or 'unnamed'} denies all"
                        f" {service} actions without conditions"
                    )

    # Check for no deny statements
    if not policy.has_deny_statements:
        warnings.append("Policy has no Deny statements - SCP will have no effect")

    return warnings


def load_policies(*paths: str | Path) -> list[SCPPolicy]:
    """
    Load policies from multiple paths (files or directories).

    Args:
        *paths: Paths to policy files or directories

    Returns:
        List of SCPPolicy objects
    """
    policies = []

    for path in paths:
        path = Path(path)

        if path.is_dir():
            policies.extend(load_policies_from_dir(path))
        elif path.is_file():
            policies.append(load_policy(path))
        else:
            raise FileNotFoundError(f"Path not found: {path}")

    return policies
