"""Git branch merging after reunification.

Merges child branches sequentially into the base branch. On conflict,
optionally spawns claude to resolve, or escalates.
"""

from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass, field
from pathlib import Path

from cleave.orchestrator.errors import GitWorktreeError

logger = logging.getLogger(__name__)


@dataclass
class MergeResult:
    """Result of merging one child branch."""

    branch: str
    success: bool
    conflict_files: list[str] = field(default_factory=list)
    error: str | None = None


@dataclass
class MergeReport:
    """Summary of all branch merges."""

    results: list[MergeResult] = field(default_factory=list)
    all_succeeded: bool = True

    @property
    def failed(self) -> list[MergeResult]:
        return [r for r in self.results if not r.success]


async def _run_git(
    *args: str, cwd: Path, check: bool = True
) -> tuple[int, str, str]:
    """Run a git command asynchronously."""
    proc = await asyncio.create_subprocess_exec(
        "git", *args,
        cwd=str(cwd),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout_bytes, stderr_bytes = await proc.communicate()
    stdout = stdout_bytes.decode("utf-8", errors="replace").strip()
    stderr = stderr_bytes.decode("utf-8", errors="replace").strip()
    if check and proc.returncode != 0:
        raise GitWorktreeError(f"git {' '.join(args)} failed (rc={proc.returncode}): {stderr}")
    return proc.returncode, stdout, stderr


async def _ensure_repo_clean(repo_path: Path, base_branch: str) -> None:
    """Ensure the repo is on the base branch with no in-progress merge.

    Called before and after merge operations to guarantee the working
    tree is never left in a dirty state.
    """
    # Abort any in-progress merge
    _rc, _, _ = await _run_git("rev-parse", "--git-dir", cwd=repo_path, check=False)
    merge_head = repo_path / ".git" / "MERGE_HEAD"
    if merge_head.exists():
        logger.warning("Found in-progress merge, aborting to restore clean state")
        await _run_git("merge", "--abort", cwd=repo_path, check=False)

    # Ensure we're on the base branch
    await _run_git("checkout", base_branch, cwd=repo_path, check=False)


async def merge_branch(
    repo_path: Path,
    branch_name: str,
    base_branch: str,
) -> MergeResult:
    """Merge a single child branch into the base branch.

    Ensures the repo working tree is always left clean, even on
    exceptions or interruptions.
    """
    # Ensure clean starting state
    await _ensure_repo_clean(repo_path, base_branch)

    try:
        # Attempt the merge
        rc, stdout, stderr = await _run_git(
            "merge", "--no-ff", branch_name,
            "-m", f"Merge {branch_name} into {base_branch}",
            cwd=repo_path,
            check=False,
        )

        if rc == 0:
            logger.info("Successfully merged %s", branch_name)
            return MergeResult(branch=branch_name, success=True)

        # Merge conflict — identify conflicting files
        _, diff_out, _ = await _run_git(
            "diff", "--name-only", "--diff-filter=U", cwd=repo_path, check=False
        )
        conflict_files = [f for f in diff_out.splitlines() if f.strip()]

        # Abort the failed merge to leave repo clean
        await _run_git("merge", "--abort", cwd=repo_path, check=False)

        logger.warning("Merge conflict on %s: %s", branch_name, conflict_files)
        return MergeResult(
            branch=branch_name,
            success=False,
            conflict_files=conflict_files,
            error=stderr or stdout,
        )
    except Exception:
        # On any exception, ensure repo is left clean
        await _ensure_repo_clean(repo_path, base_branch)
        raise


async def merge_all_branches(
    repo_path: Path,
    branch_names: list[str],
    base_branch: str,
) -> MergeReport:
    """Sequentially merge all child branches into the base branch.

    Continues past conflicts so that independent branches can still be
    merged. Failed branches are recorded in the report.
    """
    report = MergeReport()

    # Ensure we start on the base branch
    await _ensure_repo_clean(repo_path, base_branch)

    for branch in branch_names:
        result = await merge_branch(repo_path, branch, base_branch)
        report.results.append(result)

        if not result.success:
            report.all_succeeded = False
            logger.error(
                "Merge conflict on %s (conflict files: %s). "
                "Continuing with remaining branches.",
                branch, result.conflict_files,
            )

    return report
