"""Line matching and alignment between batch source and working tree."""

from __future__ import annotations

from collections.abc import Hashable, Iterable, Sequence
from pathlib import Path
from typing import TypeVar

from .line_mapping import IntVector as _IntVector, LineMapping as _LineMapping
from .match_workspace import MatcherWorkspace
from ...core.mapped_storage import (
    MappedIntVector,
    MappedRecordVector,
    sort_mapped_records,
)
from ...core.text_lines import AcquirableLineSequence, as_acquirable_line_sequence


LineContent = TypeVar("LineContent", bound=Hashable)
_MAX_UINT32 = (1 << 32) - 1
_MAX_UINT64 = (1 << 64) - 1
_LINE_PAIR_RECORD_FORMAT = "QQ"
_LINE_INDEX_RECORD_FORMAT = "Q"
_DIRECT_TARGET_RANK_SPAN_FACTOR = 8
_OCCURRENCE_RECORD_FORMAT = "QQQQQQ"
_OCCURRENCE_HASH = 0
_OCCURRENCE_SOURCE_INDEX = 1
_OCCURRENCE_SOURCE_COUNT = 2
_OCCURRENCE_TARGET_INDEX = 3
_OCCURRENCE_TARGET_COUNT = 4
_OCCURRENCE_NEXT = 5


def _line_mapping_width(max_line_number: int) -> int:
    if max_line_number <= _MAX_UINT32:
        return 4
    return 8


def _new_line_mapping(
    size: int,
    max_line_number: int,
    *,
    spool_dir: str | Path | None = None,
) -> MappedIntVector:
    return MappedIntVector(
        size,
        width=_line_mapping_width(max_line_number),
        fill=0,
        spool_dir=spool_dir,
    )


class _LineOccurrenceTable:
    """Storage-backed occurrence table for one active source/target segment."""

    def __init__(
        self,
        workspace: MatcherWorkspace,
        source_lines: Sequence[LineContent],
        source_start: int,
        source_end: int,
    ) -> None:
        source_length = source_end - source_start
        self._workspace = workspace
        self._source_lines = source_lines
        self._bucket_count = _occurrence_bucket_capacity(source_length)
        self._buckets = workspace.int_vector(
            self._bucket_count,
            width=8,
            fill=0,
        )
        self._records = workspace.record_vector(
            source_length,
            _OCCURRENCE_RECORD_FORMAT,
        )
        self._has_common_lines = False
        self._closed = False

    @property
    def has_common_lines(self) -> bool:
        """Return whether target scanning found source-equal content."""
        return self._has_common_lines

    def scan_source(self, start: int, end: int) -> None:
        """Record source-side occurrence counts."""
        for index in range(start, end):
            line_hash = _line_hash(self._source_lines[index])
            record_index = self._find_record(
                self._source_lines,
                index,
                line_hash,
            )
            if record_index is None:
                bucket_index = self._bucket_index(line_hash)
                next_record = self._buckets[bucket_index]
                new_record_index = self._records.append((
                    line_hash,
                    index,
                    1,
                    0,
                    0,
                    next_record,
                ))
                self._buckets[bucket_index] = new_record_index + 1
                continue

            record = self._records[record_index]
            self._records[record_index] = _replace_record_value(
                record,
                _OCCURRENCE_SOURCE_COUNT,
                min(2, record[_OCCURRENCE_SOURCE_COUNT] + 1),
            )

    def scan_target(
        self,
        target_lines: Sequence[LineContent],
        start: int,
        end: int,
    ) -> None:
        """Record target-side occurrences for source-known content."""
        for index in range(start, end):
            line_hash = _line_hash(target_lines[index])
            record_index = self._find_record(target_lines, index, line_hash)
            if record_index is None:
                continue

            self._has_common_lines = True
            record = self._records[record_index]
            target_count = record[_OCCURRENCE_TARGET_COUNT]
            if target_count == 0:
                record = _replace_record_value(
                    record,
                    _OCCURRENCE_TARGET_INDEX,
                    index,
                )
            self._records[record_index] = _replace_record_value(
                record,
                _OCCURRENCE_TARGET_COUNT,
                min(2, target_count + 1),
            )

    def emit_candidate_pairs(
        self,
        workspace: MatcherWorkspace,
        source_start: int,
        source_end: int,
    ) -> MappedRecordVector:
        """Return source-ordered candidate anchor pairs."""
        candidates = workspace.record_vector(
            source_end - source_start,
            _LINE_PAIR_RECORD_FORMAT,
        )

        for source_index in range(source_start, source_end):
            line_hash = _line_hash(self._source_lines[source_index])
            record_index = self._find_record(
                self._source_lines,
                source_index,
                line_hash,
            )
            if record_index is None:
                continue

            record = self._records[record_index]
            if (
                record[_OCCURRENCE_SOURCE_INDEX] == source_index
                and record[_OCCURRENCE_SOURCE_COUNT] == 1
                and record[_OCCURRENCE_TARGET_COUNT] == 1
            ):
                candidates.append((
                    source_index,
                    record[_OCCURRENCE_TARGET_INDEX],
                ))

        return candidates

    def close(self) -> None:
        """Release mapped occurrence storage."""
        if self._closed:
            return
        self._workspace.close_resource(self._records)
        self._workspace.close_resource(self._buckets)
        self._closed = True

    def _find_record(
        self,
        lines: Sequence[LineContent],
        index: int,
        line_hash: int,
    ) -> int | None:
        record_number = self._buckets[self._bucket_index(line_hash)]
        line = lines[index]

        while record_number != 0:
            record_index = record_number - 1
            record = self._records[record_index]
            if (
                record[_OCCURRENCE_HASH] == line_hash
                and self._source_lines[record[_OCCURRENCE_SOURCE_INDEX]] == line
            ):
                return record_index
            record_number = record[_OCCURRENCE_NEXT]

        return None

    def _bucket_index(self, line_hash: int) -> int:
        return line_hash & (self._bucket_count - 1)


def _occurrence_bucket_capacity(source_length: int) -> int:
    capacity = 1
    target_capacity = max(1, source_length * 2)
    while capacity < target_capacity:
        capacity <<= 1
    return capacity


def _line_hash(line: Hashable) -> int:
    return hash(line) & _MAX_UINT64


def _replace_record_value(
    record: tuple[int, ...],
    field_index: int,
    value: int,
) -> tuple[int, ...]:
    mutable = list(record)
    mutable[field_index] = value
    return tuple(mutable)


def _longest_increasing_subsequence_records(
    pairs: MappedRecordVector,
    target_start: int,
    target_end: int,
    workspace: MatcherWorkspace,
) -> MappedRecordVector:
    """Return mapped LIS anchors for source-ordered candidate records."""
    pair_count = len(pairs)

    if pair_count == 0 or target_end <= target_start:
        return workspace.record_vector(0, _LINE_PAIR_RECORD_FORMAT)

    target_indices = None
    target_span = target_end - target_start
    use_direct_target_ranks = (
        target_span <= pair_count * _DIRECT_TARGET_RANK_SPAN_FACTOR
    )
    if use_direct_target_ranks:
        target_rank_count = target_span
    else:
        target_indices = _sorted_unique_target_indices(pairs, workspace)
        target_rank_count = len(target_indices)

    best_lengths = workspace.int_vector(target_rank_count + 1, width=8, fill=0)
    best_indexes = workspace.int_vector(target_rank_count + 1, width=8, fill=0)
    predecessors = workspace.int_vector(pair_count, width=8, fill=0)
    result_indexes = None

    try:
        best_length = 0
        best_index_number = 0

        for pair_index in range(pair_count):
            _, target_index = pairs[pair_index]
            if use_direct_target_ranks:
                target_rank = target_index - target_start + 1
                if target_rank < 1 or target_rank > target_span:
                    raise ValueError(
                        "LIS candidate target index is outside its segment"
                    )
            else:
                assert target_indices is not None
                target_rank = _mapped_target_rank(target_indices, target_index) + 1
            predecessor_length, predecessor_index_number = (
                _query_best_record_by_target_rank(
                    best_lengths,
                    best_indexes,
                    target_rank - 1,
                )
            )
            current_length = predecessor_length + 1

            if predecessor_index_number != 0:
                predecessors[pair_index] = predecessor_index_number

            if current_length > best_length:
                best_length = current_length
                best_index_number = pair_index + 1

            _update_best_record_by_target_rank(
                best_lengths,
                best_indexes,
                target_rank,
                current_length,
                pair_index + 1,
            )

        anchors = workspace.record_vector(best_length, _LINE_PAIR_RECORD_FORMAT)
        if best_length == 0:
            return anchors

        result_indexes = workspace.int_vector(best_length, width=8, fill=0)
        result_offset = best_length - 1
        index_number = best_index_number

        while index_number != 0:
            result_indexes[result_offset] = index_number
            result_offset -= 1
            index_number = predecessors[index_number - 1]

        for result_offset in range(best_length):
            source_index, target_index = pairs[result_indexes[result_offset] - 1]
            anchors.append((source_index, target_index))

        return anchors
    finally:
        if result_indexes is not None:
            workspace.close_resource(result_indexes)
        workspace.close_resource(predecessors)
        workspace.close_resource(best_indexes)
        workspace.close_resource(best_lengths)
        if target_indices is not None:
            workspace.close_resource(target_indices)


def _sorted_unique_target_indices(
    pairs: MappedRecordVector,
    workspace: MatcherWorkspace,
) -> MappedRecordVector:
    """Return storage-backed sorted target coordinates from candidate pairs."""
    target_indices = workspace.record_vector(
        len(pairs),
        _LINE_INDEX_RECORD_FORMAT,
    )
    for pair_index in range(len(pairs)):
        target_indices.append((pairs[pair_index][1],))
    sort_mapped_records(target_indices)

    unique_count = 0
    previous_target_index: int | None = None
    for read_index in range(len(target_indices)):
        target_index = target_indices[read_index][0]
        if target_index == previous_target_index:
            continue
        target_indices[unique_count] = (target_index,)
        unique_count += 1
        previous_target_index = target_index
    target_indices.truncate(unique_count)
    return target_indices


def _mapped_target_rank(
    target_indices: MappedRecordVector,
    target_index: int,
) -> int:
    """Return the zero-based insertion rank in mapped sorted coordinates."""
    low = 0
    high = len(target_indices)
    while low < high:
        middle = (low + high) // 2
        if target_indices[middle][0] < target_index:
            low = middle + 1
        else:
            high = middle
    return low


def _query_best_record_by_target_rank(
    best_lengths: _IntVector,
    best_indexes: _IntVector,
    target_rank: int,
) -> tuple[int, int]:
    """Return the best mapped LIS record ending before a target rank."""
    best_length = 0
    best_index_number = 0

    while target_rank > 0:
        candidate_length = best_lengths[target_rank]
        candidate_index_number = best_indexes[target_rank]
        if _is_better_lis_record_entry(
            candidate_length,
            candidate_index_number,
            best_length,
            best_index_number,
        ):
            best_length = candidate_length
            best_index_number = candidate_index_number
        target_rank -= target_rank & -target_rank

    return best_length, best_index_number


def _update_best_record_by_target_rank(
    best_lengths: _IntVector,
    best_indexes: _IntVector,
    target_rank: int,
    candidate_length: int,
    candidate_index_number: int,
) -> None:
    """Record a mapped LIS candidate ending at a target rank."""
    while target_rank < len(best_lengths):
        if _is_better_lis_record_entry(
            candidate_length,
            candidate_index_number,
            best_lengths[target_rank],
            best_indexes[target_rank],
        ):
            best_lengths[target_rank] = candidate_length
            best_indexes[target_rank] = candidate_index_number
        target_rank += target_rank & -target_rank


def _is_better_lis_record_entry(
    candidate_length: int,
    candidate_index_number: int,
    current_length: int,
    current_index_number: int,
) -> bool:
    """Prefer longer mapped subsequences, then earlier source endings."""
    if candidate_length != current_length:
        return candidate_length > current_length

    if candidate_length == 0:
        return False

    if current_index_number == 0:
        return True

    return candidate_index_number < current_index_number


def _map_equal_prefix(
    source_lines: Sequence[LineContent],
    target_lines: Sequence[LineContent],
    source_start: int,
    source_end: int,
    target_start: int,
    target_end: int,
    source_to_target: _IntVector,
    target_to_source: _IntVector,
) -> tuple[int, int]:
    """Map equal prefix lines and return new segment starts."""
    while (
        source_start < source_end
        and target_start < target_end
        and source_lines[source_start] == target_lines[target_start]
    ):
        source_to_target[source_start] = target_start + 1
        target_to_source[target_start] = source_start + 1
        source_start += 1
        target_start += 1

    return source_start, target_start


def _map_equal_suffix(
    source_lines: Sequence[LineContent],
    target_lines: Sequence[LineContent],
    source_start: int,
    source_end: int,
    target_start: int,
    target_end: int,
    source_to_target: _IntVector,
    target_to_source: _IntVector,
) -> tuple[int, int]:
    """Map equal suffix lines and return new segment ends."""
    while (
        source_start < source_end
        and target_start < target_end
        and source_lines[source_end - 1] == target_lines[target_end - 1]
    ):
        source_to_target[source_end - 1] = target_end
        target_to_source[target_end - 1] = source_end
        source_end -= 1
        target_end -= 1

    return source_end, target_end


def _align_segment(
    source_lines: Sequence[LineContent],
    target_lines: Sequence[LineContent],
    source_start: int,
    source_end: int,
    target_start: int,
    target_end: int,
    source_to_target: _IntVector,
    target_to_source: _IntVector,
    workspace: MatcherWorkspace,
) -> bool:
    """Conservatively align one source/target segment.

    Strategy:
    - map exact equal prefix
    - map exact equal suffix
    - use lines unique within both remaining segments as anchors
    - recurse between anchors
    - if no trustworthy anchors exist, leave the region unmapped

    This function is intentionally conservative. It never guesses inside
    structurally ambiguous regions.
    """
    occurrence_table: _LineOccurrenceTable
    candidate_pairs: MappedRecordVector
    anchors: MappedRecordVector
    previous_source: int
    previous_target: int
    anchor_source: int
    anchor_target: int

    source_start, target_start = _map_equal_prefix(
        source_lines,
        target_lines,
        source_start,
        source_end,
        target_start,
        target_end,
        source_to_target,
        target_to_source
    )

    source_end, target_end = _map_equal_suffix(
        source_lines,
        target_lines,
        source_start,
        source_end,
        target_start,
        target_end,
        source_to_target,
        target_to_source
    )

    if source_start >= source_end or target_start >= target_end:
        return False

    occurrence_table = _LineOccurrenceTable(
        workspace,
        source_lines,
        source_start,
        source_end,
    )
    try:
        occurrence_table.scan_source(source_start, source_end)
        occurrence_table.scan_target(target_lines, target_start, target_end)
        candidate_pairs = occurrence_table.emit_candidate_pairs(
            workspace,
            source_start,
            source_end,
        )
        has_common_lines = occurrence_table.has_common_lines
    finally:
        occurrence_table.close()

    try:
        anchors = _longest_increasing_subsequence_records(
            candidate_pairs,
            target_start,
            target_end,
            workspace,
        )
    finally:
        workspace.close_resource(candidate_pairs)

    if not anchors:
        workspace.close_resource(anchors)
        return has_common_lines

    previous_source = source_start
    previous_target = target_start

    try:
        for anchor_source, anchor_target in anchors:
            _align_segment(
                source_lines,
                target_lines,
                previous_source,
                anchor_source,
                previous_target,
                anchor_target,
                source_to_target,
                target_to_source,
                workspace,
            )

            source_to_target[anchor_source] = anchor_target + 1
            target_to_source[anchor_target] = anchor_source + 1

            previous_source = anchor_source + 1
            previous_target = anchor_target + 1

        _align_segment(
            source_lines,
            target_lines,
            previous_source,
            source_end,
            previous_target,
            target_end,
            source_to_target,
            target_to_source,
            workspace,
        )
    finally:
        workspace.close_resource(anchors)
    return has_common_lines


def _validated_anchor_pairs(
    source_lines: Sequence[LineContent],
    target_lines: Sequence[LineContent],
    anchor_pairs: Sequence[tuple[int, int]],
    workspace: MatcherWorkspace,
) -> MappedRecordVector:
    """Return sorted one-based anchor pairs after validating their alignment."""
    for pair in anchor_pairs:
        if (
            not isinstance(pair, tuple)
            or len(pair) != 2
            or type(pair[0]) is not int
            or type(pair[1]) is not int
        ):
            raise ValueError("line-matching anchors must contain integer line numbers")

    validated = workspace.record_vector(
        len(anchor_pairs),
        _LINE_PAIR_RECORD_FORMAT,
    )
    for source_line, target_line in anchor_pairs:
        if source_line < 1 or source_line > len(source_lines):
            raise ValueError("line-matching source anchor is out of range")
        if target_line < 1 or target_line > len(target_lines):
            raise ValueError("line-matching target anchor is out of range")
        if source_lines[source_line - 1] != target_lines[target_line - 1]:
            raise ValueError("line-matching anchor content differs")
        validated.append((source_line, target_line))

    sort_mapped_records(validated)
    previous_source_line = 0
    previous_target_line = 0
    previous_pair: tuple[int, int] | None = None
    validated_count = 0

    for pair_record in validated:
        source_line, target_line = pair_record
        pair = (source_line, target_line)
        if pair == previous_pair:
            continue
        if source_line <= previous_source_line or target_line <= previous_target_line:
            raise ValueError("line-matching anchors must be strictly increasing")

        previous_pair = pair
        previous_source_line = source_line
        previous_target_line = target_line
        validated[validated_count] = pair
        validated_count += 1

    validated.truncate(validated_count)
    return validated


def _align_segments_around_anchors(
    source_lines: Sequence[LineContent],
    target_lines: Sequence[LineContent],
    anchor_pairs: Iterable[tuple[int, int]],
    source_to_target: _IntVector,
    target_to_source: _IntVector,
    workspace: MatcherWorkspace,
) -> bool:
    """Align independent segments separated by trusted equal-line anchors."""
    source_start = 0
    target_start = 0
    may_have_unmapped_equal_lines = False

    for source_line, target_line in anchor_pairs:
        source_index = source_line - 1
        target_index = target_line - 1
        may_have_unmapped_equal_lines |= _align_segment(
            source_lines,
            target_lines,
            source_start,
            source_index,
            target_start,
            target_index,
            source_to_target,
            target_to_source,
            workspace,
        )
        source_to_target[source_index] = target_line
        target_to_source[target_index] = source_line
        source_start = source_index + 1
        target_start = target_index + 1

    may_have_unmapped_equal_lines |= _align_segment(
        source_lines,
        target_lines,
        source_start,
        len(source_lines),
        target_start,
        len(target_lines),
        source_to_target,
        target_to_source,
        workspace,
    )
    return may_have_unmapped_equal_lines


def match_acquirable_lines(
    source_lines: AcquirableLineSequence[LineContent],
    target_lines: AcquirableLineSequence[LineContent],
    *,
    anchor_pairs: Sequence[tuple[int, int]] = (),
    spool_dir: str | Path | None = None,
) -> _LineMapping:
    """Compute conservative structural alignment between source and target.

    This matcher is bytes-safe and ambiguity-intolerant.

    It only creates mappings when they are structurally trustworthy:
    - exact equal prefixes and suffixes
    - unique lines that occur exactly once in both corresponding segments
    - recursive alignment between trustworthy anchors

    Ambiguous regions are left unmapped.

    This is not trying to maximize matches. It is trying to avoid false matches.

    Args:
        source_lines: Batch source file lines.
        target_lines: Working tree file lines.
        anchor_pairs: Verified one-based equal-line pairs that divide matching.

    Returns:
        A bidirectional line mapping with ambiguous lines left unmapped.
    """
    source_to_target: MappedIntVector | None = None
    target_to_source: MappedIntVector | None = None
    workspace: MatcherWorkspace
    max_line_number: int
    source_line_count: int
    target_line_count: int

    source_line_count = len(source_lines)
    target_line_count = len(target_lines)
    max_line_number = max(source_line_count, target_line_count)
    try:
        source_to_target = _new_line_mapping(
            source_line_count,
            max_line_number,
            spool_dir=spool_dir,
        )
        target_to_source = _new_line_mapping(
            target_line_count,
            max_line_number,
            spool_dir=spool_dir,
        )
        with (
            MatcherWorkspace(spool_dir=spool_dir) as workspace,
            source_lines.acquire_lines() as acquired_source_lines,
            target_lines.acquire_lines() as acquired_target_lines,
        ):
            validated_anchor_pairs = _validated_anchor_pairs(
                acquired_source_lines,
                acquired_target_lines,
                anchor_pairs,
                workspace,
            )
            may_have_unmapped_equal_lines = _align_segments_around_anchors(
                acquired_source_lines,
                acquired_target_lines,
                (
                    (pair[0], pair[1])
                    for pair in validated_anchor_pairs
                ),
                source_to_target,
                target_to_source,
                workspace,
            )

        return _LineMapping(
            source_to_target=source_to_target,
            target_to_source=target_to_source,
            may_have_unmapped_equal_lines=may_have_unmapped_equal_lines,
        )
    except BaseException:
        if source_to_target is not None:
            source_to_target.close()
        if target_to_source is not None:
            target_to_source.close()
        raise


def match_lines(
    source_lines: Sequence[LineContent],
    target_lines: Sequence[LineContent],
    *,
    anchor_pairs: Sequence[tuple[int, int]] = (),
    spool_dir: str | Path | None = None,
) -> _LineMapping:
    """Compute conservative structural alignment between source and target."""
    return match_acquirable_lines(
        as_acquirable_line_sequence(source_lines),
        as_acquirable_line_sequence(target_lines),
        anchor_pairs=anchor_pairs,
        spool_dir=spool_dir,
    )
