"""Parser for structured ``dispatch-outcome`` blocks from goal-pursuit agents.

The ``goal-pursue`` skill instructs the agent to end each dispatch with a
fenced YAML-style block:

    ```dispatch-outcome
    outcome: keep
    hypothesis: <why this step was picked>
    action: <what was attempted>
    observation: <raw result>
    metric_delta: <delta vs success_metric>
    reason: <why keep/discard/crash>
    ```

A minimal per-experiment record: outcome is the ternary
``keep | discard | crash`` — the same judgement a human would make
reviewing an experiment — and the surrounding fields explain WHY so
post-hoc analysis isn't guessing from a free-text note.

Failure policy: this parser is **fail-open**. An absent, malformed, or
half-complete block returns an empty dict (plus whichever fields did
parse). The engine then falls back to the legacy ``note`` path. We never
block a dispatch on the agent producing the exact block shape — a
working retry with a missing block is better than a refused retry with a
perfect block.
"""

from __future__ import annotations

import re
from typing import Final

# Fenced-block markers. We accept the LLM being slightly sloppy: any
# fence opener followed by ``dispatch-outcome`` on the same line, any
# closing ``` anywhere after. Case-insensitive, leading/trailing whitespace
# tolerated. We do NOT try to parse generic YAML — only our 6 keys.
_BLOCK_RE: Final[re.Pattern[str]] = re.compile(
    r"```\s*dispatch-outcome\s*\n(.*?)\n```",
    re.IGNORECASE | re.DOTALL,
)

# Keys the agent must use. Order matters only for documentation; the
# parser doesn't care what order they appear in.
_ALLOWED_KEYS: Final[tuple[str, ...]] = (  # arch:deterministic — dispatch-outcome parser schema, not strategy
    "outcome",
    "hypothesis",
    "action",
    "observation",
    "metric_delta",
    "reason",
)

_VALID_OUTCOMES: Final[frozenset[str]] = frozenset({"keep", "discard", "crash"})  # arch:deterministic — outcome enum mirrored in coach/service.py

# One ``key: value`` per line. Value captures everything up to the next
# newline (we don't support YAML multi-line scalars — keep the surface tiny
# so the LLM can't produce something the parser disagrees with).
_LINE_RE: Final[re.Pattern[str]] = re.compile(
    r"^\s*(" + "|".join(_ALLOWED_KEYS) + r")\s*:\s*(.*?)\s*$",
    re.IGNORECASE | re.MULTILINE,
)


def parse_dispatch_outcome(text: str) -> dict:
    """Extract structured dispatch fields from an agent response.

    Returns a dict with any subset of: ``outcome`` (``keep|discard|crash``),
    ``hypothesis``, ``action``, ``observation``, ``metric_delta``, ``reason``.

    Rules:
      - Empty dict when no fenced ``dispatch-outcome`` block is present.
      - Only the *first* matching block is consumed. The agent emitting two
        means something is wrong; we ignore the later block rather than
        merging (avoid silent shadowing).
      - ``outcome`` is normalized to lowercase and dropped if not in the
        valid ternary. The caller can still use the other fields even when
        outcome is missing — they still describe the dispatch.
      - All values are stripped of trailing whitespace and line-normalized
        (newlines → spaces) so a sneaky multi-line value can't split a
        downstream log entry.
    """
    if not text:
        return {}

    block_match = _BLOCK_RE.search(text)
    if not block_match:
        return {}

    body = block_match.group(1)
    result: dict[str, str] = {}
    for match in _LINE_RE.finditer(body):
        key = match.group(1).lower()
        if key in result:
            # Duplicate key — first wins. Silently ignore the second so
            # pasting history in the block doesn't overwrite the real entry.
            continue
        value = match.group(2).replace("\r", " ").replace("\n", " ").strip()
        if not value:
            continue
        if key == "outcome":
            value = value.lower().strip()
            if value not in _VALID_OUTCOMES:
                continue
        result[key] = value

    return result


__all__ = ["parse_dispatch_outcome"]
