from __future__ import annotations

from copy import deepcopy
import re
from dataclasses import dataclass
from typing import Any, Callable

from acl_engine.actions import data as data_actions
from acl_engine.actions import file as file_actions
from acl_engine.actions import memory as memory_actions
from acl_engine.actions import net as net_actions
from acl_engine.actions import system as system_actions
from acl_engine.actions import util as util_actions

_ACTION_NAME_PATTERN = re.compile(r"^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$")
_MISSING = object()


@dataclass(frozen=True)
class ParameterDefinition:
    name: str
    types: tuple[type, ...]
    required: bool = True
    default: Any = _MISSING
    description: str = ""
    sensitive: bool = False
    min_length: int | None = None
    max_length: int | None = None
    pattern: str | None = None
    min_value: int | float | None = None
    max_value: int | float | None = None

    def has_default(self) -> bool:
        return self.default is not _MISSING


@dataclass(frozen=True)
class ActionDefinition:
    name: str
    func: Callable[..., Any]
    params: dict[str, ParameterDefinition]
    description: str = ""
    returns: str = ""
    pass_runtime: bool = False
    capabilities: frozenset[str] = frozenset()

    def apply_defaults(self, params: dict[str, Any]) -> dict[str, Any]:
        normalized = dict(params)
        for name, definition in self.params.items():
            if name not in normalized and definition.has_default():
                normalized[name] = deepcopy(definition.default)
        return normalized


class ActionRegistry:
    def __init__(self) -> None:
        self._actions: dict[str, ActionDefinition] = {}

    def register(
        self,
        name: str,
        func: Callable[..., Any],
        schema: dict[str, type | tuple[type, ...] | dict[str, Any]],
        *,
        required: set[str] | None = None,
        pass_runtime: bool = False,
        description: str = "",
        returns: str = "",
        capabilities: set[str] | None = None,
    ) -> None:
        if not _ACTION_NAME_PATTERN.match(name):
            raise ValueError(f"invalid action name '{name}'")
        if name in self._actions:
            raise ValueError(f"action '{name}' is already registered")

        normalized_params = {
            param_name: _build_parameter_definition(param_name, spec, required)
            for param_name, spec in schema.items()
        }
        required_params = {name for name, definition in normalized_params.items() if definition.required}
        if required is not None and not set(required).issubset(normalized_params):
            raise ValueError("required parameters must exist in the schema")

        self._actions[name] = ActionDefinition(
            name=name,
            func=func,
            params=normalized_params,
            description=description,
            returns=returns,
            pass_runtime=pass_runtime,
            capabilities=frozenset(capabilities) if capabilities else frozenset(),
        )

    def get(self, name: str) -> ActionDefinition:
        if name not in self._actions:
            raise KeyError(name)
        return self._actions[name]

    def has(self, name: str) -> bool:
        return name in self._actions

    def list_actions(self) -> list[str]:
        return sorted(self._actions)

    def describe_actions(self) -> dict[str, dict[str, Any]]:
        descriptions: dict[str, dict[str, Any]] = {}
        for name in self.list_actions():
            definition = self._actions[name]
            descriptions[name] = {
                "description": definition.description,
                "returns": definition.returns,
                "capabilities": sorted(definition.capabilities),
                "params": {
                    param_name: _describe_parameter(param)
                    for param_name, param in definition.params.items()
                },
            }
        return descriptions


def register_builtin_actions(registry: ActionRegistry) -> None:
    registry.register(
        "file.read",
        file_actions.read,
        {"path": {"type": str, "description": "Workspace-relative file path", "min_length": 1}},
        pass_runtime=True,
        description="Read a text file from the workspace",
        returns="string",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.read_json",
        file_actions.read_json,
        {"path": {"type": str, "description": "Workspace-relative JSON file path", "min_length": 1}},
        pass_runtime=True,
        description="Read and parse a JSON file from the workspace",
        returns="any",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.write",
        file_actions.write,
        {
            "path": {"type": str, "description": "Workspace-relative file path", "min_length": 1},
            "content": {"type": object, "description": "Content to write"},
        },
        pass_runtime=True,
        description="Write text content to a workspace file",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.write_json",
        file_actions.write_json,
        {
            "path": {"type": str, "description": "Workspace-relative JSON file path", "min_length": 1},
            "value": {"type": object, "description": "Value to serialize as JSON"},
            "indent": {"type": (int, type(None)), "default": 2, "description": "Indent width or null", "min_value": 0},
            "sort_keys": {"type": bool, "default": False, "description": "Sort object keys in output"},
        },
        pass_runtime=True,
        description="Serialize a value and write it as a JSON file",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.append",
        file_actions.append,
        {
            "path": {"type": str, "description": "Workspace-relative file path", "min_length": 1},
            "content": {"type": object, "description": "Content to append"},
        },
        pass_runtime=True,
        description="Append text content to a workspace file",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.list",
        file_actions.list_dir,
        {"path": {"type": str, "default": ".", "description": "Directory to list"}},
        pass_runtime=True,
        description="List files in a workspace directory",
        returns="list",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.delete",
        file_actions.delete,
        {"path": {"type": str, "description": "Workspace-relative file path", "min_length": 1}},
        pass_runtime=True,
        description="Delete a file from the workspace",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.exists",
        file_actions.exists,
        {"path": {"type": str, "description": "Workspace-relative file path", "min_length": 1}},
        pass_runtime=True,
        description="Check whether a workspace path exists",
        returns="bool",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.stat",
        file_actions.stat,
        {"path": {"type": str, "description": "Workspace-relative file path", "min_length": 1}},
        pass_runtime=True,
        description="Return metadata for a workspace path",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.mkdir",
        file_actions.mkdir,
        {"path": {"type": str, "description": "Directory path to create", "min_length": 1}},
        pass_runtime=True,
        description="Create a workspace directory",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.copy",
        file_actions.copy,
        {
            "source": {"type": str, "description": "Source file path", "min_length": 1},
            "destination": {"type": str, "description": "Destination file path", "min_length": 1},
            "overwrite": {"type": bool, "default": False, "description": "Replace destination if it exists"},
        },
        pass_runtime=True,
        description="Copy a workspace file to another workspace path",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.move",
        file_actions.move,
        {
            "source": {"type": str, "description": "Source file path", "min_length": 1},
            "destination": {"type": str, "description": "Destination file path", "min_length": 1},
            "overwrite": {"type": bool, "default": False, "description": "Replace destination if it exists"},
        },
        pass_runtime=True,
        description="Move a workspace file to another workspace path",
        returns="object",
        capabilities={"filesystem"},
    )
    registry.register(
        "file.glob",
        file_actions.glob,
        {
            "path": {"type": str, "default": ".", "description": "Base directory to search"},
            "pattern": {"type": str, "default": "*", "description": "Glob pattern", "min_length": 1},
        },
        pass_runtime=True,
        description="Find workspace paths matching a glob pattern",
        returns="list",
        capabilities={"filesystem"},
    )

    registry.register(
        "system.run",
        system_actions.run,
        {
            "command": {"type": str, "description": "Allowed command to execute", "min_length": 1},
            "args": {"type": list, "default": [], "description": "Argument list"},
            "cwd": {"type": (str, type(None)), "default": None, "description": "Optional working directory"},
        },
        pass_runtime=True,
        description="Run a whitelisted system command",
        returns="object",
        capabilities={"process"},
    )

    registry.register(
        "net.request",
        net_actions.request,
        {
            "method": {"type": str, "description": "HTTP method", "min_length": 1},
            "url": {"type": str, "description": "Request URL", "min_length": 1},
            "headers": {"type": (dict, type(None)), "default": None, "description": "Optional headers", "sensitive": True},
            "query": {"type": (dict, type(None)), "default": None, "description": "Optional query parameters"},
            "body": {"type": (dict, list, str, type(None)), "default": None, "description": "Optional request body"},
            "json_body": {"type": object, "default": None, "description": "Optional JSON request body"},
        },
        pass_runtime=True,
        description="Perform an HTTP request",
        returns="object",
        capabilities={"network"},
    )

    registry.register(
        "memory.store",
        memory_actions.store,
        {
            "key": {"type": str, "description": "Memory key", "min_length": 1},
            "value": {"type": object, "description": "Value to store"},
        },
        pass_runtime=True,
        description="Store a value in execution memory",
        returns="object",
        capabilities={"memory"},
    )
    registry.register(
        "memory.get",
        memory_actions.get_value,
        {"key": {"type": str, "description": "Memory key", "min_length": 1}},
        pass_runtime=True,
        description="Read a value from execution memory",
        returns="any",
        capabilities={"memory"},
    )
    registry.register(
        "memory.delete",
        memory_actions.delete,
        {"key": {"type": str, "description": "Memory key", "min_length": 1}},
        pass_runtime=True,
        description="Delete a value from execution memory",
        returns="object",
        capabilities={"memory"},
    )
    registry.register(
        "memory.list",
        memory_actions.list_keys,
        {},
        pass_runtime=True,
        description="List stored memory keys",
        returns="list",
        capabilities={"memory"},
    )
    registry.register(
        "memory.clear",
        memory_actions.clear,
        {},
        pass_runtime=True,
        description="Clear execution memory",
        returns="object",
        capabilities={"memory"},
    )

    registry.register(
        "data.from_json",
        data_actions.from_json,
        {"value": {"type": str, "description": "JSON string to parse", "min_length": 1}},
        pass_runtime=True,
        description="Parse a JSON string into structured data",
        returns="any",
        capabilities={"data"},
    )
    registry.register(
        "data.to_json",
        data_actions.to_json,
        {
            "value": {"type": object, "description": "Value to serialize"},
            "indent": {"type": (int, type(None)), "default": 2, "description": "Indent width or null", "min_value": 0},
            "sort_keys": {"type": bool, "default": False, "description": "Sort object keys"},
        },
        pass_runtime=True,
        description="Serialize a value as JSON text",
        returns="string",
        capabilities={"data"},
    )
    registry.register(
        "data.merge",
        data_actions.merge,
        {
            "left": {"type": dict, "description": "Base object"},
            "right": {"type": dict, "description": "Override object"},
            "deep": {"type": bool, "default": False, "description": "Recursively merge nested objects"},
        },
        pass_runtime=True,
        description="Merge two objects into a single object",
        returns="object",
        capabilities={"data"},
    )
    registry.register(
        "data.template",
        data_actions.template,
        {
            "template": {"type": str, "description": "Template string with {placeholders}", "min_length": 1},
            "values": {"type": dict, "description": "Template values"},
        },
        pass_runtime=True,
        description="Render a template string from object values",
        returns="string",
        capabilities={"data"},
    )
    registry.register(
        "data.select",
        data_actions.select,
        {
            "value": {"type": object, "description": "Structured value to inspect"},
            "path": {"type": str, "description": "Selector path", "min_length": 1},
        },
        pass_runtime=True,
        description="Select a nested value from an object or list using a path",
        returns="any",
        capabilities={"data"},
    )
    registry.register(
        "data.pick",
        data_actions.pick,
        {
            "value": {"type": dict, "description": "Object to extract fields from"},
            "keys": {"type": list, "description": "List of keys to keep"},
        },
        pass_runtime=True,
        description="Build a smaller object by picking specific keys",
        returns="object",
        capabilities={"data"},
    )
    registry.register(
        "data.omit",
        data_actions.omit,
        {
            "value": {"type": dict, "description": "Object to remove fields from"},
            "keys": {"type": list, "description": "List of keys to remove"},
        },
        pass_runtime=True,
        description="Build a smaller object by removing specific keys",
        returns="object",
        capabilities={"data"},
    )

    registry.register(
        "util.echo",
        util_actions.echo,
        {"value": {"type": object, "description": "Value to echo"}},
        pass_runtime=True,
        description="Return the given value unchanged",
        returns="any",
        capabilities={"utility"},
    )
    registry.register(
        "util.sleep",
        util_actions.sleep,
        {"seconds": {"type": (int, float), "description": "Sleep duration in seconds", "min_value": 0}},
        pass_runtime=True,
        description="Sleep for a bounded duration",
        returns="object",
        capabilities={"utility"},
    )
    registry.register(
        "util.now",
        util_actions.now,
        {},
        pass_runtime=True,
        description="Return the current UTC timestamp",
        returns="string",
        capabilities={"utility"},
    )
    registry.register(
        "util.uuid",
        util_actions.uuid,
        {},
        pass_runtime=True,
        description="Generate a random UUID",
        returns="string",
        capabilities={"utility"},
    )
    registry.register(
        "util.hash",
        util_actions.hash_value,
        {
            "value": {"type": object, "description": "Value to hash"},
            "algorithm": {"type": str, "default": "sha256", "description": "Hash algorithm"},
        },
        pass_runtime=True,
        description="Hash a value using a standard algorithm",
        returns="string",
        capabilities={"utility"},
    )
    registry.register(
        "util.base64_encode",
        util_actions.base64_encode,
        {"value": {"type": object, "description": "Value to encode"}},
        pass_runtime=True,
        description="Base64-encode a value",
        returns="string",
        capabilities={"utility"},
    )
    registry.register(
        "util.base64_decode",
        util_actions.base64_decode,
        {"value": {"type": str, "description": "Base64 string to decode"}},
        pass_runtime=True,
        description="Base64-decode a string",
        returns="string",
        capabilities={"utility"},
    )


def _normalize_expected_types(expected_types: type | tuple[type, ...]) -> tuple[type, ...]:
    if isinstance(expected_types, tuple):
        return expected_types
    return (expected_types,)


def _build_parameter_definition(
    name: str,
    spec: type | tuple[type, ...] | dict[str, Any],
    required: set[str] | None,
) -> ParameterDefinition:
    if isinstance(spec, dict):
        types = _normalize_expected_types(spec.get("type", object))
        has_default = "default" in spec
        is_required = spec.get("required")
        if is_required is None:
            is_required = (name in required) if required is not None else not has_default
        return ParameterDefinition(
            name=name,
            types=types,
            required=bool(is_required),
            default=deepcopy(spec["default"]) if has_default else _MISSING,
            description=str(spec.get("description", "")),
            sensitive=bool(spec.get("sensitive", False)),
            min_length=spec.get("min_length"),
            max_length=spec.get("max_length"),
            pattern=spec.get("pattern"),
            min_value=spec.get("min_value"),
            max_value=spec.get("max_value"),
        )

    return ParameterDefinition(
        name=name,
        types=_normalize_expected_types(spec),
        required=(name in required) if required is not None else True,
    )


def _describe_parameter(param: ParameterDefinition) -> dict[str, Any]:
    info: dict[str, Any] = {
        "types": [type_.__name__ for type_ in param.types],
        "required": param.required,
        "default": _serialize_default(param.default),
        "description": param.description,
        "sensitive": param.sensitive,
    }
    if param.min_length is not None:
        info["min_length"] = param.min_length
    if param.max_length is not None:
        info["max_length"] = param.max_length
    if param.pattern is not None:
        info["pattern"] = param.pattern
    if param.min_value is not None:
        info["min_value"] = param.min_value
    if param.max_value is not None:
        info["max_value"] = param.max_value
    return info


def _serialize_default(value: Any) -> Any:
    if value is _MISSING:
        return None
    return deepcopy(value)
