"""
Lint rule implementations for SCP validation.

Each function takes a LintReport (and AWSIAMReference where needed) as parameters
rather than being bound to a class instance. This keeps the SCPLinter class focused
on orchestration while the actual validation logic lives here.

Error code scheme:
    Exx  -- Errors   (invalid structure, will break deployment)
    Wxx  -- Warnings (valid but problematic, likely unintended)
    Ixx  -- Info     (style suggestions, not necessarily wrong)

    E001-E003  JSON/file errors          E030-E034  Statement structure
    E010       Policy size               E040-E041  Action format
    E050-E052  Condition block            W010       Policy size warning
    W020-W021  Version/unknown fields     W030-W032  Statement warnings
    W040-W042  Unknown service/action     W043       Unknown condition key
    W044-W046  Condition block warnings   W050-W052  Broad deny / resource-level
    W060-W061  Duplicate Sid/statement    W070-W071  IP/date format
    W080       ARN format                 W090-W091  NotAction/NotResource info
    I030-I031  Resource/Sid info          I050       Allow-only SCP info

Adding a new lint rule:
    1. Write a function here: validate_xxx(args..., report, iam_ref)
    2. Pick the next unused code in the appropriate range
    3. Call report.add(LintResult(severity=..., code="Wxx", message=...))
    4. Wire it up in SCPLinter (scp_linter.py) by importing and calling it
"""

from __future__ import annotations

import ipaddress
from typing import Any

from dateutil.parser import ParserError  # type: ignore[import-untyped]
from dateutil.parser import parse as parse_date  # type: ignore[import-untyped]

from ..data.global_actions import is_global_action
from ..data.iam_reference import AWSIAMReference
from ..models.lint import LintReport, LintResult, LintSeverity

# Valid condition operators (base operators, without modifiers)
VALID_CONDITION_OPERATORS = {
    # String operators
    "StringEquals",
    "StringNotEquals",
    "StringEqualsIgnoreCase",
    "StringNotEqualsIgnoreCase",
    "StringLike",
    "StringNotLike",
    # ARN operators
    "ArnEquals",
    "ArnNotEquals",
    "ArnLike",
    "ArnNotLike",
    # Numeric operators
    "NumericEquals",
    "NumericNotEquals",
    "NumericLessThan",
    "NumericLessThanEquals",
    "NumericGreaterThan",
    "NumericGreaterThanEquals",
    # Date operators
    "DateEquals",
    "DateNotEquals",
    "DateLessThan",
    "DateLessThanEquals",
    "DateGreaterThan",
    "DateGreaterThanEquals",
    # Boolean and Null
    "Bool",
    "Null",
    # IP address operators
    "IpAddress",
    "NotIpAddress",
    # Binary operators
    "BinaryEquals",
}


# =========================================================================
# Action validation
# =========================================================================


def validate_actions(
    actions: str | list,
    location: str,
    report: LintReport,
    iam_ref: AWSIAMReference,
) -> None:
    """Validate action format."""
    if isinstance(actions, str):
        actions = [actions]

    if not isinstance(actions, list):
        report.add(
            LintResult(
                severity=LintSeverity.ERROR,
                code="E040",
                message="Action must be a string or array of strings",
                location=location,
            )
        )
        return

    for i, action in enumerate(actions):
        if not isinstance(action, str):
            report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E041",
                    message=f"Action must be a string, got {type(action).__name__}",
                    location=f"{location}[{i}]",
                )
            )
            continue

        # Check action format (service:action or *)
        if action != "*" and ":" not in action:
            report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W040",
                    message=f"Action '{action}' missing service prefix",
                    location=f"{location}[{i}]",
                    suggestion='Use format "service:Action" (e.g., "s3:GetObject")',
                )
            )
        else:
            # Validate action exists in IAM reference
            validate_action_exists(action, f"{location}[{i}]", report, iam_ref)


def validate_action_exists(
    action: str,
    location: str,
    report: LintReport,
    iam_ref: AWSIAMReference,
) -> None:
    """
    Validate that an action exists in the IAM reference database.

    Checks service prefix validity and action name validity.
    Handles wildcards appropriately.

    Error codes:
        W041: Unknown service prefix
        W042: Unknown action (service exists but action doesn't)
    """
    # Skip universal wildcard
    if action == "*":
        return

    # Skip actions with wildcards in action part (can't validate)
    if ":" in action:
        service, action_name = action.split(":", 1)
        if "*" in action_name or "?" in action_name:
            # Can only validate the service prefix
            if service.lower() not in [s.lower() for s in iam_ref.service_prefixes]:
                suggestion = find_similar_service(service, iam_ref)
                report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W041",
                        message=f"Unknown service prefix: '{service}'",
                        location=location,
                        suggestion=suggestion,
                    )
                )
            return

    # Full action validation
    if not is_valid_action_with_case_insensitive(action, iam_ref):
        if ":" in action:
            service, action_name = action.split(":", 1)
            # Check if service exists
            if service.lower() not in [s.lower() for s in iam_ref.service_prefixes]:
                suggestion = find_similar_service(service, iam_ref)
                report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W041",
                        message=f"Unknown service prefix: '{service}'",
                        location=location,
                        suggestion=suggestion,
                    )
                )
            else:
                # Service exists, but action doesn't
                suggestion = find_similar_action(service, action_name, iam_ref)
                report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W042",
                        message=f"Unknown action: '{action}'",
                        location=location,
                        suggestion=suggestion,
                    )
                )


def is_valid_action_with_case_insensitive(action: str, iam_ref: AWSIAMReference) -> bool:
    """Check if action is valid (case-insensitive matching)."""
    if ":" not in action:
        return False

    service, action_name = action.split(":", 1)

    # Find the service (case-insensitive)
    actual_service = None
    for svc_prefix in iam_ref.service_prefixes:
        if svc_prefix.lower() == service.lower():
            actual_service = svc_prefix
            break

    if not actual_service:
        return False

    svc_info = iam_ref.get_service(actual_service)
    if not svc_info:
        return False

    # Check action (case-insensitive)
    for act in svc_info.all_actions:
        if act.lower() == action_name.lower():
            return True

    return False


def find_similar_service(service: str, iam_ref: AWSIAMReference) -> str | None:
    """Find similar service prefixes for suggestions."""
    service_lower = service.lower()
    candidates = []

    for svc in iam_ref.service_prefixes:
        svc_lower = svc.lower()
        # Check for common typos
        if levenshtein_distance(service_lower, svc_lower) <= 2:
            candidates.append(svc)
        elif service_lower in svc_lower or svc_lower in service_lower:
            candidates.append(svc)

    if candidates:
        return f"Did you mean: {', '.join(candidates[:3])}?"
    return None


def find_similar_action(
    service: str, action_name: str, iam_ref: AWSIAMReference
) -> str | None:
    """Find similar action names for suggestions."""
    # Find actual service prefix (case-insensitive)
    actual_service = None
    for svc_prefix in iam_ref.service_prefixes:
        if svc_prefix.lower() == service.lower():
            actual_service = svc_prefix
            break

    if not actual_service:
        return None

    svc_info = iam_ref.get_service(actual_service)
    if not svc_info:
        return None

    action_lower = action_name.lower()
    candidates = []

    for act in svc_info.all_actions:
        act_lower = act.lower()
        # Check for common typos
        if levenshtein_distance(action_lower, act_lower) <= 2:
            candidates.append(f"{actual_service}:{act}")
        elif action_lower in act_lower or act_lower in action_lower:
            candidates.append(f"{actual_service}:{act}")

    if candidates:
        return f"Did you mean: {', '.join(candidates[:3])}?"
    return None


def levenshtein_distance(s1: str, s2: str) -> int:
    """Calculate Levenshtein distance between two strings."""
    if len(s1) < len(s2):
        return levenshtein_distance(s2, s1)

    if len(s2) == 0:
        return len(s1)

    previous_row: list[int] = list(range(len(s2) + 1))
    for i, c1 in enumerate(s1):
        current_row: list[int] = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            substitutions = previous_row[j] + (c1 != c2)
            current_row.append(min(insertions, deletions, substitutions))
        previous_row = current_row

    return previous_row[-1]


# =========================================================================
# Best practices checks
# =========================================================================


def check_best_practices(policy_dict: dict, report: LintReport) -> None:
    """Check for best practices and common issues."""
    if "Statement" not in policy_dict:
        return

    statements = policy_dict["Statement"]
    if isinstance(statements, dict):
        statements = [statements]

    deny_all_count = 0
    allow_all_count = 0

    for i, stmt in enumerate(statements):
        if not isinstance(stmt, dict):
            continue

        effect = stmt.get("Effect")
        actions_raw = stmt.get("Action", stmt.get("NotAction", []))
        if isinstance(actions_raw, str):
            actions: list[str] = [actions_raw]
        elif isinstance(actions_raw, list):
            actions = actions_raw
        else:
            actions = []

        conditions = stmt.get("Condition")

        # Check for overly broad deny without conditions
        if effect == "Deny" and "*" in actions and not conditions:
            deny_all_count += 1
            report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W050",
                    message="Blanket Deny on all actions without conditions",
                    location=f"Statement[{i}]",
                    suggestion="Add conditions or be more specific about actions",
                )
            )

        # Check for service-wide deny without conditions
        if effect == "Deny" and not conditions:
            for action in actions:
                if isinstance(action, str) and action.endswith(":*") and action != "*":
                    service = action.split(":")[0]
                    report.add(
                        LintResult(
                            severity=LintSeverity.WARNING,
                            code="W051",
                            message=f"Denies all {service} actions without conditions",
                            location=f"Statement[{i}]",
                            suggestion=f"Add conditions or specify {service} actions",
                        )
                    )

        # Check for Allow statements (unusual in SCPs)
        if effect == "Allow":
            if "*" in actions:
                allow_all_count += 1

    # SCP with only Allow * (probably wrong)
    if allow_all_count > 0 and deny_all_count == 0 and len(statements) == allow_all_count:
        report.add(
            LintResult(
                severity=LintSeverity.INFO,
                code="I050",
                message="SCP only contains Allow statements",
                suggestion="SCPs typically use Deny statements to restrict actions",
            )
        )


# =========================================================================
# Condition key validation
# =========================================================================


def validate_condition_key(
    key: str,
    location: str,
    report: LintReport,
    iam_ref: AWSIAMReference,
) -> None:
    """
    Validate that a condition key exists in the IAM reference database.

    Error codes:
        W043: Unknown condition key
    """
    # Check if key exists in IAM reference
    key_info = iam_ref.get_condition_key(key)

    if key_info is None:
        # Key not found - check for common aws: global keys that might
        # not be in reference but are valid
        if key.lower().startswith("aws:"):
            # Some common global keys that are always valid
            common_global_keys = {
                "aws:sourceip",
                "aws:sourcevpc",
                "aws:sourcevpce",
                "aws:principalarn",
                "aws:principalaccount",
                "aws:principalorgid",
                "aws:principalorgpaths",
                "aws:principaltag/",
                "aws:principaltype",
                "aws:requesttag/",
                "aws:resourcetag/",
                "aws:tagkeys",
                "aws:requestedregion",
                "aws:currenttime",
                "aws:epochtime",
                "aws:tokenissuetime",
                "aws:multifactorauthpresent",
                "aws:multifactorauthage",
                "aws:securetransport",
                "aws:useragent",
                "aws:userid",
                "aws:username",
                "aws:via-awsservice",
                "aws:calledalias",
                "aws:calledviaservice",
                "aws:referer",
                "aws:fatalerror",
            }
            key_lower = key.lower()
            # Check exact match or prefix match (for tag keys)
            if key_lower in common_global_keys:
                return
            for common_key in common_global_keys:
                if common_key.endswith("/") and key_lower.startswith(common_key):
                    return

        suggestion = find_similar_condition_key(key, iam_ref)
        report.add(
            LintResult(
                severity=LintSeverity.WARNING,
                code="W043",
                message=f"Unknown condition key: '{key}'",
                location=location,
                suggestion=suggestion,
            )
        )


def find_similar_condition_key(key: str, iam_ref: AWSIAMReference) -> str | None:
    """Find similar condition keys for suggestions."""
    key_lower = key.lower()
    candidates = []

    # Check global keys
    for gk in iam_ref.global_condition_keys:
        if levenshtein_distance(key_lower, gk.lower()) <= 3:
            candidates.append(gk)

    # Check service-specific keys if key has service prefix
    if ":" in key:
        prefix = key.split(":")[0]
        svc = iam_ref.get_service(prefix)
        if svc:
            for ck in svc.condition_keys:
                if levenshtein_distance(key_lower, ck.lower()) <= 3:
                    candidates.append(ck)

    if candidates:
        return f"Did you mean: {', '.join(candidates[:3])}?"
    return None


# =========================================================================
# Condition block syntax validation
# =========================================================================


def validate_condition_block(
    conditions: Any,
    location: str,
    report: LintReport,
    iam_ref: AWSIAMReference,
) -> None:
    """
    Validate the structure and content of a Condition block.

    Expected structure:
    {
        "Operator": {
            "ConditionKey": "value" | ["value1", "value2"]
        }
    }

    Error codes:
        E050: Condition block must be an object
        E051: Condition operator value must be an object
        E052: Unknown condition operator
        W043: Unknown condition key (via validate_condition_key)
        W044: Empty condition block
        W045: Empty operator block
        W046: Empty condition values
    """
    if conditions is None:
        return

    if not isinstance(conditions, dict):
        report.add(
            LintResult(
                severity=LintSeverity.ERROR,
                code="E050",
                message="Condition block must be an object",
                location=location,
            )
        )
        return

    if len(conditions) == 0:
        report.add(
            LintResult(
                severity=LintSeverity.WARNING,
                code="W044",
                message="Empty Condition block",
                location=location,
                suggestion="Remove empty Condition or add conditions",
            )
        )
        return

    for operator, key_values in conditions.items():
        op_location = f"{location}.{operator}"

        # Validate operator
        base_operator = extract_base_operator(operator)
        if base_operator not in VALID_CONDITION_OPERATORS:
            suggestion = find_similar_operator(base_operator)
            report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E052",
                    message=f"Unknown condition operator: '{operator}'",
                    location=op_location,
                    suggestion=suggestion,
                )
            )
            continue

        # Validate operator value is a dict
        if not isinstance(key_values, dict):
            report.add(
                LintResult(
                    severity=LintSeverity.ERROR,
                    code="E051",
                    message=f"Condition operator '{operator}' value must be an object",
                    location=op_location,
                    suggestion='Use format: {"ConditionKey": "value"}',
                )
            )
            continue

        if len(key_values) == 0:
            report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W045",
                    message=f"Empty condition operator block: '{operator}'",
                    location=op_location,
                    suggestion="Add condition keys or remove the operator",
                )
            )
            continue

        # Validate each condition key
        for cond_key, cond_values in key_values.items():
            key_location = f"{op_location}.{cond_key}"

            # Validate condition key exists
            validate_condition_key(cond_key, key_location, report, iam_ref)

            # W070: IP address format validation for IpAddress/NotIpAddress operators
            if base_operator in ("IpAddress", "NotIpAddress"):
                validate_ip_address_format(cond_values, key_location, report)

            # W071: Date format validation for Date* operators
            if base_operator.startswith("Date"):
                validate_date_format(cond_values, key_location, report)

            # Validate condition values are not empty
            if cond_values is None:
                report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W046",
                        message=f"Null condition value for key: '{cond_key}'",
                        location=key_location,
                        suggestion="Provide a value or remove the condition",
                    )
                )
            elif isinstance(cond_values, list) and len(cond_values) == 0:
                report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W046",
                        message=f"Empty condition values for key: '{cond_key}'",
                        location=key_location,
                        suggestion="Provide values or remove the condition",
                    )
                )


def extract_base_operator(operator: str) -> str:
    """
    Extract the base operator from an operator string.

    Handles modifiers like IfExists suffix and ForAllValues/ForAnyValue prefix.
    """
    base = operator

    # Remove IfExists suffix
    if base.endswith("IfExists"):
        base = base[:-8]

    # Remove ForAllValues/ForAnyValue prefix
    if base.startswith("ForAllValues:"):
        base = base[13:]
    elif base.startswith("ForAnyValue:"):
        base = base[12:]

    return base


def find_similar_operator(operator: str) -> str | None:
    """Find similar operator names for suggestions."""
    op_lower = operator.lower()
    candidates = []

    for valid_op in VALID_CONDITION_OPERATORS:
        if levenshtein_distance(op_lower, valid_op.lower()) <= 3:
            candidates.append(valid_op)

    if candidates:
        return f"Did you mean: {', '.join(candidates[:3])}?"
    return None


# =========================================================================
# IP address, date, and ARN validation
# =========================================================================


def validate_ip_address_format(value: Any, location: str, report: LintReport) -> None:
    """
    Validate IP address or CIDR format.

    Error codes:
        W070: Invalid IP address or CIDR format
    """
    if isinstance(value, list):
        for i, v in enumerate(value):
            validate_ip_address_format(v, f"{location}[{i}]", report)
        return

    if not isinstance(value, str):
        report.add(
            LintResult(
                severity=LintSeverity.WARNING,
                code="W070",
                message=f"IP address value must be a string, got {type(value).__name__}",
                location=location,
                suggestion="Use string format like '192.0.2.0/24' or '192.0.2.1'",
            )
        )
        return

    try:
        if "/" in value:
            ipaddress.ip_network(value, strict=False)
        else:
            ipaddress.ip_address(value)
    except ValueError:
        report.add(
            LintResult(
                severity=LintSeverity.WARNING,
                code="W070",
                message=f"Invalid IP address or CIDR format: '{value}'",
                location=location,
                suggestion="Use valid IPv4 (192.0.2.0/24) or IPv6 (2001:db8::/32) format",
            )
        )


def validate_date_format(value: Any, location: str, report: LintReport) -> None:
    """
    Validate date format for date condition operators.

    Error codes:
        W071: Invalid date format
    """
    if isinstance(value, list):
        for i, v in enumerate(value):
            validate_date_format(v, f"{location}[{i}]", report)
        return

    if not isinstance(value, str):
        report.add(
            LintResult(
                severity=LintSeverity.WARNING,
                code="W071",
                message=f"Date value must be a string, got {type(value).__name__}",
                location=location,
                suggestion="Use ISO 8601 format like '2023-01-15T00:00:00Z'",
            )
        )
        return

    try:
        parse_date(value)
    except (ParserError, ValueError):
        report.add(
            LintResult(
                severity=LintSeverity.WARNING,
                code="W071",
                message=f"Invalid date format: '{value}'",
                location=location,
                suggestion="Use ISO 8601 format like '2023-01-15T00:00:00Z'",
            )
        )


def validate_resources(resources: Any, location: str, report: LintReport) -> None:
    """
    Validate Resource/NotResource ARN format.

    Error codes:
        W080: Invalid ARN format
    """
    if isinstance(resources, str):
        resources = [resources]

    if not isinstance(resources, list):
        report.add(
            LintResult(
                severity=LintSeverity.WARNING,
                code="W080",
                message="Resource must be a string or array of strings",
                location=location,
            )
        )
        return

    for i, resource in enumerate(resources):
        res_location = f"{location}[{i}]" if len(resources) > 1 else location

        if not isinstance(resource, str):
            report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W080",
                    message=f"Resource must be a string, got {type(resource).__name__}",
                    location=res_location,
                )
            )
            continue

        # "*" is always valid (matches all resources)
        if resource == "*":
            continue

        # Wildcard patterns not starting with arn: are allowed
        if not resource.startswith("arn:"):
            if "*" not in resource and "?" not in resource:
                report.add(
                    LintResult(
                        severity=LintSeverity.WARNING,
                        code="W080",
                        message=f"Resource '{resource}' does not appear to be a valid ARN",
                        location=res_location,
                        suggestion=(
                            "ARNs should follow format:"
                            " arn:partition:service:region:account:resource"
                        ),
                    )
                )
            continue

        # Parse ARN components
        parts = resource.split(":")
        if len(parts) < 6:
            report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W080",
                    message=f"ARN has too few components: '{resource}'",
                    location=res_location,
                    suggestion="ARN format: arn:partition:service:region:account:resource",
                )
            )
            continue

        # Validate partition (parts[1])
        partition = parts[1]
        valid_partitions = {"aws", "aws-cn", "aws-us-gov", "*"}
        if partition not in valid_partitions and "*" not in partition and "?" not in partition:
            report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W080",
                    message=f"Unknown partition in ARN: '{partition}'",
                    location=res_location,
                    suggestion="Valid partitions: aws, aws-cn, aws-us-gov",
                )
            )


# =========================================================================
# Resource-level permissions validation
# =========================================================================


def validate_resource_level_permissions(
    stmt: dict,
    location: str,
    report: LintReport,
) -> None:
    """Warn when a statement restricts Resource but uses global actions.

    Actions that do not support resource-level permissions (e.g.
    ``iam:ListUsers``, ``sts:GetCallerIdentity``) ignore the Resource
    element entirely.  An SCP with a non-``*`` Resource and such actions
    will not behave as expected.

    Error codes:
        W052: Action does not support resource-level permissions
    """
    # Only check statements with explicit Action (not NotAction)
    actions_raw = stmt.get("Action")
    if actions_raw is None:
        return

    # Normalize to list
    if isinstance(actions_raw, str):
        actions_list: list[str] = [actions_raw]
    elif isinstance(actions_raw, list):
        actions_list = [a for a in actions_raw if isinstance(a, str)]
    else:
        return

    # Get the resource element
    resources_raw = stmt.get("Resource", stmt.get("NotResource"))
    if resources_raw is None:
        return  # No resource restriction -> nothing to warn about

    # Normalize to list
    if isinstance(resources_raw, str):
        resources_list: list[str] = [resources_raw]
    elif isinstance(resources_raw, list):
        resources_list = [r for r in resources_raw if isinstance(r, str)]
    else:
        return

    # If all resources are "*", no restriction to warn about
    if all(r == "*" for r in resources_list):
        return

    # Check each concrete action (skip wildcards -- can't determine if global)
    for action in actions_list:
        if "*" in action or "?" in action:
            continue
        if is_global_action(action):
            report.add(
                LintResult(
                    severity=LintSeverity.WARNING,
                    code="W052",
                    message=(
                        f"Action '{action}' does not support resource-level "
                        f"permissions but statement restricts Resource"
                    ),
                    location=location,
                    suggestion=(
                        f"'{action}' ignores the Resource element and only "
                        f"works with Resource: \"*\". The resource restriction "
                        f"will have no effect on this action."
                    ),
                )
            )
