import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const pagePath = join(repoRoot, "content", "pages", "home.json");
const templateRoot = join(repoRoot, "templates", "pages", "home");
const shellPath = join(templateRoot, "shell.html");
const blockRoot = join(templateRoot, "blocks");

const REGION_TOKEN = {
  beforeCViz: "{{NODEDC_BLOCKS_BEFORE_CVIZ}}",
  cViz: "{{NODEDC_BLOCKS_CVIZ}}",
};

const page = JSON.parse(await readFile(pagePath, "utf8"));
const shell = await readFile(shellPath, "utf8");
const templateCache = new Map();

async function readTemplate(templateName) {
  if (!templateName || templateName.includes("..") || templateName.includes("/")) {
    throw new Error(`Unsafe template name: ${templateName}`);
  }

  if (!templateCache.has(templateName)) {
    templateCache.set(templateName, await readFile(join(blockRoot, templateName), "utf8"));
  }

  return templateCache.get(templateName);
}

function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function getByPath(source, path) {
  return path.split(".").reduce((value, key) => value?.[key], source);
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

function escapeAttr(value) {
  return escapeHtml(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;");
}

function escapeJsString(value) {
  return String(value)
    .replaceAll("\\", "\\\\")
    .replaceAll('"', '\\"')
    .replaceAll("\n", "\\n")
    .replaceAll("\r", "\\r")
    .replaceAll("\u2028", "\\u2028")
    .replaceAll("\u2029", "\\u2029")
    .replaceAll("</script", "<\\/script");
}

function renderTemplateTokens(html, block) {
  return html.replace(/\{\{\s*(text|html|attr|js):([a-zA-Z0-9_.-]+)\s*\}\}/g, (_match, mode, path) => {
    const value = getByPath(block, path);

    if (value === undefined) {
      throw new Error(`Missing token value "${path}" in block "${block.id}"`);
    }

    if (mode === "html") {
      return value == null ? "" : String(value);
    }

    if (mode === "attr") {
      return value == null ? "" : escapeAttr(value);
    }

    if (mode === "js") {
      return value == null ? "" : escapeJsString(value);
    }

    return value == null ? "" : escapeHtml(value);
  });
}

function renderEachBlocks(html, block) {
  return html.replace(/\{\{#each\s+([a-zA-Z0-9_.-]+)\s*\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, path, innerHtml) => {
    const items = getByPath(block, path);

    if (!Array.isArray(items)) {
      throw new Error(`Each token "${path}" in block "${block.id}" must point to an array`);
    }

    return items.map((item) => renderTemplateTokens(innerHtml, item ?? {})).join("");
  });
}

function scopeDuplicateIds(html, scope) {
  const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
  if (!ids.length) {
    return html;
  }

  let scoped = html.replace(/\sid="([^"]+)"/g, (_match, id) => ` id="${scope}-${id}"`);

  for (const id of ids) {
    const escaped = escapeRegExp(id);
    scoped = scoped.replace(
      new RegExp(`\\s(data-target|for|aria-controls|aria-labelledby|aria-describedby)="${escaped}"`, "g"),
      (_match, attr) => ` ${attr}="${scope}-${id}"`,
    );
    scoped = scoped.replace(new RegExp(`(href="[^"]*)#${escaped}(")`, "g"), `$1#${scope}-${id}$2`);
  }

  return scoped;
}

async function renderBlock(block, renderCounts) {
  if (block.enabled === false) {
    return "";
  }

  const templateName = block.template;
  const renderCount = renderCounts.get(templateName) ?? 0;
  renderCounts.set(templateName, renderCount + 1);

  const html = renderTemplateTokens(renderEachBlocks(await readTemplate(templateName), block), block);

  if (renderCount === 0 && block.scopeIds !== true) {
    return html;
  }

  return scopeDuplicateIds(html, block.id);
}

async function renderRegion(region) {
  const renderCounts = new Map();
  const blocks = [];

  for (const block of region.blocks ?? []) {
    blocks.push(await renderBlock(block, renderCounts));
  }

  return blocks.join("");
}

let output = renderTemplateTokens(renderEachBlocks(shell, { id: page.slug, ...page }), { id: page.slug, ...page });
for (const region of page.regions ?? []) {
  const token = REGION_TOKEN[region.id];
  if (!token) {
    throw new Error(`Unknown region: ${region.id}`);
  }

  output = output.replace(token, await renderRegion(region));
}

for (const token of Object.values(REGION_TOKEN)) {
  if (output.includes(token)) {
    throw new Error(`Unrendered token left in shell: ${token}`);
  }
}

const outputPath = join(repoRoot, page.output || "index.html");
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, output);

console.log(`Rendered ${page.slug} -> ${outputPath}`);
