"""Phase 19 — translate ProposalKind decisions to engine actions.

Build A Phase 3 deleted the 8 exec=None ProposalKinds. Phase 5 adds
GenerationProposal.

Phase 19 wires the **decision** path (approve / reject / defer) to the
EventBus + per-process state. It does NOT yet wire the **execution** path
(actually firing a regen with a swapped ref, or inserting a beat into
the script) — that requires modality-specific engine glue per kind, none
of which exists today. Approval emits a ``proposal_accepted`` event with
``deferred_execution: true`` for kinds without engine glue.

Every approve/reject/defer also emits an EventBus event so the SSE
stream surfaces decisions in real time.
"""
from __future__ import annotations

import logging
from typing import Any, Optional

from recoil.api.eventbus import BUS
from recoil.api.fallback_bridge import emit_fallback

logger = logging.getLogger(__name__)


# Build A Phase 3 deleted the 8 exec=None ProposalKinds. Phase 5 adds
# GenerationProposal here with a working exec callable. The dispatch dict
# stays so the lifecycle abstraction (originate/review/approve/reject) keeps
# its registry shape — _enforce_proposal_executor() in main.py guarantees
# every entry has a non-None exec at boot.


# Phase 5: 9th ProposalKind with working executor. _enforce_proposal_executor()
# in main.py guarantees this exec is callable at boot.
def _generation_exec(_proposal: dict[str, Any]) -> dict[str, Any]:
    """Callable marker — actual dispatch goes through POST /api/proposals/generate
    in generation_routes.py. This placeholder satisfies the boot guard's
    exec != None requirement and serves as the dispatch indirection for
    future per-kind glue."""
    return {"deferred_to": "generation_routes.post_proposals_generate"}


_KIND_INFO: dict[str, dict[str, Any]] = {
    "GenerationProposal": {"exec": _generation_exec, "label": "Generate"},
}


def kind_label(kind: Optional[str]) -> str:
    if not kind:
        return "Proposal"
    info = _KIND_INFO.get(kind)
    return info["label"] if info else kind


def emit_decision(
    *,
    proposal_id: str,
    decision: str,  # "approved" | "rejected" | "deferred"
    was_pending: bool,
    kind: Optional[str] = None,
    scope: str = "engine",
    extra: Optional[dict[str, Any]] = None,
) -> None:
    """Emit an EventBus event reflecting a proposal decision.

    Severity:
      • approved   → success
      • rejected   → info  (rejection is a normal editorial choice)
      • deferred   → info

    ``was_pending`` must be resolved by the caller (mutation_routes) BEFORE
    calling ``_acted_add`` — that way the check reflects whether the id was
    genuinely pending, not whether it has already been acted on. If False,
    fires the ``proposal_id_not_pending`` sanctioned fallback and overrides
    severity to ``"fallback"`` (body still returns ``{"ok": True}``).
    Discharges Law 4 + Tenet 6 prong-1.
    """
    severity_map = {"approved": "success", "rejected": "info", "deferred": "info"}
    try:
        base_severity = severity_map[decision]
    except KeyError as exc:
        raise ValueError(
            f"Unknown proposal decision {decision!r}; "
            f"valid: {sorted(severity_map)}"
        ) from exc

    if not was_pending:
        emit_fallback(
            "proposal_id_not_pending",
            scope="api/proposals",
            payload={"proposal_id": proposal_id, "decision": decision},
        )
        severity = "fallback"
    else:
        severity = base_severity

    label = kind_label(kind)
    summary = f"{label} {decision} ({proposal_id})"

    info = _KIND_INFO.get(kind or "")
    deferred_execution = info is None or info.get("exec") is None
    payload: dict[str, Any] = {
        "proposal_id": proposal_id,
        "decision": decision,
        "kind": kind,
        "deferred_execution": deferred_execution,
    }
    if extra:
        payload.update(extra)

    BUS.emit_sync(
        severity=severity,  # type: ignore[arg-type]
        scope=scope,
        summary=summary,
        detail=None,
        payload=payload,
    )


def emit_take_action(
    *,
    take_id: str,
    action: str,  # "marked_primary" | "circled_toggled" | "rejected"
    scope: str = "engine",
    extra: Optional[dict[str, Any]] = None,
    severity: Optional[str] = None,
) -> None:
    """Emit a take-action event on the EventBus.

    Phase 2 (console-v2-fix): callers may pass an explicit ``severity``
    keyword to override the action-based default — the mutation routes
    pass ``severity="fallback"`` when ``_try_disk_mutation`` fired the
    ``take_id_not_on_disk`` sanctioned fallback, so the events drawer's
    severity filter sees one coherent fallback signal across both the
    fallback event itself AND the take-action event.
    """
    severity_map = {
        "marked_primary": "success",
        "circled_toggled": "info",
        "rejected": "info",
    }
    if severity is None:
        severity = severity_map.get(action, "info")
    summary = f"Take {action.replace('_', ' ')}: {take_id}"
    payload: dict[str, Any] = {"take_id": take_id, "action": action}
    if extra:
        payload.update(extra)
    BUS.emit_sync(
        severity=severity,  # type: ignore[arg-type]
        scope=scope,
        summary=summary,
        detail=None,
        payload=payload,
    )


def emit_memory_action(
    *,
    entry_id: str,
    on: bool,
    scope: str = "engine/memory",
    extra: Optional[dict[str, Any]] = None,
) -> None:
    summary = f"Memory entry {'enabled' if on else 'disabled'}: {entry_id}"
    payload: dict[str, Any] = {"entry_id": entry_id, "on": on}
    if extra:
        payload.update(extra)
    BUS.emit_sync(
        severity="info",
        scope=scope,
        summary=summary,
        detail=None,
        payload=payload,
    )


__all__ = [
    "emit_decision",
    "emit_take_action",
    "emit_memory_action",
    "kind_label",
]
