"""Основной API библиотеки TextHumanize."""

from __future__ import annotations

import logging
import re
import threading
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import TYPE_CHECKING, Any, cast

from texthumanize.analyzer import TextAnalyzer
from texthumanize.cache import result_cache
from texthumanize.exceptions import ConfigError, InputTooLargeError
from texthumanize.lang_detect import detect_language
from texthumanize.pipeline import Pipeline
from texthumanize.utils import AnalysisReport, DetectionReport, HumanizeOptions, HumanizeResult

if TYPE_CHECKING:
    from collections.abc import Generator

    from texthumanize.stylistic import StylisticFingerprint

logger = logging.getLogger(__name__)

# ─── Generic lazy module loader ──────────────────────────────

_lazy_lock = threading.Lock()
_lazy_modules: dict[str, object] = {}


def _lazy_import(module_path: str) -> object:
    """Thread-safe lazy import with double-checked locking.

    Args:
        module_path: Fully qualified module name (e.g. 'texthumanize.detectors').

    Returns:
        The imported module object (cached after first import).
    """
    mod = _lazy_modules.get(module_path)
    if mod is not None:
        return mod
    with _lazy_lock:
        mod = _lazy_modules.get(module_path)
        if mod is not None:
            return mod
        import importlib
        mod = importlib.import_module(module_path)
        _lazy_modules[module_path] = mod
        return mod


def _get_detectors() -> Any:
    return _lazy_import("texthumanize.detectors")


def _get_paraphrase() -> Any:
    return _lazy_import("texthumanize.paraphrase")


def _get_tone() -> Any:
    return _lazy_import("texthumanize.tone")


def _get_watermark() -> Any:
    return _lazy_import("texthumanize.watermark")


def _get_spinner() -> Any:
    return _lazy_import("texthumanize.spinner")


def _get_stat_detector() -> Any:
    return _lazy_import("texthumanize.statistical_detector")


def _get_ai_backend() -> Any:
    return _lazy_import("texthumanize.ai_backend")


def _get_coherence() -> Any:
    return _lazy_import("texthumanize.coherence")


def _get_neural_detector() -> Any:
    return _lazy_import("texthumanize.neural_detector")


def _get_neural_lm() -> Any:
    return _lazy_import("texthumanize.neural_lm")


def _get_word_embeddings() -> Any:
    return _lazy_import("texthumanize.word_embeddings")


def _get_hmm_tagger() -> Any:
    return _lazy_import("texthumanize.hmm_tagger")


def humanize(
    text: str,
    lang: str = "auto",
    profile: str = "web",
    intensity: int = 60,
    preserve: dict | None = None,
    constraints: dict | None = None,
    seed: int | None = None,
    target_style: object | str | None = None,
    only_flagged: bool = False,
    custom_dict: dict[str, str | list[str]] | None = None,
    *,
    auto_evade: bool = False,
    target_ai_score: float = 0.30,
    max_evade_attempts: int = 4,
    phantom: bool = False,
    phantom_budget: float = 1.0,
    phantom_target: float = 0.30,
    backend: str = "local",
    openai_api_key: str | None = None,
    openai_model: str = "gpt-4o-mini",
    oss_api_url: str | None = None,
    ollama_model: str = "llama3.2",
    ollama_url: str | None = None,
) -> HumanizeResult:
    """Гуманизировать текст — сделать его более естественным.

    Основная функция библиотеки. Принимает текст и возвращает
    его обработанную версию с метриками и списком изменений.

    Args:
        text: Текст для обработки.
        lang: Код языка: 'auto', 'ru', 'uk', 'en', 'de', 'fr', 'es', 'pl', 'pt', 'it'
            или любой ISO-код. При 'auto' язык определяется автоматически.
            Для языков без полного словаря используется универсальный процессор.
        profile: Профиль обработки:
            - 'chat' — живой, разговорный стиль
            - 'web' — нейтральный веб-контент
            - 'seo' — SEO-безопасный режим
            - 'docs' — документация, технический стиль
            - 'formal' — формальный стиль
        intensity: Интенсивность обработки (0-100).
            0 = без изменений, 100 = максимум.
        preserve: Настройки защиты элементов:
            - code_blocks (bool): Защита блоков кода. По умолчанию True.
            - urls (bool): Защита URL. По умолчанию True.
            - emails (bool): Защита email. По умолчанию True.
            - hashtags (bool): Защита хэштегов. По умолчанию True.
            - mentions (bool): Защита @упоминаний. По умолчанию True.
            - markdown (bool): Защита markdown. По умолчанию True.
            - html (bool): Защита HTML-тегов. По умолчанию True.
            - numbers (bool): Защита чисел. По умолчанию False.
            - brand_terms (list[str]): Список брендовых терминов.
        constraints: Ограничения обработки:
            - max_change_ratio (float): Максимальная доля изменений (0-1).
            - min_sentence_length (int): Минимальная длина предложения.
            - keep_keywords (list[str]): Ключевые слова для SEO.
        seed: Сид для воспроизводимости результатов.
        target_style: Целевой стилистический отпечаток.
            Может быть StylisticFingerprint или имя пресета (str):
            'student', 'copywriter', 'scientist', 'journalist', 'blogger'.
        only_flagged: Если True, гуманизировать только предложения,
            которые detect_ai_sentences помечает как AI (ai_probability > 0.5).
            Предложения с label="human" остаются без изменений.
        custom_dict: Пользовательский словарь замен.
            Формат: {"слово": "замена"} или {"слово": ["вар1", "вар2"]}.
            Замены применяются дополнительно к встроенным словарям.
            При списке вариантов выбирается случайный.
        backend: Бэкенд гуманизации:
            - 'local' — встроенный rule-based движок (по умолчанию, offline)
            - 'ollama' — локальная LLM через Ollama (без API key, нужен сервер)
            - 'oss' — бесплатная OSS LLM (amd/gpt-oss-120b) через Gradio, без ключей
            - 'openai' — OpenAI API (требуется api_key)
            - 'auto' — пробует openai → ollama → oss → local (fallback)
        openai_api_key: API ключ OpenAI (только для backend='openai'/'auto').
        openai_model: Модель OpenAI (по умолчанию 'gpt-4o-mini').
        oss_api_url: URL для OSS Gradio endpoint (по умолчанию amd/gpt-oss-120b-chatbot).
        ollama_model: Модель Ollama (по умолчанию 'llama3.2').
        ollama_url: URL сервера Ollama (по умолчанию 'http://localhost:11434').

    Returns:
        HumanizeResult с полями:
            - text: Обработанный текст
            - original: Исходный текст
            - lang: Определённый язык
            - profile: Использованный профиль
            - intensity: Использованная интенсивность
            - changes: Список сделанных изменений
            - metrics_before: Метрики до обработки
            - metrics_after: Метрики после обработки
            - change_ratio: Доля изменений

    Examples:
        >>> result = humanize("Данный текст является примером.")
        >>> print(result.text)
        Этот текст - пример.

        >>> # offline, без ключей — использует бесплатную OSS LLM
        >>> result = humanize("AI-generated text here.", backend="oss")

        >>> # с OpenAI
        >>> result = humanize("Text.", backend="openai", openai_api_key="sk-...")

        >>> # auto-evade: повторяет гуманизацию, пока AI-score не упадёт ниже 0.30
        >>> result = humanize("AI text.", auto_evade=True, target_ai_score=0.25)
    """
    # Input sanitization
    if not isinstance(text, str):
        raise ConfigError(f"Expected str, got {type(text).__name__}")
    if not text or not text.strip():
        return HumanizeResult(
            original=text, text=text, lang=lang or "en",
            profile=profile, intensity=intensity,
            changes=[], metrics_before={}, metrics_after={},
        )
    MAX_TEXT_LENGTH = 1_000_000  # 1M chars safety limit
    if len(text) > MAX_TEXT_LENGTH:
        raise InputTooLargeError(len(text), MAX_TEXT_LENGTH)

    # ── auto_evade shortcut ──────────────────────────────────────
    # Delegates to humanize_until_human() with adaptive strategy.
    if auto_evade:
        return humanize_until_human(
            text, lang=lang, profile=profile, intensity=intensity,
            target_score=target_ai_score, max_attempts=max_evade_attempts,
            intensity_step=8, seed=seed, strategy="adaptive",
        )

    # ── AI backend routing ──────────────────────────────────────
    _valid_backends = ("local", "ollama", "oss", "openai", "auto")
    if backend not in _valid_backends:
        raise ConfigError(
            f"backend must be one of {_valid_backends}, got {backend!r}"
        )
    if backend != "local":
        return _humanize_via_backend(
            text=text,
            lang=lang,
            profile=profile,
            intensity=intensity,
            backend=backend,
            openai_api_key=openai_api_key,
            openai_model=openai_model,
            oss_api_url=oss_api_url,
            ollama_model=ollama_model,
            ollama_url=ollama_url,
        )

    # Определяем язык
    detected_lang = lang
    if lang == "auto":
        detected_lang = detect_language(text)

    # Строим опции
    options = HumanizeOptions(
        lang=detected_lang,
        profile=profile,
        intensity=intensity,
        seed=seed,
        target_style=target_style,
        custom_dict=custom_dict,
        openai_api_key=openai_api_key,
        openai_model=openai_model,
    )

    if preserve:
        options.preserve.update(preserve)
    if constraints:
        options.constraints.update(constraints)

    # ── Cache lookup ────────────────────────────────────────────
    if seed is not None:
        cached = result_cache.get(
            text, lang=detected_lang, profile=profile, intensity=intensity,
            seed=seed, phantom=phantom,
        )
        if cached is not None:
            return cast(HumanizeResult, cached)

    # Запускаем пайплайн
    pipeline = Pipeline(options=options)

    # ── Selective humanization ────────────────────────────────
    if only_flagged:
        return _humanize_flagged_only(
            text, detected_lang, pipeline, options,
        )

    result = pipeline.run(text, detected_lang)

    # ── PHANTOM™ post-processing ─────────────────────────────
    # Gradient-guided neural optimization: fine-tunes the humanized text
    # to minimize detection score by targeting specific neural features.
    # Only runs if the base result still has a high detection score.
    if phantom:
        try:
            current_det = detect_ai(result.text, lang=detected_lang)
            current_combined = current_det.get("combined_score", current_det.get("score", 0.5))
            if current_combined > phantom_target:
                from texthumanize.phantom import get_phantom
                engine = get_phantom()
                phantom_result = engine.optimize(
                    result.text, lang=detected_lang,
                    target_score=phantom_target,
                    budget=phantom_budget,
                    max_iterations=15,
                    seed=seed,
                )
                if phantom_result.final_score < current_combined:
                    result = HumanizeResult(
                        original=result.original,
                        text=phantom_result.optimized_text,
                        lang=result.lang,
                        profile=result.profile,
                        intensity=result.intensity,
                        changes=result.changes + [
                            {"type": "phantom", "description": f"PHANTOM™: {phantom_result.original_score:.3f}→{phantom_result.final_score:.3f}"}
                        ],
                        metrics_before=result.metrics_before,
                        metrics_after=result.metrics_after,
                    )
        except Exception as e:
            logger.warning("PHANTOM™ post-processing failed: %s", e)

    # ── Grammar post-processing for Slavic languages ──────────
    # Applied regardless of whether PHANTOM ran,
    # fixes agreement/government issues from dictionary replacements.
    # Uses paragraph-safe grammar fixes (not full _cleanup_text which
    # may collapse paragraph structure).
    if detected_lang in ("ru", "uk"):
        try:
            from texthumanize.phantom import (
                _fix_grammar_slavic,
                _fix_sentence_caps,
            )
            lines = result.text.split("\n")
            fixed_lines = []
            in_code_block = False
            for line in lines:
                stripped = line.strip()
                if stripped.startswith("```"):
                    in_code_block = not in_code_block
                    fixed_lines.append(line)
                    continue
                if in_code_block:
                    fixed_lines.append(line)
                    continue
                if stripped:
                    line = _fix_grammar_slavic(line, detected_lang)
                    line = _fix_sentence_caps(line)
                fixed_lines.append(line)
            fixed_text = "\n".join(fixed_lines)
            if fixed_text != result.text:
                result = HumanizeResult(
                    original=result.original,
                    text=fixed_text,
                    lang=result.lang,
                    profile=result.profile,
                    intensity=result.intensity,
                    changes=result.changes,
                    metrics_before=result.metrics_before,
                    metrics_after=result.metrics_after,
                )
        except Exception:
            pass  # Grammar cleanup is best-effort

    # ── Cache result (only deterministic calls with seed) ─────
    if seed is not None:
        result_cache.put(
            text, result, lang=detected_lang, profile=profile,
            intensity=intensity, seed=seed, phantom=phantom,
        )

    return result


# ── AI-backend humanization helper ──────────────────────────

def _humanize_via_backend(
    text: str,
    lang: str,
    profile: str,
    intensity: int,
    backend: str,
    openai_api_key: str | None,
    openai_model: str,
    oss_api_url: str | None,
    ollama_model: str = "llama3.2",
    ollama_url: str | None = None,
) -> HumanizeResult:
    """Route humanization through an AI backend, then polish via local pipeline.

    1. Sends text to the chosen backend (ollama / oss / openai / auto).
    2. Runs the result through the local pipeline at low intensity
       for typography, grammar, and coherence cleanup.
    """
    detected_lang = lang
    if lang == "auto":
        detected_lang = detect_language(text)

    ab = _get_ai_backend()

    # Build backend with appropriate settings
    enable_oss = backend in ("oss", "auto")
    enable_ollama = backend in ("ollama", "auto")
    api_key = openai_api_key if backend in ("openai", "auto") else None

    prefer = "auto" if backend == "auto" else backend
    if prefer == "oss":
        prefer = "oss"
    elif prefer == "openai":
        prefer = "openai"
    elif prefer == "ollama":
        prefer = "ollama"

    try:
        ai = ab.AIBackend(
            openai_api_key=api_key,
            openai_model=openai_model,
            enable_ollama=enable_ollama,
            ollama_model=ollama_model,
            ollama_url=ollama_url,
            enable_oss=enable_oss,
            oss_api_url=oss_api_url,
            prefer=prefer,
        )
        rewritten = ai.paraphrase(text, lang=detected_lang, style=profile)
        used = ai.active_backend()
        logger.info("AI backend used: %s", used)
    except Exception:
        logger.warning("AI backend failed, falling back to local pipeline", exc_info=True)
        rewritten = text

    # Polish via local rule-based pipeline (low intensity for cleanup only)
    cleanup_intensity = min(40, intensity)
    return humanize(
        rewritten,
        lang=detected_lang,
        profile=profile,
        intensity=cleanup_intensity,
        backend="local",  # avoid recursion
    )


_ai_cache: dict[Any, Any] = {}
_ai_order: list[Any] = []
_ai_cache_lock = threading.Lock()


def humanize_ai(
    text: str,
    lang: str = "auto",
    *,
    openai_api_key: str | None = None,
    openai_model: str = "gpt-4o-mini",
    enable_oss: bool = False,
    profile: str = "web",
) -> HumanizeResult:
    """Humanize using AI backend (OpenAI / OSS / fallback).

    Three-tier strategy:
    1. OpenAI (if api_key provided)
    2. OSS model via Gradio (if enable_oss=True)
    3. Built-in rules (always available)
    """
    if lang == "auto":
        lang = detect_language(text)

    ab = _get_ai_backend()
    # LRU cache for backend instances (preserves circuit breaker state,
    # bounded to 16 entries to prevent memory leaks in server contexts)
    _cache_key = (openai_api_key, openai_model, enable_oss)
    with _ai_cache_lock:
        if _cache_key not in _ai_cache:
            _MAX_CACHE = 16
            if len(_ai_order) >= _MAX_CACHE:
                _evict = _ai_order.pop(0)
                _ai_cache.pop(_evict, None)
            _ai_cache[_cache_key] = ab.AIBackend(
                openai_api_key=openai_api_key,
                openai_model=openai_model,
                enable_oss=enable_oss,
            )
            _ai_order.append(_cache_key)
        backend = _ai_cache[_cache_key]
    result_text = backend.paraphrase(text, lang=lang, style=profile)

    # Run through pipeline for cleanup
    return humanize(result_text, lang=lang, profile=profile, intensity=40)


def _humanize_flagged_only(
    text: str,
    lang: str,
    pipeline: Pipeline,
    options: HumanizeOptions,
) -> HumanizeResult:
    """Гуманизировать только предложения, помеченные как AI.

    Разбивает текст на предложения, пропускает «человеческие» и
    обрабатывает только те, где ``ai_probability > 0.5``.
    """
    sentences = detect_ai_sentences(text, lang=lang)
    if not sentences:
        return pipeline.run(text, lang)

    parts: list[str] = []
    all_changes: list[dict] = []
    flagged_count = 0
    skipped_count = 0

    # Recover whitespace between sentences using original offsets
    prev_end = 0
    for sent in sentences:
        start = sent.get("start", prev_end)
        # Preserve inter-sentence whitespace
        if start > prev_end:
            parts.append(text[prev_end:start])

        sent_text = sent["text"]
        if sent.get("ai_probability", 0) > 0.5:
            # Humanize this sentence individually
            flagged_count += 1
            sub = pipeline.run(sent_text, lang)
            parts.append(sub.text)
            all_changes.extend(sub.changes)
        else:
            # Keep untouched
            skipped_count += 1
            parts.append(sent_text)
            all_changes.append({
                "type": "selective_skip",
                "description": (
                    f"Пропущено (human, p={sent.get('ai_probability', 0):.2f}): "
                    f"{sent_text[:60]}…"
                    if len(sent_text) > 60 else
                    f"Пропущено (human, p={sent.get('ai_probability', 0):.2f}): "
                    f"{sent_text}"
                ),
            })
        prev_end = sent.get("end", start + len(sent_text))

    # Trailing text
    if prev_end < len(text):
        parts.append(text[prev_end:])

    combined = "".join(parts)

    # Analyze before/after
    analyzer_obj = TextAnalyzer(lang=lang)
    metrics_before = analyzer_obj.analyze(text)
    metrics_after = analyzer_obj.analyze(combined)

    all_changes.insert(0, {
        "type": "selective_mode",
        "description": (
            f"Selective: {flagged_count} AI / {skipped_count} human sentences"
        ),
    })

    return HumanizeResult(
        original=text,
        text=combined,
        lang=lang,
        profile=options.profile,
        intensity=options.intensity,
        changes=all_changes,
        metrics_before={
            "artificiality_score": metrics_before.artificiality_score,
            "avg_sentence_length": metrics_before.avg_sentence_length,
            "bureaucratic_ratio": metrics_before.bureaucratic_ratio,
            "connector_ratio": metrics_before.connector_ratio,
            "repetition_score": metrics_before.repetition_score,
            "typography_score": metrics_before.typography_score,
        },
        metrics_after={
            "artificiality_score": metrics_after.artificiality_score,
            "avg_sentence_length": metrics_after.avg_sentence_length,
            "bureaucratic_ratio": metrics_after.bureaucratic_ratio,
            "connector_ratio": metrics_after.connector_ratio,
            "repetition_score": metrics_after.repetition_score,
            "typography_score": metrics_after.typography_score,
        },
    )


def analyze(text: str, lang: str = "auto") -> AnalysisReport:
    """Анализировать текст — получить метрики «искусственности».

    Args:
        text: Текст для анализа.
        lang: Код языка: 'auto', 'ru', 'uk', 'en', 'de', 'fr', 'es', 'pl', 'pt', 'it'
            или любой ISO-код.

    Returns:
        AnalysisReport с метриками:
            - artificiality_score: Общий балл (0-100)
            - avg_sentence_length: Средняя длина предложения
            - bureaucratic_ratio: Доля канцеляризмов
            - connector_ratio: Доля ИИ-связок
            - repetition_score: Показатель повторяемости
            - typography_score: «Идеальность» типографики

    Examples:
        >>> report = analyze("Данный текст является примером.")
        >>> print(f"Искусственность: {report.artificiality_score:.1f}/100")
        Искусственность: 35.0/100
    """
    if not text or not text.strip():
        return AnalysisReport(lang=lang if lang != "auto" else "en")

    detected_lang = lang
    if lang == "auto":
        detected_lang = detect_language(text)

    analyzer = TextAnalyzer(lang=detected_lang)
    return analyzer.analyze(text)


def explain(
    result: HumanizeResult,
    fmt: str = "text",
    **kwargs: Any,
) -> str:
    """Объяснить что было изменено — в нескольких форматах.

    Args:
        result: Результат humanize().
        fmt: Формат вывода: ``"text"`` (default), ``"html"``,
            ``"json"`` (RFC 6902 patch), ``"diff"`` (unified diff).
        **kwargs: Передаются в соответствующий рендерер.

    Returns:
        Отчёт об изменениях в выбранном формате.

    Examples:
        >>> result = humanize("Данный текст является примером.")
        >>> print(explain(result))
        === Отчёт TextHumanize ===
        Язык: ru | Профиль: web | Интенсивность: 60
        ...
        >>> html = explain(result, fmt="html")
        >>> json_str = explain(result, fmt="json")
    """
    if fmt == "html":
        from texthumanize.diff_report import explain_html
        return explain_html(result, **kwargs)
    if fmt == "json":
        from texthumanize.diff_report import explain_json_patch
        return explain_json_patch(result, **kwargs)
    if fmt == "diff":
        from texthumanize.diff_report import explain_side_by_side
        return explain_side_by_side(result, **kwargs)
    lines = [
        "=== Отчёт TextHumanize ===",
        f"Язык: {result.lang} | Профиль: {result.profile} "
        f"| Интенсивность: {result.intensity}",
        f"Доля изменений: {result.change_ratio:.1%}",
        "",
    ]

    # Метрики
    if result.metrics_before and result.metrics_after:
        lines.append("--- Метрики ---")
        before = result.metrics_before
        after = result.metrics_after

        metrics = [
            ("Искусственность", "artificiality_score", ""),
            ("Средн. длина предложения", "avg_sentence_length", " сл."),
            ("Канцеляризмы", "bureaucratic_ratio", ""),
            ("ИИ-связки", "connector_ratio", ""),
            ("Повторяемость", "repetition_score", ""),
            ("Типографика", "typography_score", ""),
        ]

        for label, key, unit in metrics:
            b = before.get(key, 0)
            a = after.get(key, 0)
            direction = "↓" if a < b else "↑" if a > b else "="
            lines.append(f"  {label}: {b:.2f}{unit} → {a:.2f}{unit} {direction}")

        lines.append("")

    # Изменения
    if result.changes:
        lines.append(f"--- Изменения ({len(result.changes)}) ---")
        for change in result.changes[:20]:  # Ограничиваем вывод
            change_type = change.get("type", "unknown")
            if "original" in change and "replacement" in change:
                lines.append(
                    f"  [{change_type}] "
                    f'"{change["original"]}" → "{change["replacement"]}"'
                )
            elif "description" in change:
                lines.append(f"  [{change_type}] {change['description']}")

        if len(result.changes) > 20:
            lines.append(f"  ... и ещё {len(result.changes) - 20} изменений")
    else:
        lines.append("--- Изменений нет ---")

    return "\n".join(lines)


def humanize_until_human(
    text: str,
    lang: str = "auto",
    profile: str = "web",
    intensity: int = 60,
    target_score: float = 0.35,
    max_attempts: int = 5,
    intensity_step: int = 10,
    seed: int | None = None,
    verbose: bool = False,
    strategy: str = "adaptive",
) -> HumanizeResult:
    """Humanize text repeatedly until AI detection score drops below target.

    Two strategies:
    - ``"escalate"`` — simple intensity escalation each round (legacy).
    - ``"adaptive"`` — analyses per-metric AI signals and adjusts
      pipeline parameters to target the weakest metrics (default).

    Args:
        text: Text to humanize.
        lang: Language code.
        profile: Humanization profile.
        intensity: Starting intensity.
        target_score: Target AI score (stop when below this).
        max_attempts: Maximum retry attempts.
        intensity_step: Intensity increase per retry.
        seed: Random seed for reproducibility.
        verbose: Log each attempt.
        strategy: ``"adaptive"`` (default) or ``"escalate"``.

    Returns:
        HumanizeResult from the best (lowest AI score) attempt.
    """
    if lang == "auto":
        lang = detect_language(text)

    best_result: HumanizeResult | None = None
    best_score = float("inf")
    current_text = text
    current_intensity = min(intensity, 100)

    # Adaptive state: per-metric overrides accumulate across rounds
    _adapt_overrides: dict[str, int] = {}

    for attempt in range(max_attempts):
        # Build effective intensity for this round
        effective_intensity = current_intensity
        if strategy == "adaptive" and _adapt_overrides:
            # Use the max of all individual boosts
            boost = max(_adapt_overrides.values())
            effective_intensity = min(current_intensity + boost, 100)

        # Humanize
        result = humanize(
            current_text, lang=lang, profile=profile,
            intensity=effective_intensity, seed=seed,
        )

        # Detect AI score on humanized text
        detection = detect_ai(result.text, lang=lang)
        score = detection.get("combined_score", detection.get("score", 0.5))

        if verbose:
            logger.info(
                "Attempt %d/%d: intensity=%d (eff=%d), ai_score=%.3f (target <%.3f)%s",
                attempt + 1, max_attempts, current_intensity,
                effective_intensity, score, target_score,
                f" overrides={_adapt_overrides}" if _adapt_overrides else "",
            )

        if score < best_score:
            best_score = score
            best_result = result

        # Check if we've reached the target
        if score <= target_score:
            if verbose:
                logger.info("Target reached at attempt %d", attempt + 1)
            break

        # ── Adaptive metric analysis ────────────────────────
        if strategy == "adaptive":
            metrics: dict[str, float] = dict(detection.get("metrics", {}))  # type: ignore[arg-type]
            _adapt_overrides = _compute_adaptive_overrides(
                metrics, current_intensity, verbose=verbose,
            )

        # Increase intensity for next attempt
        current_intensity = min(current_intensity + intensity_step, 100)
        # Use the humanized text as input for next round
        current_text = result.text

    if best_result is None:
        # Should not happen, but fallback
        best_result = humanize(text, lang=lang, profile=profile, intensity=intensity)

    # ── PHANTOM™ final refinement ─────────────────────────────
    # If the naturalizer pipeline didn't reach target, apply PHANTOM™
    # gradient-guided optimization as a last-mile refinement.
    if best_score > target_score:
        try:
            from texthumanize.phantom import get_phantom
            engine = get_phantom()
            phantom_result = engine.optimize(
                best_result.text, lang=lang,
                target_score=target_score,
                budget=1.0,
                max_iterations=15,
                seed=seed,
            )
            if phantom_result.final_score < best_score:
                best_result = HumanizeResult(
                    original=best_result.original,
                    text=phantom_result.optimized_text,
                    lang=best_result.lang,
                    profile=best_result.profile,
                    intensity=best_result.intensity,
                    changes=best_result.changes + [
                        {"type": "phantom", "description": f"PHANTOM™: {phantom_result.original_score:.3f}→{phantom_result.final_score:.3f}"}
                    ],
                    metrics_before=best_result.metrics_before,
                    metrics_after=best_result.metrics_after,
                )
                best_score = phantom_result.final_score
                if verbose:
                    logger.info(
                        "PHANTOM™ refinement: %.3f → %.3f (%s)",
                        phantom_result.original_score, phantom_result.final_score,
                        "HUMAN" if phantom_result.bypassed else "mixed",
                    )
        except Exception as e:
            logger.warning("PHANTOM™ refinement failed: %s", e)

    return best_result


# Metric → pipeline capability mapping for adaptive strategy.
# Each entry: (threshold_above_which_flagged, intensity_boost).
_METRIC_BOOST_MAP: dict[str, tuple[float, int]] = {
    "entropy":                  (0.55, 15),  # low entropy → boost entropy_injection
    "burstiness":               (0.55, 15),  # uniform sentence length → boost variation
    "vocabulary":               (0.55, 12),  # limited vocab → boost lexical diversity
    "ai_patterns":              (0.55, 15),  # AI phrase patterns → boost naturalizer
    "punctuation":              (0.55, 8),   # perfect punctuation → boost typography
    "coherence":                (0.55, 10),  # too coherent → boost entropy
    "grammar_perfection":       (0.55, 8),   # too perfect → inject imperfections
    "opening_diversity":        (0.55, 10),  # repetitive openings → boost restructuring
    "readability_consistency":  (0.55, 8),   # uniform readability → boost burstiness
    "rhythm":                   (0.55, 12),  # rhythmic patterns → boost entropy
    "stylometry":               (0.55, 10),  # stylistic uniformity → boost paraphrasing
    "zipf":                     (0.55, 8),   # non-Zipfian dist → boost vocab diversity
}


def _compute_adaptive_overrides(
    metrics: dict[str, float],
    current_intensity: int,
    *,
    verbose: bool = False,
) -> dict[str, int]:
    """Analyze detection metrics and compute per-area intensity boosts.

    Returns a dict of area → extra intensity to apply.
    """
    overrides: dict[str, int] = {}

    if not metrics:
        return overrides

    # Find metrics that are still flagging AI (score > threshold)
    flagged: list[tuple[str, float, int]] = []
    for metric_name, (threshold, boost) in _METRIC_BOOST_MAP.items():
        value = metrics.get(metric_name, 0.0)
        if value > threshold:
            flagged.append((metric_name, value, boost))

    if not flagged:
        return overrides

    # Sort by severity (highest score first)
    flagged.sort(key=lambda x: x[1], reverse=True)

    # Group boosted metrics into pipeline areas
    # entropy/burstiness/rhythm/coherence/readability_consistency → "entropy"
    # vocabulary/zipf → "vocabulary"
    # ai_patterns/grammar_perfection/punctuation → "naturalizer"
    # opening_diversity/stylometry → "structure"
    area_map = {
        "entropy": "entropy",
        "burstiness": "entropy",
        "rhythm": "entropy",
        "coherence": "entropy",
        "readability_consistency": "entropy",
        "vocabulary": "vocabulary",
        "zipf": "vocabulary",
        "ai_patterns": "naturalizer",
        "grammar_perfection": "naturalizer",
        "punctuation": "naturalizer",
        "opening_diversity": "structure",
        "stylometry": "structure",
    }

    for metric_name, value, boost in flagged:
        area = area_map.get(metric_name, "general")
        # Scale boost by severity: the higher the score, the more boost
        severity_factor = min((value - 0.55) / 0.3, 1.0)  # 0..1
        scaled_boost = int(boost * (0.5 + 0.5 * severity_factor))
        overrides[area] = max(overrides.get(area, 0), scaled_boost)

    if verbose:
        for metric_name, value, _boost in flagged[:5]:
            area = area_map.get(metric_name, "general")
            logger.info(
                "  Adaptive: %s=%.3f (flagged) → boost %s +%d",
                metric_name, value, area, overrides.get(area, 0),
            )

    return overrides


def humanize_chunked(
    text: str,
    chunk_size: int = 5000,
    overlap: int = 200,
    lang: str = "auto",
    profile: str = "web",
    intensity: int = 60,
    preserve: dict | None = None,
    constraints: dict | None = None,
    seed: int | None = None,
    max_workers: int | None = None,
) -> HumanizeResult:
    """Process large texts by splitting into manageable chunks.

    Splits the text at paragraph or sentence boundaries, processes each
    chunk independently (optionally in parallel), then reassembles.

    Args:
        text: Text to process (any length).
        chunk_size: Target chunk size in characters (default 5000).
        overlap: Character overlap between chunks to preserve context.
        lang: Language code ('auto' for auto-detection).
        profile: Processing profile.
        intensity: Processing intensity (0-100).
        preserve: Preservation settings.
        constraints: Processing constraints.
        seed: Random seed for reproducibility.
        max_workers: Number of parallel workers (None = sequential,
            1 = sequential, 2+ = parallel threads).

    Returns:
        HumanizeResult with the fully processed text.
    """
    if not text or not text.strip():
        return HumanizeResult(
            original=text or "",
            text=text or "",
            lang=lang if lang != "auto" else "en",
            profile=profile,
            intensity=intensity,
        )

    # For small texts, just use the regular function
    if len(text) <= chunk_size:
        return humanize(
            text, lang=lang, profile=profile, intensity=intensity,
            preserve=preserve, constraints=constraints, seed=seed,
        )

    # Split into paragraph-based chunks (overlap applied between adjacent chunks)
    chunks = _split_into_chunks(text, chunk_size, overlap=overlap)

    detected_lang = lang
    if lang == "auto":
        detected_lang = detect_language(text[:2000])

    def _process_chunk(idx_chunk: tuple[int, str]) -> tuple[int, HumanizeResult]:
        i, chunk = idx_chunk
        chunk_seed = seed + i if seed is not None else None
        result = humanize(
            chunk,
            lang=detected_lang,
            profile=profile,
            intensity=intensity,
            preserve=preserve,
            constraints=constraints,
            seed=chunk_seed,
        )
        return (i, result)

    indexed_chunks = list(enumerate(chunks))
    results_map: dict[int, HumanizeResult] = {}

    if max_workers and max_workers >= 2 and len(chunks) > 1:
        # Параллельная обработка
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {
                executor.submit(_process_chunk, ic): ic[0]
                for ic in indexed_chunks
            }
            for future in as_completed(futures):
                idx, result = future.result()
                results_map[idx] = result
    else:
        # Последовательная обработка
        for ic in indexed_chunks:
            idx, result = _process_chunk(ic)
            results_map[idx] = result

    # Собираем в порядке
    ordered = [results_map[i] for i in range(len(chunks))]
    all_processed = [r.text for r in ordered]
    all_changes: list[dict] = []
    for r in ordered:
        all_changes.extend(r.changes)

    processed_text = "\n\n".join(all_processed)

    return HumanizeResult(
        original=text,
        text=processed_text,
        lang=detected_lang,
        profile=profile,
        intensity=intensity,
        changes=all_changes,
    )


def humanize_batch(
    texts: list[str],
    lang: str = "auto",
    profile: str = "web",
    intensity: int = 60,
    preserve: dict | None = None,
    constraints: dict | None = None,
    seed: int | None = None,
    on_progress: Callable[[int, int, HumanizeResult], None] | None = None,
    max_workers: int | None = None,
) -> list[HumanizeResult]:
    """Гуманизировать несколько текстов за один вызов.

    Удобная обёртка для пакетной обработки. Каждый текст обрабатывается
    независимо с собственным сидом (seed + index) для воспроизводимости.

    Args:
        texts: Список текстов для обработки.
        lang: Код языка ('auto' для автоопределения).
        profile: Профиль обработки.
        intensity: Интенсивность обработки (0-100).
        preserve: Настройки защиты элементов.
        constraints: Ограничения обработки.
        seed: Базовый сид. Для i-го текста используется seed + i.
        on_progress: Callback, вызываемый после обработки каждого текста.
            Принимает (current_index, total_count, result).
        max_workers: Число потоков (None/1 = последовательно, 2+ = параллельно).
            Внимание: при max_workers > 1 on_progress может вызываться не по порядку.

    Returns:
        Список HumanizeResult — по одному для каждого входного текста.
    """
    total = len(texts)

    def _process_item(idx: int) -> tuple[int, HumanizeResult]:
        item_seed = seed + idx if seed is not None else None
        result = humanize(
            texts[idx],
            lang=lang,
            profile=profile,
            intensity=intensity,
            preserve=preserve,
            constraints=constraints,
            seed=item_seed,
        )
        if on_progress is not None:
            on_progress(idx, total, result)
        return (idx, result)

    results_map: dict[int, HumanizeResult] = {}

    if max_workers and max_workers >= 2 and total > 1:
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {
                executor.submit(_process_item, i): i
                for i in range(total)
            }
            for future in as_completed(futures):
                idx, result = future.result()
                results_map[idx] = result
    else:
        for i in range(total):
            idx, result = _process_item(i)
            results_map[idx] = result

    return [results_map[i] for i in range(total)]


def _split_into_chunks(text: str, chunk_size: int, overlap: int = 0) -> list[str]:
    """Split text at paragraph boundaries, respecting chunk_size.

    When *overlap* > 0, the last `overlap` characters of each chunk
    are prepended to the next chunk to preserve cross-boundary context.
    """
    paragraphs = re.split(r'\n\s*\n', text)
    chunks: list[str] = []
    current: list[str] = []
    current_len = 0

    for para in paragraphs:
        para = para.strip()
        if not para:
            continue
        para_len = len(para)

        if current_len + para_len > chunk_size and current:
            chunks.append("\n\n".join(current))
            # Overlap: carry trailing characters into next chunk
            if overlap > 0:
                tail = chunks[-1][-overlap:]
                current = [tail]
                current_len = len(tail)
            else:
                current = []
                current_len = 0

        current.append(para)
        current_len += para_len

    if current:
        chunks.append("\n\n".join(current))

    return chunks if chunks else [text]


# ═══════════════════════════════════════════════════════════════
#  НОВЫЕ API ФУНКЦИИ v0.4.0
# ═══════════════════════════════════════════════════════════════

def detect_ai(text: str, lang: str = "auto") -> DetectionReport:
    """Определить вероятность AI-генерации текста.

    Использует 12 независимых статистических метрик:
    entropy, burstiness, vocabulary, zipf, stylometry,
    ai_patterns, punctuation, coherence, grammar_perfection,
    opening_diversity, readability_consistency, rhythm.

    Args:
        text: Текст для проверки (рекомендуется 100+ слов).
        lang: Код языка ('auto' для автоопределения).

    Returns:
        Словарь с результатами:
            - score (float): 0..1, вероятность AI-генерации
            - verdict (str): 'human', 'mixed', или 'ai'
            - confidence (float): 0..1, уверенность
            - metrics (dict): Подробные метрики

    Examples:
        >>> result = detect_ai("This is a remarkably compelling text.")
        >>> print(f"AI: {result['score']:.2f}, verdict: {result['verdict']}")
    """
    if not isinstance(text, str):
        raise ConfigError(f"Expected str, got {type(text).__name__}")
    if not text or not text.strip():
        return {"score": 0.0, "combined_score": 0.0, "stat_probability": None,
                "verdict": "human", "confidence": 0.0, "metrics": {}}
    MAX_DETECT_LENGTH = 1_000_000
    if len(text) > MAX_DETECT_LENGTH:
        raise InputTooLargeError(len(text), MAX_DETECT_LENGTH)

    if lang == "auto":
        lang = detect_language(text)

    det = _get_detectors()
    result = det.detect_ai(text, lang=lang)

    # Enhance with statistical detector
    try:
        sd = _get_stat_detector()
        stat_result = sd.detect_ai_statistical(text, lang=lang)
        stat_prob = stat_result.get("probability", 0.5)
    except Exception:
        stat_prob = None

    # Enhance with neural MLP detector (35→64→32→1)
    neural_prob = None
    neural_details: dict = {}
    try:
        nd_mod = _get_neural_detector()
        nd = nd_mod.NeuralAIDetector()
        neural_result = nd.detect(text, lang=lang)
        neural_prob = neural_result.get("score")
        neural_details = {
            "neural_score": neural_prob,
            "neural_verdict": neural_result.get("verdict"),
            "neural_confidence": neural_result.get("confidence"),
            "neural_top_features": neural_result.get("top_features"),
        }
    except Exception:
        pass

    # Enhance with neural perplexity (character-level LSTM)
    # Use max_chars=500 — sufficient for accurate perplexity estimation
    # while being ~4x faster than 2000. Score is derived from perplexity
    # without a second forward pass.
    neural_ppl = None
    neural_ppl_score = None
    try:
        nlm_mod = _get_neural_lm()
        nlm = nlm_mod.get_neural_lm()
        neural_ppl = nlm.perplexity(text, max_chars=500)
        # Inline score computation (avoids second forward pass)
        neural_ppl_score = max(0.0, min(1.0,
            1.0 / (1.0 + __import__('math').exp(0.8 * (neural_ppl - 4.5)))))
    except Exception:
        pass

    # Ensemble: weighted merge of 3 signals
    # Neural trained MLP is most accurate for EN/RU (trained on those).
    # For other languages, heuristic+stat get higher weight since the
    # neural model may not generalise well.
    heuristic_score = result.ai_probability
    stat_score = stat_prob if stat_prob is not None else heuristic_score
    neural_score = neural_prob if neural_prob is not None else heuristic_score

    _neural_trained_langs = {"en", "ru"}
    if lang in _neural_trained_langs:
        # High confidence in neural model for trained languages
        combined_score = (
            heuristic_score * 0.05
            + stat_score * 0.15
            + neural_score * 0.80
        )
    else:
        # For other languages: boost heuristic + stat weight
        combined_score = (
            heuristic_score * 0.35
            + stat_score * 0.30
            + neural_score * 0.35
        )

    return {
        "score": combined_score,
        "heuristic_score": result.ai_probability,
        "combined_score": combined_score,
        "stat_probability": stat_prob,
        "neural_probability": neural_prob,
        "neural_perplexity": neural_ppl,
        "neural_perplexity_score": neural_ppl_score,
        "verdict": (
            "ai" if combined_score > 0.55 else
            "mixed" if combined_score > 0.34 else
            "human"
        ),
        "confidence": result.confidence,
        "metrics": {
            "entropy": result.entropy_score,
            "burstiness": result.burstiness_score,
            "vocabulary": result.vocabulary_score,
            "zipf": result.zipf_score,
            "stylometry": result.stylometry_score,
            "ai_patterns": result.pattern_score,
            "punctuation": result.punctuation_score,
            "coherence": result.coherence_score,
            "grammar_perfection": result.grammar_score,
            "opening_diversity": result.opening_score,
            "readability_consistency": result.readability_score,
            "rhythm": result.rhythm_score,
            "perplexity": result.perplexity_score,
            "discourse": result.discourse_score,
            "semantic_repetition": result.semantic_rep_score,
            "entity_specificity": result.entity_score,
            "voice": result.voice_score,
            "topic_sentence": result.topic_sent_score,
        },
        "neural_details": neural_details,
        "explanations": result.explanations,
        "domain": result.detected_domain,
        "lang": lang,
    }


def detect_ai_fast(text: str, lang: str = "auto") -> dict:
    """Fast AI detection using only the neural MLP.

    ~3-5x faster than full detect_ai() — skips heuristic analysis,
    statistical detector, and character-level LSTM. Uses only the
    trained MLP (which accounts for 80% of the combined score).
    Suitable for in-the-loop detection where speed matters more than
    detailed metrics.

    Returns:
        dict with ``combined_score``, ``verdict``, ``confidence``.
    """
    if not text or not text.strip():
        return {"combined_score": 0.0, "verdict": "human", "confidence": 0.0}

    if lang == "auto":
        lang = detect_language(text)

    try:
        nd_mod = _get_neural_detector()
        nd = nd_mod.NeuralAIDetector()
        neural_result = nd.detect(text, lang=lang)
        score = neural_result.get("score", 0.5)
    except Exception:
        score = 0.5

    return {
        "combined_score": score,
        "verdict": (
            "ai" if score > 0.55 else
            "mixed" if score > 0.34 else
            "human"
        ),
        "confidence": min(abs(score - 0.5) * 2.0, 1.0),
    }


def detect_ai_batch(texts: list[str], lang: str = "auto") -> list[DetectionReport]:
    """Пакетная проверка текстов на AI-генерацию.

    Args:
        texts: Список текстов.
        lang: Код языка.

    Returns:
        Список результатов detect_ai для каждого текста.
    """
    return [detect_ai(t, lang=lang) for t in texts]


def detect_ai_sentences(
    text: str,
    lang: str = "auto",
    *,
    window: int = 3,
) -> list[dict]:
    """Per-sentence AI detection.

    Returns a list of dicts, one per sentence, with keys:
    text, start, end, ai_probability, label ("human"/"mixed"/"ai").

    Args:
        text: Text to analyse.
        lang: Language code (or "auto").
        window: Sliding window size in sentences.
    """
    mod = _get_detectors()
    detector = mod.AIDetector(lang=lang)
    results = detector.detect_sentences(text, lang=lang, window=window)
    return [
        {
            "text": r.text,
            "start": r.start,
            "end": r.end,
            "ai_probability": r.ai_probability,
            "label": r.label,
        }
        for r in results
    ]


def detect_ai_mixed(text: str, lang: str = "auto") -> list[dict]:
    """Detect mixed AI/human text by finding boundaries.

    Groups consecutive sentences with the same label into segments.

    Returns a list of dicts with keys:
    text, start, end, label, ai_probability, sentence_count.
    """
    mod = _get_detectors()
    detector = mod.AIDetector(lang=lang)
    segments = detector.detect_mixed(text, lang=lang)
    return [
        {
            "text": seg.text,
            "start": seg.start,
            "end": seg.end,
            "label": seg.label,
            "ai_probability": seg.ai_probability,
            "sentence_count": seg.sentence_count,
        }
        for seg in segments
    ]


def paraphrase(
    text: str,
    lang: str = "auto",
    intensity: float = 0.5,
    seed: int | None = None,
) -> str:
    """Перефразировать текст, сохраняя смысл.

    Применяет синтаксические трансформации:
    - Перестановка клауз
    - Active ↔ Passive (EN)
    - Номинализация/вербализация
    - Расщепление/объединение предложений

    Args:
        text: Текст для перефразирования.
        lang: Код языка.
        intensity: 0..1, доля предложений для изменения.
        seed: Зерно RNG.

    Returns:
        Перефразированный текст.
    """
    if lang == "auto":
        lang = detect_language(text)
    return str(_get_paraphrase().paraphrase_text(
        text, lang=lang, intensity=intensity, seed=seed
    ))


def analyze_tone(text: str, lang: str = "auto") -> dict:
    """Анализировать тональность текста.

    Определяет тон (formal, casual, academic, marketing и т.д.),
    формальность, субъективность.

    Args:
        text: Текст для анализа.
        lang: Код языка.

    Returns:
        Словарь с результатами:
            - primary_tone (str): Основной тон.
            - formality (float): 0=разговорный, 1=формальный.
            - subjectivity (float): 0=объективный, 1=субъективный.
            - scores (dict): Баллы по всем тонам.
            - confidence (float): Уверенность.
    """
    if lang == "auto":
        lang = detect_language(text)

    report = _get_tone().analyze_tone(text, lang=lang)
    return {
        "primary_tone": report.primary_tone.value,
        "formality": report.formality,
        "subjectivity": report.subjectivity,
        "scores": report.scores,
        "confidence": report.confidence,
        "markers": report.markers,
    }


def adjust_tone(
    text: str,
    target: str = "neutral",
    lang: str = "auto",
    intensity: float = 0.5,
) -> str:
    """Скорректировать тональность текста.

    Args:
        text: Текст.
        target: Целевой тон — 'formal', 'casual', 'friendly',
                'academic', 'professional', 'neutral', 'marketing'.
        lang: Код языка.
        intensity: 0..1, степень коррекции.

    Returns:
        Текст с скорректированной тональностью.
    """
    if lang == "auto":
        lang = detect_language(text)
    return str(_get_tone().adjust_tone(
        text, target=target, lang=lang, intensity=intensity
    ))


def detect_watermarks(text: str, lang: str = "auto") -> dict:
    """Обнаружить скрытые водяные знаки в тексте.

    Проверяет: zero-width символы, гомоглифы, стеганографию,
    статистические AI watermarks.

    Args:
        text: Текст для проверки.
        lang: Код языка.

    Returns:
        Словарь:
            - has_watermarks (bool): Найдены ли водяные знаки.
            - watermark_types (list): Типы обнаруженных знаков.
            - cleaned_text (str): Очищенный текст.
            - details (list): Подробности.
    """
    if lang == "auto":
        lang = detect_language(text)

    report = _get_watermark().detect_watermarks(text, lang=lang)
    return {
        "has_watermarks": report.has_watermarks,
        "watermark_types": report.watermark_types,
        "cleaned_text": report.cleaned_text,
        "details": report.details,
        "characters_removed": report.characters_removed,
        "confidence": report.confidence,
    }


def clean_watermarks(text: str, lang: str = "auto") -> str:
    """Очистить текст от водяных знаков.

    Args:
        text: Текст.
        lang: Код языка.

    Returns:
        Текст без водяных знаков.
    """
    if lang == "auto":
        lang = detect_language(text)
    return str(_get_watermark().clean_watermarks(text, lang=lang))


def spin(
    text: str,
    lang: str = "auto",
    intensity: float = 0.5,
    seed: int | None = None,
) -> str:
    """Спиннинг текста — создание уникальной версии.

    Args:
        text: Исходный текст.
        lang: Код языка.
        intensity: 0..1, доля слов для замены.
        seed: Зерно RNG.

    Returns:
        Уникальная версия текста.
    """
    if lang == "auto":
        lang = detect_language(text)
    return str(_get_spinner().spin_text(
        text, lang=lang, intensity=intensity, seed=seed
    ))


def spin_variants(
    text: str,
    count: int = 5,
    lang: str = "auto",
    intensity: float = 0.5,
) -> list[str]:
    """Сгенерировать несколько уникальных версий текста.

    Args:
        text: Исходный текст.
        count: Количество вариантов.
        lang: Код языка.
        intensity: 0..1, доля слов для замены.

    Returns:
        Список уникальных версий.
    """
    if lang == "auto":
        lang = detect_language(text)
    return list(_get_spinner().generate_variants(
        text, count=count, lang=lang, intensity=intensity
    ))


def analyze_coherence(text: str, lang: str = "auto") -> dict:
    """Анализ когерентности (связности) текста.

    Args:
        text: Текст для анализа.
        lang: Код языка.

    Returns:
        Словарь с метриками когерентности:
            - overall (float): 0..1
            - lexical_cohesion (float)
            - transition_score (float)
            - topic_consistency (float)
            - issues (list): Обнаруженные проблемы
    """
    if lang == "auto":
        lang = detect_language(text)

    coh = _get_coherence()
    analyzer = coh.CoherenceAnalyzer(lang=lang)
    report = analyzer.analyze(text)
    return {
        "overall": report.overall,
        "lexical_cohesion": report.lexical_cohesion,
        "transition_score": report.transition_score,
        "topic_consistency": report.topic_consistency,
        "sentence_opening_diversity": report.sentence_opening_diversity,
        "paragraph_count": report.paragraph_count,
        "avg_paragraph_length": report.avg_paragraph_length,
        "issues": report.issues,
    }


def full_readability(text: str, lang: str = "auto") -> dict:
    """Полный анализ читабельности текста.

    Включает: Flesch-Kincaid, Coleman-Liau, ARI, SMOG,
    Gunning Fog, Dale-Chall.

    Args:
        text: Текст для анализа.
        lang: Код языка.

    Returns:
        Словарь со всеми метриками читабельности.
    """
    if lang == "auto":
        lang = detect_language(text)

    analyzer = TextAnalyzer(lang=lang)
    return analyzer.full_readability(text)


# ═════════════════════════════════════════════════════════════
#  AUTHOR FINGERPRINT
# ═════════════════════════════════════════════════════════════

def build_author_profile(texts: list[str], lang: str = "auto") -> dict:
    """Build a style profile from reference texts by one author.

    Args:
        texts: Reference texts written by the author.
        lang: Language code.

    Returns:
        Dict with profile data (can be stored as JSON).
    """
    from texthumanize.fingerprint import AuthorFingerprint
    fp = AuthorFingerprint()
    profile = fp.build_profile(texts, lang=lang)
    # Convert to dict for serialization
    from dataclasses import asdict
    return asdict(profile)


def compare_fingerprint(
    profile: dict,
    text: str,
    lang: str | None = None,
) -> dict:
    """Compare text against an author profile.

    Args:
        profile: Profile dict from build_author_profile().
        text: New text to compare.
        lang: Language code (uses profile's lang if None).

    Returns:
        Dict with similarity, verdict, confidence, deviations.
    """
    from texthumanize.fingerprint import AuthorFingerprint, StyleProfile
    fp = AuthorFingerprint()

    # Reconstruct StyleProfile from dict
    style = StyleProfile(**{
        k: v for k, v in profile.items()
        if k in StyleProfile.__dataclass_fields__
    })

    result = fp.compare(style, text, lang=lang)
    return {
        "similarity": result.similarity,
        "verdict": result.verdict,
        "confidence": result.confidence,
        "deviations": result.deviations,
    }


# ═════════════════════════════════════════════════════════════
#  A/B DETECTION
# ═════════════════════════════════════════════════════════════

def detect_ab(
    original: str,
    processed: str,
    lang: str = "auto",
) -> dict:
    """Compare AI detection scores before and after processing.

    Useful for checking how humanization or editing changed
    the AI detection outcome.

    Args:
        original: Original text.
        processed: Processed (humanized/edited) text.
        lang: Language code.

    Returns:
        Dict with before/after scores and per-metric deltas.
    """
    before = detect_ai(original, lang=lang)
    after = detect_ai(processed, lang=lang)

    before_metrics = cast(dict[str, float], before["metrics"])
    after_metrics = cast(dict[str, float], after["metrics"])
    deltas: dict[str, float] = {}
    for key in before_metrics:
        deltas[key] = round(after_metrics[key] - before_metrics[key], 4)

    return {
        "before": {
            "score": before["score"],
            "verdict": before["verdict"],
        },
        "after": {
            "score": after["score"],
            "verdict": after["verdict"],
        },
        "score_delta": round(after["score"] - before["score"], 4),
        "metric_deltas": deltas,
        "improved": after["score"] < before["score"],
    }


# ═════════════════════════════════════════════════════════════
#  EVASION RESISTANCE SCORE
# ═════════════════════════════════════════════════════════════

def evasion_resistance(text: str, lang: str = "auto") -> dict:
    """Score how many detection signals catch this text.

    A high resistance score means the text is detectable by
    many independent metrics — harder to evade.

    Args:
        text: Text to evaluate.
        lang: Language code.

    Returns:
        Dict with resistance score, triggered metrics, and details.
    """
    result = detect_ai(text, lang=lang)
    metrics = cast(dict[str, float], result["metrics"])

    # Count how many metrics flag the text as AI
    ai_threshold = 0.55
    triggered = {k: v for k, v in metrics.items() if v >= ai_threshold}
    resistance = len(triggered) / len(metrics) if metrics else 0.0

    # Categorize strength
    if resistance >= 0.7:
        strength = "strong"
    elif resistance >= 0.4:
        strength = "moderate"
    else:
        strength = "weak"

    return {
        "resistance_score": round(resistance, 4),
        "strength": strength,
        "triggered_count": len(triggered),
        "total_metrics": len(metrics),
        "triggered_metrics": triggered,
        "overall_score": result["score"],
        "verdict": result["verdict"],
    }


# ═════════════════════════════════════════════════════════════
#  ADVERSARIAL LOOP CALIBRATION
# ═════════════════════════════════════════════════════════════

def adversarial_calibrate(
    text: str,
    lang: str = "auto",
    *,
    max_rounds: int = 5,
    target_score: float = 0.35,
    intensity: int = 50,
) -> dict:
    """Run humanize → detect loop until target score is reached.

    Repeatedly humanizes and checks AI score, adjusting parameters
    to find the minimum humanization needed to pass detection.

    Args:
        text: AI-generated text to calibrate.
        lang: Language code.
        max_rounds: Maximum humanization rounds.
        target_score: Target AI probability (stop when reached).
        intensity: Starting humanization intensity.

    Returns:
        Dict with: final_text, rounds, score_history, final_score.
    """
    if lang == "auto":
        lang = detect_language(text)

    current_text = text
    score_history: list[dict] = []

    for round_num in range(1, max_rounds + 1):
        # Detect current
        result = detect_ai(current_text, lang=lang)
        score_history.append({
            "round": round_num,
            "score": result["score"],
            "verdict": result["verdict"],
        })

        if result["score"] <= target_score:
            break

        # Humanize with current intensity
        current_text = humanize(
            current_text,
            lang=lang,
            intensity=min(intensity, 100),
        ).text

        # Increase intensity slightly for next round
        intensity = min(intensity + 10, 100)

    # Final check
    final = detect_ai(current_text, lang=lang)

    return {
        "original_score": score_history[0]["score"] if score_history else 0.0,
        "final_score": final["score"],
        "final_verdict": final["verdict"],
        "rounds": len(score_history),
        "target_reached": final["score"] <= target_score,
        "score_history": score_history,
        "final_text": current_text,
    }


def humanize_sentences(
    text: str,
    lang: str = "auto",
    *,
    profile: str = "web",
    intensity: int = 60,
    ai_threshold: float = 0.5,
    preserve: dict | None = None,
    seed: int | None = None,
) -> dict:
    """Humanize only sentences flagged as AI-generated.

    Unlike humanize(only_flagged=True) which uses a binary flag,
    this function applies graduated intensity per sentence based
    on individual AI probability scores.

    Args:
        text: Input text.
        lang: Language code.
        profile: Processing profile.
        intensity: Base intensity (0-100).
        ai_threshold: Minimum AI probability to trigger processing.
        preserve: Preservation settings.
        seed: Random seed.

    Returns:
        Dict with: text, original, sentences (list of per-sentence results),
        human_kept, ai_processed, avg_ai_before, avg_ai_after.
    """
    if lang == "auto":
        lang = detect_language(text)

    # Score each sentence
    sentences_data = detect_ai_sentences(text, lang=lang)

    result_sentences = []
    processed_parts = []
    human_count = 0
    ai_count = 0
    scores_before = []
    scores_after = []

    for sent_info in sentences_data:
        sent_text = sent_info.get("sentence", sent_info.get("text", ""))
        ai_prob = sent_info.get("ai_probability", 0.0)
        scores_before.append(ai_prob)

        if ai_prob >= ai_threshold and len(sent_text.split()) >= 3:
            # Apply graduated intensity based on AI score
            grad_intensity = min(100, int(intensity * (ai_prob / 0.8)))
            result = humanize(
                sent_text,
                lang=lang,
                profile=profile,
                intensity=grad_intensity,
                preserve=preserve,
                seed=seed,
            )
            processed_parts.append(result.text)
            result_sentences.append({
                "original": sent_text,
                "processed": result.text,
                "ai_probability": ai_prob,
                "intensity_applied": grad_intensity,
                "action": "humanized",
            })
            ai_count += 1
            # Re-score after humanization
            re_sents = detect_ai_sentences(result.text, lang=lang)
            scores_after.append(
                re_sents[0].get("ai_probability", ai_prob) if re_sents else ai_prob
            )
        else:
            processed_parts.append(sent_text)
            result_sentences.append({
                "original": sent_text,
                "processed": sent_text,
                "ai_probability": ai_prob,
                "intensity_applied": 0,
                "action": "kept",
            })
            human_count += 1
            scores_after.append(ai_prob)

    final_text = " ".join(processed_parts)

    return {
        "text": final_text,
        "original": text,
        "lang": lang,
        "sentences": result_sentences,
        "human_kept": human_count,
        "ai_processed": ai_count,
        "avg_ai_before": sum(scores_before) / len(scores_before) if scores_before else 0.0,
        "avg_ai_after": sum(scores_after) / len(scores_after) if scores_after else 0.0,
    }


def humanize_variants(
    text: str,
    lang: str = "auto",
    *,
    variants: int = 3,
    profile: str = "web",
    intensity: int = 60,
    preserve: dict | None = None,
    seed: int | None = None,
) -> list[dict]:
    """Generate multiple humanization variants for comparison.

    Each variant uses a different random seed derived from the base seed,
    producing different but valid humanizations. Results are sorted
    by quality score (best first).

    Args:
        text: Input text.
        lang: Language code.
        variants: Number of variants to generate (1-10).
        profile: Processing profile.
        intensity: Processing intensity (0-100).
        preserve: Preservation settings.
        seed: Base seed (variants derive from this).

    Returns:
        List of dicts sorted by quality, each with: text, variant_id,
        seed_used, change_ratio, quality_score, ai_score, changes_count.
    """
    import random as _rnd

    variants = max(1, min(variants, 10))

    if lang == "auto":
        lang = detect_language(text)

    base_seed = seed if seed is not None else _rnd.randint(0, 2**31)
    results: list[dict[str, Any]] = []

    for i in range(variants):
        variant_seed = base_seed + i * 7919  # prime offset
        result = humanize(
            text,
            lang=lang,
            profile=profile,
            intensity=intensity,
            preserve=preserve,
            seed=variant_seed,
        )

        # Score the variant
        ai_check = detect_ai(result.text, lang=lang)

        results.append({
            "text": result.text,
            "variant_id": i + 1,
            "seed_used": variant_seed,
            "change_ratio": round(result.change_ratio, 4),
            "quality_score": round(result.quality_score, 4),
            "ai_score": round(ai_check.get("score", 0.0), 4),
            "changes_count": len(result.changes),
            "metrics_after": result.metrics_after,
        })

    # Sort by quality: low AI score + high quality score
    results.sort(key=lambda r: (r["ai_score"], -r["quality_score"]))

    return results


def humanize_stream(
    text: str,
    lang: str = "auto",
    *,
    profile: str = "web",
    intensity: int = 60,
    preserve: dict | None = None,
    seed: int | None = None,
    chunk_size: int = 500,
) -> Generator[dict[str, Any], None, None]:
    """Stream humanized text in chunks (generator).

    Processes text paragraph-by-paragraph and yields results
    incrementally. Useful for real-time UIs and chat integrations.

    Args:
        text: Input text.
        lang: Language code.
        profile: Processing profile.
        intensity: Processing intensity (0-100).
        preserve: Preservation settings.
        seed: Random seed.
        chunk_size: Approximate characters per chunk.

    Yields:
        Dict with: chunk, chunk_index, is_last, progress (0.0-1.0),
        original_chunk.
    """
    if lang == "auto":
        lang = detect_language(text)

    # Split into paragraphs
    paragraphs = re.split(r'\n\s*\n', text)
    if not paragraphs:
        return

    total_chars = len(text)
    processed_chars = 0
    chunk_index = 0

    # Group paragraphs into chunks of approximate size
    current_chunk = []
    current_size = 0
    chunks = []

    for para in paragraphs:
        current_chunk.append(para)
        current_size += len(para)
        if current_size >= chunk_size:
            chunks.append("\n\n".join(current_chunk))
            current_chunk = []
            current_size = 0

    if current_chunk:
        chunks.append("\n\n".join(current_chunk))

    total_chunks = len(chunks)

    for i, chunk in enumerate(chunks):
        result = humanize(
            chunk,
            lang=lang,
            profile=profile,
            intensity=intensity,
            preserve=preserve,
            seed=seed + i if seed is not None else None,
        )

        processed_chars += len(chunk)
        chunk_index = i

        yield {
            "chunk": result.text,
            "chunk_index": chunk_index,
            "total_chunks": total_chunks,
            "is_last": i == total_chunks - 1,
            "progress": min(1.0, processed_chars / total_chars) if total_chars > 0 else 1.0,
            "original_chunk": chunk,
            "change_ratio": round(result.change_ratio, 4),
        }


def anonymize_style(
    text: str,
    lang: str = "auto",
    target: StylisticFingerprint | str | None = None,
    seed: int | None = None,
) -> dict:
    """Анонимизировать стилистический отпечаток текста.

    Трансформирует текст так, чтобы его стилистика отличалась от
    оригинального авторского стиля. Применимо для whistleblower
    protection, anonymous peer review, authorship privacy.

    Args:
        text: Текст для анонимизации.
        lang: Код языка (или ``"auto"``).
        target: Целевой стиль — ``StylisticFingerprint``, имя пресета
            (``'student'``, ``'copywriter'``, ``'scientist'``,
            ``'journalist'``, ``'blogger'``) или ``None`` (default
            = ``'journalist'``).
        seed: Сид для воспроизводимости.

    Returns:
        dict с ключами: ``text``, ``original``, ``target_preset``,
        ``similarity_before``, ``similarity_after``, ``changes``.

    Examples:
        >>> result = anonymize_style("My very recognizable text...", target="blogger")
        >>> print(result["similarity_before"], "→", result["similarity_after"])
    """
    from texthumanize.stylistic import StylometricAnonymizer

    detected_lang = lang
    if lang == "auto":
        detected_lang = detect_language(text)

    anonymizer = StylometricAnonymizer(lang=detected_lang, seed=seed)
    result = anonymizer.anonymize(text, target=target)

    return {
        "text": result.text,
        "original": result.original,
        "target_preset": result.target_preset,
        "similarity_before": result.similarity_before,
        "similarity_after": result.similarity_after,
        "changes": result.changes,
    }


# ══════════════════════════════════════════════════════════════════
#  ASH™ — Adaptive Statistical Humanization
#  Proprietary technology by Oleksandr K. / TextHumanize
# ══════════════════════════════════════════════════════════════════

def ash_humanize(
    text: str,
    lang: str = "auto",
    *,
    preset: str = "balanced",
    intensity: float | None = None,
    seed: int | None = None,
    use_pipeline: bool = True,
    pipeline_intensity: int = 60,
    pipeline_profile: str = "web",
) -> Any:
    """Гуманизировать текст через полный ASH™ конвейер.

    ASH™ (Adaptive Statistical Humanization) — проприетарная
    технология, объединяющая 5 методов + ядро пайплайна:

    1. Watermark Forensics™ — нейтрализация водяных знаков LLM
    2. Perplexity Sculpting™ — ремоделирование кривой перплексии
    3. Statistical Signature Transfer™ — перенос статистического отпечатка
    3½. **Base Pipeline** — 20-stage core humanization (typography,
        debureaucratization, paraphrasing, naturalization, etc.)
    4. Sentence Restructuring™ — структурная трансформация предложений
    5. Cognitive Load Modeling™ — моделирование когнитивных паттернов
    6. Adversarial Ensemble Self-Play™ — итеративная доводка

    Args:
        text: Текст для обработки.
        lang: Код языка ('auto', 'en', 'ru', 'uk').
        preset: Пресет: 'stealth', 'balanced', 'light', 'forensic', 'academic'.
        intensity: Интенсивность (0.0–1.0), перекрывает пресет.
        seed: Зерно RNG.
        use_pipeline: Использовать ядро пайплайна (рекомендуется).
        pipeline_intensity: Интенсивность ядра пайплайна (0-100).
        pipeline_profile: Профиль ядра пайплайна.

    Returns:
        ASHResult с полями: text, original_text, lang,
        watermark, perplexity, signature, cognitive, adversarial,
        elapsed_ms, steps_applied, methods_used.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.ash_engine")
    return mod.ASHEngine(
        lang=lang, seed=seed,
        pipeline_intensity=pipeline_intensity,
        pipeline_profile=pipeline_profile,
    ).humanize(
        text, preset=preset, intensity=intensity,
        use_pipeline=use_pipeline,
    )


def ash_analyze(text: str, lang: str = "auto") -> dict:
    """Комплексный ASH™ анализ текста.

    Возвращает: watermark verdict, perplexity curve,
    signature distance, cognitive uniformity, problem map.

    Args:
        text: Текст для анализа.
        lang: Код языка.

    Returns:
        dict со всеми диагностиками ASH™.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.ash_engine")
    return mod.ASHEngine(lang=lang).analyze(text)


def sculpt_perplexity(
    text: str,
    lang: str = "auto",
    intensity: float = 0.5,
    seed: int | None = None,
) -> Any:
    """Perplexity Sculpting™ — ремоделирование кривой перплексии.

    Делает кривую перплексии похожей на человеческую: вводит
    контролируемые «сюрпризы» в слишком гладкие участки.

    Args:
        text: Текст для обработки.
        lang: Код языка.
        intensity: 0.0–1.0.
        seed: Зерно RNG.

    Returns:
        SculptResult.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.perplexity_sculptor")
    return mod.PerplexitySculptor(lang=lang, seed=seed).sculpt(text, intensity)


def transfer_signature(
    text: str,
    lang: str = "auto",
    intensity: float = 0.5,
    seed: int | None = None,
) -> Any:
    """Statistical Signature Transfer™ — перенос статистического отпечатка.

    Сдвигает 30-мерный статистический отпечаток текста
    в зону человеческих текстов.

    Args:
        text: Текст для обработки.
        lang: Код языка.
        intensity: 0.0–1.0.
        seed: Зерно RNG.

    Returns:
        TransferResult.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.signature_transfer")
    return mod.SignatureTransfer(lang=lang, seed=seed).transfer(text, intensity)


def detect_statistical_watermark(text: str, lang: str = "auto") -> Any:
    """Watermark Forensics™ — обнаружение статистических водяных знаков LLM.

    Обнаруживает Kirchenbauer-style green/red token bias
    по 8 хэш-схемам.

    Args:
        text: Текст для проверки.
        lang: Код языка.

    Returns:
        ForensicResult с verdict, z_score, green_ratio.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.watermark_forensics")
    return mod.WatermarkForensics(lang=lang).detect(text)


def neutralise_watermark(
    text: str,
    lang: str = "auto",
    intensity: float = 0.5,
    seed: int | None = None,
) -> Any:
    """Watermark Forensics™ — нейтрализация водяных знаков LLM.

    Заменяет «зелёные» токены на синонимы из «красной» зоны,
    разрушая статистический паттерн watermark.

    Args:
        text: Текст для обработки.
        lang: Код языка.
        intensity: 0.0–1.0.
        seed: Зерно RNG.

    Returns:
        ForensicResult.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.watermark_forensics")
    return mod.WatermarkForensics(lang=lang, seed=seed).neutralise(text, intensity)


def model_cognition(
    text: str,
    lang: str = "auto",
    intensity: float = 0.5,
    seed: int | None = None,
) -> Any:
    """Cognitive Load Modeling™ — моделирование когнитивных паттернов.

    Вносит человеческие «дефекты»: хеджи, самокоррекции,
    усталостное упрощение к концу текста.

    Args:
        text: Текст для обработки.
        lang: Код языка.
        intensity: 0.0–1.0.
        seed: Зерно RNG.

    Returns:
        CognitiveResult.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.cognitive_model")
    return mod.CognitiveModeler(lang=lang, seed=seed).model(text, intensity)


def adversarial_humanize(
    text: str,
    lang: str = "auto",
    intensity: float = 0.5,
    max_rounds: int = 4,
    target_score: float = 0.35,
    seed: int | None = None,
) -> Any:
    """Adversarial Ensemble Self-Play™ — итеративная гуманизация.

    Три детектора строят «карту проблем» по предложениям;
    точечные правки применяются только к помеченным предложениям;
    цикл повторяется до достижения target_score или max_rounds.

    Args:
        text: Текст для обработки.
        lang: Код языка.
        intensity: Базовая интенсивность (0.0–1.0).
        max_rounds: Максимум раундов.
        target_score: Целевой AI-score.
        seed: Зерно RNG.

    Returns:
        PlayResult.
    """
    if lang == "auto":
        lang = detect_language(text)

    mod = _lazy_import("texthumanize.adversarial_play")
    return mod.AdversarialPlay(lang=lang, seed=seed).play(
        text, intensity=intensity,
        max_rounds=max_rounds, target_score=target_score,
    )


def list_ash_presets() -> dict:
    """Список доступных ASH™ пресетов.

    Returns:
        dict с конфигурацией каждого пресета:
        stealth, balanced, light, forensic, academic.
    """
    mod = _lazy_import("texthumanize.ash_engine")
    return mod.list_ash_presets()

