"""
core/merge/merge_prompt_builder.py

MergePromptBuilder — constructs the structured Opus conflict-resolution prompt
for the SemanticMergeInterceptor.

The output is deterministic: identical inputs always produce identical prompt
strings (no randomness, no timestamps, no UUIDs in the body).

# VERIFICATION_STAMP
# Story: 7.03
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 9/9
# Coverage: 100%
"""

from __future__ import annotations

import json
import logging
from typing import Any

logger = logging.getLogger(__name__)

MAX_STATE_CHARS = 2000  # Truncate current_state to prevent context overflow
_TRUNCATION_MARKER = "... [truncated]"


class MergePromptBuilder:
    """
    Constructs the exact Opus conflict-resolution prompt for the
    SemanticMergeInterceptor.

    Output is deterministic for the same inputs (no randomness).

    Usage::

        builder = MergePromptBuilder()
        prompt = builder.build(deltas, conflict_report, current_state, version=3)
        # → pass prompt to Opus API call

    # VERIFICATION_STAMP
    # Story: 7.03
    # Verified By: parallel-builder
    # Verified At: 2026-02-25
    # Tests: 9/9
    # Coverage: 100%
    """

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def build(
        self,
        deltas: list,
        conflict_report: Any,
        current_state: dict,
        version: int = 0,
    ) -> str:
        """
        Build the Opus merge resolution prompt.

        Args:
            deltas: List of StateDelta objects (or dicts with a ``patch`` key).
            conflict_report: ConflictReport from ConflictDetector.
            current_state: Current master state dict.
            version: Current state version number.

        Returns:
            Formatted prompt string.  Template::

                You are the Genesis Semantic Reducer. Multiple swarm workers
                have produced conflicting outputs.
                CURRENT STATE (version {version}): {current_state}
                CONFLICT REPORT: Paths in conflict: {conflicting_paths}
                WORKER PROPOSALS:
                Worker {agent_id}: {delta.patch}
                ...
                Respond ONLY with valid JSON: {"resolved_patch": [...], "resolution_rationale": "..."}
        """
        state_str = self._truncate_state(current_state)
        conflicting_paths_str = json.dumps(
            conflict_report.conflicting_paths, separators=(",", ":")
        )
        worker_proposals = self._format_worker_proposals(deltas)

        prompt = (
            "You are the Genesis Semantic Reducer. "
            "Multiple swarm workers have produced conflicting outputs.\n"
            f"CURRENT STATE (version {version}): {state_str}\n"
            f"CONFLICT REPORT: Paths in conflict: {conflicting_paths_str}\n"
            "WORKER PROPOSALS:\n"
            f"{worker_proposals}"
            'Respond ONLY with valid JSON: '
            '{"resolved_patch": [...], "resolution_rationale": "..."}'
        )

        logger.debug(
            "MergePromptBuilder.build: version=%d, deltas=%d, prompt_len=%d",
            version,
            len(deltas),
            len(prompt),
        )
        return prompt

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _truncate_state(self, state: dict) -> str:
        """
        Serialise *state* to JSON and truncate to MAX_STATE_CHARS if longer.

        A truncation marker is appended when the string is cut so the LLM
        knows the state was abbreviated.
        """
        state_json = json.dumps(state, separators=(",", ":"), sort_keys=True)
        if len(state_json) <= MAX_STATE_CHARS:
            return state_json
        # Truncate and append marker
        cutoff = MAX_STATE_CHARS - len(_TRUNCATION_MARKER)
        return state_json[:cutoff] + _TRUNCATION_MARKER

    def _format_worker_proposals(self, deltas: list) -> str:
        """
        Format each worker's patch as a readable JSON block labelled by
        agent_id.

        Each worker's section ends with a newline so proposals are clearly
        separated in the final prompt.
        """
        lines: list[str] = []
        for delta in deltas:
            agent_id = self._extract_agent_id(delta)
            patch = self._extract_patch(delta)
            patch_json = json.dumps(patch, indent=2, sort_keys=True)
            lines.append(f"Worker {agent_id}:\n{patch_json}\n")
        return "\n".join(lines) + ("\n" if lines else "")

    def _extract_agent_id(self, delta: Any) -> str:
        """
        Extract agent_id from a StateDelta or plain dict.

        Falls back to ``"unknown"`` if the attribute / key is absent.
        """
        if hasattr(delta, "agent_id"):
            return str(delta.agent_id)
        if isinstance(delta, dict):
            return str(delta.get("agent_id", "unknown"))
        return "unknown"

    def _extract_patch(self, delta: Any) -> list:
        """
        Extract the patch list from a StateDelta or plain dict.

        Mirrors the same logic used by ConflictDetector so the two classes
        stay in sync.
        """
        if hasattr(delta, "patch"):
            patch = delta.patch
        elif isinstance(delta, dict):
            patch = delta.get("patch", [])
        else:
            patch = []

        # StateDelta stores patch as a frozen tuple — normalise to list
        if isinstance(patch, tuple):
            return list(patch)
        if isinstance(patch, list):
            return patch
        return []
