"""Operation-level candidate planning and state for batch apply/include."""

from __future__ import annotations

from itertools import product
from pathlib import Path

from ..core.buffer import LineBuffer
from ..core.replacement import ReplacementPayload
from ..core.text_lifecycle import (
    TextFileChangeType,
    selected_text_target_change_type,
)
from ..exceptions import MergeError
from ..i18n import _
from .merge.merge import (
    enumerate_merge_batch_candidates_from_line_sequences,
    merge_batch_from_line_sequences_as_buffer,
)
from .merge.candidates import (
    MergeCandidate,
    MergeCandidateSetOutcome,
)
from .ownership.model import BatchOwnership
from .state.metadata_types import BatchFileMetadataDict
from .operation_candidate_types import (
    CandidateEnumerationLimitError as _CandidateEnumerationLimitError,
    CandidateTarget as _CandidateTarget,
    MAX_OPERATION_CANDIDATES as _MAX_OPERATION_CANDIDATES,
    OperationCandidatePreview as _OperationCandidatePreview,
    TargetCandidatePreview as _TargetCandidatePreview,
)
from .operation_candidate_fingerprints import (
    batch_fingerprint as _fingerprint_batch,
    candidate_id as _fingerprint_candidate_id,
    scope_fingerprint as _fingerprint_scope,
    target_fingerprint as _fingerprint_target,
    target_result_fingerprint as _fingerprint_target_result,
)


def _merge_candidates_or_unambiguous(
    source_lines: LineBuffer,
    ownership: BatchOwnership,
    target_lines: LineBuffer,
    *,
    spool_dir: str | Path | None = None,
) -> tuple[MergeCandidate, ...] | None:
    candidate_set = enumerate_merge_batch_candidates_from_line_sequences(
        source_lines,
        ownership,
        target_lines,
        max_candidates=_MAX_OPERATION_CANDIDATES,
        spool_dir=spool_dir,
    )
    if candidate_set.outcome is MergeCandidateSetOutcome.REVIEW_REQUIRED:
        return candidate_set.candidates
    if (
        candidate_set.outcome
        is MergeCandidateSetOutcome.ORDINARY_MERGE_SUCCEEDED
    ):
        return None
    raise MergeError(_("Batch was created from a different version of the file"))


def _materialize_target_candidate(
    *,
    target: _CandidateTarget,
    file_path: str,
    source_lines: LineBuffer,
    ownership: BatchOwnership,
    before_lines: LineBuffer,
    candidate: MergeCandidate | None,
    file_mode: str | None,
    text_change_type: str | TextFileChangeType,
    destination_exists: bool,
    selected_ids: set[int] | None,
    spool_dir: str | Path | None = None,
) -> _TargetCandidatePreview:
    before_copy = (
        before_lines.clone()
        if spool_dir is None
        else before_lines.clone(spool_dir=spool_dir)
    )
    try:
        after = merge_batch_from_line_sequences_as_buffer(
            source_lines,
            ownership,
            before_lines,
            resolution=None if candidate is None else candidate.resolution,
            spool_dir=spool_dir,
        )
    except BaseException:
        before_copy.close()
        raise
    try:
        return _TargetCandidatePreview(
            target=target,
            file_path=file_path,
            before_buffer=before_copy,
            after_buffer=after,
            file_mode=file_mode,
            change_type=selected_text_target_change_type(
                text_change_type,
                selected_ids,
                after,
            ).value,
            destination_exists=destination_exists,
            resolution=None if candidate is None else candidate.resolution,
            resolution_ordinal=1 if candidate is None else candidate.ordinal,
            resolution_count=1 if candidate is None else candidate.count,
            summary="unambiguous" if candidate is None else candidate.summary,
            explanation="" if candidate is None else candidate.explanation,
            ambiguity_target_line_range=(
                None
                if candidate is None
                else candidate.ambiguity_target_line_range
            ),
        )
    except BaseException:
        before_copy.close()
        after.close()
        raise


def build_apply_candidate_previews(
    *,
    batch_name: str,
    file_path: str,
    source_lines: LineBuffer,
    ownership: BatchOwnership,
    worktree_lines: LineBuffer,
    batch_source_commit: str,
    file_meta: BatchFileMetadataDict,
    text_change_type: str | TextFileChangeType,
    worktree_file_mode: str | None,
    worktree_exists: bool,
    selected_ids: set[int] | None,
    selection_ids: set[int] | None,
    spool_dir: str | Path | None = None,
) -> tuple[_OperationCandidatePreview, ...]:
    """Return apply candidates for a single file, or an empty tuple."""
    merge_candidates = _merge_candidates_or_unambiguous(
        source_lines,
        ownership,
        worktree_lines,
        spool_dir=spool_dir,
    )
    if merge_candidates is None:
        return ()

    batch_fingerprint = _fingerprint_batch(
        batch_name=batch_name,
        file_path=file_path,
        source_buffer=source_lines,
        ownership=ownership,
        batch_source_commit=batch_source_commit,
        file_meta=file_meta,
    )
    scope_fingerprint = _fingerprint_scope(
        operation="apply",
        batch_name=batch_name,
        file_path=file_path,
        selection_ids=selection_ids,
    )
    count = len(merge_candidates)
    previews: list[_OperationCandidatePreview] = []
    try:
        for ordinal, merge_candidate in enumerate(
            merge_candidates,
            start=1,
        ):
            target_preview = _materialize_target_candidate(
                target="worktree",
                file_path=file_path,
                source_lines=source_lines,
                ownership=ownership,
                before_lines=worktree_lines,
                candidate=merge_candidate,
                file_mode=worktree_file_mode,
                text_change_type=text_change_type,
                destination_exists=worktree_exists,
                selected_ids=selected_ids,
                spool_dir=spool_dir,
            )
            try:
                target_fingerprints = {
                    "worktree": _fingerprint_target(
                        "worktree",
                        file_path,
                        target_preview.before_buffer,
                    )
                }
                target_result_fingerprints = {
                    "worktree": _fingerprint_target_result(target_preview)
                }
                targets = (target_preview,)
                preview = _OperationCandidatePreview(
                    operation="apply",
                    batch_name=batch_name,
                    file_path=file_path,
                    ordinal=ordinal,
                    count=count,
                    candidate_id="",
                    targets=targets,
                    batch_fingerprint=batch_fingerprint,
                    target_fingerprints=target_fingerprints,
                    target_result_fingerprints=(
                        target_result_fingerprints
                    ),
                    scope_fingerprint=scope_fingerprint,
                )
                preview.candidate_id = _fingerprint_candidate_id(
                    operation="apply",
                    batch_name=batch_name,
                    file_path=file_path,
                    scope_fingerprint=scope_fingerprint,
                    batch_fingerprint=batch_fingerprint,
                    target_fingerprints=target_fingerprints,
                    target_result_fingerprints=(
                        target_result_fingerprints
                    ),
                    targets=targets,
                )
                previews.append(preview)
            except BaseException:
                target_preview.close()
                raise
    except BaseException:
        for preview in previews:
            preview.close()
        raise
    return tuple(previews)


def build_include_candidate_previews(
    *,
    batch_name: str,
    file_path: str,
    source_lines: LineBuffer,
    ownership: BatchOwnership,
    index_lines: LineBuffer,
    worktree_lines: LineBuffer,
    batch_source_commit: str,
    file_meta: BatchFileMetadataDict,
    text_change_type: str | TextFileChangeType,
    index_file_mode: str | None,
    worktree_file_mode: str | None,
    index_exists: bool,
    worktree_exists: bool,
    selected_ids: set[int] | None,
    selection_ids: set[int] | None,
    replacement_payload: ReplacementPayload | None = None,
    spool_dir: str | Path | None = None,
) -> tuple[_OperationCandidatePreview, ...]:
    """Return include candidates for a single file, or an empty tuple."""
    index_candidates = _merge_candidates_or_unambiguous(
        source_lines,
        ownership,
        index_lines,
        spool_dir=spool_dir,
    )
    worktree_candidates = _merge_candidates_or_unambiguous(
        source_lines,
        ownership,
        worktree_lines,
        spool_dir=spool_dir,
    )
    if index_candidates is None and worktree_candidates is None:
        return ()

    index_choices = (
        (None,) if index_candidates is None else index_candidates
    )
    worktree_choices = (
        (None,) if worktree_candidates is None else worktree_candidates
    )
    product_count = len(index_choices) * len(worktree_choices)
    if product_count > _MAX_OPERATION_CANDIDATES:
        raise _CandidateEnumerationLimitError(
            _("too many include candidates to preview safely")
        )

    batch_fingerprint = _fingerprint_batch(
        batch_name=batch_name,
        file_path=file_path,
        source_buffer=source_lines,
        ownership=ownership,
        batch_source_commit=batch_source_commit,
        file_meta=file_meta,
    )
    scope_fingerprint = _fingerprint_scope(
        operation="include",
        batch_name=batch_name,
        file_path=file_path,
        selection_ids=selection_ids,
        replacement_payload=replacement_payload,
    )
    count = product_count
    previews: list[_OperationCandidatePreview] = []
    try:
        for ordinal, (
            index_candidate,
            worktree_candidate,
        ) in enumerate(
            product(index_choices, worktree_choices),
            start=1,
        ):
            index_preview = _materialize_target_candidate(
                target="index",
                file_path=file_path,
                source_lines=source_lines,
                ownership=ownership,
                before_lines=index_lines,
                candidate=index_candidate,
                file_mode=index_file_mode,
                text_change_type=text_change_type,
                destination_exists=index_exists,
                selected_ids=selected_ids,
                spool_dir=spool_dir,
            )
            try:
                worktree_preview = _materialize_target_candidate(
                    target="worktree",
                    file_path=file_path,
                    source_lines=source_lines,
                    ownership=ownership,
                    before_lines=worktree_lines,
                    candidate=worktree_candidate,
                    file_mode=worktree_file_mode,
                    text_change_type=text_change_type,
                    destination_exists=worktree_exists,
                    selected_ids=selected_ids,
                    spool_dir=spool_dir,
                )
            except BaseException:
                index_preview.close()
                raise
            try:
                targets = (index_preview, worktree_preview)
                target_fingerprints = {
                    "index": _fingerprint_target(
                        "index",
                        file_path,
                        index_preview.before_buffer,
                    ),
                    "worktree": _fingerprint_target(
                        "worktree",
                        file_path,
                        worktree_preview.before_buffer,
                    ),
                }
                target_result_fingerprints = {
                    "index": _fingerprint_target_result(index_preview),
                    "worktree": _fingerprint_target_result(
                        worktree_preview
                    ),
                }
                preview = _OperationCandidatePreview(
                    operation="include",
                    batch_name=batch_name,
                    file_path=file_path,
                    ordinal=ordinal,
                    count=count,
                    candidate_id="",
                    targets=targets,
                    batch_fingerprint=batch_fingerprint,
                    target_fingerprints=target_fingerprints,
                    target_result_fingerprints=(
                        target_result_fingerprints
                    ),
                    scope_fingerprint=scope_fingerprint,
                )
                preview.candidate_id = _fingerprint_candidate_id(
                    operation="include",
                    batch_name=batch_name,
                    file_path=file_path,
                    scope_fingerprint=scope_fingerprint,
                    batch_fingerprint=batch_fingerprint,
                    target_fingerprints=target_fingerprints,
                    target_result_fingerprints=(
                        target_result_fingerprints
                    ),
                    targets=targets,
                )
                previews.append(preview)
            except BaseException:
                index_preview.close()
                worktree_preview.close()
                raise
    except BaseException:
        for preview in previews:
            preview.close()
        raise
    return tuple(previews)
