from __future__ import annotations

import re
from typing import Any

from acl_engine.actions.registry import ActionDefinition, ParameterDefinition
from acl_engine.parser import ACLCommand, VariableRef


class ACLValidationError(ValueError):
    def __init__(self, message: str, action: str | None = None, line_number: int | None = None) -> None:
        self.message = message
        self.action = action
        self.line_number = line_number
        self.code = "validation_error"

        location = []
        if action:
            location.append(f"action={action}")
        if line_number is not None:
            location.append(f"line={line_number}")

        suffix = f" ({', '.join(location)})" if location else ""
        super().__init__(f"{message}{suffix}")


class Validator:
    def __init__(self, registry: Any) -> None:
        self._registry = registry

    def validate(self, command: ACLCommand) -> tuple[ActionDefinition, dict[str, Any]]:
        definition = self._get_definition(command)
        params = definition.apply_defaults(command.params)
        self._validate_required(command, definition.params, params)
        self._validate_supported(command, definition.params)
        self._validate_types(command, params, definition.params, allow_variable_refs=True)
        return definition, params

    def validate_resolved(
        self,
        command: ACLCommand,
        definition: ActionDefinition,
        params: dict[str, Any],
    ) -> None:
        self._validate_types(command, params, definition.params, allow_variable_refs=False)
        self._validate_constraints(command, params, definition.params)

    def _get_definition(self, command: ACLCommand) -> Any:
        try:
            return self._registry.get(command.action)
        except KeyError as error:
            raise ACLValidationError(
                f"unknown action '{command.action}'",
                action=command.action,
                line_number=command.line_number,
            ) from error

    def _validate_required(
        self,
        command: ACLCommand,
        definitions: dict[str, ParameterDefinition],
        params: dict[str, Any],
    ) -> None:
        missing = sorted(name for name, definition in definitions.items() if definition.required and name not in params)
        if missing:
            raise ACLValidationError(
                f"missing required parameters: {', '.join(missing)}",
                action=command.action,
                line_number=command.line_number,
            )

    def _validate_supported(
        self,
        command: ACLCommand,
        definitions: dict[str, ParameterDefinition],
    ) -> None:
        unexpected = sorted(name for name in command.params if name not in definitions)
        if unexpected:
            raise ACLValidationError(
                f"unexpected parameters: {', '.join(unexpected)}",
                action=command.action,
                line_number=command.line_number,
            )

    def _validate_types(
        self,
        command: ACLCommand,
        params: dict[str, Any],
        definitions: dict[str, ParameterDefinition],
        *,
        allow_variable_refs: bool,
    ) -> None:
        for name, value in params.items():
            if allow_variable_refs and isinstance(value, VariableRef):
                continue

            expected_types = definitions[name].types
            if not _matches_type(value, expected_types):
                expected_label = " or ".join(type_.__name__ for type_ in expected_types)
                actual_label = type(value).__name__
                raise ACLValidationError(
                    f"parameter '{name}' expected {expected_label}, got {actual_label}",
                    action=command.action,
                    line_number=command.line_number,
                )

    def _validate_constraints(
        self,
        command: ACLCommand,
        params: dict[str, Any],
        definitions: dict[str, ParameterDefinition],
    ) -> None:
        for name, value in params.items():
            defn = definitions.get(name)
            if defn is None:
                continue

            if isinstance(value, str):
                if defn.min_length is not None and len(value) < defn.min_length:
                    raise ACLValidationError(
                        f"parameter '{name}' length {len(value)} is below minimum {defn.min_length}",
                        action=command.action,
                        line_number=command.line_number,
                    )
                if defn.max_length is not None and len(value) > defn.max_length:
                    raise ACLValidationError(
                        f"parameter '{name}' length {len(value)} exceeds maximum {defn.max_length}",
                        action=command.action,
                        line_number=command.line_number,
                    )
                if defn.pattern is not None and not re.fullmatch(defn.pattern, value):
                    raise ACLValidationError(
                        f"parameter '{name}' does not match pattern '{defn.pattern}'",
                        action=command.action,
                        line_number=command.line_number,
                    )

            if isinstance(value, (int, float)) and not isinstance(value, bool):
                if defn.min_value is not None and value < defn.min_value:
                    raise ACLValidationError(
                        f"parameter '{name}' value {value} is below minimum {defn.min_value}",
                        action=command.action,
                        line_number=command.line_number,
                    )
                if defn.max_value is not None and value > defn.max_value:
                    raise ACLValidationError(
                        f"parameter '{name}' value {value} exceeds maximum {defn.max_value}",
                        action=command.action,
                        line_number=command.line_number,
                    )


def _matches_type(value: Any, expected_types: tuple[type, ...]) -> bool:
    if object in expected_types:
        return True

    for expected_type in expected_types:
        if expected_type is int:
            if isinstance(value, int) and not isinstance(value, bool):
                return True
            continue
        if expected_type is float:
            if isinstance(value, float):
                return True
            continue
        if isinstance(value, expected_type):
            return True
    return False
