import re

# Satzende: . ! ? … gefolgt von Whitespace (oder Stringende beim flush).
_SENTENCE_END = re.compile(r"[.!?…]+(?=\s)")

# Abkürzungen, nach denen NICHT getrennt werden darf (sonst zerschneidet der
# Streaming-Chunker mitten in "z. | B." und die Normalisierung greift nicht mehr).
_ABBREVS = (
    "z.b.", "z. b.", "d.h.", "d. h.", "u.a.", "u. a.", "bzw.", "ca.", "usw.",
    "etc.", "dr.", "prof.", "nr.", "str.", "evtl.", "inkl.", "ggf.", "max.",
    "min.", "vgl.", "sog.", "u.ä.", "o.ä.", "bspw.",
)
_ABBR_END = re.compile(
    r"(?:^|[\s(„\"'])(" + "|".join(re.escape(a) for a in _ABBREVS) + r")$",
    re.IGNORECASE,
)


class SentenceChunker:
    """Inkrementelle Satzsegmentierung fuer gestreamte LLM-Token.

    `feed(delta)` liefert die seit dem letzten Aufruf fertig gewordenen Saetze,
    `flush()` den verbleibenden Rest (z. B. der letzte Satz ohne abschliessendes
    Leerzeichen). Damit kann pro Satz schon TTS erzeugt werden, waehrend das LLM
    noch weiterschreibt.

    Es wird NICHT getrennt, wenn der Punkt zu einer Ordinal-/Datumszahl
    ("1. Mai") oder einer bekannten Abkuerzung ("z. B.") gehoert.
    """

    def __init__(self):
        self._buffer = ""

    def _is_real_end(self, match) -> bool:
        start, end = match.start(), match.end()
        punct = match.group()
        dot_only = set(punct) <= {".", "…"}
        if dot_only and start > 0:
            prev = self._buffer[start - 1]
            # Ziffer + Punkt ("1.") = Ordinal-/Listenmarker, kein Satzende.
            if prev.isdigit():
                return False
            # Einzelner Buchstabe + Punkt ("z. B.", Initialen "A.") -> kein Satzende.
            if prev.isalpha():
                before = self._buffer[start - 2] if start >= 2 else ""
                if before == "" or not before.isalpha():
                    return False
        # Bekannte (mehrbuchstabige) Abkuerzung vor dem Punkt -> kein Satzende.
        if _ABBR_END.search(self._buffer[:end]):
            return False
        return True

    def feed(self, text: str) -> list[str]:
        self._buffer += text
        sentences: list[str] = []
        search_start = 0
        while True:
            match = _SENTENCE_END.search(self._buffer, search_start)
            if not match:
                break
            end = match.end()
            if not self._is_real_end(match):
                search_start = end  # diese Stelle nicht trennen, weitersuchen
                continue
            sentence = self._buffer[:end].strip()
            self._buffer = self._buffer[end:]
            search_start = 0
            if sentence:
                sentences.append(sentence)
        return sentences

    def flush(self) -> str:
        rest = self._buffer.strip()
        self._buffer = ""
        return rest
