"""Biometric typing emulation for SEB-compatible input.

Generates human-like typing delays based on character bigrams,
position in text, and punctuation context. Optionally introduces
realistic typos with automatic corrections.

All functions are pure (no CGEvent calls) — they produce timing
and action sequences consumed by engine.py.
"""

from __future__ import annotations

import random
from enum import Enum
from typing import NamedTuple

# ----------------------------------------------------------------
# Bigram speed coefficients
# ----------------------------------------------------------------
# Values < 1.0 mean the pair is typed faster (common digraphs).
# Values > 1.0 mean the pair is typed slower (awkward combos).

_BIGRAM_COEFFICIENTS: dict[str, float] = {
    # English fast pairs
    "th": 0.75,
    "he": 0.78,
    "in": 0.80,
    "er": 0.78,
    "an": 0.82,
    "re": 0.80,
    "on": 0.82,
    "at": 0.85,
    "en": 0.82,
    "nd": 0.83,
    "ti": 0.84,
    "es": 0.82,
    "or": 0.83,
    "te": 0.80,
    "of": 0.85,
    "ed": 0.83,
    "is": 0.82,
    "it": 0.84,
    "al": 0.85,
    "ar": 0.84,
    "st": 0.82,
    "to": 0.83,
    "nt": 0.84,
    "ng": 0.85,
    "se": 0.82,
    "ha": 0.84,
    "ou": 0.83,
    "io": 0.85,
    "le": 0.83,
    "ve": 0.84,
    "co": 0.85,
    "me": 0.84,
    "de": 0.83,
    "hi": 0.85,
    "ri": 0.84,
    "ro": 0.85,
    "ic": 0.86,
    "ne": 0.84,
    "ea": 0.83,
    "ra": 0.84,
    # Russian fast pairs
    "ст": 0.78,
    "но": 0.80,
    "то": 0.80,
    "на": 0.80,
    "ен": 0.82,
    "ни": 0.82,
    "ра": 0.83,
    "ов": 0.83,
    "ко": 0.84,
    "по": 0.82,
    "ро": 0.84,
    "ал": 0.85,
    "ет": 0.82,
    "ан": 0.83,
    "ол": 0.85,
    "ел": 0.84,
    "ор": 0.83,
    "ре": 0.82,
    "от": 0.83,
    "ли": 0.84,
    "ва": 0.83,
    "ос": 0.84,
    "ка": 0.85,
    "пр": 0.83,
    "ве": 0.84,
    "ла": 0.85,
    "ть": 0.83,
    "не": 0.82,
    "де": 0.84,
    "ле": 0.84,
    # Chinese pinyin fast pairs
    "sh": 0.78,
    "ch": 0.80,
    "zh": 0.82,
    # Slow/awkward pairs
    "qw": 1.25,
    "xc": 1.20,
    "zx": 1.30,
    "bv": 1.25,
    "mn": 1.15,
    "zy": 1.20,
    "wq": 1.25,
    "xz": 1.30,
    "дж": 1.25,
    "ьъ": 1.35,
    "шщ": 1.30,
    "фы": 1.20,
    "цщ": 1.25,
    "эж": 1.20,
    "бю": 1.15,
}

_PUNCT_SENTENCE_END = frozenset(".!?…")
_PUNCT_MID = frozenset(",;:—–")


def get_bigram_coefficient(prev_char: str | None, char: str) -> float:
    """Return speed coefficient for a character pair."""
    if prev_char is None:
        return 1.0
    bigram = (prev_char + char).lower()
    return _BIGRAM_COEFFICIENTS.get(bigram, 1.0)


def get_char_delay(
    char: str,
    prev_char: str | None,
    position: int,
    total_length: int,
    *,
    base_ms: float = 50.0,
    variation_ms: float = 20.0,
) -> float:
    """Calculate biometric delay before typing a character.

    Returns:
        Delay in seconds.
    """
    delay_ms = random.gauss(base_ms, variation_ms)
    delay_ms = max(base_ms - variation_ms * 2, delay_ms)

    coeff = get_bigram_coefficient(prev_char, char)
    delay_ms *= coeff

    if total_length > 1:
        fatigue = 1.0 + 0.08 * (position / (total_length - 1))
        delay_ms *= fatigue

    if char == " ":
        delay_ms += random.uniform(100.0, 200.0)
    elif char in _PUNCT_SENTENCE_END:
        delay_ms += random.uniform(150.0, 300.0)
    elif char in _PUNCT_MID:
        delay_ms += random.uniform(50.0, 120.0)
    elif char == "\n":
        delay_ms += random.uniform(200.0, 400.0)

    if random.random() < 0.02:
        delay_ms += random.uniform(100.0, 250.0)

    return delay_ms / 1000.0


# ----------------------------------------------------------------
# Typo simulation
# ----------------------------------------------------------------


class TypoActionKind(Enum):
    TYPE = "type"
    BACKSPACE = "backspace"
    PAUSE = "pause"


class TypoAction(NamedTuple):
    kind: TypoActionKind
    char: str
    delay: float


_ADJACENT_KEYS: dict[str, str] = {
    "q": "wa",
    "w": "qeas",
    "e": "wrds",
    "r": "etdf",
    "t": "ryfg",
    "y": "tugh",
    "u": "yijh",
    "i": "uokj",
    "o": "iplk",
    "p": "ol",
    "a": "qwsz",
    "s": "awedxz",
    "d": "serfcx",
    "f": "drtgvc",
    "g": "ftyhbv",
    "h": "gyujnb",
    "j": "huiknm",
    "k": "jiolm",
    "l": "kop",
    "z": "asx",
    "x": "zsdc",
    "c": "xdfv",
    "v": "cfgb",
    "b": "vghn",
    "n": "bhjm",
    "m": "njk",
    "й": "цф",
    "ц": "йуыф",
    "у": "цквы",
    "к": "уеав",
    "е": "кнпа",
    "н": "егрп",
    "г": "ншор",
    "ш": "гщло",
    "щ": "шзлд",
    "з": "щхдж",
    "х": "зжэ",
    "ф": "йцыя",
    "ы": "фцувач",
    "в": "ыукапс",
    "а": "векнмп",
    "п": "ванрис",
    "р": "пнготи",
    "о": "ргшлть",
    "л": "ошщдбь",
    "д": "лщзжюб",
    "ж": "дзхэю",
    "э": "хж",
    "я": "фыч",
    "ч": "яысм",
    "с": "чвпаи",
    "м": "савпт",
    "и": "спрот",
    "т": "иролб",
    "ь": "олдб",
    "б": "лдтью",
    "ю": "джбь",
}


def _get_adjacent_char(char: str) -> str | None:
    lower = char.lower()
    neighbors = _ADJACENT_KEYS.get(lower)
    if not neighbors:
        return None
    adj = random.choice(neighbors)
    return adj.upper() if char.isupper() else adj


def maybe_introduce_typo(
    text: str,
    *,
    typo_probability: float = 0.01,
    base_ms: float = 50.0,
    variation_ms: float = 20.0,
) -> list[TypoAction]:
    """Generate typing actions with possible typos.

    The final text after executing all actions matches the original input.
    """
    if not text:
        return []

    actions: list[TypoAction] = []
    words = _split_with_separators(text)

    prev_char: str | None = None
    global_pos = 0

    for word in words:
        alpha_count = sum(1 for c in word if c.isalpha())
        introduce_typo = alpha_count >= 3 and random.random() < typo_probability

        if introduce_typo:
            typo_actions = _generate_word_with_typo(
                word,
                prev_char,
                global_pos,
                len(text),
                base_ms,
                variation_ms,
            )
            actions.extend(typo_actions)
        else:
            for i, char in enumerate(word):
                delay = get_char_delay(
                    char,
                    prev_char,
                    global_pos + i,
                    len(text),
                    base_ms=base_ms,
                    variation_ms=variation_ms,
                )
                actions.append(TypoAction(TypoActionKind.TYPE, char, delay))
                prev_char = char

        global_pos += len(word)
        if word:
            prev_char = word[-1]

    return actions


def _split_with_separators(text: str) -> list[str]:
    parts: list[str] = []
    current: list[str] = []
    for char in text:
        if char in (" ", "\t", "\n"):
            if current:
                parts.append("".join(current))
                current = []
            parts.append(char)
        else:
            current.append(char)
    if current:
        parts.append("".join(current))
    return parts


def _generate_word_with_typo(
    word: str,
    prev_char: str | None,
    global_pos: int,
    total_length: int,
    base_ms: float,
    variation_ms: float,
) -> list[TypoAction]:
    actions: list[TypoAction] = []

    alpha_positions = [i for i, c in enumerate(word) if c.isalpha() and 0 < i < len(word) - 1]
    if not alpha_positions:
        pc = prev_char
        for i, char in enumerate(word):
            delay = get_char_delay(
                char, pc, global_pos + i, total_length, base_ms=base_ms, variation_ms=variation_ms
            )
            actions.append(TypoAction(TypoActionKind.TYPE, char, delay))
            pc = char
        return actions

    typo_pos = random.choice(alpha_positions)
    roll = random.random()
    if roll < 0.50:
        typo_type = "adjacent"
    elif roll < 0.80:
        typo_type = "skip"
    else:
        typo_type = "transpose"

    pc = prev_char

    for i in range(typo_pos):
        char = word[i]
        delay = get_char_delay(
            char, pc, global_pos + i, total_length, base_ms=base_ms, variation_ms=variation_ms
        )
        actions.append(TypoAction(TypoActionKind.TYPE, char, delay))
        pc = char

    if typo_type == "adjacent":
        correct_char = word[typo_pos]
        wrong_char = _get_adjacent_char(correct_char) or correct_char
        delay = get_char_delay(
            correct_char,
            pc,
            global_pos + typo_pos,
            total_length,
            base_ms=base_ms,
            variation_ms=variation_ms,
        )
        actions.append(TypoAction(TypoActionKind.TYPE, wrong_char, delay))
        actions.append(TypoAction(TypoActionKind.PAUSE, "", random.uniform(0.15, 0.35)))
        actions.append(TypoAction(TypoActionKind.BACKSPACE, "", random.uniform(0.04, 0.08)))
        actions.append(TypoAction(TypoActionKind.TYPE, correct_char, random.uniform(0.05, 0.10)))
        pc = correct_char
        rest_start = typo_pos + 1

    elif typo_type == "skip":
        correct_char = word[typo_pos]
        if typo_pos + 1 < len(word):
            next_char = word[typo_pos + 1]
            delay = get_char_delay(
                next_char,
                pc,
                global_pos + typo_pos,
                total_length,
                base_ms=base_ms,
                variation_ms=variation_ms,
            )
            actions.append(TypoAction(TypoActionKind.TYPE, next_char, delay))
            actions.append(TypoAction(TypoActionKind.PAUSE, "", random.uniform(0.20, 0.40)))
            actions.append(TypoAction(TypoActionKind.BACKSPACE, "", random.uniform(0.04, 0.08)))
            actions.append(
                TypoAction(TypoActionKind.TYPE, correct_char, random.uniform(0.05, 0.10))
            )
            actions.append(TypoAction(TypoActionKind.TYPE, next_char, random.uniform(0.04, 0.08)))
            pc = next_char
            rest_start = typo_pos + 2
        else:  # pragma: no cover
            delay = get_char_delay(
                correct_char,
                pc,
                global_pos + typo_pos,
                total_length,
                base_ms=base_ms,
                variation_ms=variation_ms,
            )
            actions.append(TypoAction(TypoActionKind.TYPE, correct_char, delay))
            pc = correct_char
            rest_start = typo_pos + 1

    else:  # transpose
        char_a = word[typo_pos]
        if typo_pos + 1 < len(word):
            char_b = word[typo_pos + 1]
            delay = get_char_delay(
                char_b,
                pc,
                global_pos + typo_pos,
                total_length,
                base_ms=base_ms,
                variation_ms=variation_ms,
            )
            actions.append(TypoAction(TypoActionKind.TYPE, char_b, delay))
            actions.append(TypoAction(TypoActionKind.TYPE, char_a, random.uniform(0.03, 0.06)))
            actions.append(TypoAction(TypoActionKind.PAUSE, "", random.uniform(0.20, 0.40)))
            actions.append(TypoAction(TypoActionKind.BACKSPACE, "", random.uniform(0.04, 0.08)))
            actions.append(TypoAction(TypoActionKind.BACKSPACE, "", random.uniform(0.04, 0.08)))
            actions.append(TypoAction(TypoActionKind.TYPE, char_a, random.uniform(0.05, 0.10)))
            actions.append(TypoAction(TypoActionKind.TYPE, char_b, random.uniform(0.04, 0.08)))
            pc = char_b
            rest_start = typo_pos + 2
        else:  # pragma: no cover
            delay = get_char_delay(
                char_a,
                pc,
                global_pos + typo_pos,
                total_length,
                base_ms=base_ms,
                variation_ms=variation_ms,
            )
            actions.append(TypoAction(TypoActionKind.TYPE, char_a, delay))
            pc = char_a
            rest_start = typo_pos + 1

    for i in range(rest_start, len(word)):
        char = word[i]
        delay = get_char_delay(
            char, pc, global_pos + i, total_length, base_ms=base_ms, variation_ms=variation_ms
        )
        actions.append(TypoAction(TypoActionKind.TYPE, char, delay))
        pc = char

    return actions


def replay_text(actions: list[TypoAction]) -> str:
    """Simulate executing actions and return the resulting text."""
    buffer: list[str] = []
    for action in actions:
        if action.kind == TypoActionKind.TYPE:
            buffer.append(action.char)
        elif action.kind == TypoActionKind.BACKSPACE:
            if buffer:
                buffer.pop()
    return "".join(buffer)
