from __future__ import annotations

import re
from typing import Any, Callable, Dict, List, Sequence, Set


_CONTROL_TOKEN_RE = re.compile(r"<\|[^>]+?\|>")
_BARE_TAG_RE = re.compile(r"</?[^>\s]+>")
_CJK_KANA_HANGUL_RE = re.compile("[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]")


class HotwordTrie:
    """Prefix trie over hotword token-id sequences."""

    def __init__(
        self,
        token_sequences: Sequence[Sequence[int]],
        *,
        start_boost: float,
        continuation_boost: float,
    ) -> None:
        self.start_boost = float(start_boost)
        self.continuation_boost = float(continuation_boost)
        self.trie: Dict[int, Dict[int, Any]] = {}
        self.max_sequence_len = 0
        for seq in token_sequences:
            ids = [int(token_id) for token_id in seq]
            if not ids:
                continue
            node = self.trie
            for token_id in ids:
                node = node.setdefault(token_id, {})
            self.max_sequence_len = max(self.max_sequence_len, len(ids))
        self.start_token_ids = sorted(self.trie.keys())

    def __bool__(self) -> bool:
        return bool(self.trie)

    def boosts_for_generated(self, generated_ids: Sequence[int]) -> Dict[int, float]:
        boosts: Dict[int, float] = {}
        if self.start_boost:
            for token_id in self.start_token_ids:
                boosts[token_id] = max(boosts.get(token_id, 0.0), self.start_boost)

        if not generated_ids or not self.continuation_boost or self.max_sequence_len <= 1:
            return boosts

        max_prefix_len = min(len(generated_ids), self.max_sequence_len - 1)
        for prefix_len in range(1, max_prefix_len + 1):
            node: Dict[int, Any] = self.trie
            matched = True
            for token_id in generated_ids[-prefix_len:]:
                next_node = node.get(int(token_id))
                if next_node is None:
                    matched = False
                    break
                node = next_node
            if not matched:
                continue
            for next_token_id in node.keys():
                boosts[int(next_token_id)] = max(
                    boosts.get(int(next_token_id), 0.0),
                    self.continuation_boost,
                )
        return boosts


def _has_cjk_or_kana_or_hangul(text: str) -> bool:
    return bool(_CJK_KANA_HANGUL_RE.search(str(text or "")))


def _hotword_text_variants(word: str) -> List[str]:
    word = str(word or "").strip()
    if not word:
        return []
    variants = [word]
    if not _has_cjk_or_kana_or_hangul(word) and re.search(r"[A-Za-z0-9_]", word):
        variants.append(" " + word)
    out: List[str] = []
    seen: Set[str] = set()
    for value in variants:
        if value not in seen:
            seen.add(value)
            out.append(value)
    return out


def token_is_control_or_special(token: str, token_id: int, special_ids: Set[int]) -> bool:
    if int(token_id) in special_ids:
        return True
    token = str(token)
    return bool(_CONTROL_TOKEN_RE.fullmatch(token) or _BARE_TAG_RE.fullmatch(token))


def build_hotword_sequences(
    hotwords: Sequence[str],
    *,
    encode: Callable[[str], List[int]],
    id_to_token: Callable[[int], str],
    special_ids: Set[int],
) -> Dict[str, List[List[int]]]:
    special_ids = set(int(value) for value in special_ids if value is not None)
    sequences: Dict[str, List[List[int]]] = {}
    seen_global: Set[tuple[int, ...]] = set()
    for word in hotwords:
        word = str(word or "").strip()
        if not word:
            continue
        variants: List[List[int]] = []
        for text in _hotword_text_variants(word):
            ids = [
                int(token_id)
                for token_id in encode(text)
                if not token_is_control_or_special(id_to_token(int(token_id)), int(token_id), special_ids)
            ]
            key = tuple(ids)
            if not key or key in seen_global:
                continue
            seen_global.add(key)
            variants.append(ids)
        if variants:
            sequences[word] = variants
    return sequences


def flatten_sequences(sequences_by_word: Dict[str, List[List[int]]]) -> List[List[int]]:
    return [ids for variants in sequences_by_word.values() for ids in variants]


def parse_hotwords(raw: Any) -> List[str]:
    values: List[str] = []
    if isinstance(raw, (list, tuple)):
        values = [str(value).strip() for value in raw]
    elif raw:
        values = [value.strip() for value in re.split(r"[,，]", str(raw))]
    out: List[str] = []
    seen: Set[str] = set()
    for value in values:
        if value and value not in seen:
            seen.add(value)
            out.append(value)
    return out


def build_trie_from_hotwords(
    hotwords: Sequence[str],
    *,
    encode: Callable[[str], List[int]],
    id_to_token: Callable[[int], str],
    special_ids: Set[int],
    start_boost: float,
    continuation_boost: float,
) -> tuple[HotwordTrie, Dict[str, List[List[int]]]]:
    sequences_by_word = build_hotword_sequences(
        hotwords,
        encode=encode,
        id_to_token=id_to_token,
        special_ids=special_ids,
    )
    trie = HotwordTrie(
        flatten_sequences(sequences_by_word),
        start_boost=start_boost,
        continuation_boost=continuation_boost,
    )
    return trie, sequences_by_word
