"""The Tool contract.

Every capability is a Tool. The AI decides *which* Tool to run; a Tool contains
no AI logic of its own. That separation is what makes each one independently
testable and replaceable.

Tools never call each other. A Tool that invoked another would build a hidden
call graph the workflow engine cannot see, resume, or audit — and "every
workflow must be resumable" is only achievable if the engine owns sequencing.
"""

from __future__ import annotations

import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class ExecutionPolicy(str, Enum):
    """How much autonomy a Tool has (ADR-0004).

    Declared per Tool, in code — never decided at runtime by the model. A Tool
    that fails to declare one gets DISABLED, so the unsafe default is
    unreachable rather than merely discouraged.
    """

    AUTOMATIC = "automatic"
    """Read-only or side-effect free. Being wrong wastes time, not data."""

    REQUIRES_APPROVAL = "requires_approval"
    """Writes to production client data. The AI proposes; a human approves."""

    DISABLED = "disabled"
    """Not executable. Deletion and permission changes live here permanently."""


@dataclass(frozen=True, slots=True)
class ToolResult:
    """What every Tool returns — success or failure, always this shape.

    Structured rather than raw values so an agent never has to guess whether it
    is holding a result or an error, and so the workflow engine can log every
    step identically.
    """

    ok: bool
    data: dict[str, Any] = field(default_factory=dict)
    error: str | None = None
    confidence: float | None = None
    """Only meaningful for Tools that infer (OCR, classification). None means
    "not an inference", which is different from "inferred with no confidence"."""

    duration_ms: int | None = None

    @classmethod
    def success(
        cls, data: dict[str, Any] | None = None, confidence: float | None = None
    ) -> ToolResult:
        return cls(ok=True, data=data or {}, confidence=confidence)

    @classmethod
    def failure(cls, error: str, data: dict[str, Any] | None = None) -> ToolResult:
        return cls(ok=False, data=data or {}, error=error)

    #: A Tool started something whose outcome is decided elsewhere.
    AWAITING_DECISION = "awaiting_decision"

    @classmethod
    def pending(cls, data: dict[str, Any] | None = None) -> ToolResult:
        """The work has been handed to someone else and has not come back.

        Not a failure and not a success — a third outcome, and one this platform
        genuinely has: submitting a proposal to the CMS hands the decision to a
        human, and nothing here knows when or how they will answer.

        Reporting it as failure would make the workflow retry a submission that
        already landed. Reporting it as success would carry on as though a
        document had been filed.
        """
        return cls(ok=False, data=data or {}, error=cls.AWAITING_DECISION)


class Tool(ABC):
    """Base class for every capability."""

    name: str = ""
    description: str = ""
    policy: ExecutionPolicy = ExecutionPolicy.DISABLED

    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)

        # Caught when the class is defined rather than when it is first invoked,
        # which is typically in production at 3am.
        if not getattr(cls, "__abstractmethods__", None):
            if not cls.name:
                raise TypeError(f"{cls.__name__} must declare a name.")
            if not cls.description:
                raise TypeError(f"{cls.__name__} must declare a description.")

    @abstractmethod
    def run(self, **kwargs: Any) -> ToolResult:
        """Do the thing. Structured input, structured output, no side channels."""

    def execute(self, **kwargs: Any) -> ToolResult:
        """Run with the policy enforced and the duration recorded.

        Agents call this, never ``run`` directly: it is where DISABLED is
        refused and where REQUIRES_APPROVAL is stopped short of acting. A Tool
        author cannot forget to check, because they never write the check.
        """
        if self.policy is ExecutionPolicy.DISABLED:
            return ToolResult.failure(f"{self.name} is disabled and cannot be executed.")

        if self.policy is ExecutionPolicy.REQUIRES_APPROVAL:
            # Not an error — the correct outcome. The Tool's job here is to
            # produce a proposal, which the caller routes to a human.
            return ToolResult(
                ok=False,
                error="approval_required",
                data={"tool": self.name, "proposed": kwargs},
            )

        return self._invoke(**kwargs)

    def execute_approved(self, **kwargs: Any) -> ToolResult:
        """Run an approval-gated Tool *after* a human has approved it.

        Deliberately a separate method rather than a flag on ``execute``: the
        one path that performs an approved write is greppable, and no caller
        reaches it by passing an argument it did not think about.

        DISABLED is unreachable from here. Approval authorises work the design
        already permits; it cannot promote deletion or permission changes into
        existence, which is the entire meaning of that policy.
        """
        if self.policy is not ExecutionPolicy.REQUIRES_APPROVAL:
            return ToolResult.failure(
                f"{self.name} is {self.policy.value}; approval does not apply to it."
            )

        return self._invoke(**kwargs)

    # ── Internals ─────────────────────────────────────────────────────────

    def _invoke(self, **kwargs: Any) -> ToolResult:
        started = time.perf_counter()
        try:
            result = self.run(**kwargs)
        except Exception as exc:  # noqa: BLE001 - a failing Tool must not kill the workflow
            return ToolResult.failure(f"{self.name} raised: {exc}")

        return ToolResult(
            ok=result.ok,
            data=result.data,
            error=result.error,
            confidence=result.confidence,
            duration_ms=int((time.perf_counter() - started) * 1000),
        )
