# #region GitServiceMergeMixin [C:4] [TYPE Module] [SEMANTICS git, merge, branch, conflict, resolution, lock]
# @LAYER Infrastructure
# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote (all concurrent-safe via per-dashboard locks).
# @RELATION CALLED_BY -> [GitService]

import os
from pathlib import Path
from typing import Any

from fastapi import HTTPException
from git import Repo
from git.exc import GitCommandError
from git.objects.blob import Blob

from src.core.logger import belief_scope, logger


# #region GitServiceMergeMixin [C:3] [TYPE Class]
# @BRIEF Mixin providing merge operations for GitService.
class GitServiceMergeMixin:
    # region _read_blob_text [TYPE Function]
    # @PURPOSE: Read text from a Git blob.
    def _read_blob_text(self, blob: Blob) -> str:
        with belief_scope("GitService._read_blob_text"):
            if blob is None:
                return ""
            try:
                return blob.data_stream.read().decode("utf-8", errors="replace")
            except Exception:
                logger.debug("[_read_blob_text] Could not decode blob text")
                return ""
    # endregion _read_blob_text

    # region _get_unmerged_file_paths [TYPE Function]
    # @PURPOSE: List files with merge conflicts.
    def _get_unmerged_file_paths(self, repo: Repo) -> list[str]:
        with belief_scope("GitService._get_unmerged_file_paths"):
            try:
                return sorted(list(repo.index.unmerged_blobs().keys()))
            except Exception:
                return []
    # endregion _get_unmerged_file_paths

    # region _build_unfinished_merge_payload [TYPE Function]
    # @PURPOSE: Build payload for unfinished merge state.
    def _build_unfinished_merge_payload(self, repo: Repo) -> dict[str, Any]:
        with belief_scope("GitService._build_unfinished_merge_payload"):
            merge_head_path = os.path.join(repo.git_dir, "MERGE_HEAD")
            merge_head_value = ""
            merge_msg_preview = ""
            current_branch = "unknown"
            try:
                merge_head_value = Path(merge_head_path).read_text(encoding="utf-8").strip()
            except Exception:
                merge_head_value = "<unreadable>"
            try:
                merge_msg_path = os.path.join(repo.git_dir, "MERGE_MSG")
                if os.path.exists(merge_msg_path):
                    merge_msg_preview = (
                        Path(merge_msg_path).read_text(encoding="utf-8").strip().splitlines()[:1] or [""]
                    )[0]
            except Exception:
                merge_msg_preview = "<unreadable>"
            try:
                current_branch = repo.active_branch.name
            except Exception:
                current_branch = "detached_or_unknown"
            conflicts_count = len(self._get_unmerged_file_paths(repo))
            return {
                "error_code": "GIT_UNFINISHED_MERGE",
                "message": (
                    "\u0412 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 \u0435\u0441\u0442\u044c \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043d\u043d\u043e\u0435 \u0441\u043b\u0438\u044f\u043d\u0438\u0435. "
                    "\u0417\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0435 \u0438\u043b\u0438 \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u0435 \u0441\u043b\u0438\u044f\u043d\u0438\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e."
                ),
                "repository_path": repo.working_tree_dir,
                "git_dir": repo.git_dir,
                "current_branch": current_branch,
                "merge_head": merge_head_value,
                "merge_message_preview": merge_msg_preview,
                "conflicts_count": conflicts_count,
                "next_steps": [
                    "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u043f\u043e \u043f\u0443\u0442\u0438 repository_path",
                    "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435: git status",
                    "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u044b \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 commit, \u043b\u0438\u0431\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u0435: git merge --abort",
                    "\u041f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f/\u043e\u0442\u043c\u0435\u043d\u044b \u0441\u043b\u0438\u044f\u043d\u0438\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 Pull \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430",
                ],
                "manual_commands": ["git status", "git add <resolved-files>", 'git commit -m "resolve merge conflicts"', "git merge --abort"],
            }
    # endregion _build_unfinished_merge_payload

    # region get_merge_status [C:4] [TYPE Function] [SEMANTICS git,merge,status,lock]
    # @PURPOSE: Get current merge status for a dashboard repository (concurrent-safe).
    def get_merge_status(self, dashboard_id: int) -> dict[str, Any]:
        with self._locked(dashboard_id):
            with belief_scope("GitService.get_merge_status"):
                repo = self.get_repo(dashboard_id)
            merge_head_path = os.path.join(repo.git_dir, "MERGE_HEAD")
            if not os.path.exists(merge_head_path):
                current_branch = "unknown"
                try:
                    current_branch = repo.active_branch.name
                except Exception:
                    current_branch = "detached_or_unknown"
                return {
                    "has_unfinished_merge": False,
                    "repository_path": repo.working_tree_dir,
                    "git_dir": repo.git_dir,
                    "current_branch": current_branch,
                    "merge_head": None,
                    "merge_message_preview": None,
                    "conflicts_count": 0,
                }
            payload = self._build_unfinished_merge_payload(repo)
            return {
                "has_unfinished_merge": True,
                "repository_path": payload["repository_path"],
                "git_dir": payload["git_dir"],
                "current_branch": payload["current_branch"],
                "merge_head": payload["merge_head"],
                "merge_message_preview": payload["merge_message_preview"],
                "conflicts_count": int(payload.get("conflicts_count") or 0),
            }
    # endregion get_merge_status

    # region get_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,list,lock]
    # @PURPOSE: List all files with conflicts and their contents (concurrent-safe).
    def get_merge_conflicts(self, dashboard_id: int) -> list[dict[str, Any]]:
        with self._locked(dashboard_id):
            with belief_scope("GitService.get_merge_conflicts"):
                repo = self.get_repo(dashboard_id)
            conflicts = []
            unmerged = repo.index.unmerged_blobs()
            for file_path, stages in unmerged.items():
                mine_blob = None
                theirs_blob = None
                for stage, blob in stages:
                    if stage == 2:
                        mine_blob = blob
                    elif stage == 3:
                        theirs_blob = blob
                conflicts.append({
                    "file_path": file_path,
                    "mine": self._read_blob_text(mine_blob) if mine_blob else "",
                    "theirs": self._read_blob_text(theirs_blob) if theirs_blob else "",
                })
            return sorted(conflicts, key=lambda item: item["file_path"])
    # endregion get_merge_conflicts

    # region resolve_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,resolve,lock]
    # @PURPOSE: Resolve conflicts using specified strategy (concurrent-safe).
    def resolve_merge_conflicts(self, dashboard_id: int, resolutions: list[dict[str, Any]]) -> list[str]:
        with self._locked(dashboard_id):
            with belief_scope("GitService.resolve_merge_conflicts"):
                repo = self.get_repo(dashboard_id)
            resolved_files: list[str] = []
            repo_root = os.path.abspath(str(repo.working_tree_dir or ""))
            if not repo_root:
                raise HTTPException(status_code=500, detail="Repository working tree directory is unavailable")
            for item in resolutions or []:
                file_path = str(item.get("file_path") or "").strip()
                strategy = str(item.get("resolution") or "").strip().lower()
                content = item.get("content")
                if not file_path:
                    raise HTTPException(status_code=400, detail="resolution.file_path is required")
                if strategy not in {"mine", "theirs", "manual"}:
                    raise HTTPException(status_code=400, detail=f"Unsupported resolution strategy: {strategy}")
                if strategy == "mine":
                    repo.git.checkout("--ours", "--", file_path)
                elif strategy == "theirs":
                    repo.git.checkout("--theirs", "--", file_path)
                else:
                    abs_target = os.path.abspath(os.path.join(repo_root, file_path))
                    if abs_target != repo_root and not abs_target.startswith(repo_root + os.sep):
                        raise HTTPException(status_code=400, detail=f"Invalid conflict file path: {file_path}")
                    os.makedirs(os.path.dirname(abs_target), exist_ok=True)
                    with open(abs_target, "w", encoding="utf-8") as file_obj:
                        file_obj.write(str(content or ""))
                repo.git.add(file_path)
                resolved_files.append(file_path)
            return resolved_files
    # endregion resolve_merge_conflicts

    # region abort_merge [C:4] [TYPE Function] [SEMANTICS git,merge,abort,lock]
    # @PURPOSE: Abort ongoing merge (concurrent-safe).
    def abort_merge(self, dashboard_id: int) -> dict[str, Any]:
        with self._locked(dashboard_id):
            with belief_scope("GitService.abort_merge"):
                repo = self.get_repo(dashboard_id)
            try:
                repo.git.merge("--abort")
            except GitCommandError as e:
                details = str(e)
                lowered = details.lower()
                if "there is no merge to abort" in lowered or "no merge to abort" in lowered:
                    return {"status": "no_merge_in_progress"}
                raise HTTPException(status_code=409, detail=f"Cannot abort merge: {details}")
            return {"status": "aborted"}
    # endregion abort_merge

    # region continue_merge [C:4] [TYPE Function] [SEMANTICS git,merge,continue,lock]
    # @PURPOSE: Finalize merge after conflict resolution (concurrent-safe).
    def continue_merge(self, dashboard_id: int, message: str | None = None) -> dict[str, Any]:
        with self._locked(dashboard_id):
            with belief_scope("GitService.continue_merge"):
                repo = self.get_repo(dashboard_id)
            unmerged_files = self._get_unmerged_file_paths(repo)
            if unmerged_files:
                raise HTTPException(
                    status_code=409,
                    detail={
                        "error_code": "GIT_MERGE_CONFLICTS_REMAIN",
                        "message": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c merge: \u043e\u0441\u0442\u0430\u043b\u0438\u0441\u044c \u043d\u0435\u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043d\u043d\u044b\u0435 \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u044b.",
                        "unresolved_files": unmerged_files,
                    },
                )
            try:
                normalized_message = str(message or "").strip()
                if normalized_message:
                    repo.git.commit("-m", normalized_message)
                else:
                    repo.git.commit("--no-edit")
            except GitCommandError as e:
                details = str(e)
                lowered = details.lower()
                if "nothing to commit" in lowered:
                    return {"status": "already_clean"}
                raise HTTPException(status_code=409, detail=f"Cannot continue merge: {details}")
            commit_hash = ""
            try:
                commit_hash = repo.head.commit.hexsha
            except Exception:
                commit_hash = ""
            return {"status": "committed", "commit_hash": commit_hash}
    # endregion continue_merge

    # region promote_direct_merge [C:4] [TYPE Function] [SEMANTICS git,merge,promote,branch,isolation]
    # @PURPOSE: Perform direct merge between branches with branch isolation — original branch restored on error.
    # @PRE  Repository exists and both branches are valid.
    # @POST  Target branch contains merged changes from source branch. Active branch restored to original.
    #        Merge survives locally even if push fails (partial success).
    # @SIDE_EFFECT  Changes local branch state during merge; restores original branch in finally block.
    # @RETURN  Dict[str, Any]
    def promote_direct_merge(self, dashboard_id: int, from_branch: str, to_branch: str) -> dict[str, Any]:
        with self._locked(dashboard_id), belief_scope("GitService.promote_direct_merge"):
            if not from_branch or not to_branch:
                raise HTTPException(status_code=400, detail="from_branch and to_branch are required")
            repo = self.get_repo(dashboard_id)
            source = from_branch.strip()
            target = to_branch.strip()
            if source == target:
                raise HTTPException(status_code=400, detail="from_branch and to_branch must be different")
            try:
                origin = repo.remote(name="origin")
            except ValueError:
                raise HTTPException(status_code=400, detail="Remote 'origin' not configured")

            # Remember original branch for restoration in finally
            original_branch = None
            try:
                original_branch = repo.active_branch.name
            except Exception:
                original_branch = None

            try:
                origin.fetch()
                if source not in [head.name for head in repo.heads]:
                    if f"origin/{source}" in [ref.name for ref in repo.refs]:
                        repo.git.checkout("-b", source, f"origin/{source}")
                    else:
                        raise HTTPException(status_code=404, detail=f"Source branch '{source}' not found")
                if target in [head.name for head in repo.heads]:
                    repo.git.checkout(target)
                elif f"origin/{target}" in [ref.name for ref in repo.refs]:
                    repo.git.checkout("-b", target, f"origin/{target}")
                else:
                    raise HTTPException(status_code=404, detail=f"Target branch '{target}' not found")
                try:
                    origin.pull(target)
                except Exception:
                    logger.debug("Could not pull target branch %s before direct promote", target)
                repo.git.merge(source, "--no-ff", "-m", f"chore(flow): promote {source} -> {target}")
                origin.push(refspec=f"{target}:{target}")
            except HTTPException:
                raise
            except Exception as e:
                message = str(e)
                if "CONFLICT" in message.upper():
                    raise HTTPException(status_code=409, detail=f"Merge conflict during direct promote: {message}")
                raise HTTPException(status_code=500, detail=f"Direct promote failed: {message}")
            finally:
                # Restore original branch even if merge/push partially failed
                if original_branch:
                    try:
                        repo.git.checkout(original_branch)
                    except Exception:
                        logger.debug("Could not restore original branch %s after direct promote", original_branch)
            return {"mode": "direct", "from_branch": source, "to_branch": target, "status": "merged"}
    # endregion promote_direct_merge
# #endregion GitServiceMergeMixin
# #endregion GitServiceMergeMixin
