from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass
from difflib import SequenceMatcher
from typing import Iterable, Sequence


WORD_RE = re.compile(r"[^\W_]+(?:['’\-][^\W_]+)*", re.UNICODE)


@dataclass(frozen=True)
class AlignmentMetrics:
    matched_tokens: int
    token_similarity: float
    character_similarity: float
    reference_coverage: float
    occurrence_coverage: float
    reference_start: int
    occurrence_start: int
    anchor_size: int
    opcodes: tuple[tuple[str, int, int, int, int], ...]


@dataclass(frozen=True)
class WindowAlignment:
    start: int
    end: int
    metrics: AlignmentMetrics


def normalize_word(text: str) -> str:
    """Normalize one spoken token without assigning product match semantics."""
    normalized = unicodedata.normalize("NFKC", text).casefold().replace("’", "'")
    return "".join(WORD_RE.findall(normalized))


def measure_alignment(
    reference: Sequence[str], occurrence: Sequence[str]
) -> AlignmentMetrics:
    """Return raw lexical measurements; callers own thresholds and classes."""
    reference_tuple = tuple(reference)
    occurrence_tuple = tuple(occurrence)
    matcher = SequenceMatcher(
        a=reference_tuple,
        b=occurrence_tuple,
        autojunk=False,
    )
    blocks = [block for block in matcher.get_matching_blocks() if block.size]
    matched = sum(block.size for block in blocks)
    longest = max(
        blocks,
        key=lambda block: (block.size, -block.a, -block.b),
        default=None,
    )
    return AlignmentMetrics(
        matched_tokens=matched,
        token_similarity=matcher.ratio(),
        character_similarity=SequenceMatcher(
            a=" ".join(reference_tuple),
            b=" ".join(occurrence_tuple),
            autojunk=False,
        ).ratio(),
        reference_coverage=matched / max(len(reference_tuple), 1),
        occurrence_coverage=matched / max(len(occurrence_tuple), 1),
        reference_start=longest.a if longest else 0,
        occurrence_start=longest.b if longest else 0,
        anchor_size=longest.size if longest else 0,
        opcodes=tuple(matcher.get_opcodes()),
    )


def build_informative_index(
    sequences: Iterable[Sequence[str]], low_information_words: set[str]
) -> dict[str, set[int]]:
    index: dict[str, set[int]] = {}
    for sequence_index, words in enumerate(sequences):
        for word in set(words):
            if word not in low_information_words:
                index.setdefault(word, set()).add(sequence_index)
    return index


def anchored_candidate_indices(
    words: Sequence[str],
    index: dict[str, set[int]],
    low_information_words: set[str],
    *,
    maximum_anchor_words: int = 64,
) -> set[int]:
    anchors = sorted(
        {
            word
            for word in words
            if word not in low_information_words and word in index
        },
        key=lambda word: (len(index[word]), word),
    )
    if not anchors:
        return set()
    rarest_frequency = len(index[anchors[0]])
    maximum_frequency = max(8, rarest_frequency * 4)
    useful_anchors = [
        word for word in anchors if len(index[word]) <= maximum_frequency
    ]
    candidates: set[int] = set()
    for word in useful_anchors[:maximum_anchor_words]:
        candidates.update(index[word])
    return candidates


def local_window_alignments(
    reference: Sequence[str],
    occurrence: Sequence[str],
    low_information_words: set[str],
    *,
    minimum_window_tokens: int = 4,
) -> list[WindowAlignment]:
    """Generate bounded value-only local windows without accepting any match."""
    reference_tuple = tuple(reference)
    occurrence_tuple = tuple(occurrence)
    if not reference_tuple or not occurrence_tuple:
        return []
    maximum_length = min(
        len(occurrence_tuple),
        max(
            len(reference_tuple) * 2,
            len(reference_tuple) + 12,
            minimum_window_tokens,
        ),
    )
    candidate_lengths = {
        minimum_window_tokens,
        *(
            max(minimum_window_tokens, len(reference_tuple) + delta)
            for delta in range(-2, 3)
        ),
        *(
            max(minimum_window_tokens, round(len(reference_tuple) * factor))
            for factor in (0.4, 0.6, 0.75, 1.25, 1.5, 2.0)
        ),
    }
    candidate_lengths = {
        length for length in candidate_lengths if length <= maximum_length
    }
    reference_positions: dict[str, list[int]] = {}
    for index, word in enumerate(reference_tuple):
        if word not in low_information_words:
            reference_positions.setdefault(word, []).append(index)
    candidate_starts = {0}
    for occurrence_index, word in enumerate(occurrence_tuple):
        for reference_index in reference_positions.get(word, []):
            for base in (occurrence_index - reference_index, occurrence_index):
                candidate_starts.update(
                    start
                    for start in range(base - 2, base + 3)
                    if 0 <= start < len(occurrence_tuple)
                )
    windows: list[WindowAlignment] = []
    for start in sorted(candidate_starts):
        ends = {
            start + length
            for length in candidate_lengths
            if start + length <= len(occurrence_tuple)
        }
        if start == 0:
            ends.add(len(occurrence_tuple))
        for end in sorted(ends):
            windows.append(
                WindowAlignment(
                    start=start,
                    end=end,
                    metrics=measure_alignment(
                        reference_tuple,
                        occurrence_tuple[start:end],
                    ),
                )
            )
    return windows


def intervals_overlap(
    first_start: int,
    first_end: int,
    second_start: int,
    second_end: int,
) -> bool:
    return min(first_end, second_end) > max(first_start, second_start)
