from __future__ import annotations

import importlib.util
from pathlib import Path
from typing import Any, Callable
from uuid import uuid4

from acl_engine.actions.registry import ActionRegistry, register_builtin_actions
from acl_engine.context import ACLVariableError
from acl_engine.executor import Executor, format_result
from acl_engine.parser import ACLCommand, ACLParseError, VariableRef, parse_acl
from acl_engine.policy import ExecutionPolicy, build_policy
from acl_engine.validator import ACLValidationError, Validator


class ACLEngine:
    def __init__(
        self,
        *,
        workspace: str | Path = "./workspace",
        timeout: int = 30,
        stop_on_error: bool = True,
        policy: str | ExecutionPolicy = "standard",
        allowed_commands: set[str] | None = None,
        blocked_patterns: tuple[str, ...] | None = None,
        allowed_domains: set[str] | None = None,
        max_file_bytes: int | None = None,
        allowed_capabilities: set[str] | None = None,
        trace_id: str | None = None,
    ) -> None:
        self.workspace = Path(workspace).resolve()
        self.workspace.mkdir(parents=True, exist_ok=True)

        self.timeout = timeout
        self.stop_on_error = stop_on_error
        self.policy = build_policy(
            policy,
            allowed_commands=allowed_commands,
            blocked_patterns=blocked_patterns,
            allowed_domains=allowed_domains,
            max_file_bytes=max_file_bytes,
            allowed_capabilities=allowed_capabilities,
        )
        self.trace_id = trace_id or str(uuid4())

        self.registry = ActionRegistry()
        register_builtin_actions(self.registry)
        self.validator = Validator(self.registry)
        self.executor = Executor(
            self.registry,
            self.validator,
            workspace_root=self.workspace,
            timeout=self.timeout,
            policy=self.policy,
            stop_on_error=self.stop_on_error,
            trace_id=self.trace_id,
        )

    def register_action(
        self,
        name: str,
        func: Callable[..., Any],
        schema: dict[str, type | tuple[type, ...] | dict[str, Any]],
        *,
        capabilities: set[str] | None = None,
        description: str = "",
        returns: str = "",
    ) -> None:
        self.registry.register(
            name, func, schema,
            capabilities=capabilities,
            description=description,
            returns=returns,
        )

    def load_plugin_module(self, module_path: str | Path) -> None:
        module_path = Path(module_path).resolve()
        spec = importlib.util.spec_from_file_location("acl_plugin", str(module_path))
        if spec is None or spec.loader is None:
            raise ImportError(f"cannot load plugin from '{module_path}'")
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        register_fn = getattr(module, "register", None)
        if register_fn is None:
            raise ImportError(f"plugin '{module_path}' has no register() function")
        register_fn(self.registry)

    def execute(self, acl_text: str) -> dict[str, Any]:
        try:
            commands = parse_acl(acl_text)
        except ACLParseError as error:
            return format_result(
                "error",
                {},
                [],
                error,
                trace_id=self.trace_id,
                summary={"policy": self.policy.name, "steps_total": 0, "steps_completed": 0, "steps_failed": 0},
            )
        return self.executor.execute(commands)

    def validate(self, acl_text: str) -> dict[str, Any]:
        return self._inspect(acl_text, include_plan=False)

    def dry_run(self, acl_text: str) -> dict[str, Any]:
        return self._inspect(acl_text, include_plan=True)

    def list_actions(
        self,
        *,
        namespace: str | None = None,
        capability: str | None = None,
    ) -> list[str]:
        return sorted(self.describe_actions(namespace=namespace, capability=capability))

    def describe_actions(
        self,
        *,
        namespace: str | None = None,
        capability: str | None = None,
    ) -> dict[str, dict[str, Any]]:
        descriptions = self.registry.describe_actions()
        if namespace is not None:
            descriptions = {
                name: definition
                for name, definition in descriptions.items()
                if name.startswith(f"{namespace}.")
            }
        if capability is not None:
            descriptions = {
                name: definition
                for name, definition in descriptions.items()
                if capability in definition.get("capabilities", [])
            }
        return descriptions

    def describe_policy(self) -> dict[str, Any]:
        return {
            "name": self.policy.name,
            "allowed_commands": sorted(self.policy.allowed_commands),
            "blocked_patterns": list(self.policy.blocked_patterns),
            "allow_network": self.policy.allow_network,
            "allow_delete": self.policy.allow_delete,
            "allowed_domains": sorted(self.policy.allowed_domains) if self.policy.allowed_domains is not None else None,
            "max_file_bytes": self.policy.max_file_bytes,
            "allowed_capabilities": sorted(self.policy.allowed_capabilities) if self.policy.allowed_capabilities is not None else None,
        }

    def _inspect(self, acl_text: str, *, include_plan: bool) -> dict[str, Any]:
        try:
            commands = parse_acl(acl_text)
        except ACLParseError as error:
            return format_result(
                "error",
                {},
                [],
                error,
                trace_id=self.trace_id,
                summary={"policy": self.policy.name, "steps_total": 0, "steps_completed": 0, "steps_failed": 0},
            )

        assigned: set[str] = set()
        inspected_commands: list[dict[str, Any]] = []

        try:
            for command in commands:
                definition, params = self.validator.validate(command)
                missing_reference = _find_missing_reference(params, assigned)
                if missing_reference is not None:
                    raise ACLVariableError(
                        missing_reference.name,
                        f"variable referenced before assignment: {_render_reference(missing_reference)}",
                    )

                inspected_commands.append(
                    {
                        "line": command.line_number,
                        "action": command.action,
                        "assign": command.assign,
                        "meta": dict(command.meta),
                        "params": params,
                        "description": definition.description,
                    }
                )
                if command.assign:
                    assigned.add(command.assign)
        except (ACLValidationError, ACLVariableError) as error:
            return format_result(
                "error",
                {},
                [],
                error,
                trace_id=self.trace_id,
                summary={"policy": self.policy.name, "steps_total": len(commands), "steps_completed": 0, "steps_failed": 1},
            )

        payload: dict[str, Any] = {"commands": len(inspected_commands), "policy": self.policy.name}
        if include_plan:
            payload["plan"] = inspected_commands
        return format_result(
            "success",
            payload,
            [],
            trace_id=self.trace_id,
            summary={"policy": self.policy.name, "steps_total": len(inspected_commands), "steps_completed": 0, "steps_failed": 0},
        )


def execute_acl(
    acl_text: str,
    *,
    workspace: str | Path = "./workspace",
    timeout: int = 30,
    stop_on_error: bool = True,
    policy: str | ExecutionPolicy = "standard",
    allowed_domains: set[str] | None = None,
    allowed_capabilities: set[str] | None = None,
) -> dict[str, Any]:
    engine = ACLEngine(
        workspace=workspace,
        timeout=timeout,
        stop_on_error=stop_on_error,
        policy=policy,
        allowed_domains=allowed_domains,
        allowed_capabilities=allowed_capabilities,
    )
    return engine.execute(acl_text)


def _find_missing_reference(
    params: dict[str, Any],
    assigned: set[str],
) -> VariableRef | None:
    for value in params.values():
        reference = _find_reference(value, assigned)
        if reference is not None:
            return reference
    return None


def _find_reference(value: Any, assigned: set[str]) -> VariableRef | None:
    if isinstance(value, VariableRef):
        if value.name not in assigned:
            return value
        return None
    if isinstance(value, list):
        for item in value:
            reference = _find_reference(item, assigned)
            if reference is not None:
                return reference
    if isinstance(value, dict):
        for item in value.values():
            reference = _find_reference(item, assigned)
            if reference is not None:
                return reference
    return None


def _render_reference(reference: VariableRef) -> str:
    parts = [reference.name]
    for selector in reference.selectors:
        if isinstance(selector, int):
            parts.append(f"[{selector}]")
        else:
            parts.append(f".{selector}")
    return "".join(parts)