"""Helpers for expressing replacement text in batch source space."""

from __future__ import annotations

from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from pathlib import Path
from types import TracebackType

from ..core.replacement import (
    ReplacementPayload,
    coerce_replacement_payload,
    replacement_line_chunks,
)
from ..core.buffer import LineBuffer
from ..i18n import _
from .ownership.model import BatchOwnership
from .ownership.absence_claims import AbsenceClaim
from .ownership.replacement_units import ReplacementUnit


__all__ = [
    "ReplacementBatchView",
    "build_replacement_batch_view_from_lines",
]


def _localized_selection_error(message: str) -> str | None:
    """Translate the replacement-selection errors exposed by this module."""
    if message == (
        "Replacement selection must resolve to one contiguous "
        "batch-source line range."
    ):
        return _(
            "Replacement selection must resolve to one contiguous "
            "batch-source line range."
        )
    if message == (
        "Replacement selection must resolve to one contiguous "
        "batch-source region."
    ):
        return _(
            "Replacement selection must resolve to one contiguous "
            "batch-source region."
        )
    return None


@dataclass(slots=True)
class ReplacementBatchView:
    """Batch source buffer and ownership produced for replacement text."""

    source_buffer: LineBuffer
    ownership: BatchOwnership

    def close(self) -> None:
        """Close the generated source buffer."""
        self.source_buffer.close()

    def __enter__(self) -> ReplacementBatchView:
        return self

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


def _format_presence_range(start_line: int, line_count: int) -> list[str]:
    """Format one contiguous presence range without expanding its line IDs."""
    if line_count <= 0:
        return []
    end_line = start_line + line_count - 1
    return [
        str(start_line)
        if start_line == end_line
        else f"{start_line}-{end_line}"
    ]


def build_replacement_batch_view_from_lines(
    source_lines: Sequence[bytes],
    ownership: BatchOwnership,
    replacement_text: str | ReplacementPayload,
    *,
    spool_dir: str | Path | None = None,
) -> ReplacementBatchView:
    """Build replacement source content from an indexed byte-line sequence."""
    payload = coerce_replacement_payload(replacement_text)
    with replacement_line_chunks(
        payload,
        spool_dir=spool_dir,
    ) as replacement_lines:
        try:
            return _build_replacement_batch_view(
                source_lines,
                ownership,
                replacement_lines,
                spool_dir=spool_dir,
            )
        except ValueError as error:
            localized_error = _localized_selection_error(str(error))
            if localized_error is None:
                raise
            raise ValueError(localized_error) from error


def _build_replacement_batch_view(
    source_lines: Sequence[bytes],
    ownership: BatchOwnership,
    replacement_lines: Sequence[bytes],
    *,
    spool_dir: str | Path | None = None,
) -> ReplacementBatchView:
    """Build a replacement view while its replacement line sequence is open."""
    claimed_source_ranges = ownership.presence_line_set().ranges()

    if claimed_source_ranges:
        if len(claimed_source_ranges) != 1:
            raise ValueError(
                "Replacement selection must resolve to one contiguous "
                "batch-source line range."
            )

        start_line, end_line = claimed_source_ranges[0]
        removed_count = end_line - start_line + 1
        added_count = len(replacement_lines)

        new_presence_lines = _format_presence_range(start_line, added_count)
        new_deletions = []
        for deletion in ownership.deletions:
            anchor = deletion.anchor_line
            if anchor is None:
                new_anchor = None
            elif anchor < start_line:
                new_anchor = anchor
            elif anchor > end_line:
                new_anchor = anchor - removed_count + added_count
            elif added_count > 0:
                new_anchor = start_line + added_count - 1
            elif start_line > 1:
                new_anchor = start_line - 1
            else:
                new_anchor = None

            new_deletions.append(AbsenceClaim(
                anchor_line=new_anchor,
                content_lines=deletion.content_lines,
                baseline_reference=(
                    deletion.baseline_reference
                    if deletion.source_alternative
                    else None
                ),
                source_alternative=deletion.source_alternative,
            ))

        return ReplacementBatchView(
            source_buffer=LineBuffer.from_chunks(
                _replacement_source_chunks(
                    source_lines=source_lines,
                    prefix_end=start_line - 1,
                    replacement_lines=replacement_lines,
                    suffix_start=end_line,
                ),
                spool_dir=spool_dir,
            ),
            ownership=BatchOwnership.from_presence_lines(
                new_presence_lines,
                new_deletions,
                replacement_units=[
                    ReplacementUnit(
                        presence_lines=new_presence_lines,
                        deletion_indices=list(range(len(new_deletions))),
                    )
                ] if new_presence_lines and new_deletions else [],
            ),
        )

    distinct_anchors = {deletion.anchor_line for deletion in ownership.deletions}
    if len(distinct_anchors) > 1:
        raise ValueError(
            "Replacement selection must resolve to one contiguous "
            "batch-source region."
        )

    anchor_line = next(iter(distinct_anchors), None)
    insert_at = 0 if anchor_line is None else anchor_line
    added_count = len(replacement_lines)

    new_start_line = 1 if anchor_line is None else anchor_line + 1
    new_presence_lines = _format_presence_range(new_start_line, added_count)

    new_deletions = []
    for deletion in ownership.deletions:
        if deletion.anchor_line is None:
            new_anchor = None
        elif anchor_line is None:
            new_anchor = deletion.anchor_line + added_count
        elif deletion.anchor_line <= anchor_line:
            new_anchor = deletion.anchor_line
        else:
            new_anchor = deletion.anchor_line + added_count

        new_deletions.append(AbsenceClaim(
            anchor_line=new_anchor,
            content_lines=deletion.content_lines,
            baseline_reference=(
                deletion.baseline_reference
                if deletion.source_alternative
                else None
            ),
            source_alternative=deletion.source_alternative,
        ))

    return ReplacementBatchView(
        source_buffer=LineBuffer.from_chunks(
            _replacement_source_chunks(
                source_lines=source_lines,
                prefix_end=insert_at,
                replacement_lines=replacement_lines,
                suffix_start=insert_at,
            ),
            spool_dir=spool_dir,
        ),
        ownership=BatchOwnership.from_presence_lines(
            new_presence_lines,
            new_deletions,
            replacement_units=[
                ReplacementUnit(
                    presence_lines=new_presence_lines,
                    deletion_indices=list(range(len(new_deletions))),
                )
            ] if new_presence_lines and new_deletions else [],
        ),
    )


def _replacement_source_chunks(
    *,
    source_lines: Sequence[bytes],
    prefix_end: int,
    replacement_lines: Iterable[bytes],
    suffix_start: int,
) -> Iterable[bytes]:
    """Yield replacement source content without materializing source lines."""
    for line_index in range(prefix_end):
        yield source_lines[line_index]

    yield from replacement_lines

    for line_index in range(suffix_start, len(source_lines)):
        yield source_lines[line_index]
