"""M-18 패치 파서 — unified diff 파싱/적용/롤백 유틸리티.

Python difflib 기반으로 unified diff 형식의 패치를 파싱하고,
atomic_write를 사용해 파일에 안전하게 적용합니다.

Usage:
    from utils.patch_parser import parse_patch, apply_patch, generate_patch, verify_patch

    patch = generate_patch(original_text, modified_text, fromfile="a/src.py", tofile="b/src.py")
    success = apply_patch("src.py", patch)
"""

import difflib
import re
from pathlib import Path
from typing import TypedDict, Union

from utils.atomic_write import atomic_text_write


class Hunk(TypedDict):
    """unified diff hunk 구조체."""

    old_start: int
    old_count: int
    new_start: int
    new_count: int
    lines: list[str]


class FilePatch(TypedDict):
    """파일 단위 패치 구조체."""

    old_file: str
    new_file: str
    hunks: list[Hunk]


# unified diff hunk 헤더 패턴: @@ -l,s +l,s @@
_HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")


def parse_patch(patch_text: str) -> list[FilePatch]:
    """unified diff 텍스트를 파싱하여 FilePatch 목록을 반환합니다."""
    if not patch_text or not patch_text.strip():
        return []

    results: list[FilePatch] = []
    current_file: FilePatch | None = None
    current_hunk: Hunk | None = None

    for line in patch_text.splitlines():
        if line.startswith("--- "):
            if current_file is not None:
                if current_hunk is not None:
                    current_file["hunks"].append(current_hunk)
                    current_hunk = None
                results.append(current_file)
            current_file = FilePatch(old_file=line[4:].strip(), new_file="", hunks=[])
        elif line.startswith("+++ ") and current_file is not None:
            current_file["new_file"] = line[4:].strip()
        else:
            m = _HUNK_HEADER_RE.match(line)
            if m and current_file is not None:
                if current_hunk is not None:
                    current_file["hunks"].append(current_hunk)
                g = m.groups()
                current_hunk = Hunk(
                    old_start=int(g[0]),
                    old_count=int(g[1]) if g[1] is not None else 1,
                    new_start=int(g[2]),
                    new_count=int(g[3]) if g[3] is not None else 1,
                    lines=[],
                )
            elif current_hunk is not None and line and line[0] in (" ", "+", "-"):
                current_hunk["lines"].append(line)

    if current_file is not None:
        if current_hunk is not None:
            current_file["hunks"].append(current_hunk)
        results.append(current_file)

    return results


def generate_patch(original: str, modified: str, fromfile: str = "original", tofile: str = "modified") -> str:
    """두 텍스트 사이의 unified diff 패치를 생성합니다. 변경 없으면 빈 문자열."""
    diff_lines = list(
        difflib.unified_diff(
            original.splitlines(keepends=True),
            modified.splitlines(keepends=True),
            fromfile=fromfile,
            tofile=tofile,
        )
    )
    return "".join(diff_lines)


def _apply_hunks(original_lines: list[str], hunks: list[Hunk]) -> list[str] | None:
    """hunk 목록을 원본 라인에 적용. 컨텍스트 불일치 시 None 반환."""
    result: list[str] = list(original_lines)
    offset = 0

    for hunk in hunks:
        old_start = hunk["old_start"] - 1 + offset  # 0-indexed
        lines = hunk["lines"]

        # 검증: context/delete 라인이 원본과 일치하는지 확인
        src_idx = old_start
        for hl in lines:
            if hl and hl[0] in ("-", " "):
                if src_idx >= len(result) or result[src_idx].rstrip("\n") != hl[1:].rstrip("\n"):
                    return None
                src_idx += 1

        # 적용: context 라인은 원본 유지, add 라인은 줄바꿈 보정 후 삽입, delete 라인 제거
        new_chunk: list[str] = []
        orig_idx = old_start
        for hl in lines:
            if not hl:
                continue
            if hl[0] == " ":
                new_chunk.append(result[orig_idx] if orig_idx < len(result) else hl[1:])
                orig_idx += 1
            elif hl[0] == "+":
                content = hl[1:]
                new_chunk.append(content if content.endswith("\n") else content + "\n")
            elif hl[0] == "-":
                orig_idx += 1

        result[old_start : old_start + hunk["old_count"]] = new_chunk
        offset += len(new_chunk) - hunk["old_count"]

    return result


def _load_and_apply(target_file: Union[str, Path], patch_text: str) -> list[str] | None:
    """파일을 읽어 패치를 적용한 결과 라인 목록을 반환. 실패 시 None."""
    path = Path(target_file)
    if not path.exists():
        return None
    if not patch_text or not patch_text.strip():
        return path.read_text(encoding="utf-8").splitlines(keepends=True)

    file_patches = parse_patch(patch_text)
    if not file_patches:
        return path.read_text(encoding="utf-8").splitlines(keepends=True)

    lines: list[str] | None = path.read_text(encoding="utf-8").splitlines(keepends=True)
    for fp in file_patches:
        if lines is None:
            return None
        lines = _apply_hunks(list(lines), fp["hunks"])
    return lines


def apply_patch(target_file: Union[str, Path], patch_text: str) -> bool:
    """파일에 unified diff 패치를 원자적으로 적용합니다. 성공 시 True."""
    result = _load_and_apply(target_file, patch_text)
    if result is None:
        return False
    atomic_text_write(Path(target_file), "".join(result))
    return True


def verify_patch(target_file: Union[str, Path], patch_text: str) -> bool:
    """파일에 패치를 적용할 수 있는지 확인합니다 (실제 적용 없음). 가능 시 True."""
    return _load_and_apply(target_file, patch_text) is not None
