"""
Проход 2: замена сущностей в документах.

Два метода:
1. apply_replacements — замена в Markdown/plain-тексте (основной)
2. replace_in_docx — замена в DOCX с сохранением форматирования
   (для опционального DOCX-выхода, будет добавлен позже)
"""

import io
import re
from typing import Dict, List


def _md_tolerant_pattern(original: str) -> str:
    r"""Паттерн, допускающий ** между частями строки (Markdown bold).

    DOCX → Markdown: жирность добавляет ** вокруг слов.
    Если ** попадает внутрь сущности (например, **ООО **"**НУБЕС**"),
    точное совпадение не срабатывает.

    Этот паттерн вставляет (?:\*{1,2})? между каждым словом/символом,
    позволяя найти сущность даже с ** внутри.

    Args:
        original: Исходная строка сущности (например, 'ООО "НУБЕС"')

    Returns:
        Regex-паттерн, находящий ту же строку с ** между частями
    """
    MD = r'(?:\*{1,2})?'
    parts = re.findall(r'[А-ЯЁа-яёA-Za-z0-9]+|[^А-ЯЁа-яёA-Za-z0-9]', original)
    if len(parts) <= 1:
        return None  # Одно слово — ** не может разорвать изнутри
    return MD + MD.join(re.escape(p) for p in parts) + MD


def apply_replacements(text: str, mapping: Dict[str, str], sorted_keys: List[str]) -> str:
    """Применить все замены из словаря mapping к строке.

    Ключи применяются в порядке убывания длины (sorted_keys).
    Это гарантирует, что более длинные совпадения заменяются раньше
    коротких (например, «ИНН 123456789012» до «ИНН 1234567890»).

    Для сущностей, начинающихся и заканчивающихся на букву/цифру,
    используются границы слова (\\b), чтобы избежать частичных замен.

    Args:
        text: Исходный текст
        mapping: Словарь {оригинал: замена}
        sorted_keys: Ключи mapping, отсортированные по длине (убывание)

    Returns:
        Текст с заменами
    """
    result = text

    for original in sorted_keys:
        replacement = mapping[original]

        # Если сущность обрамлена буквами/цифрами — используем границы слова
        if original and original[0].isalnum() and original[-1].isalnum():
            pattern = r'(?<!\w)' + re.escape(original) + r'(?!\w)'
        else:
            pattern = re.escape(original)

        new_result = re.sub(pattern, replacement, result)

        # Фоллбэк: Markdown-жирность могла разорвать строку
        md_pattern = _md_tolerant_pattern(original)
        if md_pattern is not None:
            new_result = re.sub(md_pattern, replacement, new_result)

        result = new_result

    return result


def replace_in_docx(doc, mapping: Dict[str, str], sorted_keys: List[str]) -> bytes:
    """Заменить сущности в DOCX-документе с сохранением форматирования.

    Алгоритм для каждого параграфа:
    1. Склеиваем текст всех runs в одну строку
    2. Применяем замены
    3. Пишем результат в первый run, очищаем остальные

    Это нужно потому что python-docx разбивает текст на runs
    (например, mid-docx форматирование), и сущность может быть
    разорвана между несколькими runs.

    Args:
        doc: Объект python-docx Document
        mapping: Словарь замен
        sorted_keys: Ключи mapping по убыванию длины

    Returns:
        Бинарное содержимое изменённого DOCX-файла
    """
    # ── Обработка параграфов ──
    for para in doc.paragraphs:
        if not para.runs:
            continue
        # Склеиваем все runs в одну строку
        full_text = "".join(run.text for run in para.runs)
        replaced = apply_replacements(full_text, mapping, sorted_keys)

        # Если были замены — пишем в первый run, очищаем остальные
        if replaced != full_text:
            para.runs[0].text = replaced
            for run in para.runs[1:]:
                run.text = ""

    # ── Обработка таблиц ──
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                for para in cell.paragraphs:
                    if not para.runs:
                        continue
                    full_text = "".join(run.text for run in para.runs)
                    replaced = apply_replacements(full_text, mapping, sorted_keys)

                    if replaced != full_text:
                        para.runs[0].text = replaced
                        for run in para.runs[1:]:
                            run.text = ""

    # Сохраняем в буфер
    buf = io.BytesIO()
    doc.save(buf)
    return buf.getvalue()
