"""Construct staged and working-tree content buffers from line selections."""

from __future__ import annotations

from collections.abc import Collection, Iterator, Sequence
from typing import cast, overload

from ..core.coordinates import (
    BaselineSpace,
    FileSnapshot,
    LineBoundary,
    LineSpan,
    WorktreeSpace,
    content_snapshot,
    require_same_snapshot,
)
from ..core.edit_plan import ReplacementEditPlan
from ..core.models import LineEntry, LineLevelChange
from ..core.repeated_context_replacement import (
    find_repeated_context_suffix_replacement,
)
from ..core.replacement import (
    ReplacementPayload,
    coerce_replacement_payload,
    replacement_line_bodies,
)
from ..core.buffer import (
    LineBuffer,
)
from ..core.mapped_storage import MappedIntVector
from ..editor.line_endings import detect_line_ending
from ..editor.line_export import ensure_line_chunk_boundaries
from ..i18n import _


def _line_payload(line: bytes) -> bytes:
    if line.endswith(b"\r\n"):
        return line[:-2]
    if line.endswith(b"\n"):
        return line[:-1]
    return line


def _line_payload_at(lines: Sequence[bytes], index: int) -> bytes:
    return _line_payload(lines[index])


def _line_entry_content(
    line_entry: LineEntry,
) -> bytes:
    payload = _line_entry_payload(line_entry)
    if not line_entry.has_trailing_newline:
        return payload
    line_ending = b"\r\n" if line_entry.text_bytes.endswith(b"\r") else b"\n"
    return payload + line_ending


def _line_entry_payload(
    line_entry: LineEntry,
) -> bytes:
    """Return patch-row content without its CRLF carriage return."""
    if line_entry.has_trailing_newline and line_entry.text_bytes.endswith(b"\r"):
        return line_entry.text_bytes[:-1]
    return line_entry.text_bytes


def _line_ending_from_line(line: bytes) -> bytes | None:
    """Return an LF-based ending carried by one indexed source line."""
    if line.endswith(b"\r\n"):
        return b"\r\n"
    if line.endswith(b"\n"):
        return b"\n"
    return None


def _replacement_line_ending(
    source_lines: Sequence[bytes],
    selection_start: int,
    selection_end: int,
) -> bytes:
    """Choose an ending near the replaced source span."""
    source_line_count = len(source_lines)
    for index in range(selection_start, min(selection_end, source_line_count)):
        line_ending = _line_ending_from_line(source_lines[index])
        if line_ending is not None:
            return line_ending
    for index in range(min(selection_start, source_line_count) - 1, -1, -1):
        line_ending = _line_ending_from_line(source_lines[index])
        if line_ending is not None:
            return line_ending
    for index in range(selection_end, source_line_count):
        line_ending = _line_ending_from_line(source_lines[index])
        if line_ending is not None:
            return line_ending
    return detect_line_ending(source_lines) or b"\n"


def _edit_lines_preserving_source_endings_as_buffer(
    source_lines: Sequence[bytes],
    edited_lines: Sequence[bytes],
    *,
    selection_start: int,
    selection_end: int,
    has_trailing_newline: bool,
    add_trailing_newline_when_nonempty: bool = False,
) -> LineBuffer:
    """Replace full lines while retaining each unchanged source line exactly."""
    source_line_count = len(source_lines)
    if (
        selection_start < 0
        or selection_end < selection_start
        or selection_end > source_line_count
    ):
        raise ValueError(_("invalid line selection"))

    edited_line_count = len(edited_lines)
    output_line_count = (
        selection_start
        + edited_line_count
        + source_line_count
        - selection_end
    )
    generated_line_ending = _replacement_line_ending(
        source_lines,
        selection_start,
        selection_end,
    )

    def output_lines() -> Iterator[tuple[bytes, bool]]:
        for index in range(selection_start):
            yield source_lines[index], True
        for line in edited_lines:
            yield line, False
        for index in range(selection_end, source_line_count):
            yield source_lines[index], True

    def output_chunks() -> Iterator[bytes]:
        for output_index, (line, is_source_line) in enumerate(output_lines()):
            is_last = output_index == output_line_count - 1
            needs_line_ending = (
                not is_last
                or has_trailing_newline
                or add_trailing_newline_when_nonempty
            )
            payload = _line_payload(line)
            if not needs_line_ending:
                yield payload
                continue

            source_line_ending = (
                _line_ending_from_line(line) if is_source_line else None
            )
            yield payload + (source_line_ending or generated_line_ending)

    return LineBuffer.from_chunks(output_chunks())


def _replacement_result_has_trailing_newline(
    source_has_trailing_newline: bool,
    source_line_count: int,
    selection_start: int,
    selection_end: int,
    replacement_payload: ReplacementPayload,
    replacement_line_count: int,
) -> bool:
    """Return the target EOF state without altering surviving source lines."""
    if not replacement_payload.exact:
        return (
            replacement_payload.has_trailing_lf
            or source_has_trailing_newline
        )
    if selection_end != source_line_count:
        return source_has_trailing_newline
    if replacement_line_count:
        return replacement_payload.has_trailing_lf
    if selection_start < selection_end:
        # An empty replacement removed source lines at EOF. Any surviving
        # predecessor was not the source's final line, so retain its ending.
        return selection_start > 0
    return source_has_trailing_newline


def _line_content_at(lines: Sequence[bytes], index: int) -> bytes:
    """Return exact index bytes so untouched lines retain their endings."""
    return lines[index]


def _working_tree_line_content_at(
    lines: Sequence[bytes],
    index: int,
) -> bytes:
    """Return exact worktree bytes so untouched lines retain their endings."""
    return lines[index]


class _LinePayloadView(Sequence[bytes]):
    """Lazy line-body view over a contiguous source range."""

    def __init__(
        self,
        lines: Sequence[bytes],
        indices: range,
    ) -> None:
        self._lines = lines
        self._indices = indices

    def __len__(self) -> int:
        return len(self._indices)

    @overload
    def __getitem__(self, index: int) -> bytes: ...

    @overload
    def __getitem__(self, index: slice) -> Sequence[bytes]: ...

    def __getitem__(self, index: int | slice) -> bytes | Sequence[bytes]:
        if isinstance(index, slice):
            return _LinePayloadView(self._lines, self._indices[index])
        try:
            line_index = self._indices[index]
        except IndexError as error:
            raise IndexError(index) from error
        return _line_payload_at(self._lines, line_index)


def _line_payloads(
    lines: Sequence[bytes],
    start: int,
    end: int,
) -> Sequence[bytes]:
    return _LinePayloadView(lines, range(start, end))


def _longest_edge_context_match(
    candidate_lines: Sequence[bytes],
    context_lines: Sequence[bytes],
    *,
    reverse: bool,
) -> int:
    """Return a longest edge overlap in linear time with mapped scratch."""
    candidate_count = len(candidate_lines)
    context_count = len(context_lines)
    if candidate_count == 0 or context_count == 0:
        return 0

    def candidate_at(index: int) -> bytes:
        return candidate_lines[
            candidate_count - index - 1 if reverse else index
        ]

    def context_at(index: int) -> bytes:
        return context_lines[
            context_count - index - 1 if reverse else index
        ]

    width = 4 if candidate_count <= (1 << 32) - 1 else 8
    with MappedIntVector(candidate_count, width=width) as prefix_lengths:
        matched = 0
        for index in range(1, candidate_count):
            current = candidate_at(index)
            while matched and current != candidate_at(matched):
                matched = prefix_lengths[matched - 1]
            if current == candidate_at(matched):
                matched += 1
            prefix_lengths[index] = matched

        matched = 0
        for index in range(context_count):
            if matched == candidate_count:
                matched = prefix_lengths[matched - 1]
            current = context_at(index)
            while matched and current != candidate_at(matched):
                matched = prefix_lengths[matched - 1]
            if current == candidate_at(matched):
                matched += 1
        return matched


def _longest_prefix_context_match(
    candidate_lines: Sequence[bytes],
    context_lines: Sequence[bytes],
) -> int:
    return _longest_edge_context_match(
        candidate_lines,
        context_lines,
        reverse=False,
    )


def _longest_suffix_context_match(
    candidate_lines: Sequence[bytes],
    context_lines: Sequence[bytes],
) -> int:
    return _longest_edge_context_match(
        candidate_lines,
        context_lines,
        reverse=True,
    )


def _is_synthetic_gap_line(line_entry: LineEntry) -> bool:
    return (
        line_entry.kind == " "
        and line_entry.old_line_number is None
        and line_entry.new_line_number is None
    )


def _replacement_selection_span_indices(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    *,
    allow_incomplete_addition_span: bool = False,
) -> tuple[int, int]:
    """Return the display span for one contiguous run of changed rows."""
    line_index = 0
    while line_index < len(line_changes.lines):
        if line_changes.lines[line_index].kind not in ("+", "-"):
            line_index += 1
            continue

        run_start = line_index
        first_addition: int | None = None
        first_selected_index: int | None = None
        last_selected_index: int | None = None
        selected_count_in_run = 0
        selected_indices_are_contiguous = True
        malformed_run = False
        while (
            line_index < len(line_changes.lines)
            and line_changes.lines[line_index].kind in ("+", "-")
        ):
            line = line_changes.lines[line_index]
            if line.id in replace_ids:
                selected_count_in_run += 1
                if first_selected_index is None:
                    first_selected_index = line_index
                elif last_selected_index != line_index - 1:
                    selected_indices_are_contiguous = False
                last_selected_index = line_index
            if line.kind == "+":
                if first_addition is None:
                    first_addition = line_index
            elif first_addition is not None:
                malformed_run = True
            line_index += 1

        if (
            malformed_run
            or first_addition is None
            or first_addition == run_start
        ):
            continue
        replacement_stop = first_addition + min(
            first_addition - run_start,
            line_index - first_addition,
        )
        if (
            first_selected_index is None
            or first_selected_index >= replacement_stop
        ):
            continue
        replacement_core_is_incomplete = any(
            line_changes.lines[run_index].id is None
            or line_changes.lines[run_index].id not in replace_ids
            for run_index in range(run_start, replacement_stop)
        )
        deletion_side_is_complete = all(
            line_changes.lines[run_index].id is not None
            and line_changes.lines[run_index].id in replace_ids
            for run_index in range(run_start, first_addition)
        )
        deletion_side_is_available = all(
            line_changes.lines[run_index].id is not None
            for run_index in range(run_start, first_addition)
        )
        addition_span_is_contiguous = (
            deletion_side_is_available
            and first_selected_index >= first_addition
            and selected_count_in_run == len(replace_ids)
            and selected_indices_are_contiguous
        )
        selected_addition_count = 0
        for run_index in range(first_addition, line_index):
            line_id = line_changes.lines[run_index].id
            if line_id is None or line_id not in replace_ids:
                break
            selected_addition_count += 1
        addition_prefix_is_partial = (
            0 < selected_addition_count < line_index - first_addition
            and all(
                line_changes.lines[run_index].id not in replace_ids
                for run_index in range(
                    first_addition + selected_addition_count,
                    line_index,
                )
            )
        )
        if replacement_core_is_incomplete and not (
            (allow_incomplete_addition_span and addition_span_is_contiguous)
            or (deletion_side_is_complete and addition_prefix_is_partial)
        ):
            raise ValueError(
                _(
                    "Replacement selection must include one complete replacement "
                    "run; another changed line is unavailable or unselected"
                )
            )

    selected_count = 0
    span_start_index: int | None = None
    span_end_index: int | None = None
    selection_started = False
    selection_ended = False
    selection_is_discontiguous = False

    for display_index, line in enumerate(line_changes.lines):
        if line.kind not in ("+", "-"):
            continue

        if line.id is not None and line.id in replace_ids:
            if selection_ended:
                selection_is_discontiguous = True
            if span_start_index is None:
                span_start_index = display_index
            span_end_index = display_index
            selection_started = True
            selected_count += 1
        elif selection_started:
            selection_ended = True

    if selected_count != len(replace_ids):
        raise ValueError(
            _(
                "Replacement selection contains line IDs outside the current hunk"
            )
        )
    if selection_is_discontiguous:
        raise ValueError(
            _("Replacement selection must be one contiguous line range")
        )

    assert span_start_index is not None
    assert span_end_index is not None
    return span_start_index, span_end_index


def replacement_working_tree_span_indices(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    working_line_count: int,
    *,
    allow_incomplete_addition_span: bool = False,
) -> tuple[int, int]:
    """Return the exact working-tree line span replaced by selected IDs."""
    span_start_index, span_end_index = _replacement_selection_span_indices(
        line_changes,
        replace_ids,
        allow_incomplete_addition_span=allow_incomplete_addition_span,
    )
    return _replacement_working_tree_span_from_display_span(
        line_changes,
        span_start_index,
        span_end_index,
        working_line_count,
    )


def _replacement_working_tree_span_from_display_span(
    line_changes: LineLevelChange,
    span_start_index: int,
    span_end_index: int,
    working_line_count: int,
) -> tuple[int, int]:
    """Translate one validated display span into worktree geometry."""

    def next_new_line_number(start_index: int) -> int | None:
        for line_index in range(start_index, len(line_changes.lines)):
            line_entry = line_changes.lines[line_index]
            if _is_synthetic_gap_line(line_entry):
                return None
            if line_entry.new_line_number is not None:
                return line_entry.new_line_number
        return None

    def previous_new_line_number(end_index: int) -> int | None:
        for line_index in range(end_index, -1, -1):
            line_entry = line_changes.lines[line_index]
            if _is_synthetic_gap_line(line_entry):
                return None
            if line_entry.new_line_number is not None:
                return line_entry.new_line_number
        return None

    first_selected_line = line_changes.lines[span_start_index]
    if first_selected_line.new_line_number is not None:
        replace_start = max(first_selected_line.new_line_number - 1, 0)
    elif first_selected_line.old_line_number is not None:
        replace_start = min(
            _new_index_for_old_anchor(
                line_changes,
                first_selected_line.old_line_number - 1,
                span_start_index,
            ),
            working_line_count,
        )
    else:
        previous_line = previous_new_line_number(span_start_index - 1)
        next_line = next_new_line_number(span_start_index + 1)
        if previous_line is not None:
            replace_start = min(previous_line, working_line_count)
        elif next_line is not None:
            replace_start = max(next_line - 1, 0)
        else:
            replace_start = min(
                line_changes.header.new_prefix_line_count(),
                working_line_count,
            )

    replace_end = replace_start
    for line_index in range(span_end_index, span_start_index - 1, -1):
        line_entry = line_changes.lines[line_index]
        if line_entry.new_line_number is not None:
            replace_end = line_entry.new_line_number
            break
    return replace_start, replace_end


def replacement_baseline_span_indices(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    working_line_count: int,
    *,
    allow_incomplete_addition_span: bool = False,
) -> tuple[int, int]:
    """Return the old-file span consumed by a working-tree replacement."""
    span_start_index, span_end_index = _replacement_selection_span_indices(
        line_changes,
        replace_ids,
        allow_incomplete_addition_span=allow_incomplete_addition_span,
    )
    working_start, working_end = _replacement_working_tree_span_from_display_span(
        line_changes,
        span_start_index,
        span_end_index,
        working_line_count,
    )
    baseline_start = _old_index_for_new_anchor(
        line_changes,
        working_start,
        span_start_index,
    )
    baseline_end = _old_index_for_new_anchor(
        line_changes,
        working_end,
        span_end_index + 1,
    )
    return baseline_start, max(baseline_start, baseline_end)


def resolve_replacement_edit_plan(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    baseline_lines: Sequence[bytes],
    worktree_lines: Sequence[bytes],
) -> ReplacementEditPlan:
    """Resolve display selection geometry once at the staging boundary."""
    span_start_index, span_end_index = _replacement_selection_span_indices(
        line_changes,
        replace_ids,
    )
    working_start, working_end = _replacement_working_tree_span_from_display_span(
        line_changes,
        span_start_index,
        span_end_index,
        len(worktree_lines),
    )
    baseline_start = _old_index_for_new_anchor(
        line_changes,
        working_start,
        span_start_index,
    )
    baseline_end = _old_index_for_new_anchor(
        line_changes,
        working_end,
        span_end_index + 1,
    )
    baseline_end = max(baseline_start, baseline_end)
    return ReplacementEditPlan(
        path=line_changes.path,
        baseline_snapshot=cast(
            FileSnapshot[BaselineSpace],
            content_snapshot(
                line_changes.path,
                baseline_lines,
                space=BaselineSpace,
            ),
        ),
        worktree_snapshot=cast(
            FileSnapshot[WorktreeSpace],
            content_snapshot(
                line_changes.path,
                worktree_lines,
                space=WorktreeSpace,
            ),
        ),
        baseline_span=LineSpan(
            LineBoundary(baseline_start),
            LineBoundary(baseline_end),
        ),
        worktree_span=LineSpan(
            LineBoundary(working_start),
            LineBoundary(working_end),
        ),
    )


def _old_index_for_new_anchor(
    line_changes: LineLevelChange,
    new_anchor: int,
    before_index: int,
) -> int:
    """Translate a new-file anchor to an old-file insertion index.

    File-scoped views can concatenate separate hunks and omit their individual
    zero-length headers. Counting earlier changed entries restores the line
    number delta at the selected row.
    """
    old_index = new_anchor
    for line_index in range(before_index):
        line_entry = line_changes.lines[line_index]
        if line_entry.kind == "+":
            old_index -= 1
        elif line_entry.kind == "-":
            old_index += 1
    return max(old_index, 0)


def _new_index_for_old_anchor(
    line_changes: LineLevelChange,
    old_anchor: int,
    before_index: int,
) -> int:
    """Translate an old-file anchor to a new-file insertion index."""
    new_index = old_anchor
    for line_index in range(before_index):
        line_entry = line_changes.lines[line_index]
        if line_entry.kind == "-":
            new_index -= 1
        elif line_entry.kind == "+":
            new_index += 1
    return max(new_index, 0)


def _target_index_line_contents(
    line_changes: LineLevelChange,
    include_ids: Collection[int] | None,
    base_lines: Sequence[bytes],
    base_line_count: int,
) -> Iterator[bytes]:
    pending_addition_start: int | None = None

    base_pointer = line_changes.header.old_prefix_line_count()

    def line_is_included(line_entry: LineEntry) -> bool:
        return (
            line_entry.id is not None
            and (include_ids is None or line_entry.id in include_ids)
        )

    def flush_pending_additions(before_index: int) -> Iterator[bytes]:
        nonlocal pending_addition_start
        if pending_addition_start is None:
            return
        for pending_index in range(pending_addition_start, before_index):
            pending_line = line_changes.lines[pending_index]
            if pending_line.kind == "+" and line_is_included(pending_line):
                yield _line_entry_content(pending_line)
        pending_addition_start = None

    def base_line_matches(line_entry: LineEntry) -> bool:
        return (
            base_pointer < base_line_count
            and _line_payload_at(base_lines, base_pointer)
            == _line_entry_payload(line_entry)
        )

    def copy_unchanged_lines_before(old_line_number: int | None) -> Iterator[bytes]:
        nonlocal base_pointer
        if old_line_number is None:
            return
        target_index = max(old_line_number - 1, 0)
        while base_pointer < min(target_index, base_line_count):
            yield _line_content_at(base_lines, base_pointer)
            base_pointer += 1

    for index in range(0, min(base_pointer, base_line_count)):
        yield _line_content_at(base_lines, index)

    for line_index, line_entry in enumerate(line_changes.lines):
        if _is_synthetic_gap_line(line_entry):
            yield from flush_pending_additions(line_index)
            continue

        if line_entry.kind == " ":
            yield from copy_unchanged_lines_before(line_entry.old_line_number)
            yield from flush_pending_additions(line_index)
            if not base_line_matches(line_entry):
                raise ValueError(
                    _("Index content no longer matches the selected line view")
                )
            yield _line_content_at(base_lines, base_pointer)
            base_pointer += 1
        elif line_entry.kind == "-":
            yield from copy_unchanged_lines_before(line_entry.old_line_number)
            yield from flush_pending_additions(line_index)
            if not base_line_matches(line_entry):
                raise ValueError(
                    _("Index content no longer matches the selected line view")
                )
            if line_is_included(line_entry):
                base_pointer += 1
            else:
                yield _line_content_at(base_lines, base_pointer)
                base_pointer += 1
        elif line_entry.kind == "+":
            if line_is_included(line_entry) and pending_addition_start is None:
                pending_addition_start = line_index

    yield from flush_pending_additions(len(line_changes.lines))
    while 0 <= base_pointer < base_line_count:
        yield _line_content_at(base_lines, base_pointer)
        base_pointer += 1


def build_target_index_buffer_from_lines(
    line_changes: LineLevelChange,
    include_ids: Collection[int] | None,
    base_lines: Sequence[bytes],
    *,
    base_has_trailing_newline: bool,
) -> LineBuffer:
    """Build target index content from selected or all changed lines."""
    base_line_count = len(base_lines)
    detected_line_ending = detect_line_ending(base_lines)
    default_line_ending = (
        detected_line_ending
        if detected_line_ending in (b"\n", b"\r\n")
        else b"\n"
    )
    return LineBuffer.from_chunks(
        ensure_line_chunk_boundaries(
            _target_index_line_contents(
                line_changes,
                include_ids,
                base_lines,
                base_line_count,
            ),
            default_line_ending=default_line_ending,
        )
    )


def build_target_index_buffer_with_replaced_lines(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    replacement_text: str | ReplacementPayload,
    base_lines: Sequence[bytes],
    *,
    base_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool = True,
) -> LineBuffer:
    """Build target index content by replacing a span in indexed base lines."""
    replacement_payload = coerce_replacement_payload(replacement_text)
    with replacement_line_bodies(replacement_payload) as replacement_lines:
        return _build_target_index_buffer_with_replaced_lines(
            line_changes,
            replace_ids,
            replacement_payload,
            replacement_lines,
            base_lines,
            base_has_trailing_newline=base_has_trailing_newline,
            trim_unchanged_edge_anchors=trim_unchanged_edge_anchors,
        )


def build_target_index_buffer_with_edit_plan(
    edit_plan: ReplacementEditPlan,
    replacement_text: str | ReplacementPayload,
    base_lines: Sequence[bytes],
    *,
    base_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool = True,
) -> LineBuffer:
    """Apply a pre-resolved, snapshot-bound baseline replacement plan."""
    require_same_snapshot(
        edit_plan.baseline_snapshot,
        content_snapshot(edit_plan.path, base_lines, space=BaselineSpace),
    )
    replacement_payload = coerce_replacement_payload(replacement_text)
    with replacement_line_bodies(replacement_payload) as replacement_lines:
        return _build_target_index_buffer_with_replacement_span(
            edit_plan.baseline_span.start.offset,
            edit_plan.baseline_span.end.offset,
            replacement_payload,
            replacement_lines,
            base_lines,
            base_has_trailing_newline=base_has_trailing_newline,
            trim_unchanged_edge_anchors=trim_unchanged_edge_anchors,
        )


def _build_target_index_buffer_with_replaced_lines(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    replacement_payload: ReplacementPayload,
    replacement_lines: Sequence[bytes],
    base_lines: Sequence[bytes],
    *,
    base_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool,
) -> LineBuffer:
    """Build index content while replacement line storage is open."""
    if not replace_ids:
        return _edit_lines_preserving_source_endings_as_buffer(
            base_lines,
            [],
            selection_start=0,
            selection_end=0,
            has_trailing_newline=base_has_trailing_newline,
        )

    base_line_count = len(base_lines)
    span_start_index, span_end_index = _replacement_selection_span_indices(
        line_changes,
        replace_ids,
    )

    def find_next_old_line_number(start_index: int) -> int | None:
        for line_index in range(start_index, len(line_changes.lines)):
            line_entry = line_changes.lines[line_index]
            if _is_synthetic_gap_line(line_entry):
                return None
            if line_entry.old_line_number is not None:
                return line_entry.old_line_number
        return None

    def find_previous_old_line_number(end_index: int) -> int | None:
        for line_index in range(end_index, -1, -1):
            line_entry = line_changes.lines[line_index]
            if _is_synthetic_gap_line(line_entry):
                return None
            if line_entry.old_line_number is not None:
                return line_entry.old_line_number
        return None

    def old_insertion_index(index: int) -> int:
        line_entry = line_changes.lines[index]
        if line_entry.kind == "+" and line_entry.new_line_number is not None:
            return min(
                _old_index_for_new_anchor(
                    line_changes,
                    line_entry.new_line_number - 1,
                    index,
                ),
                base_line_count,
            )

        previous_old_line_number = find_previous_old_line_number(index - 1)
        if previous_old_line_number is not None:
            return min(previous_old_line_number, base_line_count)

        next_old_line_number = find_next_old_line_number(index + 1)
        if next_old_line_number is not None:
            return max(next_old_line_number - 1, 0)

        return min(line_changes.header.old_prefix_line_count(), base_line_count)

    first_selected_line = line_changes.lines[span_start_index]
    if first_selected_line.old_line_number is not None:
        replace_start = max(first_selected_line.old_line_number - 1, 0)
    else:
        replace_start = old_insertion_index(span_start_index)

    replace_end = base_line_count
    for line_index in range(span_end_index, span_start_index - 1, -1):
        line_entry = line_changes.lines[line_index]
        if line_entry.old_line_number is not None:
            replace_end = line_entry.old_line_number
            break
    else:
        replace_end = replace_start

    return _build_target_index_buffer_with_replacement_span(
        replace_start,
        replace_end,
        replacement_payload,
        replacement_lines,
        base_lines,
        base_has_trailing_newline=base_has_trailing_newline,
        trim_unchanged_edge_anchors=trim_unchanged_edge_anchors,
    )


def _build_target_index_buffer_with_replacement_span(
    replace_start: int,
    replace_end: int,
    replacement_payload: ReplacementPayload,
    replacement_lines: Sequence[bytes],
    base_lines: Sequence[bytes],
    *,
    base_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool,
) -> LineBuffer:
    """Build indexed content from canonical half-open replacement geometry."""
    base_line_count = len(base_lines)

    if trim_unchanged_edge_anchors:
        context_limit = len(replacement_lines)
        before_context = _line_payloads(
            base_lines,
            max(0, replace_start - context_limit),
            replace_start,
        )
        after_context = _line_payloads(
            base_lines,
            replace_end,
            min(base_line_count, replace_end + context_limit),
        )

        prefix_trim = _longest_prefix_context_match(replacement_lines, before_context)
        if prefix_trim:
            replacement_lines = replacement_lines[prefix_trim:]

        suffix_trim = _longest_suffix_context_match(replacement_lines, after_context)
        if suffix_trim:
            replacement_lines = replacement_lines[:-suffix_trim]

        if _longest_prefix_context_match(replacement_lines, before_context) >= 2:
            raise ValueError(
                _(
                    "Replacement text still includes unchanged anchor lines before "
                    "the selected span. Provide replacement text only for the "
                    "selected span, use --file --as for a full-file replacement, "
                    "or pass --no-edge-overlap to keep the edge-overlap text."
                )
            )

        if _longest_suffix_context_match(replacement_lines, after_context) >= 2:
            raise ValueError(
                _(
                    "Replacement text still includes unchanged anchor lines after "
                    "the selected span. Provide replacement text only for the "
                    "selected span, use --file --as for a full-file replacement, "
                    "or pass --no-edge-overlap to keep the edge-overlap text."
                )
            )

    trailing_newline = _replacement_result_has_trailing_newline(
        base_has_trailing_newline,
        base_line_count,
        replace_start,
        replace_end,
        replacement_payload,
        len(replacement_lines),
    )
    return _edit_lines_preserving_source_endings_as_buffer(
        base_lines,
        replacement_lines,
        selection_start=replace_start,
        selection_end=replace_end,
        has_trailing_newline=trailing_newline,
        add_trailing_newline_when_nonempty=(
            base_line_count == 0 and not replacement_payload.exact
        ),
    )


def _target_working_tree_line_contents(
    line_changes: LineLevelChange,
    discard_ids: Collection[int],
    working_lines: Sequence[bytes],
    working_line_count: int,
) -> Iterator[bytes]:
    working_pointer = line_changes.header.new_prefix_line_count()

    def working_line_matches(line_entry: LineEntry) -> bool:
        return (
            working_pointer < working_line_count
            and _line_payload_at(working_lines, working_pointer)
            == _line_entry_payload(line_entry)
        )

    def copy_unchanged_lines_before(new_line_number: int | None) -> Iterator[bytes]:
        nonlocal working_pointer
        if new_line_number is None:
            return
        target_index = max(new_line_number - 1, 0)
        yield from copy_unchanged_lines_until_index(target_index)

    def copy_unchanged_lines_until_index(target_index: int) -> Iterator[bytes]:
        nonlocal working_pointer
        while working_pointer < min(target_index, working_line_count):
            yield _working_tree_line_content_at(working_lines, working_pointer)
            working_pointer += 1

    def copy_remaining_lines_before_deletion(index: int) -> Iterator[bytes]:
        line_entry = line_changes.lines[index]
        if line_entry.old_line_number is not None:
            yield from copy_unchanged_lines_until_index(
                _new_index_for_old_anchor(
                    line_changes,
                    line_entry.old_line_number - 1,
                    index,
                )
            )

        for next_index in range(index + 1, len(line_changes.lines)):
            next_entry = line_changes.lines[next_index]
            if _is_synthetic_gap_line(next_entry):
                return
            if next_entry.new_line_number is not None:
                yield from copy_unchanged_lines_before(next_entry.new_line_number)
                return

    def process_line(index: int, line_entry: LineEntry) -> Iterator[bytes]:
        nonlocal working_pointer
        if line_entry.kind == " ":
            yield from copy_unchanged_lines_before(line_entry.new_line_number)
            if not working_line_matches(line_entry):
                raise ValueError(
                    _(
                        "Working tree content no longer matches the selected line view"
                    )
                )
            yield _working_tree_line_content_at(working_lines, working_pointer)
            working_pointer += 1
        elif line_entry.kind == "-":
            if line_entry.id in discard_ids:
                yield from copy_remaining_lines_before_deletion(index)
                yield _line_entry_content(line_entry)
        elif line_entry.kind == "+":
            yield from copy_unchanged_lines_before(line_entry.new_line_number)
            if not working_line_matches(line_entry):
                raise ValueError(
                    _(
                        "Working tree content no longer matches the selected line view"
                    )
                )
            if line_entry.id in discard_ids:
                working_pointer += 1
            else:
                yield _working_tree_line_content_at(working_lines, working_pointer)
                working_pointer += 1

    for index in range(0, min(working_pointer, working_line_count)):
        yield _working_tree_line_content_at(working_lines, index)

    index = 0
    while index < len(line_changes.lines):
        line_entry = line_changes.lines[index]
        if _is_synthetic_gap_line(line_entry):
            index += 1
            continue

        if line_entry.kind not in ("+", "-"):
            yield from process_line(index, line_entry)
            index += 1
            continue

        run_start = index
        while (
            index < len(line_changes.lines)
            and line_changes.lines[index].kind in ("+", "-")
        ):
            index += 1
        run_end = index
        replacement = find_repeated_context_suffix_replacement(
            line_changes.lines,
            discard_ids,
            run_start,
            run_end,
        )
        if replacement is None:
            for run_index in range(run_start, run_end):
                yield from process_line(
                    run_index,
                    line_changes.lines[run_index],
                )
            continue

        first_addition = replacement.first_addition
        for run_index in range(first_addition, run_end):
            yield from process_line(
                run_index,
                line_changes.lines[run_index],
            )
        for run_index in range(run_start, first_addition):
            yield _line_entry_content(line_changes.lines[run_index])

    while 0 <= working_pointer < working_line_count:
        yield _working_tree_line_content_at(working_lines, working_pointer)
        working_pointer += 1


def build_target_working_tree_buffer_from_lines(
    line_changes: LineLevelChange,
    discard_ids: Collection[int],
    working_lines: Sequence[bytes],
) -> LineBuffer:
    """Build target working tree content from indexed working tree lines."""
    working_line_count = len(working_lines)
    detected_line_ending = detect_line_ending(working_lines)
    default_line_ending = (
        detected_line_ending
        if detected_line_ending in (b"\n", b"\r\n")
        else b"\n"
    )
    return LineBuffer.from_chunks(
        ensure_line_chunk_boundaries(
            _target_working_tree_line_contents(
                line_changes,
                discard_ids,
                working_lines,
                working_line_count,
            ),
            default_line_ending=default_line_ending,
        )
    )


def build_target_working_tree_buffer_with_replaced_lines(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    replacement_text: str | ReplacementPayload,
    working_lines: Sequence[bytes],
    *,
    working_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool = True,
    preserved_replacement_prefix_count: int = 0,
) -> LineBuffer:
    """Build working tree content by replacing a span in indexed lines."""
    replacement_payload = coerce_replacement_payload(replacement_text)
    with replacement_line_bodies(replacement_payload) as replacement_lines:
        return _build_target_working_tree_buffer_with_replaced_lines(
            line_changes,
            replace_ids,
            replacement_payload,
            replacement_lines,
            working_lines,
            working_has_trailing_newline=working_has_trailing_newline,
            trim_unchanged_edge_anchors=trim_unchanged_edge_anchors,
            preserved_replacement_prefix_count=(
                preserved_replacement_prefix_count
            ),
        )


def build_target_working_tree_buffer_with_edit_plan(
    edit_plan: ReplacementEditPlan,
    replacement_text: str | ReplacementPayload,
    working_lines: Sequence[bytes],
    *,
    working_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool = True,
) -> LineBuffer:
    """Apply a pre-resolved, snapshot-bound worktree replacement plan."""
    require_same_snapshot(
        edit_plan.worktree_snapshot,
        content_snapshot(edit_plan.path, working_lines, space=WorktreeSpace),
    )
    replacement_payload = coerce_replacement_payload(replacement_text)
    with replacement_line_bodies(replacement_payload) as replacement_lines:
        return _build_target_working_tree_buffer_with_replacement_span(
            edit_plan.worktree_span.start.offset,
            edit_plan.worktree_span.end.offset,
            replacement_payload,
            replacement_lines,
            working_lines,
            working_has_trailing_newline=working_has_trailing_newline,
            trim_unchanged_edge_anchors=trim_unchanged_edge_anchors,
        )


def _build_target_working_tree_buffer_with_replaced_lines(
    line_changes: LineLevelChange,
    replace_ids: set[int],
    replacement_payload: ReplacementPayload,
    replacement_lines: Sequence[bytes],
    working_lines: Sequence[bytes],
    *,
    working_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool,
    preserved_replacement_prefix_count: int,
) -> LineBuffer:
    """Build working content while replacement line storage is open."""
    if not (
        0 <= preserved_replacement_prefix_count <= len(replacement_lines)
    ):
        raise ValueError("preserved replacement prefix exceeds replacement")
    if not replace_ids:
        return _edit_lines_preserving_source_endings_as_buffer(
            working_lines,
            [],
            selection_start=0,
            selection_end=0,
            has_trailing_newline=working_has_trailing_newline,
        )

    working_line_count = len(working_lines)
    replace_start, replace_end = replacement_working_tree_span_indices(
        line_changes,
        replace_ids,
        working_line_count,
        allow_incomplete_addition_span=(preserved_replacement_prefix_count > 0),
    )
    return _build_target_working_tree_buffer_with_replacement_span(
        replace_start,
        replace_end,
        replacement_payload,
        replacement_lines,
        working_lines,
        working_has_trailing_newline=working_has_trailing_newline,
        trim_unchanged_edge_anchors=trim_unchanged_edge_anchors,
        preserved_replacement_prefix_count=preserved_replacement_prefix_count,
    )


def _build_target_working_tree_buffer_with_replacement_span(
    replace_start: int,
    replace_end: int,
    replacement_payload: ReplacementPayload,
    replacement_lines: Sequence[bytes],
    working_lines: Sequence[bytes],
    *,
    working_has_trailing_newline: bool,
    trim_unchanged_edge_anchors: bool,
    preserved_replacement_prefix_count: int = 0,
) -> LineBuffer:
    """Build working content from canonical half-open replacement geometry."""
    working_line_count = len(working_lines)

    if trim_unchanged_edge_anchors:
        context_limit = len(replacement_lines)
        before_context = _line_payloads(
            working_lines,
            max(0, replace_start - context_limit),
            replace_start,
        )
        after_context = _line_payloads(
            working_lines,
            replace_end,
            min(working_line_count, replace_end + context_limit),
        )

        if not preserved_replacement_prefix_count:
            prefix_trim = _longest_prefix_context_match(
                replacement_lines,
                before_context,
            )
            if prefix_trim:
                replacement_lines = replacement_lines[prefix_trim:]

        suffix_trim = _longest_suffix_context_match(replacement_lines, after_context)
        suffix_trim = min(
            suffix_trim,
            max(
                len(replacement_lines) - preserved_replacement_prefix_count,
                0,
            ),
        )
        if suffix_trim:
            replacement_lines = replacement_lines[:-suffix_trim]

        if (
            not preserved_replacement_prefix_count
            and _longest_prefix_context_match(
                replacement_lines,
                before_context,
            ) >= 2
        ):
            raise ValueError(
                _(
                    "Replacement text still includes unchanged anchor lines before "
                    "the selected span. Provide replacement text only for the "
                    "selected span, use --file --as for a full-file replacement, "
                    "or pass --no-edge-overlap to keep the edge-overlap text."
                )
            )

        remaining_suffix_overlap = min(
            _longest_suffix_context_match(replacement_lines, after_context),
            len(replacement_lines) - preserved_replacement_prefix_count,
        )
        if remaining_suffix_overlap >= 2:
            raise ValueError(
                _(
                    "Replacement text still includes unchanged anchor lines after "
                    "the selected span. Provide replacement text only for the "
                    "selected span, use --file --as for a full-file replacement, "
                    "or pass --no-edge-overlap to keep the edge-overlap text."
                )
            )

    trailing_newline = _replacement_result_has_trailing_newline(
        working_has_trailing_newline,
        working_line_count,
        replace_start,
        replace_end,
        replacement_payload,
        len(replacement_lines),
    )
    return _edit_lines_preserving_source_endings_as_buffer(
        working_lines,
        replacement_lines,
        selection_start=replace_start,
        selection_end=replace_end,
        has_trailing_newline=trailing_newline,
        add_trailing_newline_when_nonempty=(
            working_line_count == 0 and not replacement_payload.exact
        ),
    )
