"""
Condition operator implementations for SCP evaluation.

Each operator function takes a context value and a list of condition values,
returning True if the condition is satisfied.
"""

from __future__ import annotations

import fnmatch
import ipaddress
from datetime import datetime
from typing import Any


def string_equals(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if context value exactly matches any condition value (case-sensitive)."""
    if context_value is None:
        return False
    return context_value in condition_values


def string_not_equals(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if context value does NOT match any condition value."""
    if context_value is None:
        return True  # None doesn't equal anything
    return context_value not in condition_values


def string_equals_ignore_case(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if context value matches any condition value (case-insensitive)."""
    if context_value is None:
        return False
    lower_value = context_value.lower()
    return any(v.lower() == lower_value for v in condition_values)


def string_not_equals_ignore_case(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if context value does NOT match any condition value (case-insensitive)."""
    if context_value is None:
        return True
    lower_value = context_value.lower()
    return not any(v.lower() == lower_value for v in condition_values)


def string_like(context_value: str | None, condition_values: list[str]) -> bool:
    """
    Check if context value matches any pattern (supports * and ? wildcards).

    AWS StringLike uses SQL-like pattern matching:
    - * matches any sequence of characters
    - ? matches any single character
    """
    if context_value is None:
        return False

    for pattern in condition_values:
        # Convert AWS pattern to fnmatch pattern (they're compatible)
        if fnmatch.fnmatch(context_value, pattern):
            return True
    return False


def string_not_like(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if context value does NOT match any pattern."""
    if context_value is None:
        return True
    return not string_like(context_value, condition_values)


def arn_equals(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if ARN exactly matches any condition value."""
    # ARN comparison is case-sensitive
    return string_equals(context_value, condition_values)


def arn_not_equals(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if ARN does NOT match any condition value."""
    return string_not_equals(context_value, condition_values)


def arn_like(context_value: str | None, condition_values: list[str]) -> bool:
    """
    Check if ARN matches any pattern with wildcards.

    ARN format: arn:partition:service:region:account:resource
    Wildcards (*) can be used in any segment.
    """
    if context_value is None:
        return False

    for pattern in condition_values:
        # ARN patterns use * wildcards, compatible with fnmatch
        if fnmatch.fnmatch(context_value, pattern):
            return True
    return False


def arn_not_like(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if ARN does NOT match any pattern."""
    if context_value is None:
        return True
    return not arn_like(context_value, condition_values)


def ip_address_matches(context_value: str | None, condition_values: list[str]) -> bool:
    """
    Check if IP address is within any of the specified CIDR ranges.

    Args:
        context_value: IP address string (e.g., "192.168.1.100")
        condition_values: List of CIDR ranges (e.g., ["10.0.0.0/8", "192.168.0.0/16"])
    """
    if context_value is None:
        return False

    try:
        ip = ipaddress.ip_address(context_value)
    except ValueError:
        return False

    for cidr in condition_values:
        try:
            network = ipaddress.ip_network(cidr, strict=False)
            if ip in network:
                return True
        except ValueError:
            # Invalid CIDR, skip it
            continue

    return False


def not_ip_address(context_value: str | None, condition_values: list[str]) -> bool:
    """Check if IP address is NOT within any of the specified CIDR ranges."""
    if context_value is None:
        return True
    return not ip_address_matches(context_value, condition_values)


def _parse_datetime(value: str | datetime) -> datetime | None:
    """Parse a datetime value from string or return as-is if already datetime."""
    if isinstance(value, datetime):
        return value

    # Try common ISO formats
    formats = [
        "%Y-%m-%dT%H:%M:%SZ",
        "%Y-%m-%dT%H:%M:%S.%fZ",
        "%Y-%m-%dT%H:%M:%S%z",
        "%Y-%m-%dT%H:%M:%S",
        "%Y-%m-%d",
    ]

    for fmt in formats:
        try:
            return datetime.strptime(value, fmt)
        except ValueError:
            continue

    return None


def date_equals(context_value: datetime | str | None, condition_values: list[str]) -> bool:
    """Check if date exactly matches any condition value."""
    if context_value is None:
        return False

    ctx_dt = _parse_datetime(context_value)
    if ctx_dt is None:
        return False

    for val in condition_values:
        cond_dt = _parse_datetime(val)
        if cond_dt and ctx_dt == cond_dt:
            return True
    return False


def date_not_equals(context_value: datetime | str | None, condition_values: list[str]) -> bool:
    """Check if date does NOT match any condition value."""
    if context_value is None:
        return True
    return not date_equals(context_value, condition_values)


def date_less_than(context_value: datetime | str | None, condition_values: list[str]) -> bool:
    """Check if date is less than any condition value."""
    if context_value is None:
        return False

    ctx_dt = _parse_datetime(context_value)
    if ctx_dt is None:
        return False

    for val in condition_values:
        cond_dt = _parse_datetime(val)
        if cond_dt and ctx_dt < cond_dt:
            return True
    return False


def date_less_than_equals(
    context_value: datetime | str | None, condition_values: list[str]
) -> bool:
    """Check if date is less than or equal to any condition value."""
    if context_value is None:
        return False

    ctx_dt = _parse_datetime(context_value)
    if ctx_dt is None:
        return False

    for val in condition_values:
        cond_dt = _parse_datetime(val)
        if cond_dt and ctx_dt <= cond_dt:
            return True
    return False


def date_greater_than(context_value: datetime | str | None, condition_values: list[str]) -> bool:
    """Check if date is greater than any condition value."""
    if context_value is None:
        return False

    ctx_dt = _parse_datetime(context_value)
    if ctx_dt is None:
        return False

    for val in condition_values:
        cond_dt = _parse_datetime(val)
        if cond_dt and ctx_dt > cond_dt:
            return True
    return False


def date_greater_than_equals(
    context_value: datetime | str | None, condition_values: list[str]
) -> bool:
    """Check if date is greater than or equal to any condition value."""
    if context_value is None:
        return False

    ctx_dt = _parse_datetime(context_value)
    if ctx_dt is None:
        return False

    for val in condition_values:
        cond_dt = _parse_datetime(val)
        if cond_dt and ctx_dt >= cond_dt:
            return True
    return False


def bool_equals(context_value: Any, condition_values: list[str]) -> bool:
    """Check if boolean value matches condition."""
    if context_value is None:
        return False

    # Convert context value to bool
    if isinstance(context_value, bool):
        ctx_bool = context_value
    elif isinstance(context_value, str):
        ctx_bool = context_value.lower() == "true"
    else:
        ctx_bool = bool(context_value)

    # Check against condition values
    for val in condition_values:
        cond_bool = val.lower() == "true" if isinstance(val, str) else bool(val)
        if ctx_bool == cond_bool:
            return True
    return False


def null_check(context_value: Any, condition_values: list[str]) -> bool:
    """
    Check if context key exists (Null condition).

    Null: {"aws:SomeKey": "true"} means the key must NOT exist (be null)
    Null: {"aws:SomeKey": "false"} means the key MUST exist (not be null)
    """
    is_null = context_value is None

    for val in condition_values:
        expected_null = val.lower() == "true" if isinstance(val, str) else bool(val)
        if is_null == expected_null:
            return True
    return False


def _parse_numeric(value: Any) -> float | None:
    """Parse a value as a numeric float. Returns None if not parseable."""
    if value is None:
        return None
    try:
        return float(value)
    except (ValueError, TypeError):
        return None


def numeric_equals(context_value: Any, condition_values: list[str]) -> bool:
    """Check if numeric value equals any condition value."""
    ctx_num = _parse_numeric(context_value)
    if ctx_num is None:
        return False
    for val in condition_values:
        cond_num = _parse_numeric(val)
        if cond_num is not None and ctx_num == cond_num:
            return True
    return False


def numeric_not_equals(context_value: Any, condition_values: list[str]) -> bool:
    """Check if numeric value does NOT equal any condition value."""
    ctx_num = _parse_numeric(context_value)
    if ctx_num is None:
        return True
    return not any(
        _parse_numeric(val) is not None and ctx_num == _parse_numeric(val)
        for val in condition_values
    )


def numeric_less_than(context_value: Any, condition_values: list[str]) -> bool:
    """Check if numeric value is less than any condition value."""
    ctx_num = _parse_numeric(context_value)
    if ctx_num is None:
        return False
    for val in condition_values:
        cond_num = _parse_numeric(val)
        if cond_num is not None and ctx_num < cond_num:
            return True
    return False


def numeric_less_than_equals(context_value: Any, condition_values: list[str]) -> bool:
    """Check if numeric value is less than or equal to any condition value."""
    ctx_num = _parse_numeric(context_value)
    if ctx_num is None:
        return False
    for val in condition_values:
        cond_num = _parse_numeric(val)
        if cond_num is not None and ctx_num <= cond_num:
            return True
    return False


def numeric_greater_than(context_value: Any, condition_values: list[str]) -> bool:
    """Check if numeric value is greater than any condition value."""
    ctx_num = _parse_numeric(context_value)
    if ctx_num is None:
        return False
    for val in condition_values:
        cond_num = _parse_numeric(val)
        if cond_num is not None and ctx_num > cond_num:
            return True
    return False


def numeric_greater_than_equals(context_value: Any, condition_values: list[str]) -> bool:
    """Check if numeric value is greater than or equal to any condition value."""
    ctx_num = _parse_numeric(context_value)
    if ctx_num is None:
        return False
    for val in condition_values:
        cond_num = _parse_numeric(val)
        if cond_num is not None and ctx_num >= cond_num:
            return True
    return False


def binary_equals(context_value: Any, condition_values: list[str]) -> bool:
    """Check if binary (base64-encoded) value equals any condition value."""
    if context_value is None:
        return False
    ctx_str = str(context_value)
    return ctx_str in condition_values


# Operator mapping
OPERATORS: dict[str, Any] = {
    # String operators
    "StringEquals": string_equals,
    "StringNotEquals": string_not_equals,
    "StringEqualsIgnoreCase": string_equals_ignore_case,
    "StringNotEqualsIgnoreCase": string_not_equals_ignore_case,
    "StringLike": string_like,
    "StringNotLike": string_not_like,
    # ARN operators
    "ArnEquals": arn_equals,
    "ArnNotEquals": arn_not_equals,
    "ArnLike": arn_like,
    "ArnNotLike": arn_not_like,
    # IP address operators
    "IpAddress": ip_address_matches,
    "NotIpAddress": not_ip_address,
    # Date operators
    "DateEquals": date_equals,
    "DateNotEquals": date_not_equals,
    "DateLessThan": date_less_than,
    "DateLessThanEquals": date_less_than_equals,
    "DateGreaterThan": date_greater_than,
    "DateGreaterThanEquals": date_greater_than_equals,
    # Numeric operators
    "NumericEquals": numeric_equals,
    "NumericNotEquals": numeric_not_equals,
    "NumericLessThan": numeric_less_than,
    "NumericLessThanEquals": numeric_less_than_equals,
    "NumericGreaterThan": numeric_greater_than,
    "NumericGreaterThanEquals": numeric_greater_than_equals,
    # Boolean operators
    "Bool": bool_equals,
    # Binary operators
    "BinaryEquals": binary_equals,
    # Null check
    "Null": null_check,
}
