"""Review-aware batch file selection translation."""

from __future__ import annotations

from collections.abc import Iterable, Iterator, Mapping
from dataclasses import dataclass, replace
from typing import TYPE_CHECKING

from ...batch.file_display import render_batch_file_display
from ...batch.selection import require_single_file_context_for_line_selection_ranges
from ...batch.state.metadata_types import BatchFileMetadataDict
from ...batch.submodule_pointer import (
    is_batch_submodule_pointer,
    refuse_batch_submodule_pointer_lines,
)
from ...core.line_selection import LineRangeBuilder, LineRanges
from ...exceptions import CommandError
from ...git_paths import display_path
from ...i18n import _, pgettext
from ..batch_file_scope import resolve_batch_file_scope
from .batch_selection_freshness import fresh_batch_review_selections_for_action
from .records import FileReviewAction
from .selection_validation import validate_review_scoped_line_selection

if TYPE_CHECKING:
    from ...core.models import RenderedBatchDisplay


@dataclass(frozen=True)
class _SelectionForValidation:
    display_ids: tuple[int, ...]
    is_splittable: bool = False


def _current_action_selections_for_validation(
    rendered: "RenderedBatchDisplay",
    action: "FileReviewAction | str",
) -> list[_SelectionForValidation]:
    review_action = FileReviewAction(action).value
    return [
        _SelectionForValidation(
            display_ids=group.display_ids,
            is_splittable=group.reason in {"replacement", "structural-run"},
        )
        for group in rendered.review_action_groups
        if group.display_ids and review_action in group.actions
    ]


def _selection_ids_from_gutter_ids(
    selected_ids: Iterable[int],
    display_id_map: Mapping[int, int],
) -> Iterator[int]:
    for gutter_id in selected_ids:
        if gutter_id in display_id_map:
            yield display_id_map[gutter_id]
        else:
            raise CommandError(
                _(
                    "Line ID {id} is not available for this action. Select one of the numbered lines shown for this batch file."
                ).format(id=gutter_id)
            )


def _complete_mixed_review_composite_dependencies(
    selection_ids: set[int],
    rendered: "RenderedBatchDisplay",
    action: "FileReviewAction | str",
) -> set[int]:
    """Complete the former recommendation around mixed-action composites.

    A visual replacement can contain one independently mergeable child and a
    reset-only sibling.  Once the whole replacement becomes provably
    mergeable, a fresh review exposes the composite too, but commands copied
    from the earlier review omit that entire visual change.  Preserve that
    generated command's meaning only when the request selects every other
    action-capable ID; ordinary partial selections are unchanged.
    """
    review_action = FileReviewAction(action).value
    reset_action = FileReviewAction.RESET_FROM_BATCH.value
    reset_only_builder = LineRangeBuilder()
    action_builder = LineRangeBuilder()
    for group in rendered.review_action_groups:
        if reset_action in group.actions and review_action not in group.actions:
            for selection_id in group.selection_ids:
                reset_only_builder.add_line(selection_id)
        if review_action in group.actions:
            for selection_id in group.selection_ids:
                action_builder.add_line(selection_id)

    reset_only_ids = reset_only_builder.finish()
    if not reset_only_ids:
        return selection_ids

    action_ids = action_builder.finish()
    composite_dependency_ranges = LineRangeBuilder()
    for group in rendered.review_action_groups:
        if review_action not in group.actions:
            continue
        group_id_builder = LineRangeBuilder()
        for selection_id in group.selection_ids:
            group_id_builder.add_line(selection_id)
        group_ids = group_id_builder.finish()
        if not any(
            reset_only_ids.intersects_range(range_start, range_end)
            for range_start, range_end in group_ids.ranges()
        ):
            continue
        for range_start, range_end in group_ids.ranges():
            composite_dependency_ranges.add_range(range_start, range_end)
    composite_dependency_ids = composite_dependency_ranges.finish()
    if not composite_dependency_ids:
        return selection_ids

    former_recommendation = action_ids.difference(composite_dependency_ids)
    if len(selection_ids) != len(former_recommendation) or any(
        selection_id not in former_recommendation for selection_id in selection_ids
    ):
        return selection_ids
    return set(former_recommendation.union(composite_dependency_ids))


def translate_batch_file_gutter_ids_to_selection_ids(
    batch_name: str,
    file_path: str,
    selected_ids: set[int] | None,
    action: "FileReviewAction | str",
) -> tuple[set[int] | None, "RenderedBatchDisplay | None"]:
    """Translate displayed batch-file gutter IDs to internal selection IDs.

    If the IDs came after a fresh matching file review, validate them against
    the complete actions shown by that review before consulting the full batch
    display. Without a matching review, validate against the current review
    action groups so reset-only review IDs cannot be reinterpreted as compact
    mergeable IDs.
    """
    if selected_ids is None:
        return None, None

    review_selections = fresh_batch_review_selections_for_action(
        batch_name,
        file_path,
        action,
    )
    rendered = render_batch_file_display(batch_name, file_path)
    if rendered is None:
        return selected_ids, None

    selection_ids: set[int] | None = None
    if review_selections is not None:
        display_id_map = (
            rendered.review_gutter_to_selection_id or rendered.gutter_to_selection_id
        )
        selection_ids = set(
            _selection_ids_from_gutter_ids(selected_ids, display_id_map)
        )
        validate_review_scoped_line_selection(selected_ids, review_selections)
    else:
        current_action_selections = _current_action_selections_for_validation(
            rendered,
            action,
        )
        if current_action_selections:
            display_id_map = (
                rendered.review_gutter_to_selection_id
                or rendered.gutter_to_selection_id
            )
            selection_ids = set(
                _selection_ids_from_gutter_ids(selected_ids, display_id_map)
            )
            validate_review_scoped_line_selection(
                selected_ids,
                current_action_selections,
            )
        else:
            display_id_map = rendered.gutter_to_selection_id

    rendered_for_messages = (
        replace(
            rendered,
            gutter_to_selection_id=dict(display_id_map),
            selection_id_to_gutter={
                selection_id: gutter_id
                for gutter_id, selection_id in display_id_map.items()
            },
        )
        if review_selections is not None
        else rendered
    )
    if selection_ids is None:
        selection_ids = set(
            _selection_ids_from_gutter_ids(selected_ids, display_id_map)
        )
    if review_selections is not None:
        selection_ids = _complete_mixed_review_composite_dependencies(
            selection_ids,
            rendered,
            action,
        )
    return selection_ids, rendered_for_messages


def translate_reset_batch_file_gutter_ids_to_selection_ranges(
    batch_name: str,
    all_files: dict[str, BatchFileMetadataDict],
    file: str | None,
    patterns: list[str] | None,
    line_id_specification: str,
) -> LineRanges:
    """Translate fresh reset file-review gutter IDs to batch selection IDs.

    Reset is a metadata operation, so explicit reset line IDs must keep working
    even when a batch change is not currently mergeable into the worktree. Only
    translate through the mergeability-filtered gutter map when a fresh batch
    file review is in scope; otherwise leave the batch display IDs untouched.
    """
    files = resolve_batch_file_scope(batch_name, all_files, file, patterns)
    selected_ids = require_single_file_context_for_line_selection_ranges(
        batch_name,
        files,
        line_id_specification,
        pgettext("line-level operation noun", "reset"),
    )
    if selected_ids is None:
        return LineRanges.empty()

    file_path = list(files.keys())[0]
    if files[file_path].get("file_type") == "binary":
        raise CommandError(
            _("Cannot use --lines with binary files. Reset the whole file instead.")
        )
    if files[file_path].get("file_type") == "mode":
        raise CommandError(_("Cannot use --lines with file mode actions."))
    if is_batch_submodule_pointer(files[file_path]):
        refuse_batch_submodule_pointer_lines()

    review_selections = fresh_batch_review_selections_for_action(
        batch_name,
        file_path,
        FileReviewAction.RESET_FROM_BATCH,
    )
    if review_selections is None:
        return selected_ids
    validate_review_scoped_line_selection(selected_ids, review_selections)

    rendered = render_batch_file_display(batch_name, file_path)
    if rendered is None:
        raise CommandError(
            _("No changes for file '{file}' in batch '{name}'.").format(
                file=display_path(file_path),
                name=batch_name,
            )
        )

    display_id_map = (
        rendered.review_gutter_to_selection_id or rendered.gutter_to_selection_id
    )
    return LineRanges.from_lines(
        _selection_ids_from_gutter_ids(selected_ids, display_id_map)
    )
