"""Placement choices for missing presence-claim runs."""

from __future__ import annotations

from collections.abc import Collection, Mapping, Sequence
from dataclasses import dataclass
import hashlib
from pathlib import Path

from ...core.line_selection import LineSelection
from ..line_matching.line_mapping import LineMapping
from ..line_matching.line_range_view import LineRangeView
from ..line_matching.sequence_equality import line_slice_equals as _line_slice_matches
from ..line_matching.sequence_search import iter_exact_context_gaps
from .presence_context import (
    contextual_presence_ambiguities as _contextual_presence_ambiguities,
)
from .presence_missing_claims import (
    mapped_missing_source_lines as _mapped_missing_source_lines,
)


PRESENCE_AMBIGUITY_PREFIX = "presence:"


@dataclass(frozen=True)
class PresenceChoice:
    choice_index: int
    gap_index: int
    run_start: int
    run_end: int
    target_after_line: int | None
    target_before_line: int | None


def presence_resolution_decision(
    decisions: Mapping[str, object],
) -> tuple[str, int] | None:
    """Return the sole well-formed presence decision, if one exists."""
    selected_key: str | None = None
    selected_choice = 0
    for key, choice in decisions.items():
        if not isinstance(key, str) or not key.startswith(PRESENCE_AMBIGUITY_PREFIX):
            continue
        if selected_key is not None or type(choice) is not int or choice < 1:
            raise ValueError("invalid presence resolution")
        selected_key = key
        selected_choice = choice
    if selected_key is None:
        return None
    return selected_key, selected_choice


def presence_choices_for_missing_claimed_run(
    source_lines: Sequence[bytes],
    working_lines: Sequence[bytes],
    presence_line_set: LineSelection,
    mapping: LineMapping,
    *,
    max_results: int,
    trusted_source_lines: Collection[int] = (),
    require_distinctive_context: bool = False,
    distinctive_context_lines: LineSelection | None = None,
    spool_dir: str | Path | None = None,
) -> tuple[str | None, tuple["PresenceChoice", ...]]:
    missing_claimed = _mapped_missing_source_lines(
        presence_line_set,
        len(source_lines),
        mapping,
    )
    ranges = missing_claimed.ranges()
    if len(ranges) != 1:
        return None, ()

    run_start, run_end = ranges[0]
    adjacent_key, adjacent_choices = _adjacent_context_choices(
        source_lines,
        working_lines,
        mapping,
        run_start=run_start,
        run_end=run_end,
        max_results=max_results,
    )
    if adjacent_key is not None:
        return adjacent_key, adjacent_choices

    return _contextual_choices(
        source_lines,
        working_lines,
        presence_line_set,
        mapping,
        run_start=run_start,
        run_end=run_end,
        max_results=max_results,
        trusted_source_lines=trusted_source_lines,
        require_distinctive_context=require_distinctive_context,
        distinctive_context_lines=distinctive_context_lines,
        spool_dir=spool_dir,
    )


def _adjacent_context_choices(
    source_lines: Sequence[bytes],
    working_lines: Sequence[bytes],
    mapping: LineMapping,
    *,
    run_start: int,
    run_end: int,
    max_results: int,
) -> tuple[str | None, tuple[PresenceChoice, ...]]:
    """Return choices bracketed by immediately adjacent source lines."""
    before_source_line = run_start - 1
    after_source_line = run_end + 1
    if before_source_line < 1 or after_source_line > len(source_lines):
        return None, ()
    before_target_line = mapping.get_target_line_from_source_line(before_source_line)
    after_target_line = mapping.get_target_line_from_source_line(after_source_line)
    if before_target_line is None or after_target_line is None:
        return None, ()
    if before_target_line >= after_target_line:
        return None, ()

    left_context = (bytes(source_lines[before_source_line - 1]),)
    right_context = (bytes(source_lines[after_source_line - 1]),)
    claimed_run = LineRangeView(source_lines, run_start - 1, run_end)
    key = presence_ambiguity_key(
        run_start,
        run_end,
        claimed_run,
        before_source_line,
        after_source_line,
    )
    choices: list[PresenceChoice] = []
    for gap in iter_exact_context_gaps(
        working_lines,
        left_context=left_context,
        right_context=right_context,
        start_gap=before_target_line,
        end_gap=after_target_line - 1,
        max_results=max_results,
    ):
        if _line_slice_matches(working_lines, gap.gap_index, claimed_run):
            continue
        choices.append(
            PresenceChoice(
                choice_index=len(choices) + 1,
                gap_index=gap.gap_index,
                run_start=run_start,
                run_end=run_end,
                target_after_line=gap.target_after_line,
                target_before_line=gap.target_before_line,
            )
        )
    return key, tuple(choices)


def _contextual_choices(
    source_lines: Sequence[bytes],
    working_lines: Sequence[bytes],
    presence_line_set: LineSelection,
    mapping: LineMapping,
    *,
    run_start: int,
    run_end: int,
    max_results: int,
    trusted_source_lines: Collection[int],
    require_distinctive_context: bool,
    distinctive_context_lines: LineSelection | None,
    spool_dir: str | Path | None,
) -> tuple[str | None, tuple[PresenceChoice, ...]]:
    """Return reviewed gaps bounded by distinctive contextual anchors."""
    ambiguities = _contextual_presence_ambiguities(
        source_lines,
        working_lines,
        presence_line_set,
        mapping,
        trusted_source_lines=trusted_source_lines,
        require_distinctive_context=require_distinctive_context,
        distinctive_context_lines=distinctive_context_lines,
        spool_dir=spool_dir,
    )
    if len(ambiguities) != 1:
        return None, ()

    ambiguity = ambiguities[0]
    if ambiguity.run_start != run_start or ambiguity.run_end != run_end:
        return None, ()

    claimed_run = LineRangeView(source_lines, run_start - 1, run_end)
    before_source_line = ambiguity.before_source_line or 0
    after_source_line = ambiguity.after_source_line or len(source_lines) + 1
    key = presence_ambiguity_key(
        run_start,
        run_end,
        claimed_run,
        before_source_line,
        after_source_line,
    )
    choices: list[PresenceChoice] = []
    for gap_index in range(ambiguity.start_gap, ambiguity.end_gap + 1):
        if _line_slice_matches(working_lines, gap_index, claimed_run):
            continue
        choices.append(
            PresenceChoice(
                choice_index=len(choices) + 1,
                gap_index=gap_index,
                run_start=run_start,
                run_end=run_end,
                target_after_line=gap_index if gap_index > 0 else None,
                target_before_line=(
                    gap_index + 1 if gap_index < len(working_lines) else None
                ),
            )
        )
        if len(choices) >= max_results:
            break

    return key, tuple(choices)


def presence_ambiguity_key(
    run_start: int,
    run_end: int,
    claimed_run: Sequence[bytes],
    before_source_line: int,
    after_source_line: int,
) -> str:
    hasher = hashlib.sha256()
    for line in claimed_run:
        hasher.update(bytes(line))
    digest = hasher.hexdigest()[:12]
    return (
        f"{PRESENCE_AMBIGUITY_PREFIX}{run_start}-{run_end}:claimed:{digest}:"
        f"between:{before_source_line}-{after_source_line}"
    )


def presence_ambiguity_target_line_range(
    choices: Sequence[PresenceChoice],
    target_line_count: int,
) -> tuple[int, int] | None:
    """Return existing target lines spanning compatible insertion gaps."""
    if target_line_count == 0:
        return None

    positions = [choice.gap_index for choice in choices]
    start = max(1, min(positions))
    end = min(target_line_count, max(positions) + 1)
    if start > end:
        return None
    return start, end
