"""Live working-tree diff streams."""

from __future__ import annotations

from collections.abc import Iterable, Iterator, Sequence
from contextlib import contextmanager

from ..core.buffer import LineBuffer
from ..core.diff_parser import UnifiedDiffItem, acquire_unified_diff
from ..core.models import FileModeChange, SingleHunkPatch
from ..utils.git_command import stream_git_diff


def stream_live_git_diff(
    *,
    base: str | None = None,
    target: str | None = None,
    cached: bool = False,
    context_lines: int | None = None,
    no_color: bool = True,
    full_index: bool = False,
    find_renames: bool = True,
    no_renames: bool = False,
    ignore_submodules: str | None = None,
    submodule_format: str | None = None,
    paths: Iterable[str] = (),
    stdin_chunks: Iterable[bytes] | None = None,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
) -> Iterator[bytes]:
    """Stream actionable live changes with rename detection enabled."""
    return stream_git_diff(
        base=base,
        target=target,
        cached=cached,
        context_lines=context_lines,
        no_color=no_color,
        full_index=full_index,
        find_renames=find_renames,
        no_renames=no_renames,
        ignore_submodules=ignore_submodules,
        submodule_format=submodule_format,
        paths=paths,
        stdin_chunks=stdin_chunks,
        cwd=cwd,
        env=env,
    )


@contextmanager
def acquire_prepared_live_diff(
    *,
    base: str | None = None,
    target: str | None = None,
    cached: bool = False,
    context_lines: int | None = None,
    no_color: bool = True,
    full_index: bool = False,
    find_renames: bool = True,
    no_renames: bool = False,
    ignore_submodules: str | None = None,
    submodule_format: str | None = None,
    paths: Iterable[str] = (),
    stdin_chunks: Iterable[bytes] | None = None,
    cwd: str | None = None,
    env: dict[str, str] | None = None,
) -> Iterator[tuple[UnifiedDiffItem, ...]]:
    """Acquire one reusable live diff while owning cloned hunk buffers."""
    owned_buffers: list[LineBuffer] = []
    changes: list[UnifiedDiffItem] = []
    try:
        with acquire_unified_diff(
            stream_live_git_diff(
                base=base,
                target=target,
                cached=cached,
                context_lines=context_lines,
                no_color=no_color,
                full_index=full_index,
                find_renames=find_renames,
                no_renames=no_renames,
                ignore_submodules=ignore_submodules,
                submodule_format=submodule_format,
                paths=paths,
                stdin_chunks=stdin_chunks,
                cwd=cwd,
                env=env,
            )
        ) as parsed:
            for change in parsed:
                if isinstance(change, SingleHunkPatch):
                    lines = change.lines
                    if not isinstance(lines, LineBuffer):
                        raise TypeError("parsed text hunk must use LineBuffer storage")
                    cloned_lines = lines.clone()
                    owned_buffers.append(cloned_lines)
                    change = SingleHunkPatch(
                        old_path=change.old_path,
                        new_path=change.new_path,
                        lines=cloned_lines,
                    )
                changes.append(change)
        yield tuple(changes)
    finally:
        for buffer in owned_buffers:
            buffer.close()


def group_live_diff_by_file(
    files: Sequence[str],
    changes: Sequence[UnifiedDiffItem],
) -> dict[str, tuple[UnifiedDiffItem, ...]]:
    """Assign each live change once to its canonical requested file."""
    requested_files = set(files)
    grouped: dict[str, list[UnifiedDiffItem]] = {file_path: [] for file_path in files}
    for change in changes:
        preferred_path = change.path()
        if preferred_path in requested_files:
            grouped[preferred_path].append(change)
            continue
        if (
            isinstance(change, FileModeChange)
            and change.index_path in requested_files
        ):
            assert change.index_path is not None
            grouped[change.index_path].append(change)
            continue
        change_paths = (
            getattr(change, "old_path", None),
            getattr(change, "new_path", None),
        )
        for file_path in files:
            if file_path in change_paths:
                grouped[file_path].append(change)
                break
    return {
        file_path: tuple(file_changes) for file_path, file_changes in grouped.items()
    }


def paths_for_live_changes(
    changes: Iterable[UnifiedDiffItem],
) -> tuple[str, ...]:
    """Return every repository path touched by prepared live changes."""
    paths: list[str] = []
    for change in changes:
        if isinstance(change, FileModeChange):
            paths.append(change.file_path)
            if (
                change.index_path is not None
                and change.index_path != change.file_path
            ):
                paths.append(change.index_path)
            continue
        old_path = getattr(change, "old_path", None)
        new_path = getattr(change, "new_path", None)
        paths.extend(
            path
            for path in (old_path, new_path)
            if path is not None and path != "/dev/null"
        )
        if old_path is None and new_path is None:
            paths.append(change.path())
    return tuple(dict.fromkeys(paths))
