"""Batch source refresh and stale-source repair.

This module provides the single authoritative path for handling stale batch sources.
Command code should use these helpers rather than manually coordinating source
advancement, ownership remapping, cache updates, and line re-annotation.
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass

from ...core.models import LineEntry
from ...i18n import _
from .cache import (
    load_session_batch_sources,
    save_session_batch_sources,
)
from .snapshots import create_batch_source_commit
from ...utils.repository_buffers import (
    read_git_object_buffer_or_none,
    load_working_tree_file_as_buffer,
)
from ..ownership.model import (
    BatchOwnership,
)
from ..ownership.translation import (
    detect_stale_batch_source_for_selection,
)
from .selected_line_refresh import (
    refresh_selected_lines_against_new_source as _refresh_lines_against_new_source,
    refresh_selected_lines_against_source_lines as _refresh_lines_against_source,
    selected_lines_fit_source as _selection_matches_source,
)
from .advancement import advance_batch_source_for_file_with_provenance


@dataclass
class RefreshedBatchSelection:
    """Selection state after ensuring batch source is current for a file.

    This represents the state after stale-source detection and repair.
    If the source was stale, it has been advanced, ownership remapped,
    and lines re-annotated. If not stale, this contains the original state.
    """
    batch_source_commit: str | None
    """The current batch source commit (possibly newly created)."""

    ownership: BatchOwnership | None
    """Existing ownership, possibly remapped to new source space."""

    selected_lines: list[LineEntry]
    """Selected lines, possibly re-annotated for new source."""

    source_was_advanced: bool
    """True if batch source was advanced during this operation."""


def ensure_batch_source_current_for_selection(
    batch_name: str,
    file_path: str,
    current_batch_source_commit: str | None,
    existing_ownership: BatchOwnership | None,
    selected_lines: list[LineEntry],
    *,
    coordinate_lines: Sequence[LineEntry] | None = None,
) -> RefreshedBatchSelection:
    """Ensure batch source is current for selection, advancing if needed.

    This is the single authoritative helper for stale-source repair.

    Process:
    1. Detects whether selected lines require stale-source repair
    2. If not stale, returns original values unchanged
    3. If stale:
       - Creates new batch source commit for the file
       - Remaps ownership to the new source space
       - Updates session batch-source cache
       - Re-annotates selected lines against the new source
       - Returns a single structured result object

    The session cache is always updated correctly if source advancement occurs.
    Command code does not need to manually coordinate these steps.

    Args:
        batch_name: Name of the batch being updated
        file_path: Path to the file
        current_batch_source_commit: Current batch source commit (or None)
        existing_ownership: Existing ownership for this file (or None)
        selected_lines: LineEntry objects being added to batch
        coordinate_lines: Complete hunk entries for selected-line coordinates.

    Returns:
        RefreshedBatchSelection with current source state

    Raises:
        ValueError: If stale source remapping fails
    """
    if current_batch_source_commit is None:
        cached_source_result = _prepare_initial_cached_source_for_selection(
            file_path,
            selected_lines,
            coordinate_lines=coordinate_lines,
        )
        if cached_source_result is not None:
            batch_source_commit, prepared_selected_lines, source_was_advanced = (
                cached_source_result
            )
            return RefreshedBatchSelection(
                batch_source_commit=batch_source_commit,
                ownership=existing_ownership,
                selected_lines=prepared_selected_lines,
                source_was_advanced=source_was_advanced,
            )

    # Display annotations come from a path-scoped session cache and may have
    # been produced for another batch. Verify them against this batch's durable
    # source before deciding that their coordinates are current.
    if current_batch_source_commit is not None:
        source_buffer = read_git_object_buffer_or_none(
            f"{current_batch_source_commit}:{file_path}"
        )
        if source_buffer is None:
            raise ValueError(
                _("Cannot read batch source for {file} at {commit}").format(
                    file=file_path,
                    commit=current_batch_source_commit,
                )
            )
        with source_buffer as source_lines:
            mapped_selected_lines = _selection_mapped_to_source(
                file_path,
                selected_lines,
                source_lines,
                coordinate_lines=coordinate_lines,
            )
        if mapped_selected_lines is None:
            is_stale = True
        else:
            _cache_session_source(file_path, current_batch_source_commit)
            selected_lines = mapped_selected_lines
            is_stale = False
    else:
        is_stale = detect_stale_batch_source_for_selection(selected_lines)

    if is_stale and current_batch_source_commit and existing_ownership:
        # Batch source is stale - advance it and remap existing ownership
        with advance_batch_source_for_file_with_provenance(
            batch_name=batch_name,
            file_path=file_path,
            old_batch_source_commit=current_batch_source_commit,
            existing_ownership=existing_ownership
        ) as advance_result:
            # Update session cache so add_file_to_batch uses the new source.
            batch_sources = load_session_batch_sources()
            batch_sources[file_path] = advance_result.batch_source_commit
            save_session_batch_sources(batch_sources)

            # Re-annotate lines against the refreshed source. That source may
            # include already-owned lines that are absent from the
            # working tree after earlier discard operations.
            reannotated_lines = _refresh_lines_against_source(
                selected_lines,
                source_lines=advance_result.source_buffer,
                working_lines=(),
                lineage=advance_result.lineage,
                coordinate_lines=coordinate_lines,
            )

            return RefreshedBatchSelection(
                batch_source_commit=advance_result.batch_source_commit,
                ownership=advance_result.ownership,
                selected_lines=reannotated_lines,
                source_was_advanced=True
            )

    elif is_stale and current_batch_source_commit and not existing_ownership:
        # Inconsistent state: batch source exists but no ownership
        # This should not happen in normal operation
        raise ValueError(
            _(
                "Batch source exists for {file} in batch {batch} but no "
                "ownership found. This indicates corrupted batch state."
            ).format(file=file_path, batch=batch_name)
        )

    elif is_stale and not current_batch_source_commit:
        # First time - stale is normal. The batch source will be created from
        # the same working tree snapshot, so annotate now in that coordinate
        # space before translating ownership.
        reannotated_lines = _refresh_lines_against_new_source(
            selected_lines,
            coordinate_lines=coordinate_lines,
        )

        return RefreshedBatchSelection(
            batch_source_commit=current_batch_source_commit,
            ownership=existing_ownership,
            selected_lines=reannotated_lines,
            source_was_advanced=False
        )

    else:
        # Source is current - no changes needed
        return RefreshedBatchSelection(
            batch_source_commit=current_batch_source_commit,
            ownership=existing_ownership,
            selected_lines=selected_lines,
            source_was_advanced=False
        )


def _cache_session_source(file_path: str, batch_source_commit: str) -> None:
    batch_sources = load_session_batch_sources()
    if batch_sources.get(file_path) == batch_source_commit:
        return
    batch_sources[file_path] = batch_source_commit
    save_session_batch_sources(batch_sources)


def _create_current_working_source_for_selection(
    file_path: str,
    selected_lines: list[LineEntry],
    *,
    coordinate_lines: Sequence[LineEntry] | None = None,
) -> tuple[str, list[LineEntry]]:
    with load_working_tree_file_as_buffer(file_path) as working_lines:
        batch_source_commit = create_batch_source_commit(
            file_path,
            file_buffer_override=working_lines,
        )
    _cache_session_source(file_path, batch_source_commit)
    return (
        batch_source_commit,
        _refresh_lines_against_new_source(
            selected_lines,
            coordinate_lines=coordinate_lines,
        ),
    )


def _selection_mapped_to_source(
    file_path: str,
    selected_lines: list[LineEntry],
    source_lines: Sequence[bytes],
    *,
    coordinate_lines: Sequence[LineEntry] | None = None,
) -> list[LineEntry] | None:
    """Return selection coordinates verified against one saved source."""
    with load_working_tree_file_as_buffer(file_path) as working_lines:
        return map_selection_to_source(
            selected_lines,
            source_lines=source_lines,
            working_lines=working_lines,
            coordinate_lines=coordinate_lines,
        )


def map_selection_to_source(
    selected_lines: list[LineEntry],
    *,
    source_lines: Sequence[bytes],
    working_lines: Sequence[bytes],
    coordinate_lines: Sequence[LineEntry] | None = None,
) -> list[LineEntry] | None:
    """Return selection coordinates proven against explicit source content."""
    prepared_selected_lines = _refresh_lines_against_source(
        selected_lines,
        source_lines=source_lines,
        working_lines=working_lines,
        coordinate_lines=coordinate_lines,
    )
    if _selection_matches_source(prepared_selected_lines, source_lines):
        return prepared_selected_lines
    return None


def _prepare_initial_cached_source_for_selection(
    file_path: str,
    selected_lines: list[LineEntry],
    *,
    coordinate_lines: Sequence[LineEntry] | None = None,
) -> tuple[str, list[LineEntry], bool] | None:
    batch_source_commit = load_session_batch_sources().get(file_path)
    if not batch_source_commit:
        return None

    source_buffer = read_git_object_buffer_or_none(f"{batch_source_commit}:{file_path}")
    if source_buffer is None:
        new_batch_source_commit, reannotated_lines = (
            _create_current_working_source_for_selection(
                file_path,
                selected_lines,
                coordinate_lines=coordinate_lines,
            )
        )
        return new_batch_source_commit, reannotated_lines, True

    with source_buffer as source_lines:
        mapped_selected_lines = _selection_mapped_to_source(
            file_path,
            selected_lines,
            source_lines,
            coordinate_lines=coordinate_lines,
        )
        if mapped_selected_lines is not None:
            return batch_source_commit, mapped_selected_lines, False

    new_batch_source_commit, reannotated_lines = (
        _create_current_working_source_for_selection(
            file_path,
            selected_lines,
            coordinate_lines=coordinate_lines,
        )
    )
    return new_batch_source_commit, reannotated_lines, True


def prepare_initial_batch_source_for_selection(
    file_path: str,
    selected_lines: list[LineEntry],
    *,
    coordinate_lines: Sequence[LineEntry] | None = None,
) -> tuple[str, list[LineEntry]]:
    """Create the first source and map selected lines into its coordinates."""
    cached_source_result = _prepare_initial_cached_source_for_selection(
        file_path,
        selected_lines,
        coordinate_lines=coordinate_lines,
    )
    if cached_source_result is not None:
        batch_source_commit, prepared_selected_lines, _source_was_advanced = (
            cached_source_result
        )
        return batch_source_commit, prepared_selected_lines

    batch_source_commit = create_batch_source_commit(file_path)
    _cache_session_source(file_path, batch_source_commit)
    source_buffer = read_git_object_buffer_or_none(
        f"{batch_source_commit}:{file_path}"
    )
    if source_buffer is None:
        raise ValueError(
            _("Cannot read initial batch source for {file} at {commit}").format(
                file=file_path,
                commit=batch_source_commit,
            )
        )

    with source_buffer as source_lines:
        mapped_selected_lines = _selection_mapped_to_source(
            file_path,
            selected_lines,
            source_lines,
            coordinate_lines=coordinate_lines,
        )
        if mapped_selected_lines is not None:
            return batch_source_commit, mapped_selected_lines

    new_batch_source_commit, reannotated_lines = (
        _create_current_working_source_for_selection(
            file_path,
            selected_lines,
            coordinate_lines=coordinate_lines,
        )
    )
    return new_batch_source_commit, reannotated_lines
