"""Stable fingerprints for operation candidate previews."""

from __future__ import annotations

import hashlib
import json
from typing import TYPE_CHECKING, TypedDict

from ..core.buffer import BufferInput, buffer_byte_chunks
from .ownership.absence_claims import AbsenceClaim
from .ownership.claims import PresenceClaim
from .ownership.model import BatchOwnership
from .ownership.references import BaselineReference
from .ownership.replacement_units import ReplacementUnit
from .state.metadata_types import BatchFileMetadataDict

if TYPE_CHECKING:
    from ..core.buffer import LineBuffer
    from ..core.replacement import ReplacementPayload
    from .operation_candidate_types import (
        CandidateOperation,
        CandidateTarget,
        TargetCandidatePreview,
    )


ALGORITHM_VERSION = 2


class _BaselineReferenceFingerprint(TypedDict):
    """Canonical fingerprint fields for a baseline boundary."""

    after_line: int | None
    after_content: str | None
    has_after_line: bool
    before_line: int | None
    before_content: str | None
    has_before_line: bool


class _AbsenceClaimFingerprint(TypedDict):
    """Canonical fingerprint fields for an absence claim."""

    anchor_line: int | None
    content: str
    line_count: int
    baseline_reference: _BaselineReferenceFingerprint | None


class _PresenceClaimFingerprint(TypedDict):
    """Canonical fingerprint fields for a presence claim."""

    source_lines: list[str]
    baseline_references: list[
        tuple[int, _BaselineReferenceFingerprint | None]
    ]


class _ReplacementOriginFingerprint(TypedDict):
    """Canonical fingerprint fields for a replacement origin."""

    old_start: int
    old_end: int
    new_start: int
    new_end: int
    baseline_reference: _BaselineReferenceFingerprint | None


class _ReplacementUnitFingerprint(TypedDict):
    """Canonical fingerprint fields for a replacement unit."""

    presence_lines: list[str | int]
    deletion_indices: list[int]
    origin: _ReplacementOriginFingerprint | None


def _hash_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def _buffer_fingerprint(buffer: BufferInput) -> str:
    digest = hashlib.sha256()
    for chunk in buffer_byte_chunks(buffer):
        digest.update(chunk)
    return digest.hexdigest()


def _json_fingerprint(payload: object) -> str:
    return hashlib.sha256(
        json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
    ).hexdigest()


def target_fingerprint(
    target: CandidateTarget,
    file_path: str,
    buffer: LineBuffer,
) -> str:
    return _json_fingerprint({
        "target": target,
        "file": file_path,
        "buffer": _buffer_fingerprint(buffer),
    })


def target_result_fingerprint(target: TargetCandidatePreview) -> str:
    return _json_fingerprint({
        "target": target.target,
        "file": target.file_path,
        "before": _buffer_fingerprint(target.before_buffer),
        "after": _buffer_fingerprint(target.after_buffer),
        "file_mode": target.file_mode,
        "change_type": target.change_type,
        "destination_exists": target.destination_exists,
    })


def _baseline_reference_payload(
    reference: BaselineReference | None,
) -> _BaselineReferenceFingerprint | None:
    if reference is None:
        return None
    return {
        "after_line": reference.after_line,
        "after_content": (
            None
            if reference.after_content is None
            else _hash_bytes(reference.after_content)
        ),
        "has_after_line": reference.has_after_line,
        "before_line": reference.before_line,
        "before_content": (
            None
            if reference.before_content is None
            else _hash_bytes(reference.before_content)
        ),
        "has_before_line": reference.has_before_line,
    }


def _absence_claim_payload(claim: AbsenceClaim) -> _AbsenceClaimFingerprint:
    return {
        "anchor_line": claim.anchor_line,
        "content": _buffer_fingerprint(claim.content_lines),
        "line_count": len(claim.content_lines),
        "baseline_reference": _baseline_reference_payload(claim.baseline_reference),
    }


def _presence_claim_payload(claim: PresenceClaim) -> _PresenceClaimFingerprint:
    return {
        "source_lines": claim.source_lines,
        "baseline_references": [
            (line, _baseline_reference_payload(reference))
            for line, reference in sorted(claim.baseline_references.items())
        ],
    }


def _replacement_unit_payload(
    unit: ReplacementUnit,
) -> _ReplacementUnitFingerprint:
    origin = unit.origin
    return {
        "presence_lines": list(unit.presence_lines),
        "deletion_indices": unit.deletion_indices,
        "origin": None if origin is None else {
            "old_start": origin.old_start,
            "old_end": origin.old_end,
            "new_start": origin.new_start,
            "new_end": origin.new_end,
            "baseline_reference": _baseline_reference_payload(
                origin.baseline_reference
            ),
        },
    }


def _ownership_fingerprint(ownership: BatchOwnership) -> str:
    return _json_fingerprint({
        "presence_claims": [
            _presence_claim_payload(claim)
            for claim in ownership.presence_claims
        ],
        "deletions": [
            _absence_claim_payload(claim)
            for claim in ownership.deletions
        ],
        "replacement_units": [
            _replacement_unit_payload(unit)
            for unit in ownership.replacement_units
        ],
    })


def candidate_id(
    *,
    operation: CandidateOperation,
    batch_name: str,
    file_path: str,
    scope_fingerprint: str,
    batch_fingerprint: str,
    target_fingerprints: dict[str, str],
    target_result_fingerprints: dict[str, str],
    targets: tuple[TargetCandidatePreview, ...],
) -> str:
    payload = {
        "algorithm_version": ALGORITHM_VERSION,
        "operation": operation,
        "batch_name": batch_name,
        "file": file_path,
        "scope": scope_fingerprint,
        "batch_fingerprint": batch_fingerprint,
        "targets": target_fingerprints,
        "target_results": target_result_fingerprints,
        "decisions": [
            [
                target.target,
                None
                if target.resolution is None
                else sorted(target.resolution.decisions.items()),
            ]
            for target in targets
        ],
    }
    return hashlib.sha256(
        json.dumps(payload, sort_keys=True).encode()
    ).hexdigest()[:12]


def scope_fingerprint(
    *,
    operation: CandidateOperation,
    batch_name: str,
    file_path: str,
    selection_ids: set[int] | None,
    replacement_payload: ReplacementPayload | None = None,
) -> str:
    payload = {
        "operation": operation,
        "batch": batch_name,
        "file": file_path,
        "selection_ids": sorted(selection_ids) if selection_ids is not None else None,
        "replacement": (
            _hash_bytes(replacement_payload.data)
            if replacement_payload is not None
            else None
        ),
    }
    return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()


def batch_fingerprint(
    *,
    batch_name: str,
    file_path: str,
    source_buffer: LineBuffer,
    ownership: BatchOwnership,
    batch_source_commit: str,
    file_meta: BatchFileMetadataDict,
) -> str:
    return _json_fingerprint({
        "algorithm_version": ALGORITHM_VERSION,
        "batch_name": batch_name,
        "file": file_path,
        "batch_source_commit": batch_source_commit,
        "source": _buffer_fingerprint(source_buffer),
        "file_metadata": file_meta,
        "ownership": _ownership_fingerprint(ownership),
    })
