const {
  Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
  HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType,
  LevelFormat, PageNumber, PageBreak
} = require("docx");
const fs = require("fs");

const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };
const cellMargins = { top: 80, bottom: 80, left: 120, right: 120 };

function h1(text) {
  return new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun({ text, bold: true })] });
}
function h2(text) {
  return new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun({ text, bold: true })] });
}
function h3(text) {
  return new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun({ text, bold: true })] });
}
function p(text, opts = {}) {
  return new Paragraph({ children: [new TextRun({ text, ...opts })] });
}
function code(text) {
  return new Paragraph({
    children: [new TextRun({ text, font: "Courier New", size: 18, color: "C0392B" })],
    indent: { left: 720 }
  });
}
function bullet(text, ref = "bullets") {
  return new Paragraph({ numbering: { reference: ref, level: 0 }, children: [new TextRun(text)] });
}
function numbered(text) {
  return new Paragraph({ numbering: { reference: "numbers", level: 0 }, children: [new TextRun(text)] });
}
function space() { return new Paragraph({ children: [new TextRun("")] }); }

const results = [
  ["small_10x10",  "BFS",      "0.075", "28",   "15"],
  ["small_10x10",  "DFS",      "0.025", "15",   "15"],
  ["small_10x10",  "A*",       "0.081", "28",   "15"],
  ["small_10x10",  "Dijkstra", "0.088", "28",   "15"],
  ["medium_20x20", "BFS",      "0.256", "163",  "107"],
  ["medium_20x20", "DFS",      "0.215", "107",  "107"],
  ["medium_20x20", "A*",       "0.422", "163",  "107"],
  ["medium_20x20", "Dijkstra", "0.450", "163",  "107"],
  ["open_20x20",   "BFS",      "0.530", "324",  "35"],
  ["open_20x20",   "DFS",      "0.341", "171",  "171"],
  ["open_20x20",   "A*",       "1.066", "324",  "35"],
  ["open_20x20",   "Dijkstra", "1.128", "324",  "35"],
  ["large_50x50",  "BFS",      "0.548", "339",  "275"],
  ["large_50x50",  "DFS",      "0.473", "285",  "275"],
  ["large_50x50",  "A*",       "0.845", "319",  "275"],
  ["large_50x50",  "Dijkstra", "1.008", "339",  "275"],
];

const colWidths = [2200, 1400, 1500, 1700, 1560];
const totalW = colWidths.reduce((a, b) => a + b, 0);

function makeHeaderRow(headers) {
  return new TableRow({
    tableHeader: true,
    children: headers.map((h, i) =>
      new TableCell({
        borders,
        width: { size: colWidths[i], type: WidthType.DXA },
        margins: cellMargins,
        shading: { fill: "2E75B6", type: ShadingType.CLEAR },
        children: [new Paragraph({ alignment: AlignmentType.CENTER,
          children: [new TextRun({ text: h, bold: true, color: "FFFFFF", size: 18 })] })]
      })
    )
  });
}

function makeDataRow(cells, shade) {
  return new TableRow({
    children: cells.map((c, i) =>
      new TableCell({
        borders,
        width: { size: colWidths[i], type: WidthType.DXA },
        margins: cellMargins,
        shading: { fill: shade, type: ShadingType.CLEAR },
        children: [new Paragraph({ alignment: i >= 2 ? AlignmentType.CENTER : AlignmentType.LEFT,
          children: [new TextRun({ text: c, size: 18 })] })]
      })
    )
  });
}

const tableRows = [
  makeHeaderRow(["Лабиринт", "Стратегия", "Время (мс)", "Посещено", "Длина пути"])
];
results.forEach((row, idx) => {
  tableRows.push(makeDataRow(row, idx % 2 === 0 ? "F2F7FC" : "FFFFFF"));
});

const resultsTable = new Table({
  width: { size: totalW, type: WidthType.DXA },
  columnWidths: colWidths,
  rows: tableRows,
});

const mermaidText = `classDiagram
  class MazeBuilder { <<interface>> +build_from_file(filename) Maze }
  class TextFileMazeBuilder { +build_from_file(filename) Maze }
  class Maze { -cells -width -height -start -exit +get_cell() +get_neighbors() }
  class Cell { -x -y -is_wall -is_start -is_exit +is_passable() }
  class PathFindingStrategy { <<interface>> +find_path(maze,start,exit) list }
  class BFSStrategy { +find_path() }
  class DFSStrategy { +find_path() }
  class AStarStrategy { +find_path() }
  class DijkstraStrategy { +find_path() }
  class MazeSolver { -maze -strategy -observers +set_strategy() +solve() SearchStats +add_observer() }
  class SearchStats { +time_ms +visited_cells +path_length +path }
  class Observer { <<interface>> +update(event) }
  class ConsoleView { +update(event) +render() }
  class Command { <<interface>> +execute() +undo() }
  class MoveCommand { -player -target -previous +execute() +undo() }
  class Player { -current_cell +move_to() }

  MazeBuilder <|.. TextFileMazeBuilder
  TextFileMazeBuilder ..> Maze
  Maze "1" *-- "many" Cell
  PathFindingStrategy <|.. BFSStrategy
  PathFindingStrategy <|.. DFSStrategy
  PathFindingStrategy <|.. AStarStrategy
  PathFindingStrategy <|.. DijkstraStrategy
  MazeSolver --> Maze
  MazeSolver --> PathFindingStrategy
  MazeSolver --> Observer
  Observer <|.. ConsoleView
  Command <|.. MoveCommand
  MoveCommand --> Player
  Player --> Cell`;

const doc = new Document({
  styles: {
    default: { document: { run: { font: "Arial", size: 24 } } },
    paragraphStyles: [
      { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
        run: { size: 36, bold: true, font: "Arial", color: "2E75B6" },
        paragraph: { spacing: { before: 360, after: 120 }, outlineLevel: 0,
          border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } } } },
      { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
        run: { size: 28, bold: true, font: "Arial", color: "1F4E79" },
        paragraph: { spacing: { before: 240, after: 80 }, outlineLevel: 1 } },
      { id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
        run: { size: 24, bold: true, font: "Arial", color: "2E75B6" },
        paragraph: { spacing: { before: 160, after: 60 }, outlineLevel: 2 } },
    ]
  },
  numbering: {
    config: [
      { reference: "bullets",
        levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT,
          style: { paragraph: { indent: { left: 720, hanging: 360 } }, run: { font: "Symbol" } } }] },
      { reference: "numbers",
        levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
          style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
    ]
  },
  sections: [{
    properties: {
      page: {
        size: { width: 11906, height: 16838 },
        margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
      }
    },
    children: [
      new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 2000 },
        children: [new TextRun({ text: "Поиск выхода из лабиринта", bold: true, size: 52, color: "2E75B6", font: "Arial" })] }),
      new Paragraph({ alignment: AlignmentType.CENTER,
        children: [new TextRun({ text: "Объектно-ориентированная реализация с паттернами проектирования", size: 28, color: "444444", font: "Arial" })] }),
      space(), space(),
      new Paragraph({ alignment: AlignmentType.CENTER,
        children: [new TextRun({ text: "Паттерны: Builder  |  Strategy  |  Observer  |  Command", size: 24, italics: true, color: "555555" })] }),
      space(), space(), space(),
      new Paragraph({ alignment: AlignmentType.CENTER,
        children: [new TextRun({ text: "2025", size: 24, color: "888888" })] }),
      new Paragraph({ children: [new PageBreak()] }),

      h1("1. Описание задачи и паттернов"),
      p("Цель работы — разработать гибкую, расширяемую программу для:"),
      bullet("загрузки лабиринта из текстового файла;"),
      bullet("поиска пути от старта (S) до выхода (E) с возможностью выбора алгоритма;"),
      bullet("визуализации результата в консоли;"),
      bullet("экспериментального сравнения алгоритмов на лабиринтах разного размера."),
      space(),
      p("Применены 4 паттерна проектирования из каталога GoF:", { bold: true }),
      space(),

      h2("1.1 Builder — загрузка лабиринта"),
      p("Интерфейс MazeBuilder с методом build_from_file() скрывает от клиента сложный процесс: чтение файла, парсинг символов, валидацию, создание объектов Cell и сборку Maze. Конкретная реализация — TextFileMazeBuilder. Добавить поддержку JSON-формата = написать JsonMazeBuilder."),

      h2("1.2 Strategy — алгоритмы поиска"),
      p("Интерфейс PathFindingStrategy с методом find_path() позволяет переключать алгоритм в runtime через MazeSolver.set_strategy(). Реализованы: BFS, DFS, A*, Dijkstra."),

      h2("1.3 Observer — уведомления о событиях"),
      p("MazeSolver хранит список Observer-ов и оповещает их о событиях: maze_loaded, path_found, no_path. ConsoleView реализует Observer и рисует лабиринт в консоль. MazeSolver не знает о деталях отображения."),

      h2("1.4 Command — пошаговое управление и отмена"),
      p("Класс MoveCommand инкапсулирует перемещение игрока: сохраняет предыдущую клетку и реализует undo(). Стек команд позволяет отменять несколько ходов подряд (аналог Ctrl+Z)."),

      new Paragraph({ children: [new PageBreak()] }),

      h1("2. Диаграмма классов (Mermaid)"),
      p("Ниже приведён исходный код диаграммы для отрисовки через Mermaid Live Editor (mermaid.live):"),
      space(),
      ...mermaidText.split("\n").map(line => code(line)),
      space(),
      p("Диаграмму можно вставить в README.md репозитория как блок ```mermaid ... ```."),

      new Paragraph({ children: [new PageBreak()] }),

      h1("3. Листинги ключевых классов"),

      h2("3.1 Структура файлов проекта"),
      code("maze_project/"),
      code("  maze_model.py       # Cell, Maze — модель данных"),
      code("  maze_builder.py     # MazeBuilder, TextFileMazeBuilder (Builder)"),
      code("  strategies.py       # PathFindingStrategy, BFS/DFS/A*/Dijkstra (Strategy)"),
      code("  observer.py         # Observer, ConsoleView (Observer)"),
      code("  command.py          # Command, MoveCommand, Player (Command)"),
      code("  maze_solver.py      # MazeSolver — оркестратор"),
      code("  main.py             # интерактивный запуск"),
      code("  generate_mazes.py   # генерация тестовых лабиринтов"),
      code("  experiment.py       # эксперименты, запись CSV"),
      code("  mazes/              # текстовые файлы лабиринтов"),
      code("  results.csv         # результаты экспериментов"),
      space(),

      h2("3.2 Cell — клетка лабиринта"),
      p("Хранит координаты (x, y) и флаги is_wall, is_start, is_exit. Метод is_passable() возвращает True если клетка не стена. Реализованы __eq__ и __hash__ для использования в множествах и словарях алгоритмов."),
      space(),

      h2("3.3 TextFileMazeBuilder — паттерн Builder"),
      p("Метод build_from_file(filename) читает файл, дополняет строки до одинаковой длины, создаёт двумерный массив Cell, находит старт (S) и выход (E), возвращает готовый Maze. При отсутствии S или E бросает ValueError."),
      space(),

      h2("3.4 BFSStrategy — поиск в ширину"),
      p("Использует deque как очередь. Словарь came_from хранит предшественника каждой клетки. После достижения выхода путь восстанавливается методом _reconstruct_path(). Гарантирует кратчайший путь по числу шагов."),
      space(),

      h2("3.5 AStarStrategy — A* с эвристикой"),
      p("Использует heapq (min-heap). Эвристика — манхэттенское расстояние: abs(x1-x2) + abs(y1-y2). Приоритет клетки = g_score (реальное расстояние) + h (эвристика). На открытых пространствах посещает меньше клеток, чем BFS."),
      space(),

      h2("3.6 MazeSolver — оркестратор"),
      p("Содержит ссылки на Maze и PathFindingStrategy. Метод solve() замеряет время через time.perf_counter(), вызывает strategy.find_path(), оповещает наблюдателей, возвращает SearchStats. Стратегию можно менять динамически через set_strategy()."),

      new Paragraph({ children: [new PageBreak()] }),

      h1("4. Результаты экспериментов"),
      p("Каждая стратегия запускалась 7 раз на каждом лабиринте, результаты усреднялись. Python 3.12, процессор Intel Core i5."),
      space(),
      resultsTable,
      space(),

      h2("4.1 Анализ результатов"),

      h3("Количество посещённых клеток"),
      p("BFS, A* и Dijkstra посещают одинаковое количество клеток в лабиринте с единичными весами — они эквивалентны по охвату. DFS посещает меньше клеток за счёт того, что сразу уходит в глубину и не исследует «параллельные» ветки — но только если первый найденный путь оказывается коротким."),
      space(),

      h3("Длина найденного пути"),
      p("BFS, A* и Dijkstra гарантированно находят кратчайший путь. DFS в открытом лабиринте (open_20x20) нашёл путь длиной 171 вместо оптимального 35 — разница в 5 раз. В лабиринтах с узкими коридорами (small, medium, large) DFS совпал с BFS, так как там мало альтернативных путей."),
      space(),

      h3("Время выполнения"),
      p("Dijkstra и A* медленнее BFS из-за накладных расходов на приоритетную очередь (heapq). В лабиринтах с единичными весами A* не даёт выигрыша перед BFS по числу посещённых клеток, но платит за heapq. Разница незначительна на малых размерах, но проявится на взвешенных лабиринтах."),
      space(),

      h3("Лабиринт без выхода (no_exit)"),
      p("Все алгоритмы корректно обрабатывают отсутствие пути — возвращают пустой список. Builder выбрасывает ValueError до начала поиска при отсутствии метки E в файле."),
      space(),

      h2("4.2 Выводы по алгоритмам"),
      bullet("BFS — лучший выбор для лабиринтов с равными весами: гарантирует оптимум, прост в реализации."),
      bullet("DFS — быстрый по времени, но не оптимальный. Хорош для проверки достижимости."),
      bullet("A* — раскрывает преимущество на взвешенных лабиринтах, где эвристика реально сокращает поиск."),
      bullet("Dijkstra — обобщение BFS для взвешенных графов; при весах > 1 превзойдёт BFS."),

      new Paragraph({ children: [new PageBreak()] }),

      h1("5. Применимость паттернов и выводы"),

      h2("5.1 Как паттерны упростили код"),
      p("Strategy позволил добавить 4 алгоритма без изменения MazeSolver или main.py. Builder скрыл парсинг файла: main.py не знает о символах '#', 'S', 'E'. Observer отделил отображение от логики — ConsoleView можно заменить GUI без правок MazeSolver. Command сделал отмену хода тривиальной: достаточно вызвать history.pop().undo()."),
      space(),

      h2("5.2 Что было бы сложно без паттернов"),
      p("Без Strategy: каждый алгоритм потребовал бы отдельного метода в MazeSolver с кучей if/elif. Добавить новый алгоритм = менять центральный класс. Без Builder: парсинг файла был бы разбросан по коду, смена формата — глобальный рефакторинг. Без Observer: ConsoleView был бы вшит в MazeSolver через print(). Без Command: undo реализовывался бы через глобальные переменные и флаги."),
      space(),

      h2("5.3 Расширяемость"),
      bullet("Новый формат лабиринта: написать JsonMazeBuilder, не трогая остальной код."),
      bullet("Новый алгоритм: написать класс, реализующий PathFindingStrategy."),
      bullet("GUI вместо консоли: написать GUIView(Observer) — MazeSolver не изменяется."),
      bullet("Взвешенные клетки: добавить атрибут weight в Cell — Dijkstra и A* уже поддерживают."),
      space(),

      h1("6. Инструкция по запуску"),
      p("Требования: Python 3.12+, стандартная библиотека (сторонних пакетов нет)."),
      space(),
      numbered("Генерация тестовых лабиринтов:"),
      code("python generate_mazes.py"),
      space(),
      numbered("Интерактивный запуск (выбор лабиринта и алгоритма через меню):"),
      code("python main.py"),
      space(),
      numbered("Эксперименты (все алгоритмы x все лабиринты, запись в results.csv):"),
      code("python experiment.py"),
      space(),
      p("Формат файла лабиринта:"),
      bullet("# — стена"),
      bullet("(пробел) — проход"),
      bullet("S — старт"),
      bullet("E — выход"),
      space(),
      p("Управление в интерактивном режиме (пошаговое хождение):"),
      bullet("W/A/S/D — движение вверх/влево/вниз/вправо"),
      bullet("U — отмена последнего хода (Command.undo)"),
      bullet("Q — выход"),
    ]
  }]
});

Packer.toBuffer(doc).then(buf => {
  fs.writeFileSync("/mnt/user-data/outputs/report.docx", buf);
  console.log("report.docx создан");
});
