"""GEPA Proposer component — reflective mutation: one reflection call, one new candidate.

The variation operator of the paper. The reflection LM reads the current candidate plus the
evaluation feedback the PromptBuilder rendered and writes a **complete drop-in replacement**, which
is extracted from the ``` block by ``InstructionProposalSignature.output_extractor``
(``gepa/strategies/instruction_proposal.py``) and substituted for the candidate's text
(``ReflectiveMutationProposer.propose_new_texts`` → ``execute_proposal``).

GEPA is therefore not a diff method: it never asks for an edit script and never applies one. That is
its own value on galapagos's ``general.mutation_approach`` axis, registered here as
``reflective_rewrite`` so the run's recorded config says how the method actually mutates instead of
inheriting the framework's diff default.

**Why the extractor is not the generic ``full_rewrite`` parser.** ``parse_full_rewrite`` keeps the
*first* fenced block; GEPA keeps everything between the first ``` and the **last** ```. The
difference is deliberate upstream: the artifact under optimization is frequently itself a prompt, and
a prompt routinely contains fenced examples that a first-fence parser would truncate at.
"""
from __future__ import annotations

import re

from ...components.mutation_approach import MutationApproach, register_mutation_approach
from ...components.proposer import EditStrategy, LLMProposer

_LANGUAGE_TAG = re.compile(r"^```\S*\n?")
_LEADING_TAG = re.compile(r"^\S*\n")


def extract_new_text(lm_out: str) -> str:
    """``InstructionProposalSignature.output_extractor``, verbatim.

    Everything between the FIRST and the LAST ``` fence, with an optional language tag dropped. A
    response with only an opening (or only a closing) fence is salvaged by stripping that fence; a
    response with no fence at all is taken whole.
    """
    lm_out = lm_out or ""
    start = lm_out.find("```") + 3
    end = lm_out.rfind("```")

    if start >= end:
        # Handle incomplete blocks
        stripped = lm_out.strip()
        if stripped.startswith("```"):
            match = _LANGUAGE_TAG.match(lm_out)
            if match:
                return lm_out[match.end():].strip()
        elif stripped.endswith("```"):
            return stripped[:-3].strip()
        return stripped

    content = lm_out[start:end]
    match = _LEADING_TAG.match(content)
    if match:
        content = content[match.end():]
    return content.strip()


class ReflectiveRewrite(EditStrategy):
    """The reflection response IS the next candidate: extract it and replace the parent wholesale.

    ``changed`` — the framework's mandatory no-op signal — is false for an empty extraction or for
    text identical to the parent. Upstream substitutes such a proposal and spends an evaluation on
    it, which its strict-improvement acceptance test then rejects; reporting the no-op instead reaches
    the same verdict without burning the evaluation, and keeps the wasted turn visible in the log.
    """

    def apply(self, parent_code: str, response: str) -> tuple[str, bool, str]:
        new_code = extract_new_text(response)
        changed = bool(new_code.strip()) and new_code.strip() != (parent_code or "").strip()
        return (new_code if changed else parent_code), changed, "Reflective rewrite"


@register_mutation_approach("reflective_rewrite")
class ReflectiveRewriteApproach(MutationApproach):
    """GEPA's mutation axis value: the model returns the complete new parameter value in ``` blocks."""

    def add_prompt(self) -> str:
        return "Provide the new parameter value within ``` blocks."

    def parser(self):
        return ReflectiveRewrite()


class GepaReflectiveMutationProposer(LLMProposer):
    """One reflection call per iteration → one child (``ReflectiveMutationProposer``)."""

    evolution_operator = "reflective_mutation"
    edit_strategy = ReflectiveRewrite()
