"""Execution-layer error leaves. v1.0 PR-D — ExecutionError category.

The two carrier leaves (:class:`LLMBehaviorError`,
:class:`ToolError`) live in their topic modules (``activegraph/llm/errors.py``
and ``activegraph/tools/errors.py``) because the per-reason prose
tables are co-located with the modules that raise them. This module
holds the runtime-internal execution-layer leaves that don't fit
cleanly into either topic.

PR-D adds:

- :class:`ApprovalNotFoundError` — pending-approval lookup miss.

PR-F adds (cross-category audit findings — sites that looked like
ConfigurationError on the PR-E hand-off list but classified as
ExecutionError on closer reading):

- :class:`RuntimeContextRequiredError` — ``ctx.propose_object`` was
  called from a behavior whose context wasn't bound to a runtime.
  Fires inside a running behavior, not at construction time.
- :class:`InvalidPatchLifecycleState` — ``graph.apply_patch`` was
  called on a patch that isn't in ``"proposed"`` state. Fires during
  the patch lifecycle, mid-execution.

PR-G adds:

- :class:`InternalEvaluatorError` — for the view-filter evaluator in
  ``graph.py``. Sibling to the two pattern-evaluator internal-bug
  raises in ``activegraph/runtime/patterns.py``; uses the shared
  :func:`activegraph.errors.internal_bug_fields` helper so the
  framework-bug prose is uniform across all three sites.
"""

from __future__ import annotations

from typing import Any, Optional

from activegraph.errors import DOCS_BASE_URL, ExecutionError


class ApprovalNotFoundError(ExecutionError, LookupError):
    """Pending-approval lookup miss.

    Fires from ``runtime.approve(approval_id)`` when the id doesn't
    refer to any currently-pending approval. Multi-inherits
    :class:`LookupError` so user code that does ``except LookupError``
    around the approval API continues to work.
    """

    _doc_slug = "approval-not-found-error"

    def __init__(
        self,
        approval_id: str,
        *,
        pending_count: Optional[int] = None,
    ) -> None:
        self.approval_id = approval_id
        self.pending_count = pending_count
        ctx: dict[str, Any] = {"approval_id": approval_id}
        if pending_count is not None:
            ctx["pending_count"] = pending_count
        pending_hint = (
            f"There are currently {pending_count} pending approvals; "
            f"none of them match {approval_id!r}."
            if pending_count is not None
            else "There are no pending approvals in this runtime."
        )
        ExecutionError.__init__(
            self,
            f"no pending approval named {approval_id!r}",
            what_failed=(
                f"runtime.approve({approval_id!r}) could not find a pending "
                f"approval with that id.\n  {pending_hint}"
            ),
            why=(
                "Approval ids are generated by the runtime when a behavior "
                "calls ctx.propose_object under a gating policy "
                "(memo_approval, risk_approval, etc.). Each id is unique "
                "to its runtime instance and is consumed when approve() "
                "succeeds. A miss means either the id is stale (already "
                "approved or denied), the runtime instance is fresh "
                "(approvals don't carry across Runtime.load), or the id is "
                "a typo. The framework refuses to no-op an unknown id "
                "because that would silently corrupt the approval audit "
                "trail."
            ),
            how_to_fix=(
                "List the currently-pending approvals:\n"
                "    rt.pending_approvals()\n"
                "to confirm what's actually pending. The diligence demo's\n"
                "step_2_approval_demo (examples/diligence_real_run.py)\n"
                "shows the canonical pattern: enumerate, then approve by id."
            ),
            context=ctx,
        )


class RuntimeContextRequiredError(ExecutionError, RuntimeError):
    """``ctx.propose_object`` (or another ctx method that requires the
    runtime) was called from a behavior whose context isn't bound to a
    runtime.

    Fires at execution time, inside a running behavior — the caller is
    the behavior body that invoked the ctx method, not the framework's
    construction code. Multi-inherits :class:`RuntimeError` for
    back-compat.
    """

    _doc_slug = "runtime-context-required-error"

    def __init__(self, *, method: str = "ctx.propose_object") -> None:
        self.method = method
        ExecutionError.__init__(
            self,
            f"{method} requires a runtime-bound context",
            what_failed=(
                f"A behavior called {method} on a BehaviorGraph context that "
                f"was constructed without a Runtime — likely a test fixture "
                f"that stubbed the graph without going through Runtime."
            ),
            why=(
                "ctx.propose_object writes to the runtime's pending-approvals "
                "queue and emits a `approval.proposed` event. Without a "
                "runtime, neither side effect can happen, and a no-op would "
                "silently break the policy gate the behavior depends on — "
                "the audit trail would show no proposal and the operator "
                "would have no record to approve.\n"
                "\n"
                f"See {DOCS_BASE_URL}/concepts/failure-model "
                f"for when the framework prefers exceptions over silent no-ops."
            ),
            how_to_fix=(
                "Construct the BehaviorGraph through a Runtime:\n"
                "    rt = Runtime(graph, ...)\n"
                "    rt.run_goal('...')   # behaviors see ctx bound to rt\n"
                "\n"
                "In a test, drive the behavior through a real Runtime — the "
                "ctx is built from the runtime's context factory and bound "
                "automatically. Mocking the ctx directly bypasses this; if "
                "the test really needs to bypass the runtime, mock the "
                "policy gate as well so propose_object isn't reached."
            ),
            context={"method": method},
        )


class InvalidPatchLifecycleState(ExecutionError, ValueError):
    """``graph.apply_patch(patch_id)`` was called on a patch that isn't
    in ``"proposed"`` state.

    Patches go through ``proposed → applied`` (success) or ``proposed →
    rejected`` (policy or behavior rejection). Calling ``apply_patch``
    on an already-applied or already-rejected patch is a programmer
    error in the calling code path; the framework refuses rather than
    re-apply or silently no-op. Multi-inherits :class:`ValueError`.
    """

    _doc_slug = "invalid-patch-lifecycle-state"

    def __init__(self, *, patch_id: str, current_status: str) -> None:
        self.patch_id = patch_id
        self.current_status = current_status
        ExecutionError.__init__(
            self,
            f"patch {patch_id} is {current_status!r}, not 'proposed'",
            what_failed=(
                f"graph.apply_patch({patch_id!r}) was called, but the patch's "
                f"current status is {current_status!r}. Only patches in the "
                f"'proposed' state can be applied."
            ),
            why=(
                "Patches are one-shot: a 'proposed' patch becomes 'applied' "
                "(success) or 'rejected' (refusal) exactly once. Re-applying "
                "an already-applied patch would emit a duplicate "
                "`patch.applied` event, which would break the replay "
                "contract — replay would produce a different event stream "
                "than the original run. The framework refuses re-application "
                "rather than emit the duplicate.\n"
                "\n"
                f"See {DOCS_BASE_URL}/concepts/failure-model "
                f"for the patch-lifecycle invariants."
            ),
            how_to_fix=(
                f"Check the patch's current status before applying:\n"
                f"    patch = graph.get_patch({patch_id!r})\n"
                f"    if patch.status == 'proposed':\n"
                f"        graph.apply_patch({patch_id!r})\n"
                f"\n"
                f"If the patch is already applied and the caller wants a "
                f"new mutation, propose a new patch — patches don't get "
                f"re-used."
            ),
            context={"patch_id": patch_id, "current_status": current_status},
        )


class InternalEvaluatorError(ExecutionError, ValueError):
    """A framework-internal evaluator received input it does not
    recognize. Should not fire in normal use — the framework's parsers
    produce a closed set of operators / AST nodes, and an unrecognized
    one means either drift between parser and evaluator or external
    AST construction that bypassed the parser.

    Multi-inherits :class:`ValueError` so user code that catches the
    builtin around view operations continues to work. Used by
    ``activegraph/core/graph.py`` for the view-filter evaluator; the
    pattern-subscription evaluator's two internal-bug raises stay as
    :class:`UnsupportedPatternError` (the natural category) but use
    the same :func:`activegraph.errors.internal_bug_fields` helper so
    the prose is uniform across all three sites.
    """

    _doc_slug = "internal-evaluator-error"

    def __init__(
        self,
        summary: str,
        *,
        what_failed: str,
        why: str,
        how_to_fix: str,
        context: Optional[dict[str, Any]] = None,
    ) -> None:
        ExecutionError.__init__(
            self,
            summary,
            what_failed=what_failed,
            why=why,
            how_to_fix=how_to_fix,
            context=context,
        )
