"""Selected-change state file persistence."""

from __future__ import annotations

import json
import shutil
import tempfile
from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from types import TracebackType

from ...core.diff_parser import build_line_changes_from_patch_lines
from ...core.models import (
    LineLevelChange,
)
from ...core.buffer import LineBuffer
from ...utils.atomic_write import fsync_directory
from ...utils.buffer_io import write_buffer_to_path
from ...utils.file_io import (
    read_text_file_contents,
    write_file_bytes,
    write_text_file_contents,
)
from ...utils.paths import (
    get_index_snapshot_file_path,
    get_line_changes_json_file_path,
    get_processed_include_ids_file_path,
    get_processed_skip_ids_file_path,
    get_snapshot_metadata_file_path,
    get_selected_binary_file_json_path,
    get_selected_change_clear_reason_file_path,
    get_selected_change_kind_file_path,
    get_selected_gitlink_file_json_path,
    get_selected_mode_change_json_path,
    get_selected_hunk_hash_file_path,
    get_selected_hunk_patch_file_path,
    get_selected_rename_file_json_path,
    get_selected_text_deletion_file_json_path,
    get_working_tree_snapshot_file_path,
)
from ..line_state import convert_line_changes_to_serializable_dict
from .snapshots import write_snapshots_for_selected_file_path


class SelectedChangeKind(str, Enum):
    """Kinds of selected changes cached in session state."""

    HUNK = "hunk"
    FILE = "file"
    RENAME = "rename"
    DELETION = "deletion"
    BINARY = "binary"
    GITLINK = "submodule"
    MODE = "mode"
    BATCH_MODE = "batch-mode"
    BATCH_FILE = "batch-file"
    BATCH_BINARY = "batch-binary"
    BATCH_GITLINK = "batch-submodule"


@dataclass
class SelectedChangeStateSnapshot:
    """Temporary file copy of selected change state."""

    paths: dict[str, Path | None]
    temporary_directory: tempfile.TemporaryDirectory[str]

    def close(self) -> None:
        self.temporary_directory.cleanup()

    def __enter__(self) -> SelectedChangeStateSnapshot:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        traceback: TracebackType | None,
    ) -> None:
        self.close()


def write_selected_hunk_patch_lines(patch_lines: Sequence[bytes]) -> None:
    with LineBuffer.from_chunks(iter(patch_lines)) as patch_buffer:
        write_buffer_to_path(get_selected_hunk_patch_file_path(), patch_buffer)


def write_line_changes_state(line_changes: LineLevelChange) -> None:
    write_text_file_contents(
        get_line_changes_json_file_path(),
        json.dumps(
            convert_line_changes_to_serializable_dict(line_changes),
            ensure_ascii=False,
            indent=0,
        ),
    )


def cache_hunk_change(
    patch_lines: Sequence[bytes],
    hunk_hash: str,
    line_changes: LineLevelChange,
) -> None:
    """Cache a text hunk as the current selected change."""
    invalidate_selected_change_cache()
    write_selected_hunk_patch_lines(patch_lines)
    write_text_file_contents(get_selected_hunk_hash_file_path(), hunk_hash)
    write_line_changes_state(line_changes)
    write_snapshots_for_selected_file_path(line_changes.path)
    write_selected_change_kind(SelectedChangeKind.HUNK)


def load_line_changes_from_patch_path(patch_path: Path) -> LineLevelChange:
    with LineBuffer.from_path(patch_path) as patch_lines:
        return build_line_changes_from_patch_lines(patch_lines)


def _selected_change_state_paths() -> dict[str, Path]:
    """Return files that make up the cached selected change state."""
    return {
        "patch": get_selected_hunk_patch_file_path(),
        "hash": get_selected_hunk_hash_file_path(),
        "clear_reason": get_selected_change_clear_reason_file_path(),
        "kind": get_selected_change_kind_file_path(),
        "line_state": get_line_changes_json_file_path(),
        "rename": get_selected_rename_file_json_path(),
        "text_deletion": get_selected_text_deletion_file_json_path(),
        "binary": get_selected_binary_file_json_path(),
        "gitlink": get_selected_gitlink_file_json_path(),
        "mode": get_selected_mode_change_json_path(),
        "index_snapshot": get_index_snapshot_file_path(),
        "working_snapshot": get_working_tree_snapshot_file_path(),
        "snapshot_metadata": get_snapshot_metadata_file_path(),
        "processed_include_ids": get_processed_include_ids_file_path(),
        "processed_skip_ids": get_processed_skip_ids_file_path(),
    }


def snapshot_selected_change_state() -> SelectedChangeStateSnapshot:
    """Capture the current selected change cache."""
    temporary_directory = tempfile.TemporaryDirectory[str]()
    snapshot_root = Path(temporary_directory.name)
    snapshot_paths: dict[str, Path | None] = {}

    for name, path in _selected_change_state_paths().items():
        if not path.exists():
            snapshot_paths[name] = None
            continue

        snapshot_path = snapshot_root / name
        shutil.copyfile(path, snapshot_path)
        snapshot_paths[name] = snapshot_path

    return SelectedChangeStateSnapshot(
        paths=snapshot_paths,
        temporary_directory=temporary_directory,
    )


def restore_selected_change_state(snapshot: SelectedChangeStateSnapshot) -> None:
    """Restore a cache privately and publish its kind marker last."""
    state_paths = _selected_change_state_paths()
    invalidate_selected_change_cache()
    for name, path in state_paths.items():
        if name == "kind":
            continue
        snapshot_path = snapshot.paths.get(name)
        if snapshot_path is None:
            path.unlink(missing_ok=True)
        else:
            write_file_bytes(path, snapshot_path.read_bytes())

    kind_snapshot_path = snapshot.paths.get("kind")
    if kind_snapshot_path is not None:
        write_file_bytes(state_paths["kind"], kind_snapshot_path.read_bytes())


def clear_selected_change_persistence_files() -> None:
    """Clear cached selected change state files."""
    for path in _selected_change_state_paths().values():
        path.unlink(missing_ok=True)
    # processed_batch_ids is global state (union of all batches), not per-hunk state


def write_selected_change_kind(kind: SelectedChangeKind) -> None:
    """Persist the kind of selected change cached in session state."""
    get_selected_change_clear_reason_file_path().unlink(missing_ok=True)
    if kind != SelectedChangeKind.RENAME:
        get_selected_rename_file_json_path().unlink(missing_ok=True)
    if kind != SelectedChangeKind.DELETION:
        get_selected_text_deletion_file_json_path().unlink(missing_ok=True)
    if kind not in (SelectedChangeKind.BINARY, SelectedChangeKind.BATCH_BINARY):
        get_selected_binary_file_json_path().unlink(missing_ok=True)
    if kind not in (SelectedChangeKind.GITLINK, SelectedChangeKind.BATCH_GITLINK):
        get_selected_gitlink_file_json_path().unlink(missing_ok=True)
    if kind not in (SelectedChangeKind.MODE, SelectedChangeKind.BATCH_MODE):
        get_selected_mode_change_json_path().unlink(missing_ok=True)
    write_text_file_contents(get_selected_change_kind_file_path(), kind)


def invalidate_selected_change_cache() -> None:
    """Durably hide the old cache before replacing its component files."""
    kind_path = get_selected_change_kind_file_path()
    selected_directory = kind_path.parent
    if not selected_directory.exists():
        return
    kind_path.unlink(missing_ok=True)
    fsync_directory(selected_directory)


def read_selected_change_kind() -> SelectedChangeKind | None:
    """Return the kind of selected change cached in session state."""
    path = get_selected_change_kind_file_path()
    if not path.exists():
        return None

    raw_kind = read_text_file_contents(path).strip()
    if not raw_kind:
        return None

    try:
        return SelectedChangeKind(raw_kind)
    except ValueError:
        return None
