import fs from "node:fs/promises";
import path from "node:path";
import readline from "node:readline";
import { chromium } from "playwright";
import {
  parseMarkdownToCourseModel,
  validateCourseModel,
} from "./markdown-to-onlineacademy-json.mjs";

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

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 parseArgs(argv) {
  const options = {
    markdownPath: null,
    targetUrl: null,
    execute: false,
    pauseAtEnd: false,
    headed: true,
    slowMo: 250,
    outputDir: path.resolve("artifacts/oa-runner"),
  };

  for (let i = 2; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "--markdown") {
      options.markdownPath = argv[++i];
    } else if (arg === "--url") {
      options.targetUrl = argv[++i];
    } else if (arg === "--execute") {
      options.execute = true;
    } else if (arg === "--pause-at-end") {
      options.pauseAtEnd = true;
    } else if (arg === "--headless") {
      options.headed = false;
    } else if (arg === "--slowmo") {
      options.slowMo = Number(argv[++i]);
    } else if (arg === "--output-dir") {
      options.outputDir = path.resolve(argv[++i]);
    } else {
      throw new Error(`Onbekend argument: ${arg}`);
    }
  }

  if (!options.markdownPath) {
    throw new Error("Gebruik --markdown <bestand.md>.");
  }
  if (!options.targetUrl) {
    throw new Error("Gebruik --url <onlineacademy-edit-url>.");
  }

  return options;
}

function buildPlan(courseModel) {
  return {
    training: courseModel.title,
    chapters: courseModel.chapters.map((chapter) => ({
      title: chapter.title,
      pages: chapter.pages.map((page) => ({
        title: page.title,
        blocks: page.blocks.map((block) => ({
          type: block.type,
          summary:
            block.data.title ??
            block.data.question ??
            block.data.quote ??
            block.data.caption ??
            block.data.content?.slice(0, 80) ??
            "",
        })),
      })),
    })),
  };
}

async function ensureDir(dir) {
  await fs.mkdir(dir, { recursive: true });
}

async function writeJson(filePath, value) {
  await fs.writeFile(filePath, JSON.stringify(value, null, 2), "utf8");
}

async function capture(page, outputDir, name) {
  await page.screenshot({
    path: path.join(outputDir, `${name}.png`),
    fullPage: true,
  });
}

async function waitForUserReview() {
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
    return;
  }

  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  await new Promise((resolve) => {
    rl.question("Reviewpauze actief. Druk op Enter om de browser te sluiten. ", resolve);
  });

  rl.close();
}

async function loginIfNeeded(page, email, password) {
  await page.waitForLoadState("networkidle").catch(() => {});
  const passwordField = page.locator('input[type="password"]');
  if ((await passwordField.count()) === 0) {
    return;
  }

  const emailField = page.locator('input[type="email"], input[name="email"]');
  if ((await emailField.count()) > 0 && (await emailField.first().isVisible().catch(() => false))) {
    await emailField.first().fill(email);
  }

  await passwordField.first().fill(password);
  const submitButton = page.locator('button[type="submit"]');
  await submitButton.first().click();
  await page
    .waitForURL((url) => !url.href.startsWith("https://identity.onlineacademy.nl"), {
      timeout: 10000,
    })
    .catch(async () => {
      await passwordField.first().press("Enter").catch(() => {});
      await page.waitForURL(
        (url) => !url.href.startsWith("https://identity.onlineacademy.nl"),
        { timeout: 10000 }
      );
    });
  await page.waitForLoadState("networkidle").catch(() => {});
}

async function verifyEditor(page) {
  await page.waitForLoadState("networkidle").catch(() => {});
  const editorMain = page.locator('[data-testid="editor-main"]');
  await editorMain.waitFor({ state: "visible", timeout: 15000 });
  const chapterButton = page.locator('[data-testid="page-button"]');
  await chapterButton.waitFor({ state: "visible", timeout: 15000 });
}

async function getCurrentTrainingTitle(page) {
  const titleArea = page.locator(".page-header textarea").first();
  if ((await titleArea.count()) > 0) {
    return (await titleArea.inputValue().catch(() => "")) || "";
  }
  return "";
}

async function clickUnique(locator, name) {
  const count = await locator.count();
  if (count !== 1) {
    throw new Error(`${name} verwacht exact 1 element, gevonden: ${count}`);
  }
  await locator.click();
}

async function fillUnique(locator, value, name) {
  const count = await locator.count();
  if (count !== 1) {
    throw new Error(`${name} verwacht exact 1 element, gevonden: ${count}`);
  }
  await locator.fill(value);
}

async function createPage(page, pageTitle) {
  const pageButton = page
    .locator(".sidebar-left-actions button")
    .filter({ hasText: "Pagina" });
  await clickUnique(pageButton, "Pagina knop");
  const pageTitleArea = page.locator(".page-header textarea").first();
  await pageTitleArea.waitFor({ state: "visible", timeout: 10000 });
  await pageTitleArea.fill(pageTitle);
}

async function renameCurrentPage(page, pageTitle) {
  const pageTitleArea = page.locator(".page-header textarea").first();
  await pageTitleArea.waitFor({ state: "visible", timeout: 10000 });
  await pageTitleArea.fill(pageTitle);
}

async function clearCurrentPageBlocks(page) {
  for (let attempts = 0; attempts < 50; attempts += 1) {
    const blocks = blockCards(page);
    const count = await blocks.count();
    if (count === 0) {
      return;
    }

    const deleteButton = blocks
      .nth(0)
      .locator("button")
      .filter({ hasText: "Verwijderen" })
      .first();

    if ((await deleteButton.count()) === 0) {
      return;
    }

    await deleteButton.click({ force: true });
    await page.waitForTimeout(300);
  }
}

async function renameChapterByIndex(page, chapterIndex, chapterTitle) {
  const chapterWrapper = page.locator(".sidebar-left .editor-chapter-wrapper").nth(chapterIndex);
  await chapterWrapper.waitFor({ state: "visible", timeout: 10000 });
  await chapterWrapper.locator(".chapter-header, .page__title, .input-container").first().click({
    force: true,
  });
  await page.waitForTimeout(250);

  const chapterInput = chapterWrapper
    .locator("input[type='text'], textarea, [contenteditable='true']")
    .filter({ visible: true })
    .first();
  await chapterInput.waitFor({ state: "visible", timeout: 10000 });

  const tagName = await chapterInput.evaluate((element) => element.tagName);
  if (tagName === "INPUT" || tagName === "TEXTAREA") {
    await chapterInput.fill(chapterTitle);
  } else {
    await chapterInput.click();
    await page.keyboard.press("Meta+A").catch(() => {});
    await page.keyboard.press("Control+A").catch(() => {});
    await page.keyboard.type(chapterTitle);
  }

  await page.waitForTimeout(500);
}

async function createChapter(page, chapterTitle) {
  const chapterButton = page
    .locator(".sidebar-left-actions button")
    .filter({ hasText: "Nieuw hoofdstuk" });
  const chapterWrappers = page.locator(".sidebar-left .editor-chapter-wrapper");
  const beforeCount = await chapterWrappers.count();
  await clickUnique(chapterButton, "Nieuw hoofdstuk knop");

  await page.waitForTimeout(500);
  const afterCount = await chapterWrappers.count();
  const targetIndex = afterCount > beforeCount ? afterCount - 1 : Math.max(0, afterCount - 1);
  const targetWrapper = chapterWrappers.nth(targetIndex);

  await targetWrapper.locator(".chapter-header, .page__title, .input-container").first().click({
    force: true,
  });
  await page.waitForTimeout(250);

  const sidebarChapterInput = targetWrapper
    .locator("input[type='text'], textarea, [contenteditable='true']")
    .filter({ visible: true })
    .first();

  await sidebarChapterInput.waitFor({ state: "visible", timeout: 10000 });

  const tagName = await sidebarChapterInput.evaluate((element) => element.tagName);
  if (tagName === "INPUT" || tagName === "TEXTAREA") {
    await sidebarChapterInput.fill(chapterTitle);
  } else {
    await sidebarChapterInput.click();
    await page.keyboard.press("Meta+A").catch(() => {});
    await page.keyboard.press("Control+A").catch(() => {});
    await page.keyboard.type(chapterTitle);
  }

  await page.waitForTimeout(500);
}

function blockCards(page) {
  return page.locator(
    ".page__content .page-blocks > *," +
      " .page__content .content-block-instance," +
      " .page__content .content-block"
  );
}

async function snapshotBlocks(page) {
  return page.evaluate(() => {
    const elements = Array.from(
      document.querySelectorAll(
        ".page__content .page-blocks > *, .page__content .content-block-instance, .page__content .content-block"
      )
    );
    return elements.map((element, index) => ({
      index,
      key:
        element.getAttribute("data-block-id") ??
        element.getAttribute("data-testid") ??
        element.id ??
        null,
      textSample: (element.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 120),
    }));
  });
}

async function waitForNewBlockCount(page, previousCount) {
  const locator = blockCards(page);
  await page.waitForFunction(
    (expected) => {
      const elements = document.querySelectorAll(
        ".page__content .content-block-instance, .page__content .content-block"
      );
      return elements.length > expected;
    },
    previousCount,
    { timeout: 10000 }
  );
  return locator.count();
}

async function findInsertedBlock(page, beforeSnapshot) {
  const afterSnapshot = await snapshotBlocks(page);
  const beforeKeys = new Set(
    beforeSnapshot.map((entry) => `${entry.index}|${entry.key ?? ""}|${entry.textSample}`)
  );

  for (const entry of afterSnapshot) {
    const signature = `${entry.index}|${entry.key ?? ""}|${entry.textSample}`;
    if (!beforeKeys.has(signature)) {
      return blockCards(page).nth(entry.index);
    }
  }

  if (afterSnapshot.length === beforeSnapshot.length + 1) {
    return blockCards(page).nth(afterSnapshot.length - 1);
  }

  throw new Error("Nieuw ingevoegd blok niet eenduidig gevonden.");
}

async function dragBlockIntoCanvas(page, blockTestId) {
  const source = page.locator(`[data-testid="${blockTestId}"]`);
  const sourceCount = await source.count();
  if (sourceCount !== 1) {
    throw new Error(`Blokbron ${blockTestId} niet uniek gevonden: ${sourceCount}`);
  }

  const editorMain = page.locator('[data-testid="editor-main"]');
  const sourceBox = await source.boundingBox();
  const targetBox = await editorMain.boundingBox();

  if (!sourceBox || !targetBox) {
    throw new Error(`Geen bruikbare drag/drop-coordinaten voor ${blockTestId}`);
  }

  const existingCount = await blockCards(page).count();
  let dropX = targetBox.x + targetBox.width * 0.5;
  let dropY = targetBox.y + Math.min(220, targetBox.height * 0.35);

  if (existingCount > 0) {
    const lastBlock = blockCards(page).nth(existingCount - 1);
    const lastBlockBox = await lastBlock.boundingBox();
    if (lastBlockBox) {
      dropX = lastBlockBox.x + lastBlockBox.width * 0.5;
      dropY = Math.min(
        lastBlockBox.y + lastBlockBox.height - 8,
        targetBox.y + targetBox.height - 16
      );
    }
  }

  await page.mouse.move(
    sourceBox.x + sourceBox.width / 2,
    sourceBox.y + sourceBox.height / 2
  );
  await page.mouse.down();
  await page.mouse.move(dropX, dropY, { steps: 20 });
  await page.mouse.up();
}

async function fillTextBlock(insertedBlock, block) {
  const titles = insertedBlock.locator('input[type="text"]').filter({ visible: true });
  const editors = insertedBlock.locator(".ql-editor").filter({ visible: true });
  const titleCount = await titles.count();
  const editorCount = await editors.count();

  if (block.data.title && titleCount > 0) {
    await titles.nth(0).fill(block.data.title);
  }
  if (editorCount === 0) {
    throw new Error("Geen rich text editor gevonden voor tekstblok.");
  }
  await editors.nth(0).click();
  await editors.nth(0).fill("");
  await editors.nth(0).type(
    block.data.content
      .replace(/<[^>]+>/g, " ")
      .replace(/\s+/g, " ")
      .trim()
  );
}

async function fillHeadingBlock(insertedBlock, block) {
  const titles = insertedBlock.locator('input[type="text"]').filter({ visible: true });
  const count = await titles.count();
  if (count === 0) {
    throw new Error("Geen titelveld gevonden voor kopblok.");
  }
  await titles.nth(0).fill(block.data.title);
}

async function fillQuoteBlock(insertedBlock, block) {
  const fields = insertedBlock
    .locator("input[type='text'], textarea")
    .filter({ visible: true });
  const count = await fields.count();
  if (count < 2) {
    throw new Error("Onvoldoende tekstvelden gevonden voor quoteblok.");
  }
  await fields.nth(0).fill(block.data.quote);
  await fields.nth(1).fill(block.data.author ?? "");
}

async function fillImageBlock(page, insertedBlock, block) {
  if (!block.data.sourcePath) {
    throw new Error("Afbeeldingsblok mist bronbestand.");
  }

  const absolutePath = path.resolve(block.data.sourcePath);
  await fs.access(absolutePath);

  const fileInput = insertedBlock.locator("input[type='file']").first();
  if ((await fileInput.count()) === 0) {
    throw new Error("Geen file input gevonden voor afbeeldingsblok.");
  }
  await fileInput.setInputFiles(absolutePath);
  await page.waitForTimeout(1000);

  const visibleFields = insertedBlock
    .locator("input[type='text'], textarea")
    .filter({ visible: true });

  const values = [block.data.title ?? "", block.data.caption ?? "", block.data.alt ?? ""].filter(
    (value) => value !== ""
  );

  for (let i = 0; i < values.length; i += 1) {
    if (i >= (await visibleFields.count())) {
      break;
    }
    await visibleFields.nth(i).fill(values[i]);
  }
}

async function setToggleState(toggleLocator, enabled) {
  if ((await toggleLocator.count()) === 0) {
    return false;
  }

  const current = await toggleLocator.evaluate((element) => {
    if (element instanceof HTMLInputElement && element.type === "checkbox") {
      return element.checked;
    }
    const ariaChecked = element.getAttribute("aria-checked");
    if (ariaChecked === "true") {
      return true;
    }
    if (ariaChecked === "false") {
      return false;
    }
    return element.classList.contains("checked") || element.classList.contains("active");
  });

  if (current !== enabled) {
    await toggleLocator.click({ force: true });
    await toggleLocator.page().waitForTimeout(250);
  }
  return true;
}

async function clickControlByText(insertedBlock, pattern) {
  const matched = await insertedBlock.evaluate((element, sourcePattern) => {
    const regex = new RegExp(sourcePattern, "i");
    const controls = Array.from(element.querySelectorAll("button, [role='button'], label, div"));
    for (const control of controls) {
      const text = (control.textContent ?? "").replace(/\s+/g, " ").trim();
      if (!regex.test(text)) {
        continue;
      }
      const target =
        control.closest("button, [role='button']") ??
        control.querySelector("button, [role='button']") ??
        control;
      target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
      return true;
    }
    return false;
  }, pattern.source);

  if (!matched) {
    throw new Error(`Control met label ${pattern} niet gevonden in tabelblok.`);
  }
}

async function fillTableBlock(page, insertedBlock, block) {
  const rows = block.data.rows ?? [];
  const rowCount = rows.length;
  const columnCount = rows.reduce((max, row) => Math.max(max, row.length), 0);

  if (rowCount === 0 || columnCount === 0) {
    throw new Error("Tabelblok moet minimaal 1 rij en 1 kolom hebben.");
  }
  if (rowCount > MAX_TABLE_ROWS) {
    throw new Error(`Tabelblok mag maximaal ${MAX_TABLE_ROWS} rijen hebben.`);
  }
  if (columnCount > MAX_TABLE_COLUMNS) {
    throw new Error(`Tabelblok mag maximaal ${MAX_TABLE_COLUMNS} kolommen hebben.`);
  }

  const debugInfo = await insertedBlock.evaluate((element) => ({
    controls: Array.from(element.querySelectorAll("button, label, [role='button'], th, td")).map(
      (node) => ({
        tag: node.tagName,
        text: (node.textContent ?? "").replace(/\s+/g, " ").trim(),
        ariaLabel: node.getAttribute("aria-label"),
        title: node.getAttribute("title"),
        rect: {
          x: node.getBoundingClientRect().x,
          y: node.getBoundingClientRect().y,
          width: node.getBoundingClientRect().width,
          height: node.getBoundingClientRect().height,
        },
      })
    ),
    inputs: Array.from(element.querySelectorAll("input, textarea")).map((field) => ({
      tag: field.tagName,
      type: field.getAttribute("type"),
      placeholder: field.getAttribute("placeholder"),
      value: field.value ?? "",
      visible:
        window.getComputedStyle(field).display !== "none" &&
        window.getComputedStyle(field).visibility !== "hidden",
    })),
  }));
  console.log("TABLE_DEBUG", JSON.stringify(debugInfo, null, 2));

  const visibleFields = insertedBlock
    .locator("input[type='text'], textarea")
    .filter({ visible: true });
  if ((await visibleFields.count()) > 0 && block.data.title) {
    await visibleFields.nth(0).fill(block.data.title);
  }

  const topHeaderToggle = insertedBlock
    .getByRole("switch", { name: /top header|bovenste header/i })
    .first();
  const leftHeaderToggle = insertedBlock
    .getByRole("switch", { name: /left header|linker header/i })
    .first();
  await setToggleState(topHeaderToggle, Boolean(block.data.topHeader)).catch(() => {});
  await setToggleState(leftHeaderToggle, Boolean(block.data.leftHeader)).catch(() => {});

  const headerCells = () =>
    insertedBlock.locator("textarea[placeholder='Header'], textarea[placeholder='header']").filter({ visible: true });
  const bodyCells = () =>
    insertedBlock
      .locator("textarea:not([placeholder='Header']):not([placeholder='header'])")
      .filter({ visible: true });

  const addColumnControl = async () => {
    const button = insertedBlock.locator("button").filter({ hasText: /^Add column$/ }).first();
    if ((await button.count()) === 0) {
      throw new Error("Geen plusknop voor kolom toevoegen gevonden.");
    }
    await button.click({ force: true });
  };

  const addRowControl = async () => {
    const button = insertedBlock.locator("button").filter({ hasText: /^Rij toevoegen$/ }).first();
    if ((await button.count()) === 0) {
      throw new Error("Geen plusknop voor rij toevoegen gevonden.");
    }
    await button.click({ force: true });
  };

  for (let attempts = 0; attempts < MAX_TABLE_COLUMNS + 2; attempts += 1) {
    const currentColumns = (await headerCells().count()) || columnCount;
    console.log(`TABLE_COLUMNS before attempt ${attempts + 1}: ${currentColumns}/${columnCount}`);
    if (currentColumns >= columnCount) {
      break;
    }
    await addColumnControl();
    await page.waitForTimeout(250);
    console.log(`TABLE_COLUMNS after attempt ${attempts + 1}: ${await headerCells().count()}`);
  }

  for (let attempts = 0; attempts < MAX_TABLE_ROWS + 2; attempts += 1) {
    const currentColumns = (await headerCells().count()) || columnCount;
    const currentRows = currentColumns > 0 ? Math.ceil((await bodyCells().count()) / currentColumns) : 0;
    console.log(`TABLE_ROWS before attempt ${attempts + 1}: ${currentRows}/${rowCount - (block.data.topHeader ? 1 : 0)}`);
    if (currentRows >= rowCount - (block.data.topHeader ? 1 : 0)) {
      break;
    }
    await addRowControl();
    await page.waitForTimeout(250);
    const nextColumns = (await headerCells().count()) || columnCount;
    const nextRows = nextColumns > 0 ? Math.ceil((await bodyCells().count()) / nextColumns) : 0;
    console.log(`TABLE_ROWS after attempt ${attempts + 1}: ${nextRows}`);
  }

  const headers = headerCells();
  const headerCount = await headers.count();
  if (block.data.topHeader) {
    for (let columnIndex = 0; columnIndex < Math.min(columnCount, headerCount); columnIndex += 1) {
      await headers.nth(columnIndex).fill(rows[0][columnIndex] ?? "");
    }
  }

  const body = bodyCells();
  const bodyCount = await body.count();
  const dataRows = block.data.topHeader ? rows.slice(1) : rows;
  for (let rowIndex = 0; rowIndex < dataRows.length; rowIndex += 1) {
    for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
      const flatIndex = rowIndex * columnCount + columnIndex;
      if (flatIndex >= bodyCount) {
        return;
      }
      await body.nth(flatIndex).fill(dataRows[rowIndex][columnIndex] ?? "");
    }
  }
}

function questionTextareas(insertedBlock) {
  return insertedBlock.locator("textarea").filter({ visible: true });
}

function visibleTextareas(insertedBlock) {
  return insertedBlock.locator("textarea").filter({ visible: true });
}

function visibleInputs(insertedBlock) {
  return insertedBlock.locator("input").filter({ visible: true });
}

async function answerFieldIndexes(insertedBlock) {
  return visibleTextareas(insertedBlock).evaluateAll((elements) =>
    elements
      .map((element, index) => ({
        index,
        placeholder: element.getAttribute("placeholder") ?? "",
      }))
      .filter((item) => /antwoord/i.test(item.placeholder))
      .map((item) => item.index)
  );
}

async function answerFieldCount(insertedBlock) {
  return (await answerFieldIndexes(insertedBlock)).length;
}

async function answerFieldLocator(insertedBlock, answerIndex) {
  const indexes = await answerFieldIndexes(insertedBlock);
  const fieldIndex = indexes[answerIndex];
  if (fieldIndex === undefined) {
    throw new Error(`Antwoordveld ${answerIndex + 1} niet gevonden.`);
  }
  return visibleTextareas(insertedBlock).nth(fieldIndex);
}

async function answerControlIndexes(insertedBlock, controlType) {
  return visibleInputs(insertedBlock).evaluateAll((elements, expectedType) =>
    elements
      .map((element, index) => ({
        index,
        type: element.getAttribute("type") ?? "",
      }))
      .filter((item) => item.type === expectedType)
      .map((item) => item.index),
    controlType
  );
}

async function answerControlLocator(insertedBlock, answerIndex, controlType) {
  const indexes = await answerControlIndexes(insertedBlock, controlType);
  const controlIndex = indexes[answerIndex];
  if (controlIndex === undefined) {
    throw new Error(
      `${controlType === "checkbox" ? "Checkbox" : "Radiobutton"} voor antwoord ${
        answerIndex + 1
      } niet gevonden.`
    );
  }
  return visibleInputs(insertedBlock).nth(controlIndex);
}

async function answerControlCount(insertedBlock, controlType) {
  return (await answerControlIndexes(insertedBlock, controlType)).length;
}

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

async function clickAnswerRemove(page, inputLocator) {
  const box = await inputLocator.boundingBox();
  if (!box) {
    throw new Error("Geen coordinaten gevonden voor antwoordverwijdering.");
  }
  await page.mouse.click(box.x + box.width + 22, box.y + box.height / 2);
}

async function ensureAnswerInputCount(page, insertedBlock, expectedCount) {
  const addButton = insertedBlock
    .locator("button, [role='button']")
    .filter({ hasText: "Antwoord toevoegen" })
    .first();

  for (let attempts = 0; attempts < expectedCount + 8; attempts += 1) {
    const currentCount = await answerFieldCount(insertedBlock);
    if (currentCount === expectedCount) {
      return;
    }
    if (currentCount > expectedCount) {
      await clickAnswerRemove(page, await answerFieldLocator(insertedBlock, currentCount - 1));
      await page.waitForTimeout(250);
      continue;
    }
    await addButton.click();
    await page.waitForTimeout(250);
  }

  const finalCount = await answerFieldCount(insertedBlock);
  throw new Error(
    `Onvoldoende antwoordvelden gevonden na uitbreiden. Verwacht: ${expectedCount}, gevonden: ${finalCount}.`
  );
}

async function selectChoiceAnswer(insertedBlock, answerIndex, controlType) {
  const control = await answerControlLocator(insertedBlock, answerIndex, controlType);
  await control.check({ force: true });
}

async function clearChoiceAnswers(insertedBlock, controlType) {
  const count = await answerControlCount(insertedBlock, controlType);
  for (let i = 0; i < count; i += 1) {
    const control = await answerControlLocator(insertedBlock, i, controlType);
    if (controlType === "checkbox") {
      await control.uncheck({ force: true });
    }
  }
}

function questionField(insertedBlock) {
  return insertedBlock
    .locator("textarea:not([placeholder*='Antwoord']):not([placeholder*='antwoord'])")
    .filter({ visible: true })
    .first();
}

function feedbackEditors(insertedBlock) {
  return insertedBlock.locator(".ql-editor").filter({ visible: true });
}

async function fillQuestionAndFeedback(insertedBlock, block, emptyQuestionError) {
  const field = questionField(insertedBlock);
  if ((await field.count()) === 0) {
    throw new Error(emptyQuestionError);
  }

  const question = block.data.question?.trim() ?? "";
  if (question.length > MAX_QUESTION_LENGTH) {
    throw new Error(`Vraag is langer dan ${MAX_QUESTION_LENGTH} karakters.`);
  }
  await field.fill(question);

  const feedback = block.data.feedback ? normalizedHtmlText(block.data.feedback) : "";
  if (feedback.length > MAX_FEEDBACK_LENGTH) {
    throw new Error(`Toelichting is langer dan ${MAX_FEEDBACK_LENGTH} karakters.`);
  }

  const editors = feedbackEditors(insertedBlock);
  if ((await editors.count()) > 0 && feedback) {
    await editors.nth(0).fill(feedback);
  }
}

async function fillMultipleChoiceBlock(page, insertedBlock, block) {
  const answers = block.data.answers ?? [];
  if (answers.length < 2) {
    throw new Error("Meerkeuzeblok vereist minimaal 2 antwoorden.");
  }

  await fillQuestionAndFeedback(insertedBlock, block, "Geen vraagveld gevonden voor meerkeuzeblok.");
  await ensureAnswerInputCount(page, insertedBlock, answers.length);

  for (let i = 0; i < answers.length; i += 1) {
    await (await answerFieldLocator(insertedBlock, i)).fill(answers[i].text);
  }

  const correctIndex = answers.findIndex((answer) => answer.correct);
  if (correctIndex === -1) {
    throw new Error("Meerkeuzeblok vereist exact 1 correct antwoord.");
  }
  await selectChoiceAnswer(insertedBlock, correctIndex, "radio");
}

async function fillMultipleResponseBlock(page, insertedBlock, block) {
  const answers = block.data.answers ?? [];
  const correctIndexes = answers
    .map((answer, index) => (answer.correct ? index : -1))
    .filter((index) => index !== -1);

  if (answers.length < 2) {
    throw new Error("Multi-antwoordblok vereist minimaal 2 antwoorden.");
  }
  if (answers.length > MAX_MULTI_RESPONSE_ANSWERS) {
    throw new Error(
      `Multi-antwoordblok mag maximaal ${MAX_MULTI_RESPONSE_ANSWERS} antwoorden hebben.`
    );
  }
  if (correctIndexes.length === 0) {
    throw new Error("Multi-antwoordblok vereist minimaal 1 correct antwoord.");
  }

  await fillQuestionAndFeedback(
    insertedBlock,
    block,
    "Geen vraagveld gevonden voor multi-antwoordblok."
  );
  await ensureAnswerInputCount(page, insertedBlock, answers.length);

  for (let i = 0; i < answers.length; i += 1) {
    await (await answerFieldLocator(insertedBlock, i)).fill(answers[i].text);
  }

  await clearChoiceAnswers(insertedBlock, "checkbox");
  for (const index of correctIndexes) {
    await selectChoiceAnswer(insertedBlock, index, "checkbox");
  }
}

async function fillOpenQuestionBlock(insertedBlock, block) {
  await fillQuestionAndFeedback(insertedBlock, block, "Geen velden gevonden voor open vraag.");
}

async function fillQuestionBlock(insertedBlock, block) {
  const textareas = questionTextareas(insertedBlock);
  const count = await textareas.count();
  if (count === 0) {
    throw new Error(`Geen velden gevonden voor ${block.type}.`);
  }
  await textareas.nth(0).fill(block.data.question);
}

async function fillBlock(page, insertedBlock, block) {
  switch (block.type) {
    case "text":
      await fillTextBlock(insertedBlock, block);
      return;
    case "heading":
      await fillHeadingBlock(insertedBlock, block);
      return;
    case "quote":
      await fillQuoteBlock(insertedBlock, block);
      return;
    case "image":
      await fillImageBlock(page, insertedBlock, block);
      return;
    case "table":
      await fillTableBlock(page, insertedBlock, block);
      return;
    case "multiple-choice":
      await fillMultipleChoiceBlock(page, insertedBlock, block);
      return;
    case "multiple-response":
      await fillMultipleResponseBlock(page, insertedBlock, block);
      return;
    case "open-question":
      await fillOpenQuestionBlock(insertedBlock, block);
      return;
    default:
      throw new Error(`Invullen voor bloktype ${block.type} is nog niet geïmplementeerd.`);
  }
}

function blockTypeToTestId(type) {
  const mapping = {
    text: "drag-block-text",
    heading: "drag-block-heading",
    quote: "drag-block-quote",
    image: "drag-block-image",
    table: "drag-block-table",
    "multiple-choice": "drag-block-multiple-choice",
    "multiple-response": "drag-block-multiple-response",
    "open-question": "drag-block-open-question",
  };
  const testId = mapping[type];
  if (!testId) {
    throw new Error(`Geen drag-block mapping voor ${type}`);
  }
  return testId;
}

async function executePlan(page, courseModel, outputDir) {
  if (courseModel.chapters.length === 0) {
    throw new Error("Geen hoofdstuk/pagina in markdownmodel.");
  }

  let pageSequence = 0;

  for (let chapterIndex = 0; chapterIndex < courseModel.chapters.length; chapterIndex += 1) {
    const chapter = courseModel.chapters[chapterIndex];
    if (chapter.pages.length === 0) {
      continue;
    }

    await createChapter(page, chapter.title);
    await capture(page, outputDir, `chapter-${chapterIndex + 1}-structure`);

    for (let i = 0; i < chapter.pages.length; i += 1) {
      const modelPage = chapter.pages[i];
      pageSequence += 1;
      if (i === 0) {
        await renameCurrentPage(page, modelPage.title);
      } else {
        await createPage(page, modelPage.title);
      }
      await capture(page, outputDir, `page-${pageSequence}-structure`);

      for (let j = 0; j < modelPage.blocks.length; j += 1) {
        const block = modelPage.blocks[j];
        if (!SUPPORTED_UI_BLOCKS.has(block.type)) {
          throw new Error(`Bloktype ${block.type} valt buiten MVP UI-scope.`);
        }

        const beforeCount = await blockCards(page).count();
        const beforeSnapshot = await snapshotBlocks(page);
        await dragBlockIntoCanvas(page, blockTypeToTestId(block.type));
        await waitForNewBlockCount(page, beforeCount);
        const insertedBlock = await findInsertedBlock(page, beforeSnapshot);
        await fillBlock(page, insertedBlock, block);
        await capture(page, outputDir, `page-${pageSequence}-block-${j + 1}-${block.type}`);
      }
    }
  }
}

async function main() {
  const options = parseArgs(process.argv);
  const email = process.env.OA_EMAIL;
  const password = process.env.OA_PASSWORD;
  if (!email || !password) {
    throw new Error("Zet OA_EMAIL en OA_PASSWORD in de omgeving.");
  }

  const markdown = await fs.readFile(options.markdownPath, "utf8");
  const courseModel = parseMarkdownToCourseModel(markdown);
  const validationIssues = validateCourseModel(courseModel, SUPPORTED_UI_BLOCKS);
  const plan = buildPlan(courseModel);

  await ensureDir(options.outputDir);
  await writeJson(path.join(options.outputDir, "plan.json"), plan);
  await writeJson(path.join(options.outputDir, "validation.json"), validationIssues);

  console.log("Plan:");
  console.log(JSON.stringify(plan, null, 2));

  if (validationIssues.length > 0) {
    console.log("Validatieproblemen:");
    console.log(JSON.stringify(validationIssues, null, 2));
  }

  const browser = await chromium.launch({
    headless: !options.headed,
    slowMo: options.slowMo,
  });

  try {
    const context = await browser.newContext({
      viewport: { width: 1440, height: 960 },
    });
    const page = await context.newPage();
    try {
      await page.goto(options.targetUrl, { waitUntil: "domcontentloaded" });
      await loginIfNeeded(page, email, password);
      await verifyEditor(page);
      await capture(page, options.outputDir, "editor-ready");

      const currentTitle = await getCurrentTrainingTitle(page);
      console.log(`Huidige geopende paginatitel: ${currentTitle}`);

      if (!options.execute) {
        console.log("Reviewmodus klaar. Geen muterende acties uitgevoerd.");
        return;
      }

      if (validationIssues.length > 0) {
        throw new Error("Uitvoering geblokkeerd: markdown bevat bloktypes buiten de MVP-scope.");
      }

      await executePlan(page, courseModel, options.outputDir);
      console.log("Uitvoering gestopt na reviewpunt. Er is niet opgeslagen.");
      await capture(page, options.outputDir, "review-stop");
      if (options.pauseAtEnd) {
        await waitForUserReview();
      } else {
        await page.waitForTimeout(8000);
      }
    } catch (error) {
      await capture(page, options.outputDir, "failure-state").catch(() => {});
      throw error;
    }
  } finally {
    await browser.close();
  }
}

await main();
