import fs from "node:fs/promises";
import path from "node:path";
import crypto from "node:crypto";

const SUPPORTED_BLOCK_TYPES = new Set([
  "text",
  "heading",
  "quote",
  "table",
  "image",
  "multiple-choice",
  "multiple-response",
  "open-question",
  "matching-pairs",
]);

const MAX_QUESTION_LENGTH = 350;
const MAX_FEEDBACK_LENGTH = 999;
const MAX_MULTI_RESPONSE_ANSWERS = 10;
const MAX_TABLE_COLUMNS = 5;
const MAX_TABLE_ROWS = 20;

function makeId() {
  return crypto.randomUUID();
}

function stripQuotes(value) {
  return value.replace(/^['"]|['"]$/g, "").trim();
}

function parseFrontmatter(lines) {
  const meta = {};
  if (lines[0]?.trim() !== "---") {
    return { meta, nextIndex: 0 };
  }

  let i = 1;
  for (; i < lines.length; i += 1) {
    const line = lines[i].trim();
    if (line === "---") {
      return { meta, nextIndex: i + 1 };
    }
    if (!line) {
      continue;
    }
    const separator = line.indexOf(":");
    if (separator === -1) {
      continue;
    }
    const key = line.slice(0, separator).trim();
    const value = stripQuotes(line.slice(separator + 1).trim());
    meta[key] = value;
  }

  return { meta, nextIndex: i };
}

function parseField(line) {
  const match = line.match(/^\*\*(.+?):\*\*\s*(.*)$/);
  if (!match) {
    return null;
  }
  return {
    key: match[1].trim().toLowerCase(),
    value: match[2].trim(),
  };
}

function markdownInlineToHtml(text) {
  return text
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
    .replace(/\*(.+?)\*/g, "<em>$1</em>");
}

function markdownTextToHtml(lines) {
  const html = [];
  let paragraph = [];
  let listType = null;
  let listItems = [];

  const flushParagraph = () => {
    if (paragraph.length > 0) {
      html.push(`<p>${paragraph.map(markdownInlineToHtml).join("<br>")}</p>`);
      paragraph = [];
    }
  };

  const flushList = () => {
    if (listItems.length === 0) {
      return;
    }
    const tag = listType === "ol" ? "ol" : "ul";
    html.push(
      `<${tag}>${listItems
        .map((item) => `<li>${markdownInlineToHtml(item)}</li>`)
        .join("")}</${tag}>`
    );
    listItems = [];
    listType = null;
  };

  for (const rawLine of lines) {
    const line = rawLine.trimEnd();
    const trimmed = line.trim();

    if (!trimmed) {
      flushParagraph();
      flushList();
      continue;
    }

    const bulletMatch = trimmed.match(/^[-*]\s+(.*)$/);
    const orderedMatch = trimmed.match(/^\d+\.\s+(.*)$/);

    if (bulletMatch) {
      flushParagraph();
      if (listType && listType !== "ul") {
        flushList();
      }
      listType = "ul";
      listItems.push(bulletMatch[1]);
      continue;
    }

    if (orderedMatch) {
      flushParagraph();
      if (listType && listType !== "ol") {
        flushList();
      }
      listType = "ol";
      listItems.push(orderedMatch[1]);
      continue;
    }

    flushList();
    paragraph.push(trimmed);
  }

  flushParagraph();
  flushList();

  return html.join("");
}

function htmlToPlainText(html) {
  return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
}

function readSectionLines(lines, startIndex) {
  const sectionLines = [];
  let i = startIndex;

  for (; i < lines.length; i += 1) {
    if (/^(#|##|###)\s+/.test(lines[i])) {
      break;
    }
    sectionLines.push(lines[i]);
  }

  return { sectionLines, nextIndex: i };
}

function parseTable(lines) {
  const fields = Object.fromEntries(
    lines
      .map((line) => parseField(line.trim()))
      .filter(Boolean)
      .map((field) => [field.key, field.value])
  );

  const tableLines = lines.filter((line) => /^\|.*\|$/.test(line.trim()));
  const rows = tableLines
    .filter((line) => !/^\|(?:\s*:?-+:?\s*\|)+$/.test(line.trim()))
    .map((line) =>
      line
        .trim()
        .slice(1, -1)
        .split("|")
        .map((cell) => cell.trim())
    );

  return {
    id: makeId(),
    type: "table",
    data: {
      ...(fields.titel ? { title: fields.titel } : {}),
      topHeader:
        fields["top header"]?.toLowerCase() === "aan" ||
        fields["top header"]?.toLowerCase() === "true" ||
        fields["bovenste header"]?.toLowerCase() === "aan" ||
        fields["bovenste header"]?.toLowerCase() === "true",
      leftHeader:
        fields["left header"]?.toLowerCase() === "aan" ||
        fields["left header"]?.toLowerCase() === "true" ||
        fields["linker header"]?.toLowerCase() === "aan" ||
        fields["linker header"]?.toLowerCase() === "true",
      rows,
    },
  };
}

function parseImage(lines) {
  const fields = Object.fromEntries(
    lines
      .map((line) => parseField(line.trim()))
      .filter(Boolean)
      .map((field) => [field.key, field.value])
  );

  return {
    id: makeId(),
    type: "image",
    data: {
      ...(fields.titel ? { title: fields.titel } : {}),
      ...(fields.onderschrift ? { caption: fields.onderschrift } : {}),
      ...(fields.alt ? { alt: fields.alt } : {}),
      ...(fields.bestand ? { sourcePath: fields.bestand } : {}),
    },
  };
}

function parseQuestionBlock(lines, type) {
  const answers = [];
  const pairs = [];
  let question = "";
  let feedback = "";

  for (const rawLine of lines) {
    const trimmed = rawLine.trim();
    if (!trimmed) {
      continue;
    }

    const field = parseField(trimmed);
    if (field) {
      if (field.key === "vraag" || field.key === "instructie") {
        question = field.value;
      } else if (field.key === "toelichting") {
        feedback = field.value;
      }
      continue;
    }

    const answerMatch = trimmed.match(/^-\s+\[([ xX])\]\s+(.*)$/);
    if (answerMatch) {
      answers.push({
        id: makeId(),
        text: answerMatch[2].trim(),
        ...(answerMatch[1].toLowerCase() === "x" ? { correct: true } : {}),
      });
      continue;
    }

    const pairMatch = trimmed.match(/^-\s+(.*?)\s*=\s*(.*)$/);
    if (pairMatch) {
      pairs.push({
        id: makeId(),
        left: pairMatch[1].trim(),
        right: pairMatch[2].trim(),
      });
    }
  }

  if (type === "matching-pairs") {
    return {
      id: makeId(),
      type,
      data: {
        question,
        pairs,
        ...(feedback ? { feedback: `<p>${markdownInlineToHtml(feedback)}</p>` } : {}),
      },
    };
  }

  return {
    id: makeId(),
    type,
    data: {
      question,
      ...(answers.length > 0 ? { answers } : {}),
      ...(feedback ? { feedback: `<p>${markdownInlineToHtml(feedback)}</p>` } : {}),
    },
  };
}

function parseBlock(blockType, sectionLines) {
  const trimmedLines = sectionLines.map((line) => line.trimEnd());

  switch (blockType) {
    case "Tekst": {
      const titleField = trimmedLines
        .map((line) => parseField(line.trim()))
        .find((field) => field?.key === "titel");
      const contentLines = titleField
        ? trimmedLines.filter((line) => parseField(line.trim()) === null)
        : trimmedLines;

      return {
        id: makeId(),
        type: "text",
        data: {
          ...(titleField?.value ? { title: titleField.value } : {}),
          content: markdownTextToHtml(contentLines),
        },
      };
    }
    case "Kop":
      return {
        id: makeId(),
        type: "heading",
        data: {
          title: trimmedLines.filter(Boolean).join(" ").trim(),
        },
      };
    case "Quote": {
      const fields = Object.fromEntries(
        trimmedLines
          .map((line) => parseField(line.trim()))
          .filter(Boolean)
          .map((field) => [field.key, field.value])
      );
      return {
        id: makeId(),
        type: "quote",
        data: {
          quote: fields.tekst ?? "",
          ...(fields.auteur ? { author: fields.auteur } : {}),
        },
      };
    }
    case "Tabel":
      return parseTable(trimmedLines);
    case "Afbeelding":
      return parseImage(trimmedLines);
    case "Meerkeuze":
      return parseQuestionBlock(trimmedLines, "multiple-choice");
    case "Multi antwoord":
      return parseQuestionBlock(trimmedLines, "multiple-response");
    case "Open vraag":
      return parseQuestionBlock(trimmedLines, "open-question");
    case "Koppelvraag":
      return parseQuestionBlock(trimmedLines, "matching-pairs");
    default:
      throw new Error(`Onbekend bloktype: ${blockType}`);
  }
}

export function parseMarkdownToOnlineAcademy(markdown) {
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
  const { meta, nextIndex } = parseFrontmatter(lines);
  const documentModel = {
    training: meta.training ?? "",
    chapters: [],
    examPage: {
      enabled: false,
      blocks: [],
      examSettings: {
        showFeedbackAfterExam: false,
        questionPool: false,
        numberOfQuestions: 0,
        minimumPassingScore: 60,
      },
    },
  };

  let currentChapter = null;
  let currentPage = null;
  let i = nextIndex;

  while (i < lines.length) {
    const line = lines[i].trim();

    if (!line) {
      i += 1;
      continue;
    }

    if (line.startsWith("# Module:")) {
      currentChapter = {
        id: makeId(),
        title: line.replace("# Module:", "").trim(),
        pages: [],
      };
      documentModel.chapters.push(currentChapter);
      currentPage = null;
      i += 1;
      continue;
    }

    if (line.startsWith("## Pagina:")) {
      if (!currentChapter) {
        throw new Error("Pagina gevonden voordat een module/hoofdstuk is gestart.");
      }

      currentPage = {
        id: makeId(),
        pageType: "number",
        title: line.replace("## Pagina:", "").trim(),
        blocks: [],
      };
      currentChapter.pages.push(currentPage);
      i += 1;
      continue;
    }

    if (line.startsWith("### ")) {
      if (!currentPage) {
        throw new Error("Blok gevonden voordat een pagina is gestart.");
      }

      const blockType = line.replace("### ", "").trim();
      const { sectionLines, nextIndex: afterSection } = readSectionLines(lines, i + 1);
      currentPage.blocks.push(parseBlock(blockType, sectionLines));
      i = afterSection;
      continue;
    }

    i += 1;
  }

  return {
    title: documentModel.training,
    jsonContent: JSON.stringify({
      chapters: documentModel.chapters,
      examPage: documentModel.examPage,
    }),
  };
}

export function parseMarkdownToCourseModel(markdown) {
  const result = parseMarkdownToOnlineAcademy(markdown);
  return {
    title: result.title,
    ...JSON.parse(result.jsonContent),
  };
}

export function validateCourseModel(courseModel, supportedTypes = SUPPORTED_BLOCK_TYPES) {
  const issues = [];

  for (const chapter of courseModel.chapters) {
    for (const page of chapter.pages) {
      for (const block of page.blocks) {
        if (!supportedTypes.has(block.type)) {
          issues.push(
            `Bloktype "${block.type}" op pagina "${page.title}" wordt nog niet ondersteund.`
          );
        }

        if (block.type === "image" && !block.data.sourcePath?.trim()) {
          issues.push(`Afbeeldingsblok op pagina "${page.title}" mist een Bestand-pad.`);
        }

        if (block.type === "table") {
          const rows = block.data.rows ?? [];
          const columnCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
          if (rows.length === 0) {
            issues.push(`Tabel op pagina "${page.title}" bevat geen rijen.`);
          }
          if (rows.length > MAX_TABLE_ROWS) {
            issues.push(
              `Tabel op pagina "${page.title}" mag maximaal ${MAX_TABLE_ROWS} rijen hebben.`
            );
          }
          if (columnCount === 0) {
            issues.push(`Tabel op pagina "${page.title}" bevat geen kolommen.`);
          }
          if (columnCount > MAX_TABLE_COLUMNS) {
            issues.push(
              `Tabel op pagina "${page.title}" mag maximaal ${MAX_TABLE_COLUMNS} kolommen hebben.`
            );
          }
        }

        if (
          block.type === "multiple-choice" ||
          block.type === "multiple-response" ||
          block.type === "open-question"
        ) {
          if (!block.data.question?.trim()) {
            issues.push(`Vraag ontbreekt voor bloktype "${block.type}" op pagina "${page.title}".`);
          }
          if ((block.data.question?.trim().length ?? 0) > MAX_QUESTION_LENGTH) {
            issues.push(
              `Vraag voor bloktype "${block.type}" op pagina "${page.title}" is langer dan ${MAX_QUESTION_LENGTH} karakters.`
            );
          }
          if (
            block.data.feedback &&
            htmlToPlainText(block.data.feedback).length > MAX_FEEDBACK_LENGTH
          ) {
            issues.push(
              `Toelichting voor bloktype "${block.type}" op pagina "${page.title}" is langer dan ${MAX_FEEDBACK_LENGTH} karakters.`
            );
          }
        }

        if (block.type === "multiple-choice") {
          const answers = block.data.answers ?? [];
          const correctCount = answers.filter((answer) => answer.correct).length;
          if (answers.length < 2) {
            issues.push(
              `Meerkeuzevraag op pagina "${page.title}" moet minimaal 2 antwoorden hebben.`
            );
          }
          if (correctCount !== 1) {
            issues.push(
              `Meerkeuzevraag op pagina "${page.title}" moet exact 1 correct antwoord hebben.`
            );
          }
        }

        if (block.type === "multiple-response") {
          const answers = block.data.answers ?? [];
          const correctCount = answers.filter((answer) => answer.correct).length;
          if (answers.length < 2) {
            issues.push(
              `Multi-antwoordvraag op pagina "${page.title}" moet minimaal 2 antwoorden hebben.`
            );
          }
          if (answers.length > MAX_MULTI_RESPONSE_ANSWERS) {
            issues.push(
              `Multi-antwoordvraag op pagina "${page.title}" mag maximaal ${MAX_MULTI_RESPONSE_ANSWERS} antwoorden hebben.`
            );
          }
          if (correctCount < 1) {
            issues.push(
              `Multi-antwoordvraag op pagina "${page.title}" moet minimaal 1 correct antwoord hebben.`
            );
          }
        }
      }
    }
  }

  return issues;
}

async function main() {
  const inputPath = process.argv[2];
  const outputPath = process.argv[3];

  if (!inputPath) {
    throw new Error(
      "Gebruik: node markdown-to-onlineacademy-json.mjs <input.md> [output.json]"
    );
  }

  const markdown = await fs.readFile(inputPath, "utf8");
  const result = parseMarkdownToOnlineAcademy(markdown);
  const resolvedOutput =
    outputPath ?? path.join(path.dirname(inputPath), `${path.basename(inputPath, path.extname(inputPath))}.onlineacademy.json`);

  await fs.writeFile(resolvedOutput, JSON.stringify(result, null, 2), "utf8");
  console.log(`Dry-run JSON geschreven naar ${resolvedOutput}`);
}

if (import.meta.url === `file://${process.argv[1]}`) {
  await main();
}
