"""Selected-change staging support for include commands."""

from __future__ import annotations

import subprocess
import sys
import tempfile
from pathlib import Path

from ...core.buffer import LineBuffer
from ...core.diff_parser import patch_is_file_deletion
from ...core.models import (
    BinaryFileChange,
    FileModeChange,
    GitlinkChange,
    RenameChange,
    TextFileDeletionChange,
)
from ...data.hunk_tracking import fetch_next_change
from ...data.index_entries import read_index_entry
from ...data.progress import record_hunk_included
from ...data.session import path_is_intent_to_add
from ...data.selected_change.loading import SelectedChange, load_selected_change
from ...data.selected_change.paths import worktree_paths_for_selected_change
from ...data.undo.checkpoints import undo_checkpoint
from ...exceptions import NoMoreHunks, exit_with_error
from ...i18n import _
from ...utils.file_io import read_text_file_contents
from ...utils.git_index import (
    GitIndexEntryUpdate,
    git_add_paths,
    git_apply_to_index,
    git_update_gitlink,
    git_update_index,
    git_update_index_entries,
)
from ...utils.git_command import run_git_command
from ...utils.paths import (
    get_selected_hunk_hash_file_path,
    get_selected_hunk_patch_file_path,
)
from .action_completion import finish_selected_change_action


def include_selected_change(
    *,
    quiet: bool = False,
    auto_advance: bool | None = None,
) -> int | None:
    """Include the currently selected change in the index."""
    item = load_selected_change()
    if item is None:
        try:
            item = fetch_next_change()
        except NoMoreHunks:
            if not quiet:
                print(_("No more hunks to process."), file=sys.stderr)
            return 0

    with undo_checkpoint(
        "include",
        worktree_paths=worktree_paths_for_selected_change(item),
    ):
        _include_loaded_selected_change(
            item,
            quiet=quiet,
            auto_advance=auto_advance,
        )
        return None


def _include_loaded_selected_change(
    item: SelectedChange,
    *,
    quiet: bool,
    auto_advance: bool | None,
) -> None:
    """Include one loaded selected change in the index."""
    patch_hash = read_text_file_contents(get_selected_hunk_hash_file_path()).strip()

    if isinstance(item, FileModeChange):
        stage_file_mode_change(item)
        record_hunk_included(patch_hash)
        if not quiet:
            print(_("✓ File mode staged: {file}").format(file=item.path()), file=sys.stderr)
        finish_selected_change_action(quiet=quiet, auto_advance=auto_advance)
        return
    if isinstance(item, RenameChange):
        stage_rename_change(item)
        record_hunk_included(patch_hash)

        if not quiet:
            print(
                _("✓ Rename staged: {old} -> {new}").format(
                    old=item.old_path,
                    new=item.new_path,
                ),
                file=sys.stderr,
            )

        finish_selected_change_action(
            quiet=quiet,
            auto_advance=auto_advance,
        )
        return

    if isinstance(item, TextFileDeletionChange):
        stage_text_deletion_change(item)
        record_hunk_included(patch_hash)

        if not quiet:
            print(
                _("✓ Text file deletion staged: {file}").format(file=item.path()),
                file=sys.stderr,
            )

        finish_selected_change_action(
            quiet=quiet,
            auto_advance=auto_advance,
        )
        return

    if isinstance(item, GitlinkChange):
        result = stage_gitlink_change(item)
        if result.returncode != 0:
            exit_with_error(
                _("Failed to stage submodule pointer: {error}").format(
                    error=result.stderr,
                )
            )

        record_hunk_included(patch_hash)

        if not quiet:
            print(
                _("✓ Submodule pointer {desc}: {file}").format(
                    desc=item.change_type,
                    file=item.path(),
                ),
                file=sys.stderr,
            )

        finish_selected_change_action(
            quiet=quiet,
            auto_advance=auto_advance,
        )
        return

    if isinstance(item, BinaryFileChange):
        file_path = item.path()
        result = git_add_paths([file_path], check=False)
        if result.returncode != 0:
            exit_with_error(
                _("Failed to stage binary file: {error}").format(
                    error=result.stderr,
                )
            )

        record_hunk_included(patch_hash)

        if not quiet:
            if item.is_new_file():
                change_desc = "added"
            elif item.is_deleted_file():
                change_desc = "deleted"
            else:
                change_desc = "modified"
            print(
                _("✓ Binary file {desc}: {file}").format(
                    desc=change_desc,
                    file=file_path,
                ),
                file=sys.stderr,
            )

        finish_selected_change_action(
            quiet=quiet,
            auto_advance=auto_advance,
        )
        return

    file_path = item.path

    with LineBuffer.from_path(get_selected_hunk_patch_file_path()) as patch_buffer:
        if patch_is_file_deletion(patch_buffer):
            stage_text_file_deletion(file_path)
            apply_result = None
        else:
            apply_result = git_apply_to_index(
                patch_buffer.byte_chunks(),
                check=False,
            )

    if apply_result is not None and apply_result.returncode != 0:
        exit_with_error(
            _("Failed to apply hunk: {error}").format(error=apply_result.stderr)
        )

    record_hunk_included(patch_hash)

    if not quiet:
        print(_("✓ Hunk staged from {file}").format(file=file_path), file=sys.stderr)

    finish_selected_change_action(
        quiet=quiet,
        auto_advance=auto_advance,
    )


def stage_file_mode_change(item: FileModeChange) -> None:
    """Stage one executable-mode transition in the index."""
    index_path = item.path()
    use_alternate_worktree = False
    source_is_intent_to_add = False
    destination_entry = read_index_entry(item.path())
    destination_is_placeholder = (
        destination_entry is not None
        and path_is_intent_to_add(item.path())
    )
    if item.index_path is not None and (
        destination_entry is None or destination_is_placeholder
    ):
        if read_index_entry(item.index_path) is not None:
            index_path = item.index_path
            use_alternate_worktree = True
            source_is_intent_to_add = path_is_intent_to_add(item.index_path)

    chmod = "+x" if item.new_mode == "100755" else "-x"
    if use_alternate_worktree:
        with tempfile.TemporaryDirectory(
            prefix="git-stage-batch-mode-change-"
        ) as temporary_worktree:
            placeholder = Path(temporary_worktree) / index_path
            placeholder.parent.mkdir(parents=True, exist_ok=True)
            placeholder.touch()
            placeholder.chmod(0o755 if item.new_mode == "100755" else 0o644)
            if source_is_intent_to_add:
                result = git_update_index(
                    file_path=index_path,
                    force_remove=True,
                    check=False,
                )
                if result.returncode == 0:
                    result = run_git_command(
                        [
                            "--literal-pathspecs",
                            f"--work-tree={temporary_worktree}",
                            "-c",
                            "core.fileMode=true",
                            "add",
                            "-N",
                            "--force",
                            "--sparse",
                            "--",
                            index_path,
                        ],
                        check=False,
                    )
            else:
                result = run_git_command(
                    [
                        f"--work-tree={temporary_worktree}",
                        "update-index",
                        f"--chmod={chmod}",
                        "--",
                        index_path,
                    ],
                    check=False,
                )
    else:
        result = run_git_command(
            ["update-index", f"--chmod={chmod}", "--", index_path],
            check=False,
        )
    if result.returncode != 0:
        exit_with_error(_("Failed to stage executable mode change."))


def stage_gitlink_change(
    gitlink_change: GitlinkChange,
) -> subprocess.CompletedProcess[str]:
    """Stage a submodule pointer change in the index."""
    file_path = gitlink_change.path()
    if gitlink_change.is_deleted_file():
        return git_update_gitlink(
            file_path=file_path,
            oid=None,
            remove=True,
            check=False,
        )
    if gitlink_change.new_oid is None:
        exit_with_error(
            _("Cannot stage submodule pointer for {file}: missing target commit.").format(
                file=file_path,
            )
        )
    return git_update_gitlink(
        file_path=file_path,
        oid=gitlink_change.new_oid,
        check=False,
    )


def stage_rename_change(rename_change: RenameChange) -> None:
    """Stage only the structural rename, leaving destination content edits unstaged."""
    index_entry = read_index_entry(rename_change.old_path)
    if index_entry is None:
        exit_with_error(
            _("Cannot stage rename {old} -> {new}: missing baseline index entry.").format(
                old=rename_change.old_path,
                new=rename_change.new_path,
            )
        )

    git_update_index_entries(
        [
            GitIndexEntryUpdate(file_path=rename_change.old_path, force_remove=True),
            GitIndexEntryUpdate(
                file_path=rename_change.new_path,
                mode=index_entry.mode,
                blob_sha=index_entry.object_id,
            ),
        ]
    )


def stage_text_deletion_change(deletion_change: TextFileDeletionChange) -> None:
    """Stage a whole-text-file deletion in the index."""
    stage_text_file_deletion(deletion_change.path())


def stage_text_file_deletion(file_path: str) -> None:
    """Stage deletion of one text path regardless of its previous contents."""
    result = git_update_index(
        file_path=file_path,
        force_remove=True,
        check=False,
    )
    if result.returncode != 0:
        exit_with_error(
            _("Failed to stage deletion for {file}: {error}").format(
                file=file_path,
                error=result.stderr,
            )
        )
