"""Merge service for combining subplan sandbox outputs.

Bridges the domain-level ``SubplanMergeStrategy`` enum to the infrastructure
merge implementations (``GitMergeStrategy``, ``SequentialMergeStrategy``, etc.)
and adds the ``FAIL_ON_CONFLICT`` and ``LAST_WINS`` strategies that are specific
to the subplan execution workflow.

Each merge operation works on a *file map* — a ``dict[str, str]`` mapping
relative file paths to their text content.  The service merges each file
individually using the configured strategy and returns a combined result.
"""

from __future__ import annotations

import logging
from collections.abc import Sequence
from dataclasses import dataclass, field

from cleveragents.domain.models.core.plan import SubplanMergeStrategy
from cleveragents.infrastructure.sandbox.merge import (
    GitMergeStrategy,
    MergeResult,
    SequentialMergeStrategy,
)

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Value objects
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class FileMergeOutcome:
    """Result of merging a single file across subplan outputs.

    Attributes:
        path: Relative file path.
        content: The merged content (may contain conflict markers).
        has_conflict: Whether unresolved conflicts exist.
        source_subplan_ids: IDs of subplans that touched this file.
    """

    path: str
    content: str
    has_conflict: bool = False
    source_subplan_ids: list[str] = field(default_factory=list)


@dataclass(frozen=True)
class SubplanMergeResult:
    """Aggregate result of merging all subplan outputs.

    Attributes:
        success: ``True`` if every file merged cleanly.
        merged_files: Per-file merge outcomes.
        conflict_files: Paths of files with unresolved conflicts.
        total_files: Total number of unique files across all subplans.
    """

    success: bool
    merged_files: list[FileMergeOutcome]
    conflict_files: list[str] = field(default_factory=list)
    total_files: int = 0


# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------


class MergeConflictError(Exception):
    """Raised by FAIL_ON_CONFLICT when any file has a conflict."""

    def __init__(self, conflict_paths: list[str]) -> None:
        self.conflict_paths = conflict_paths
        paths_str = ", ".join(conflict_paths)
        super().__init__(f"Merge conflicts in: {paths_str}")


# ---------------------------------------------------------------------------
# Service
# ---------------------------------------------------------------------------


class SubplanMergeService:
    """Merge subplan sandbox outputs using a configured strategy.

    The service accepts ordered sandbox output maps (one per subplan)
    and a base content map, then merges them file by file.

    Strategies:
        - **GIT_THREE_WAY**: Three-way merge via ``git merge-file``.
        - **SEQUENTIAL_APPLY**: Apply each subplan's changes in order
          (last writer wins per file).
        - **FAIL_ON_CONFLICT**: Same as GIT_THREE_WAY but raises
          :class:`MergeConflictError` if any conflict is detected.
        - **LAST_WINS**: The final subplan's version overwrites all
          earlier versions unconditionally.

    Args:
        strategy: The merge strategy to use.

    Raises:
        ValueError: If *strategy* is ``None``.
    """

    def __init__(self, strategy: SubplanMergeStrategy) -> None:
        if strategy is None:
            raise ValueError("strategy must not be None")
        self._strategy: SubplanMergeStrategy = strategy
        self._git_merge = GitMergeStrategy()
        self._seq_merge = SequentialMergeStrategy()

    @property
    def strategy(self) -> SubplanMergeStrategy:
        """The configured merge strategy."""
        return self._strategy

    def merge(
        self,
        base_files: dict[str, str],
        subplan_outputs: Sequence[tuple[str, dict[str, str]]],
    ) -> SubplanMergeResult:
        """Merge outputs from multiple subplans against a base.

        Args:
            base_files: File contents before any subplan ran (path -> content).
            subplan_outputs: Ordered sequence of ``(subplan_id, file_map)``
                tuples.  Order matters for SEQUENTIAL_APPLY and LAST_WINS.

        Returns:
            A :class:`SubplanMergeResult` describing the merged state.

        Raises:
            ValueError: If *subplan_outputs* is empty.
            MergeConflictError: If strategy is FAIL_ON_CONFLICT and conflicts
                are detected.
        """
        if not subplan_outputs:
            raise ValueError("subplan_outputs must not be empty")

        # Collect all unique file paths and which subplans touched them
        all_paths: dict[str, list[str]] = {}
        for subplan_id, file_map in subplan_outputs:
            for path in file_map:
                if path not in all_paths:
                    all_paths[path] = []
                all_paths[path].append(subplan_id)

        merged_files: list[FileMergeOutcome] = []
        conflict_files: list[str] = []

        for path, subplan_ids in sorted(all_paths.items()):
            base_content = base_files.get(path, "")
            outcome = self._merge_file(path, base_content, subplan_ids, subplan_outputs)
            merged_files.append(outcome)
            if outcome.has_conflict:
                conflict_files.append(path)

        if self._strategy == SubplanMergeStrategy.FAIL_ON_CONFLICT and conflict_files:
            raise MergeConflictError(conflict_files)

        return SubplanMergeResult(
            success=len(conflict_files) == 0,
            merged_files=merged_files,
            conflict_files=conflict_files,
            total_files=len(all_paths),
        )

    def _merge_file(
        self,
        path: str,
        base_content: str,
        subplan_ids: list[str],
        subplan_outputs: Sequence[tuple[str, dict[str, str]]],
    ) -> FileMergeOutcome:
        """Merge a single file across contributing subplans.

        Args:
            path: The file path being merged.
            base_content: Original content before subplans.
            subplan_ids: IDs of subplans that modified this file.
            subplan_outputs: All subplan outputs for content lookup.

        Returns:
            A :class:`FileMergeOutcome` for this file.
        """
        # Collect ordered contents from subplans that touched this file
        contents: list[str] = []
        for _subplan_id, file_map in subplan_outputs:
            if path in file_map:
                contents.append(file_map[path])

        if not contents:
            return FileMergeOutcome(
                path=path,
                content=base_content,
                has_conflict=False,
                source_subplan_ids=subplan_ids,
            )

        if len(contents) == 1:
            # Only one subplan touched this file -- no merge needed
            return FileMergeOutcome(
                path=path,
                content=contents[0],
                has_conflict=False,
                source_subplan_ids=subplan_ids,
            )

        # Multiple subplans touched the same file -- strategy-dependent merge
        if self._strategy == SubplanMergeStrategy.LAST_WINS:
            return FileMergeOutcome(
                path=path,
                content=contents[-1],
                has_conflict=False,
                source_subplan_ids=subplan_ids,
            )

        if self._strategy == SubplanMergeStrategy.SEQUENTIAL_APPLY:
            return self._sequential_apply(path, base_content, contents, subplan_ids)

        # GIT_THREE_WAY and FAIL_ON_CONFLICT both use three-way merge
        return self._git_three_way(path, base_content, contents, subplan_ids)

    def _sequential_apply(
        self,
        path: str,
        base_content: str,
        contents: list[str],
        subplan_ids: list[str],
    ) -> FileMergeOutcome:
        """Apply changes sequentially (each becomes the new base)."""
        current = base_content
        for content in contents:
            result: MergeResult = self._seq_merge.merge(base_content, current, content)
            current = result.content
        return FileMergeOutcome(
            path=path,
            content=current,
            has_conflict=False,
            source_subplan_ids=subplan_ids,
        )

    def _git_three_way(
        self,
        path: str,
        base_content: str,
        contents: list[str],
        subplan_ids: list[str],
    ) -> FileMergeOutcome:
        """Iteratively three-way merge each subplan's changes."""
        current = contents[0]
        has_conflict = False

        for content in contents[1:]:
            result: MergeResult = self._git_merge.merge(base_content, current, content)
            current = result.content
            if result.has_conflicts:
                has_conflict = True

        return FileMergeOutcome(
            path=path,
            content=current,
            has_conflict=has_conflict,
            source_subplan_ids=subplan_ids,
        )
