#!/usr/bin/env node
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { basename, dirname, extname, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
  createCategoryNavigation,
  flattenCategoryNavigation
} from "../dist/taxonomy.js";

const root = process.cwd();
const args = parseArgs(process.argv.slice(2));

if (args.help || args.h) {
  printHelp();
  process.exit(0);
}

const navigationFile = resolve(root, args.navigation ?? args.taxonomy ?? defaultNavigationPath());
const categoriesDir = resolve(root, args["categories-dir"] ?? defaultCategoriesDir());
const templateFile = resolve(root, args.template ?? defaultTemplatePath());
const exportName = args.export;
const pageRoot = args["page-root"] ?? "/categories/";
const componentTag = args["component-tag"] ?? "orderly-category-page";
const siteTitle = args["site-title"] ?? defaultSiteTitle();
const generatedMarker = `<!-- Generated by orderly-generate-category-pages from ${relativePath(navigationFile)}. Do not edit directly. -->`;
const dryRun = Boolean(args["dry-run"]);
const cleanOnly = Boolean(args.clean);

if (!existsSync(navigationFile)) {
  throw new Error(`Navigation file not found: ${relativePath(navigationFile)}`);
}

const navigationModule = await loadNavigationModule(navigationFile);
const definitions = navigationDefinitions(navigationModule, exportName);
if (!Array.isArray(definitions)) {
  const expected = exportName ? `an array named ${exportName}` : "navigationDefinitions";
  throw new Error(`${relativePath(navigationFile)} must export ${expected}.`);
}

const navigation = createCategoryNavigation(definitions, {
  pageRoot,
  urlMode: "path"
});
const categories = flattenCategoryNavigation(navigation);

if (cleanOnly) {
  const removed = cleanGeneratedPages();
  console.log(`Removed ${removed} generated category page${removed === 1 ? "" : "s"}.`);
  process.exit(0);
}

if (!existsSync(templateFile)) {
  throw new Error(`Category page template not found: ${relativePath(templateFile)}`);
}

const template = readFileSync(templateFile, "utf8");
const pages = categories.map((category) => {
  const file = resolve(categoriesDir, ...category.metadata.pathSegments, "index.html");
  return {
    category,
    file,
    html: renderCategoryPage(template, category)
  };
});

if (dryRun) {
  for (const page of pages) {
    console.log(`${page.category.id} -> ${relativePath(page.file)}`);
  }
  console.log(`Would generate ${pages.length} category page${pages.length === 1 ? "" : "s"}.`);
  process.exit(0);
}

cleanGeneratedPages();
for (const page of pages) {
  mkdirSync(dirname(page.file), { recursive: true });
  writeFileSync(page.file, page.html, "utf8");
}
console.log(`Generated ${pages.length} category page${pages.length === 1 ? "" : "s"}.`);

function renderCategoryPage(templateHtml, category) {
  let html = templateHtml.replace("<!doctype html>", `<!doctype html>\n${generatedMarker}`);
  html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(category.label)} | ${escapeHtml(siteTitle)}</title>`);
  html = replaceCategoryElement(html, category);
  html = injectDescription(html, category.metadata.description);
  return html;
}

function replaceCategoryElement(html, category) {
  const emptyElement = `<${componentTag}></${componentTag}>`;
  if (html.includes(emptyElement)) {
    return html.replace(emptyElement, `<${componentTag} slug="${escapeAttribute(category.id)}"></${componentTag}>`);
  }
  const selfClosingPattern = new RegExp(`<${escapeRegExp(componentTag)}\\b([^>]*)/>`);
  if (selfClosingPattern.test(html)) {
    return html.replace(selfClosingPattern, (_match, attributes) => `<${componentTag}${setAttribute(attributes, "slug", category.id)}></${componentTag}>`);
  }
  const openingPattern = new RegExp(`<${escapeRegExp(componentTag)}\\b([^>]*)>`);
  if (openingPattern.test(html)) {
    return html.replace(openingPattern, (_match, attributes) => `<${componentTag}${setAttribute(attributes, "slug", category.id)}>`);
  }
  throw new Error(`${relativePath(templateFile)} must contain <${componentTag}></${componentTag}>.`);
}

function injectDescription(html, description) {
  const meta = `<meta name="description" content="${escapeAttribute(description)}">`;
  if (html.includes("<!-- orderly-head-includes -->")) {
    return html.replace("<!-- orderly-head-includes -->", `<!-- orderly-head-includes -->\n    ${meta}`);
  }
  if (html.includes("</head>")) {
    return html.replace("</head>", `    ${meta}\n  </head>`);
  }
  return html;
}

function cleanGeneratedPages() {
  let removed = 0;
  for (const file of generatedCategoryFiles(categoriesDir)) {
    if (readFileSync(file, "utf8").includes("Generated by orderly-generate-category-pages")) {
      rmSync(file);
      removed += 1;
    }
  }
  if (existsSync(categoriesDir)) {
    removeEmptyDirectories(categoriesDir);
    if (readdirSync(categoriesDir).length === 0) {
      rmSync(categoriesDir, { recursive: true });
    }
  }
  return removed;
}

function generatedCategoryFiles(dir) {
  if (!existsSync(dir)) {
    return [];
  }
  const files = [];
  for (const entry of readdirSync(dir)) {
    const fullPath = resolve(dir, entry);
    const stat = statSync(fullPath);
    if (stat.isDirectory()) {
      files.push(...generatedCategoryFiles(fullPath));
    } else if (entry === "index.html" && fullPath !== templateFile) {
      files.push(fullPath);
    }
  }
  return files;
}

function removeEmptyDirectories(dir) {
  for (const entry of readdirSync(dir)) {
    const fullPath = resolve(dir, entry);
    if (statSync(fullPath).isDirectory()) {
      removeEmptyDirectories(fullPath);
      if (readdirSync(fullPath).length === 0) {
        rmSync(fullPath, { recursive: true });
      }
    }
  }
}

async function loadNavigationModule(file) {
  const extension = extname(file).toLowerCase();
  if (extension === ".ts" || extension === ".tsx") {
    return loadTypeScriptNavigationModule(file);
  }
  return import(pathToFileURL(file).href);
}

async function loadTypeScriptNavigationModule(file) {
  const require = createRequire(resolve(root, "package.json"));
  let typescript;
  try {
    typescript = require("typescript");
  } catch (error) {
    throw new Error(`TypeScript navigation files require the current shop project to install typescript. Could not load typescript while reading ${relativePath(file)}.`, { cause: error });
  }

  const output = typescript.transpileModule(readFileSync(file, "utf8"), {
    fileName: file,
    compilerOptions: {
      module: typescript.ModuleKind.ES2022,
      target: typescript.ScriptTarget.ES2022,
      esModuleInterop: true,
      importsNotUsedAsValues: typescript.ImportsNotUsedAsValues?.Remove
    }
  }).outputText;
  const temporaryFile = resolve(dirname(file), `.orderly-${basename(file, extname(file))}-${process.pid}-${Date.now()}.mjs`);
  writeFileSync(temporaryFile, output, "utf8");
  try {
    return await import(pathToFileURL(temporaryFile).href);
  } finally {
    rmSync(temporaryFile, { force: true });
  }
}

function defaultNavigationPath() {
  for (const candidate of ["src/navigation.ts", "src/navigation.js", "src/navigation.mjs", "navigation.ts", "navigation.js", "navigation.mjs", "src/taxonomy.ts", "src/taxonomy.js", "src/taxonomy.mjs", "taxonomy.ts", "taxonomy.js", "taxonomy.mjs"]) {
    if (existsSync(resolve(root, candidate))) {
      return candidate;
    }
  }
  return "src/navigation.ts";
}

function defaultTemplatePath() {
  if (existsSync(resolve(root, "src/category.html"))) {
    return "src/category.html";
  }
  return "category.html";
}

function defaultCategoriesDir() {
  if (existsSync(resolve(root, "src/category.html"))) {
    return "src/categories";
  }
  return "categories";
}

function navigationDefinitions(module, exportName) {
  if (exportName) {
    return module[exportName];
  }
  return module.navigationDefinitions ?? module.categoryDefinitions;
}

function defaultSiteTitle() {
  const packageFile = resolve(root, "package.json");
  if (!existsSync(packageFile)) {
    return "Shop";
  }
  try {
    const packageJson = JSON.parse(readFileSync(packageFile, "utf8"));
    return packageJson.displayName ?? packageJson.description ?? packageJson.name ?? "Shop";
  } catch {
    return "Shop";
  }
}

function parseArgs(values) {
  const parsed = {};
  for (let index = 0; index < values.length; index += 1) {
    const value = values[index];
    if (!value.startsWith("--")) {
      continue;
    }
    const [name, inlineValue] = value.slice(2).split("=", 2);
    if (inlineValue !== undefined) {
      parsed[name] = inlineValue;
    } else if (values[index + 1] && !values[index + 1].startsWith("--")) {
      parsed[name] = values[index + 1];
      index += 1;
    } else {
      parsed[name] = true;
    }
  }
  return parsed;
}

function printHelp() {
  console.log(`Usage: orderly-generate-category-pages [options]

Generate real URL category pages for the current shop from NavigationDefinition data.

Options:
  --navigation <file>      Navigation module. Defaults to src/navigation.ts or src/navigation.js when present.
  --taxonomy <file>        Backward-compatible alias for --navigation.
  --export <name>          Export name for navigation definitions. Defaults to navigationDefinitions.
  --template <file>        Category page template. Defaults to src/category.html when present, otherwise category.html.
  --categories-dir <dir>   Output directory. Defaults to src/categories when src/category.html is present, otherwise categories.
  --page-root <path>       Category page root used for navigation. Defaults to /categories/.
  --component-tag <tag>    Category page custom element tag. Defaults to orderly-category-page.
  --site-title <title>     Site title suffix used in generated <title> tags.
  --dry-run                Print generated pages without writing files.
  --clean                  Remove generated category pages.
  --help, -h               Show this help.
`);
}

function relativePath(file) {
  return file.replace(`${root}/`, "");
}

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

function escapeAttribute(value) {
  return escapeHtml(value).replace(/"/g, "&quot;");
}

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

function setAttribute(attributes, name, value) {
  const cleanAttributes = attributes.replace(/\s*\/\s*$/g, "");
  const escapedValue = escapeAttribute(value);
  const pattern = new RegExp(`\\s${escapeRegExp(name)}=["'][^"']*["']`, "i");
  if (pattern.test(cleanAttributes)) {
    return cleanAttributes.replace(pattern, ` ${name}="${escapedValue}"`);
  }
  return `${cleanAttributes} ${name}="${escapedValue}"`;
}
