from __future__ import annotations
import math
import re
import uuid
from ..models import Fact, Citation, ClaudeFact


def _normalize_whitespace(text: str) -> str:
    return re.sub(r"\s+", " ", text).strip()


def _fuzzy_substring_match(
    normalized_raw: str, normalized_quote: str
) -> tuple[int, int] | None:
    min_length = max(60, math.ceil(len(normalized_quote) * 0.7))
    if min_length > len(normalized_quote):
        return None

    lower_raw = normalized_raw.lower()
    for length in range(len(normalized_quote), min_length - 1, -1):
        idx = lower_raw.find(normalized_quote[:length].lower())
        if idx >= 0:
            return (idx, length)

    return None


def _map_normalized_index_to_raw(raw_text: str, norm_idx: int) -> int:
    norm_pos = 0
    raw_pos = 0
    raw_len = len(raw_text)

    while raw_pos < raw_len and raw_text[raw_pos].isspace():
        raw_pos += 1

    while norm_pos < norm_idx and raw_pos < raw_len:
        if raw_text[raw_pos].isspace():
            while raw_pos < raw_len and raw_text[raw_pos].isspace():
                raw_pos += 1
            norm_pos += 1
        else:
            raw_pos += 1
            norm_pos += 1

    return raw_pos


def _map_normalized_length_to_raw(raw_text: str, raw_start: int, norm_length: int) -> int:
    norm_count = 0
    raw_pos = raw_start
    raw_len = len(raw_text)

    while norm_count < norm_length and raw_pos < raw_len:
        if raw_text[raw_pos].isspace():
            while raw_pos < raw_len and raw_text[raw_pos].isspace():
                raw_pos += 1
            norm_count += 1
        else:
            raw_pos += 1
            norm_count += 1

    return raw_pos - raw_start


def map_citations(raw_text: str, claude_facts: list[ClaudeFact]) -> list[Fact]:
    normalized_raw = _normalize_whitespace(raw_text)
    result: list[Fact] = []

    for claude_fact in claude_facts:
        citations: list[Citation] = []

        for claude_citation in claude_fact.citations:
            quote = claude_citation.exact_quote

            # Tier 1: exact match
            exact_idx = raw_text.find(quote)
            if exact_idx >= 0:
                citations.append(Citation(
                    quoteText=quote, charOffset=exact_idx,
                    length=len(quote), confidence="exact",
                ))
                continue

            # Tier 2: whitespace-normalised, case-insensitive
            normalized_quote = _normalize_whitespace(quote)
            norm_idx = normalized_raw.lower().find(normalized_quote.lower())
            if norm_idx >= 0:
                raw_idx = _map_normalized_index_to_raw(raw_text, norm_idx)
                raw_length = _map_normalized_length_to_raw(raw_text, raw_idx, len(normalized_quote))
                citations.append(Citation(
                    quoteText=raw_text[raw_idx: raw_idx + raw_length],
                    charOffset=raw_idx, length=raw_length, confidence="normalized",
                ))
                continue

            # Tier 3: fuzzy prefix match
            fuzzy = _fuzzy_substring_match(normalized_raw, normalized_quote)
            if fuzzy:
                fuzzy_idx, fuzzy_len = fuzzy
                raw_idx = _map_normalized_index_to_raw(raw_text, fuzzy_idx)
                raw_length = _map_normalized_length_to_raw(raw_text, raw_idx, fuzzy_len)
                citations.append(Citation(
                    quoteText=raw_text[raw_idx: raw_idx + raw_length],
                    charOffset=raw_idx, length=raw_length, confidence="fuzzy",
                ))
                continue

            # No match
            citations.append(Citation(
                quoteText=quote, charOffset=-1,
                length=len(quote), confidence="fuzzy",
            ))

        result.append(Fact(id=str(uuid.uuid4()), text=claude_fact.fact, citations=citations))

    return result
