from __future__ import annotations

from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
from pathlib import Path


OUTPUT_PATH = Path("protocol/Протокол_курсовой_работы_LawBot.docx")
BLACK = RGBColor(0, 0, 0)


def apply_run_format(run, *, bold: bool | None = None, font_size: int | None = None) -> None:
    if bold is not None:
        run.bold = bold
    if font_size is not None:
        run.font.size = Pt(font_size)
    run.font.name = "Times New Roman"
    run._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
    run.font.color.rgb = BLACK
    run.font.underline = False


def set_cell_text(cell, text: str, align=WD_ALIGN_PARAGRAPH.CENTER, bold: bool = False, font_size=12):
    cell.text = ""
    paragraph = cell.paragraphs[0]
    paragraph.alignment = align
    run = paragraph.add_run(text)
    apply_run_format(run, bold=bold, font_size=font_size)


def configure_document(document: Document) -> None:
    section = document.sections[0]
    section.page_width = Cm(21)
    section.page_height = Cm(29.7)
    section.top_margin = Cm(2)
    section.bottom_margin = Cm(2)
    section.left_margin = Cm(3)
    section.right_margin = Cm(1.5)
    section.different_first_page_header_footer = True

    normal = document.styles["Normal"]
    normal.font.name = "Times New Roman"
    normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
    normal.font.size = Pt(14)
    normal.font.color.rgb = BLACK
    normal.paragraph_format.first_line_indent = Cm(1.25)
    normal.paragraph_format.line_spacing = 1.5
    normal.paragraph_format.space_after = Pt(0)
    normal.paragraph_format.space_before = Pt(0)

    for style_name in ["Heading 1", "Heading 2", "Heading 3"]:
        style = document.styles[style_name]
        style.font.name = "Times New Roman"
        style._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
        style.font.size = Pt(14)
        style.font.bold = True
        style.font.color.rgb = BLACK
        style.font.underline = False
        style.paragraph_format.first_line_indent = Cm(0)
        style.paragraph_format.line_spacing = 1.5
        style.paragraph_format.space_before = Pt(12)
        style.paragraph_format.space_after = Pt(6)

    footer = section.footer
    paragraph = footer.paragraphs[0]
    paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
    add_page_number_field(paragraph)


def add_page_number_field(paragraph) -> None:
    run = paragraph.add_run()
    fld_begin = OxmlElement("w:fldChar")
    fld_begin.set(qn("w:fldCharType"), "begin")

    instr_text = OxmlElement("w:instrText")
    instr_text.set(qn("xml:space"), "preserve")
    instr_text.text = " PAGE "

    fld_separate = OxmlElement("w:fldChar")
    fld_separate.set(qn("w:fldCharType"), "separate")

    fld_end = OxmlElement("w:fldChar")
    fld_end.set(qn("w:fldCharType"), "end")

    run._r.append(fld_begin)
    run._r.append(instr_text)
    run._r.append(fld_separate)
    run._r.append(fld_end)


def add_toc(paragraph) -> None:
    run = paragraph.add_run()
    fld_begin = OxmlElement("w:fldChar")
    fld_begin.set(qn("w:fldCharType"), "begin")

    instr_text = OxmlElement("w:instrText")
    instr_text.set(qn("xml:space"), "preserve")
    instr_text.text = ' TOC \\o "1-3" \\h \\z \\u '

    fld_separate = OxmlElement("w:fldChar")
    fld_separate.set(qn("w:fldCharType"), "separate")

    placeholder = OxmlElement("w:t")
    placeholder.text = "Обновите поле содержания в Microsoft Word (ПКМ -> Обновить поле)."

    fld_end = OxmlElement("w:fldChar")
    fld_end.set(qn("w:fldCharType"), "end")

    run._r.append(fld_begin)
    run._r.append(instr_text)
    run._r.append(fld_separate)
    run._r.append(placeholder)
    run._r.append(fld_end)


def add_title_page(document: Document) -> None:
    p = document.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    apply_run_format(p.add_run("МИНОБРНАУКИ РОССИИ"), bold=True, font_size=14)

    for line in [
        "Федеральное государственное бюджетное образовательное учреждение",
        "высшего образования",
        "_______________________________",
        "_______________________________",
    ]:
        paragraph = document.add_paragraph()
        paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = paragraph.add_run(line)
        apply_run_format(run, font_size=14)

    document.add_paragraph()
    document.add_paragraph()
    document.add_paragraph()

    p = document.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = p.add_run("ПРОТОКОЛ КУРСОВОЙ РАБОТЫ")
    apply_run_format(run, bold=True, font_size=16)

    p = document.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = p.add_run(
        "на тему «Разработка Telegram-бота для юридических консультаций\n"
        "на основе технологии Retrieval-Augmented Generation»"
    )
    apply_run_format(run, font_size=14)

    document.add_paragraph()
    document.add_paragraph()

    table = document.add_table(rows=4, cols=2)
    table.alignment = WD_TABLE_ALIGNMENT.RIGHT
    table.columns[0].width = Cm(5.5)
    table.columns[1].width = Cm(7.5)
    rows = [
        ("Выполнил:", "студент группы ____________"),
        ("Ф.И.О.:", "__________________________"),
        ("Руководитель:", "__________________________"),
        ("Дата:", "__________________________"),
    ]
    for row, (left, right) in zip(table.rows, rows):
        set_cell_text(row.cells[0], left, align=WD_ALIGN_PARAGRAPH.LEFT, bold=False, font_size=14)
        set_cell_text(row.cells[1], right, align=WD_ALIGN_PARAGRAPH.LEFT, bold=False, font_size=14)

    document.add_paragraph()
    document.add_paragraph()
    document.add_paragraph()
    document.add_paragraph()
    document.add_paragraph()

    p = document.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    apply_run_format(p.add_run("Волгоград, 2026"), font_size=14)

    document.add_page_break()


def add_heading(document: Document, text: str, level: int = 1) -> None:
    paragraph = document.add_heading(text, level=level)
    paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
    for run in paragraph.runs:
        apply_run_format(run, bold=True, font_size=14)


def add_paragraph(document: Document, text: str, align=WD_ALIGN_PARAGRAPH.JUSTIFY) -> None:
    paragraph = document.add_paragraph(text)
    paragraph.alignment = align


def add_list(document: Document, items: list[str]) -> None:
    for index, item in enumerate(items, start=1):
        paragraph = document.add_paragraph(f"{index}. {item}")
        paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
        paragraph.paragraph_format.first_line_indent = Cm(0)
        paragraph.paragraph_format.left_indent = Cm(1.25)


def add_figure_placeholder(document: Document, number: int, title: str, marker: str) -> None:
    p = document.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = p.add_run(f"[МЕСТО ДЛЯ {marker.upper()}]")
    apply_run_format(run, bold=True, font_size=14)

    caption = document.add_paragraph()
    caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = caption.add_run(f"Рисунок {number} – {title}")
    apply_run_format(run, font_size=14)


def add_table_with_caption(
    document: Document,
    number: int,
    title: str,
    headers: list[str],
    rows: list[list[str]],
) -> None:
    caption = document.add_paragraph()
    caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = caption.add_run(f"Таблица {number} – {title}")
    apply_run_format(run, font_size=14)

    table = document.add_table(rows=len(rows) + 1, cols=len(headers))
    table.style = "Table Grid"
    table.alignment = WD_TABLE_ALIGNMENT.CENTER

    for cell, header in zip(table.rows[0].cells, headers):
        set_cell_text(cell, header, bold=True)

    for row_obj, row_data in zip(table.rows[1:], rows):
        for cell, value in zip(row_obj.cells, row_data):
            set_cell_text(cell, value, align=WD_ALIGN_PARAGRAPH.LEFT)

    document.add_paragraph()


def build_document() -> None:
    document = Document()
    configure_document(document)
    add_title_page(document)

    add_heading(document, "СОДЕРЖАНИЕ", level=1)
    toc_paragraph = document.add_paragraph()
    toc_paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
    add_toc(toc_paragraph)
    document.add_page_break()

    add_heading(document, "ВВЕДЕНИЕ", level=1)
    add_paragraph(
        document,
        "Развитие цифровых сервисов и широкое распространение генеративных моделей "
        "искусственного интеллекта создают предпосылки для появления специализированных "
        "информационных систем, способных предоставлять пользователю понятные и быстрые "
        "консультации в узких предметных областях. Одной из таких областей является "
        "юриспруденция, где особенно важны актуальность источников, проверяемость ответа "
        "и возможность сослаться на конкретную норму права.",
    )
    add_paragraph(
        document,
        "В рамках курсовой работы разработан Telegram-бот LawBot, предназначенный для "
        "консультирования пользователей по бытовым юридическим вопросам в правовом поле "
        "Российской Федерации. Система реализует подход Retrieval-Augmented Generation: "
        "пользовательский запрос классифицируется, затем по локальной базе нормативных "
        "документов выполняется гибридный поиск, после чего большая языковая модель "
        "формирует ответ только по найденным источникам. Такой подход позволяет объединить "
        "преимущества генеративных моделей и контролируемого поиска по локальному корпусу.",
    )
    add_paragraph(
        document,
        "Объектом исследования является процесс автоматизации первичной юридической "
        "консультации в Telegram. Предметом исследования являются методы проектирования "
        "чат-ботов, механизмы гибридного поиска по правовым документам и способы "
        "организации RAG-контура для прикладной юридической системы.",
    )
    add_paragraph(
        document,
        "Цель работы состоит в повышении доступности правовой информации для пользователя "
        "за счет разработки Telegram-бота, который принимает вопрос, подбирает релевантные "
        "нормы закона и объясняет их простым языком с сохранением ссылок на источники.",
    )
    add_paragraph(document, "Для достижения поставленной цели решены следующие задачи:")
    add_list(
        document,
        [
            "проведен анализ предметной области и существующих решений в области юридических чат-ботов;",
            "спроектированы сценарии использования, функциональные требования и архитектура системы;",
            "реализованы Telegram-бот, парсер нормативных документов, общий слой доступа к данным и RAG API;",
            "выполнено тестирование разработанного программного продукта.",
        ],
    )
    add_paragraph(
        document,
        "Работа состоит из четырех глав, заключения и списка использованных источников. "
        "В документе содержатся 10 рисунков и 4 таблицы. Первая глава посвящена анализу "
        "предметной области и подходов к разработке чат-ботов, во второй главе рассмотрены "
        "вопросы проектирования, в третьей главе описана реализация, а в четвертой приведены "
        "результаты тестирования.",
    )

    add_heading(document, "1 АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ", level=1)
    add_heading(document, "1.1 Характеристика предметной области", level=2)
    add_paragraph(
        document,
        "Предметной областью проекта является автоматизированная первичная юридическая "
        "консультация по законодательству Российской Федерации. В отличие от развлекательных "
        "или сервисных чат-ботов, юридический бот должен обеспечивать не только корректный "
        "диалог, но и высокую точность подбора правовой нормы, прозрачность происхождения "
        "ответа и сохранение истории взаимодействия. Ошибочный ответ в данной области может "
        "привести пользователя к неверным действиям, поэтому в системе сделан акцент на "
        "локальный корпус документов и обязательную ссылку на найденный источник.",
    )
    add_paragraph(
        document,
        "Для MVP-версии были выбраны наиболее частые бытовые категории вопросов: трудовые "
        "отношения, защита прав потребителей, жилье и аренда, семейные споры, долги и займы, "
        "договорные отношения, вопросы суда и процесса, а также обобщенная категория "
        "«Другое». В качестве документной базы используются федеральные нормативные акты: "
        "Конституция Российской Федерации, Гражданский кодекс РФ (части 1–4), Гражданский "
        "процессуальный кодекс РФ, Жилищный кодекс РФ, Семейный кодекс РФ, Трудовой кодекс РФ, "
        "Закон РФ «О защите прав потребителей», Федеральный закон «Об исполнительном "
        "производстве», Федеральный закон «Об ипотеке» и КоАП РФ. Источником для первичного "
        "получения текстов выбран раздел популярных документов на сайте КонсультантПлюс [1].",
    )
    add_paragraph(
        document,
        "Основная проблема пользователя в такой системе заключается в том, что ему сложно "
        "самостоятельно определить нужный нормативный акт, статью и актуальную редакцию "
        "документа. Кроме того, нормативный язык зачастую сложен для восприятия. Следовательно, "
        "бот должен решать сразу две задачи: находить релевантные правовые фрагменты и "
        "объяснять их доступным языком, не выходя за пределы найденных источников.",
    )

    add_heading(document, "1.2 Анализ существующих решений", level=2)
    add_paragraph(
        document,
        "В области юридических информационных сервисов можно выделить несколько классов "
        "решений: генеративные универсальные ИИ-ассистенты, rule-based FAQ-боты, "
        "retrieval-ориентированные справочные системы и гибридные системы, совмещающие "
        "поиск и генерацию. На практике каждое решение имеет собственные преимущества и "
        "ограничения.",
    )
    add_table_with_caption(
        document,
        1,
        "Сравнение существующих решений и подходов",
        ["Решение", "Подход", "Преимущества", "Ограничения"],
        [
            [
                "ChatGPT, GigaChat и аналогичные LLM-ассистенты",
                "generation-based",
                "Естественный диалог, гибкость формулировок, хорошие пояснения",
                "Без внешнего поиска возможны галлюцинации и отсутствие точных ссылок на нормы",
            ],
            [
                "FAQ-боты государственных и коммерческих сервисов",
                "rule-based",
                "Предсказуемость ответов, простая проверяемость сценариев",
                "Ограниченный охват, слабая работа со свободным текстом",
            ],
            [
                "Справочно-правовые системы",
                "retrieval-based",
                "Высокая полнота поиска по нормативным актам, удобная работа с документами",
                "Требуют самостоятельного анализа текста пользователем, не ориентированы на диалог в Telegram",
            ],
            [
                "Гибридные RAG-системы",
                "hybrid",
                "Сочетают поиск по корпусу и генерацию понятного ответа",
                "Сложнее в проектировании, требуют отдельного контура индексации и контроля качества",
            ],
        ],
    )
    add_paragraph(
        document,
        "Для юридической области pure generation-подход является недостаточно надежным, "
        "поскольку модель может предложить правдоподобный, но не подтвержденный нормой "
        "закона ответ. Чисто rule-based подход, напротив, слишком ограничен и плохо подходит "
        "для свободно сформулированных пользовательских ситуаций. Справочно-правовые системы "
        "эффективны как поиск по документам, однако требуют от пользователя самостоятельного "
        "изучения и интерпретации результатов. По этим причинам наиболее рациональным для "
        "поставленной задачи является гибридный RAG-подход.",
    )

    add_heading(document, "1.3 Обоснование выбранного подхода", level=2)
    add_paragraph(
        document,
        "В проекте выбран гибридный сценарий, в котором генеративная модель используется для "
        "классификации запроса, генерации поисковых фраз и финального формулирования ответа, "
        "а поиск выполняется по локальной базе нормативных актов. На первом этапе пользовательский "
        "вопрос дополняется контекстом категории, региона и типа пользователя. На втором этапе "
        "LLM формирует несколько поисковых фраз и предварительные фильтры. На третьем этапе "
        "система выполняет full-text поиск в PostgreSQL и vector search в ChromaDB, объединяет "
        "результаты и отбирает наиболее релевантные фрагменты. Финальный ответ формируется "
        "исключительно по этим фрагментам.",
    )
    add_paragraph(
        document,
        "Выбранная схема обеспечивает компромисс между качеством диалога и контролем над "
        "источниками. Она хорошо масштабируется: можно добавлять новые документы, повторно "
        "индексировать корпус, улучшать embedding-модель, не меняя пользовательский интерфейс. "
        "Именно поэтому данный подход выбран как основа архитектуры LawBot.",
    )

    add_heading(document, "2 ПРОЕКТИРОВАНИЕ ЧАТ-БОТА", level=1)
    add_heading(document, "2.1 Сценарии использования системы", level=2)
    add_paragraph(
        document,
        "Основным актором системы является пользователь Telegram, который может начать новую "
        "консультацию, выбрать категорию вопроса, описать ситуацию, указать регион, получить "
        "ответ с найденными источниками и сохранить консультацию в историю. Дополнительным "
        "актором является администратор, имеющий доступ к административным функциям управления. "
        "Также в архитектуре присутствуют внешние сервисы: источник нормативных документов, "
        "LLM-провайдер и сервисы хранения.",
    )
    add_figure_placeholder(
        document,
        1,
        "Диаграмма вариантов использования Telegram-бота LawBot",
        "диаграммы use case",
    )
    add_paragraph(
        document,
        "Ключевой пользовательский сценарий включает последовательность «выбор категории – "
        "описание ситуации – указание региона – поиск по базе – выдача ответа – сохранение "
        "в историю». Отдельно поддерживается сценарий продолжения ранее начатой консультации, "
        "в котором система учитывает краткую историю сообщений и использует ее как дополнительный "
        "контекст для генерации ответа.",
    )

    add_heading(document, "2.2 Функциональные требования", level=2)
    add_table_with_caption(
        document,
        2,
        "Ключевые функциональные требования к системе",
        ["Группа требований", "Содержание"],
        [
            [
                "Пользовательский интерфейс",
                "Система должна поддерживать команды /start, главное меню, категории вопросов, профиль и историю консультаций.",
            ],
            [
                "Юридический поиск",
                "Система должна выполнять гибридный поиск по локальному корпусу правовых документов и учитывать категорию, регион и тип пользователя.",
            ],
            [
                "Формирование ответа",
                "Система должна формировать ответ простым языком только по найденным источникам и отображать ссылки на используемые нормы.",
            ],
            [
                "Управление данными",
                "Система должна хранить пользователей, консультации, сообщения, список документов, чанки и историю RAG-запросов.",
            ],
            [
                "Подготовка корпуса",
                "Система должна скачивать, нормализовать и загружать тексты законов с сохранением версионности и исходного HTML.",
            ],
        ],
    )
    add_figure_placeholder(
        document,
        2,
        "Диаграмма требований к системе LawBot",
        "диаграммы requirements",
    )
    add_paragraph(
        document,
        "Сценарии и требования были согласованы с задачами курсовой работы. Особое внимание "
        "уделено функции контроля источников: бот не должен ссылаться на правовые нормы, которых "
        "нет среди найденных RAG-фрагментов. Это требование является критически важным для "
        "юридической предметной области.",
    )

    add_heading(document, "2.3 Архитектура решения", level=2)
    add_paragraph(
        document,
        "Архитектура системы состоит из пяти логических подсистем: Telegram-бота, парсера, "
        "общего слоя доступа к базе данных, RAG API и подсистем хранения. Telegram-бот "
        "реализован на aiogram [2] и отвечает за пользовательский диалог. Парсер выделен в "
        "отдельный CLI-модуль, который подготавливает нормативный корпус и сохраняет его в "
        "PostgreSQL [5]. FastAPI-сервис [3] выполняет индексацию в ChromaDB [6], гибридный "
        "поиск, классификацию запроса и формирование ответа. Общие модели и репозитории "
        "вынесены в пакет shared, чтобы разные сервисы работали с единым описанием данных.",
    )
    add_figure_placeholder(
        document,
        3,
        "Компонентная диаграмма системы LawBot",
        "компонентной диаграммы",
    )
    add_paragraph(
        document,
        "Физическое развертывание выполняется в Docker Compose [10]. В составе стенда работают "
        "контейнеры tgbot, api, postgredb, redisdb и chromadb. Для каждого сервиса задан healthcheck, "
        "а для логов используется драйвер json-file с ротацией. Для embedding-модели дополнительно "
        "прокинут volume, чтобы модель не загружалась заново после каждого перезапуска.",
    )
    add_figure_placeholder(
        document,
        4,
        "Схема развертывания контейнеров в Docker Compose",
        "схемы развертывания",
    )

    add_heading(document, "2.4 Проектирование хранилища данных", level=2)
    add_paragraph(
        document,
        "Для хранения структурированных данных выбрана реляционная СУБД PostgreSQL [5]. "
        "В системе используются таблицы users, consultations, messages, law_sources, law_chunks "
        "и rag_queries. Таблица users хранит профиль пользователя Telegram, consultations и messages "
        "описывают историю взаимодействия, law_sources и law_chunks содержат нормативные документы "
        "и их фрагменты, а rag_queries фиксирует сгенерированные поисковые фразы и найденные чанки. "
        "Для полнотекстового поиска в PostgreSQL используется поле tsv с русской морфологией.",
    )
    add_paragraph(
        document,
        "Векторный индекс вынесен в ChromaDB [6]. Такое разделение позволяет хранить канонический "
        "текст документов в PostgreSQL и независимо перестраивать embedding-индекс, не дублируя "
        "основную бизнес-логику. Для поддержки будущих сценариев поиска по истории консультаций "
        "и аудита источников все сообщения и RAG-запросы также сохраняются в основной базе данных.",
    )
    add_figure_placeholder(
        document,
        5,
        "ER-диаграмма и логическая модель хранения данных LawBot",
        "er-диаграммы",
    )

    add_heading(document, "3 РЕАЛИЗАЦИЯ ЧАТ-БОТА", level=1)
    add_heading(document, "3.1 Используемые технологии и среда разработки", level=2)
    add_table_with_caption(
        document,
        3,
        "Основные технологии проекта",
        ["Технология", "Назначение"],
        [
            ["Python 3.12/3.13", "Основной язык разработки сервисов"],
            ["aiogram 3.x [2]", "Telegram-бот и обработчики пользовательских состояний"],
            ["FastAPI [3]", "REST API для RAG-контура и индексации"],
            ["SQLAlchemy 2.x [4]", "ORM и общий слой доступа к базе данных"],
            ["PostgreSQL 16 [5]", "Хранение пользователей, истории консультаций и правовых документов"],
            ["ChromaDB [6]", "Векторная база данных для семантического поиска по чанкам"],
            ["Sentence Transformers [7]", "Локальная CPU-модель эмбеддингов deepvk/USER2-small [9]"],
            ["OpenRouter [8]", "OpenAI-compatible доступ к LLM openai/gpt-5.2 для классификации и генерации ответа"],
            ["Docker Compose [10]", "Контейнеризация и оркестрация сервисов"],
            ["Redis [11]", "Хранилище состояний FSM и служебных данных бота"],
        ],
    )
    add_paragraph(
        document,
        "Разработка выполнялась в виде многомодульного проекта. Для бота, API и парсера выбрана "
        "единая рабочая директория с общим пакетом shared. Такое решение позволило использовать "
        "одни и те же ORM-модели и репозитории как из Telegram-сервиса, так и из API и парсера, "
        "не дублируя схему данных.",
    )

    add_heading(document, "3.2 Реализация парсера нормативных документов", level=2)
    add_paragraph(
        document,
        "Парсер реализован как самостоятельный CLI-модуль parser. Он поддерживает команды "
        "discover, fetch, normalize, ingest и run. Команда discover анализирует страницу "
        "популярных правовых подборок на сайте КонсультантПлюс [1], формирует manifest и "
        "определяет набор документов для загрузки. Команда fetch скачивает HTML-документы и "
        "сохраняет их в raw-кеш. Команда normalize выделяет метаданные, статьи и внутреннюю "
        "структуру документа, после чего сохраняет нормализованный JSON. Команда ingest "
        "загружает полученные данные в PostgreSQL.",
    )
    add_paragraph(
        document,
        "Для каждого документа рассчитывается version_hash, что позволяет повторно запускать "
        "пайплайн без создания лишних дублей. Документы сохраняются как сущности law_sources, "
        "а статьи и их фрагменты – как law_chunks. В рамках MVP был успешно загружен набор из "
        "13 правовых источников и 5304 чанков, что покрывает ключевые бытовые юридические "
        "сценарии проекта.",
    )
    add_paragraph(
        document,
        "Сохранение исходного HTML и normalized JSON делает парсер пригодным для последующей "
        "диагностики, повторной индексации и возможной смены механизма chunking без повторной "
        "загрузки всего корпуса.",
    )

    add_heading(document, "3.3 Реализация Telegram-бота", level=2)
    add_paragraph(
        document,
        "Пользовательский интерфейс реализован в сервисе tgbot на базе aiogram [2]. Бот "
        "поддерживает главное меню, разделы «Задать вопрос», «Мои консультации», «Профиль» "
        "и «Помощь», а также административные разделы. Логика диалога построена на FSM, "
        "где отдельные состояния отвечают за выбор категории, ввод вопроса, ввод региона, "
        "продолжение консультации и редактирование профиля. Для хранения состояния используется "
        "Redis [11].",
    )
    add_paragraph(
        document,
        "В момент получения вопроса бот обращается к RAG API через отдельный модуль на базе "
        "httpx. Запрос включает идентификатор пользователя, категорию вопроса, регион, тип "
        "пользователя и при необходимости краткую историю предыдущих сообщений. После получения "
        "ответа бот отображает его пользователю и сохраняет возможность открыть или продолжить "
        "консультацию. Также реализованы суточные лимиты на число консультаций и ограничение "
        "на количество пользовательских сообщений в одной ветке консультации.",
    )
    add_figure_placeholder(
        document,
        6,
        "Главное меню Telegram-бота LawBot",
        "скриншота интерфейса",
    )
    add_figure_placeholder(
        document,
        7,
        "Пример ответа бота с найденными источниками",
        "скриншота диалога",
    )

    add_heading(document, "3.4 Реализация RAG API и гибридного поиска", level=2)
    add_paragraph(
        document,
        "RAG API реализован как отдельный FastAPI-сервис [3]. Он содержит эндпоинты /health, "
        "/api/v1/index/rebuild, /api/v1/rag/search и /api/v1/rag/answer. Во время старта сервиса "
        "инициализируется схема базы данных и запускается фоновая задача автоиндексации: если "
        "коллекция ChromaDB пуста или рассинхронизирована с PostgreSQL, сервис автоматически "
        "запускает rebuild индекса в фоне и детально логирует все этапы процесса.",
    )
    add_paragraph(
        document,
        "Алгоритм retrieval включает несколько фаз. Сначала LLM классифицирует вопрос и генерирует "
        "набор search_queries. Затем для каждой поисковой фразы выполняется полнотекстовый поиск "
        "по PostgreSQL и векторный поиск по ChromaDB. Результаты объединяются в общую таблицу "
        "оценок, сортируются и отбираются по верхнему порогу top_k. Такой механизм позволяет "
        "учитывать как точные совпадения ключевых слов, так и семантическую близость запроса "
        "и текста нормы.",
    )
    add_paragraph(
        document,
        "Векторные представления создаются локальной CPU-моделью deepvk/USER2-small [9] через "
        "библиотеку Sentence Transformers [7]. Для LLM-интеграции используется OpenRouter [8], "
        "через который подключена модель openai/gpt-5.2 по OpenAI-compatible интерфейсу. "
        "В промпте генерации ответа жёстко задано правило: использовать только те источники, "
        "которые были возвращены retrieval-контуром.",
    )
    add_figure_placeholder(
        document,
        8,
        "Фоновая индексация чанков и продовые логи RAG API",
        "скриншота логов",
    )

    add_heading(document, "3.5 Надежность и эксплуатационные особенности", level=2)
    add_paragraph(
        document,
        "Для повышения надежности проект контейнеризирован. В compose-конфигурации настроены "
        "healthcheck-проверки для PostgreSQL, Redis и API, а также ротация логов через driver "
        "json-file. Для embedding-модели настроен отдельный volume, благодаря которому модель "
        "не скачивается повторно при каждом перезапуске. Для Telegram Bot API предусмотрена "
        "поддержка proxy, параметризуемая через переменную окружения.",
    )
    add_paragraph(
        document,
        "Сервис API ведет расширенные продовые логи: фиксируются старт и завершение HTTP-запросов, "
        "загрузка embedding-модели, шаги индексации, число найденных полнотекстовых и векторных "
        "совпадений, а также параметры классификации и генерации ответа. Такая детализация "
        "позволяет анализировать производительность и находить проблемные участки в retrieval-пайплайне.",
    )

    add_heading(document, "4 ТЕСТИРОВАНИЕ ЧАТ-БОТА", level=1)
    add_heading(document, "4.1 Методика тестирования", level=2)
    add_paragraph(
        document,
        "Тестирование проекта выполнялось как на уровне отдельных сервисов, так и на уровне "
        "сквозного пользовательского сценария. Проверялись корректность парсинга документов, "
        "загрузка данных в PostgreSQL, построение индекса в ChromaDB, маршрутизация запросов "
        "через RAG API и реакция Telegram-бота на действия пользователя. Дополнительно были "
        "проверены фоновые механизмы: автоиндексация при старте API, ротация логов и сохранение "
        "кеша embedding-модели на volume.",
    )
    add_table_with_caption(
        document,
        4,
        "Основные тестовые сценарии",
        ["№", "Сценарий", "Ожидаемый результат", "Фактический результат"],
        [
            ["1", "Команда /start", "Открывается главное меню бота", "Успешно"],
            ["2", "Новая консультация", "Пользователь проходит шаги категория -> вопрос -> регион -> ответ", "Успешно"],
            ["3", "RAG answer", "API возвращает ответ со ссылками на найденные источники", "Успешно"],
            ["4", "История консультаций", "Пользователь может открыть, продолжить и удалить консультацию", "Успешно"],
            ["5", "Перестроение индекса", "API индексирует чанки в ChromaDB без ручного вмешательства", "Успешно"],
            ["6", "Повторный запуск API", "Модель эмбеддингов берется из локального volume, скачивание не повторяется", "Успешно"],
        ],
    )
    add_heading(document, "4.2 Результаты тестирования", level=2)
    add_paragraph(
        document,
        "В ходе функционального тестирования был выполнен полный пайплайн parser -> PostgreSQL -> "
        "ChromaDB -> RAG API -> Telegram-бот. После запуска parser run в базе данных присутствовали "
        "13 активных источников и 5304 чанка, что подтвердило корректность загрузки нормативного корпуса. "
        "Затем был выполнен rebuild индекса и проверены поисковые запросы по темам трудового права, "
        "защиты прав потребителей, жилья и договоров. Система корректно возвращала подборку правовых "
        "норм и формировала итоговый ответ по найденным фрагментам.",
    )
    add_paragraph(
        document,
        "При пользовательском тестировании проверялись сценарии работы с главным меню, "
        "оформление новой консультации, сохранение истории, изменение региона и типа пользователя, "
        "а также повторное продолжение старой консультации. Все ключевые сценарии были отработаны "
        "успешно, критических ошибок, приводящих к потере данных или невозможности получить ответ, "
        "не обнаружено.",
    )
    add_figure_placeholder(
        document,
        9,
        "Тестовый сценарий получения ответа по трудовому вопросу",
        "скриншота тестирования",
    )
    add_figure_placeholder(
        document,
        10,
        "Тестовый сценарий работы с историей консультаций",
        "скриншота тестирования",
    )

    add_heading(document, "ЗАКЛЮЧЕНИЕ", level=1)
    add_paragraph(
        document,
        "В результате выполнения курсовой работы был разработан Telegram-бот LawBot для "
        "юридических консультаций по законодательству Российской Федерации. В ходе работы были "
        "решены все поставленные задачи: проведен анализ предметной области, спроектированы "
        "сценарии и архитектура системы, реализованы пользовательский бот, модуль подготовки "
        "правового корпуса, общий слой работы с данными и RAG API, а также выполнено "
        "тестирование полученного решения.",
    )
    add_paragraph(
        document,
        "Практическая значимость результата заключается в том, что пользователь получает "
        "доступный интерфейс для первичной юридической консультации с опорой на локальный "
        "корпус нормативных документов. В отличие от обычных генеративных ассистентов, "
        "разработанная система контролирует происхождение ответа и ограничивает генерацию "
        "только найденными источниками.",
    )
    add_paragraph(
        document,
        "В качестве направлений дальнейшего развития можно выделить расширение корпуса "
        "документов, добавление автоматического обновления базы законов, подключение более "
        "сильных reranking-моделей, поддержку загрузки пользовательских документов и развитие "
        "аналитики по качеству retrieval и генерации ответов.",
    )

    add_heading(document, "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ", level=1)
    references = [
        "КонсультантПлюс. Популярные документы и кодексы [Электронный ресурс]. URL: https://www.consultant.ru/popular/ (дата обращения: 24.05.2026).",
        "aiogram 3 documentation [Электронный ресурс]. URL: https://docs.aiogram.dev/ (дата обращения: 24.05.2026).",
        "FastAPI Documentation [Электронный ресурс]. URL: https://fastapi.tiangolo.com/ (дата обращения: 24.05.2026).",
        "SQLAlchemy 2.0 Documentation [Электронный ресурс]. URL: https://docs.sqlalchemy.org/ (дата обращения: 24.05.2026).",
        "PostgreSQL Documentation [Электронный ресурс]. URL: https://www.postgresql.org/docs/ (дата обращения: 24.05.2026).",
        "Chroma Documentation [Электронный ресурс]. URL: https://docs.trychroma.com/ (дата обращения: 24.05.2026).",
        "Sentence Transformers Documentation [Электронный ресурс]. URL: https://www.sbert.net/ (дата обращения: 24.05.2026).",
        "OpenRouter Documentation [Электронный ресурс]. URL: https://openrouter.ai/docs (дата обращения: 24.05.2026).",
        "Модель deepvk/USER2-small на Hugging Face [Электронный ресурс]. URL: https://huggingface.co/deepvk/USER2-small (дата обращения: 24.05.2026).",
        "Docker Compose file reference [Электронный ресурс]. URL: https://docs.docker.com/reference/compose-file/ (дата обращения: 24.05.2026).",
        "Redis Documentation [Электронный ресурс]. URL: https://redis.io/docs/ (дата обращения: 24.05.2026).",
        "PlantUML language reference guide [Электронный ресурс]. URL: https://plantuml.com/ru/guide (дата обращения: 24.05.2026).",
    ]
    for index, item in enumerate(references, start=1):
        paragraph = document.add_paragraph()
        paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
        paragraph.paragraph_format.first_line_indent = Cm(0)
        paragraph.paragraph_format.left_indent = Cm(0)
        run = paragraph.add_run(f"{index}. {item}")
        run.font.name = "Times New Roman"
        run._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
        run.font.size = Pt(14)

    OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
    document.save(OUTPUT_PATH)


if __name__ == "__main__":
    build_document()
