from __future__ import annotations

import re
from dataclasses import dataclass

from lexical_alignment import normalize_word


WORD_RE = re.compile(r"[^\W_]+(?:['’\-][^\W_]+)*", re.UNICODE)
HYPHEN_RE = re.compile(r"[-‐‑‒–—]")
SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+")
CLAUSE_SPLIT_RE = re.compile(r"(?<=[,;:])\s+")
TIMING_LINE_RE = re.compile(
    r"^\s*(?:\d{1,2}:)?\d{1,2}:\d{2}[,.]\d{3}\s*-->\s*"
    r"(?:\d{1,2}:)?\d{1,2}:\d{2}[,.]\d{3}(?:\s+.*)?$"
)
LEADING_TIMESTAMP_RE = re.compile(
    r"^\s*\[?(?:\d{1,2}:)?\d{1,2}:\d{2}(?:[,.]\d{1,3})?\]?\s+"
)
SPEAKER_PREFIX_RE = re.compile(
    r"^\s*((?:speaker|sprecher(?:in)?)(?:\s+[\w.-]+)?)\s*:\s*(.+)$",
    re.IGNORECASE,
)
VTT_VOICE_RE = re.compile(r"^\s*<v(?:\s+([^>]+))?>(.*)$", re.IGNORECASE)


@dataclass(frozen=True)
class TargetUnit:
    unit_id: str
    parent_ids: tuple[str, ...]
    raw_text: str
    spoken_text: str
    normalized: tuple[str, ...]
    speaker_label: str
    metadata_types: tuple[str, ...]


def _normalized_words(text: str) -> tuple[str, ...]:
    spoken = HYPHEN_RE.sub(" ", text)
    return tuple(
        word
        for word in (
            normalize_word(match.group(0)) for match in WORD_RE.finditer(spoken)
        )
        if word
    )


def _split_spoken_text(text: str, *, maximum_tokens: int = 40) -> list[str]:
    sentences = [part.strip() for part in SENTENCE_SPLIT_RE.split(text) if part.strip()]
    output: list[str] = []
    for sentence in sentences:
        if len(_normalized_words(sentence)) <= maximum_tokens:
            output.append(sentence)
            continue
        clauses = [part.strip() for part in CLAUSE_SPLIT_RE.split(sentence) if part.strip()]
        current: list[str] = []
        current_count = 0
        for clause in clauses:
            clause_count = len(_normalized_words(clause))
            if current and current_count + clause_count > maximum_tokens:
                output.append(" ".join(current))
                current = []
                current_count = 0
            current.append(clause)
            current_count += clause_count
        if current:
            output.append(" ".join(current))
    return output


def parse_target_text(text: str) -> tuple[list[TargetUnit], dict]:
    raw = text.replace("\r\n", "\n").replace("\r", "\n").lstrip("\ufeff")
    if not raw.strip():
        raise ValueError("Target script or paper edit is empty.")
    lines = raw.split("\n")
    metadata_counts = {
        "WEBVTT_HEADER": 0,
        "CUE_INDEX": 0,
        "TIMING": 0,
        "LEADING_TIMESTAMP": 0,
        "SPEAKER_PREFIX": 0,
        "VTT_VOICE": 0,
    }
    is_webvtt = bool(lines and lines[0].strip().casefold() == "webvtt")
    metadata_counts["VTT_CUE_IDENTIFIER"] = 0
    if is_webvtt:
        metadata_counts["WEBVTT_HEADER"] += 1
        lines = lines[1:]

    records: list[tuple[str, str, str, tuple[str, ...]]] = []
    pending_timing = False
    position = 0
    while position < len(lines):
        line = lines[position]
        stripped = line.strip()
        if not stripped:
            position += 1
            continue
        next_nonempty = ""
        for following in lines[position + 1 :]:
            if following.strip():
                next_nonempty = following.strip()
                break
        if stripped.isdigit() and TIMING_LINE_RE.fullmatch(next_nonempty):
            metadata_counts["CUE_INDEX"] += 1
            position += 1
            continue
        if is_webvtt and TIMING_LINE_RE.fullmatch(next_nonempty):
            metadata_counts["VTT_CUE_IDENTIFIER"] += 1
            position += 1
            continue
        if TIMING_LINE_RE.fullmatch(stripped):
            metadata_counts["TIMING"] += 1
            pending_timing = True
            position += 1
            continue

        metadata: list[str] = []
        spoken = stripped
        timestamp_match = LEADING_TIMESTAMP_RE.match(spoken)
        if timestamp_match:
            spoken = spoken[timestamp_match.end() :].strip()
            metadata_counts["LEADING_TIMESTAMP"] += 1
            metadata.append("LEADING_TIMESTAMP")
        if pending_timing:
            metadata.append("TIMING")
            pending_timing = False

        speaker = ""
        voice_match = VTT_VOICE_RE.match(spoken)
        if voice_match:
            speaker = (voice_match.group(1) or "").strip()
            spoken = voice_match.group(2).strip()
            metadata_counts["VTT_VOICE"] += 1
            metadata.append("VTT_VOICE")
        else:
            speaker_match = SPEAKER_PREFIX_RE.match(spoken)
            if speaker_match:
                speaker = speaker_match.group(1).strip()
                spoken = speaker_match.group(2).strip()
                metadata_counts["SPEAKER_PREFIX"] += 1
                metadata.append("SPEAKER_PREFIX")

        # Unknown colon/bracket constructs remain in spoken text by design.
        if spoken and _normalized_words(spoken):
            records.append((stripped, spoken, speaker, tuple(metadata)))
        position += 1

    if not records:
        raise ValueError("Target text contains no spoken words after recognized metadata.")

    units: list[TargetUnit] = []
    for parent_index, (raw_line, spoken, speaker, metadata) in enumerate(records, 1):
        parent_id = f"target.u{parent_index:04d}"
        parts = _split_spoken_text(spoken)
        for part_index, part in enumerate(parts, 1):
            unit_id = parent_id if len(parts) == 1 else f"{parent_id}.p{part_index:02d}"
            units.append(
                TargetUnit(
                    unit_id=unit_id,
                    parent_ids=(parent_id,),
                    raw_text=raw_line,
                    spoken_text=part,
                    normalized=_normalized_words(part),
                    speaker_label=speaker,
                    metadata_types=metadata,
                )
            )
    return units, {
        "inputLineCount": len(raw.splitlines()),
        "parentUnitCount": len(records),
        "targetUnitCount": len(units),
        "recognizedMetadata": {
            key: value for key, value in metadata_counts.items() if value
        },
    }
