#!/usr/bin/env node
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { basename, dirname, extname, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { clone } from "@bufbuild/protobuf";
import { anyUnpack } from "@bufbuild/protobuf/wkt";
import {
  SearchObjectSchema,
  SearchQuerySchema
} from "@orderlyshop/core-client";
import { createOrderlyNodeClient } from "@orderlyshop/core-client/node";
import {
  createCategoryNavigation,
  flattenCategoryNavigation
} from "../dist/taxonomy.js";
import { mergeShopSearchQuery } from "../dist/shop-query.js";
import { injectCriticalSsrStyle } from "../server/ssr.mjs";

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

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

const distDir = resolve(root, args.dist ?? "dist");
const navigationFile = resolve(root, args.navigation ?? args.taxonomy ?? defaultNavigationPath());
const shopQueryFile = resolve(root, args["shop-query"] ?? defaultShopQueryPath());
const exportName = args.export;
const shopQueryExportName = args["shop-query-export"] ?? "defaultQuery";
const pageRoot = args["page-root"] ?? "/categories/";
const distCategoryRoot = args["dist-categories-dir"] ?? pathRootFromPageRoot(pageRoot);
const siteTitle = args["site-title"] ?? defaultSiteTitle();
const productPage = args["product-page"] ?? "/product.html";
const productRoot = args["product-root"];
const baseUrl = args["base-url"] ?? process.env.VITE_ORDERLY_BASE_URL ?? process.env.ORDERLY_BASE_URL ?? "https://service.orderly.shop";
const protocol = args.protocol === "grpc" ? "grpc" : "grpc-web";
const productsPerCategory = positiveInteger(args["products-per-category"], 12);
const homeProductsPerCategory = positiveInteger(args["home-products-per-category"], productsPerCategory);
const imageBaseUrl = args["image-base-url"] ?? process.env.VITE_ORDERLY_IMAGE_BASE_URL ?? process.env.ORDERLY_IMAGE_BASE_URL ?? "https://orderlyproduction.azureedge.net/";
const dryRun = Boolean(args["dry-run"]);
const skipProducts = Boolean(args["skip-products"]);
const strictProducts = Boolean(args["strict-products"]);
const strictEmptyCategories = Boolean(args["strict-empty-categories"] ?? args["fail-empty-categories"]);

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

const navigationModule = await loadModule(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 defaultQuery = existsSync(shopQueryFile)
  ? await loadDefaultQuery(shopQueryFile, shopQueryExportName)
  : undefined;
const navigation = createCategoryNavigation(definitions, {
  pageRoot,
  urlMode: "path"
});
const categories = flattenCategoryNavigation(navigation);
const categoryById = new Map(categories.map((category) => [category.id, category]));
const client = skipProducts || !baseUrl
  ? undefined
  : createOrderlyNodeClient({ baseUrl, protocol });
const productCache = new Map();
const emptyProductWarnings = [];
const failedProductSnapshots = new Set();
let updatedPages = 0;
let hydratedProducts = 0;

const homeFile = resolve(distDir, "index.html");
if (existsSync(homeFile)) {
  const homeCategories = navigation;
  const homeProducts = new Map();
  for (const category of homeCategories) {
    const products = await productsForCategory(category, homeProductsPerCategory);
    reportEmptyProducts(category, products, "homepage rail", homeProductsPerCategory);
    hydratedProducts += products.length;
    homeProducts.set(category.id, products);
  }
  const html = readFileSync(homeFile, "utf8");
  const next = hydrateHomeHtml(html, homeCategories, homeProducts);
  if (next !== html) {
    writePage(homeFile, next);
    updatedPages += 1;
  }
}

for (const category of categories) {
  const file = resolve(distDir, distCategoryRoot, ...category.metadata.pathSegments, "index.html");
  if (!existsSync(file)) {
    continue;
  }
  const products = await productsForCategory(category, productsPerCategory);
  reportEmptyProducts(category, products, "category page", productsPerCategory);
  hydratedProducts += products.length;
  const html = readFileSync(file, "utf8");
  const next = hydrateCategoryHtml(html, category, products);
  if (next !== html) {
    writePage(file, next);
    updatedPages += 1;
  }
}

const productSuffix = skipProducts
  ? "Product snapshots were skipped."
  : `${hydratedProducts} product snapshot${hydratedProducts === 1 ? "" : "s"} embedded.`;
console.log(`Hydrated ${updatedPages} static page${updatedPages === 1 ? "" : "s"}. ${productSuffix}`);
if (emptyProductWarnings.length > 0) {
  const summary = `${emptyProductWarnings.length} category/rail product snapshot quer${emptyProductWarnings.length === 1 ? "y" : "ies"} matched 0 products.`;
  if (strictEmptyCategories) {
    throw new Error(`${summary} Rework src/navigation.ts queries or remove --strict-empty-categories.`);
  }
  console.warn(`Warning: ${summary}`);
}

function writePage(file, html) {
  if (dryRun) {
    console.log(`Would hydrate ${relativePath(file)}`);
    return;
  }
  writeFileSync(file, html, "utf8");
}

async function productsForCategory(category, limit) {
  if (!client || limit <= 0) {
    return [];
  }
  const cacheKey = `${category.id}:${limit}`;
  if (productCache.has(cacheKey)) {
    return productCache.get(cacheKey);
  }
  const products = await fetchProducts(category, limit);
  productCache.set(cacheKey, products);
  return products;
}

async function fetchProducts(category, limit) {
  const request = mergeShopSearchQuery(clone(SearchQuerySchema, category.metadata.query), defaultQuery);
  request.Continuation = undefined;
  try {
    const result = await client.services.searchService.search(request);
    return result.data
      .map((item) => anyUnpack(item, SearchObjectSchema))
      .filter(Boolean)
      .slice(0, limit);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    failedProductSnapshots.add(category.id);
    if (strictProducts) {
      throw new Error(`Could not hydrate products for ${category.id}: ${message}`, { cause: error });
    }
    console.warn(`Could not hydrate products for ${category.id}: ${message}`);
    return [];
  }
}

function reportEmptyProducts(category, products, surface, requestedLimit) {
  if (skipProducts || !client || requestedLimit <= 0 || products.length > 0 || failedProductSnapshots.has(category.id)) {
    return;
  }
  const label = categoryLabelPath(category).join(" / ") || category.label || category.id;
  const message = `${surface} "${label}" (${category.id}) matched 0 products. Check its SearchQuery.Query, SearchQuery.Tags, and the shop defaultQuery; use "|" for OR terms and avoid multiple tags for alternatives.`;
  emptyProductWarnings.push(message);
  console.warn(`Warning: ${message}`);
}

function hydrateHomeHtml(html, categories, productsByCategory) {
  const title = readAttribute(html, "orderly-home-page", "title") ?? siteTitle;
  const eyebrow = readAttribute(html, "orderly-home-page", "eyebrow");
  const fallback = renderHomeFallback({ title, eyebrow, categories, productsByCategory });
  return injectCriticalSsrStyle(injectComponentFallback(html, "orderly-home-page", fallback), { mode: "visible" });
}

function hydrateCategoryHtml(html, category, products) {
  let next = html;
  next = next.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(category.label)} | ${escapeHtml(siteTitle)}</title>`);
  next = injectDescription(next, category.metadata.description);
  next = injectCanonical(next, category.href);
  const fallback = renderCategoryFallback(category, products);
  return injectCriticalSsrStyle(injectComponentFallback(next, "orderly-category-page", fallback), { mode: "visible" });
}

function renderHomeFallback({ title, eyebrow, categories, productsByCategory }) {
  return `
      <section slot="content-before" class="orderly-ssr-home" data-orderly-ssr-fallback data-orderly-page="home">
        <header class="orderly-ssr-home__intro">
          ${eyebrow ? `<p class="orderly-ssr-home__eyebrow">${escapeHtml(eyebrow)}</p>` : ""}
          <h1>${escapeHtml(title)}</h1>
        </header>
        <div class="orderly-ssr-home__rails">
          ${categories.map((category) => renderHomeRail(category, productsByCategory.get(category.id) ?? [])).join("\n")}
        </div>
      </section>
`;
}

function renderHomeRail(category, products) {
  return `
          <section class="orderly-ssr-rail" data-orderly-category="${escapeAttribute(category.id)}">
            <header class="orderly-ssr-rail__header">
              <h2><a href="${escapeAttribute(category.href ?? "#")}">${escapeHtml(category.label)}</a></h2>
              ${category.metadata.description ? `<p>${escapeHtml(category.metadata.description)}</p>` : ""}
            </header>
            ${renderProducts(products, "orderly-ssr-rail__items")}
          </section>`;
}

function renderCategoryFallback(category, products) {
  return `
      <section slot="content-before" class="orderly-ssr-category" data-orderly-ssr-fallback data-orderly-category="${escapeAttribute(category.id)}">
        ${renderBreadcrumb(category)}
        <header class="orderly-ssr-category__hero">
          ${category.metadata.heroImage ? `<img src="${escapeAttribute(category.metadata.heroImage)}" alt="" loading="eager" fetchpriority="high" decoding="async">` : ""}
          <div class="orderly-ssr-category__text">
            <p class="orderly-ssr-category__eyebrow">${escapeHtml(categoryLabelPath(category).join(" / "))}</p>
            <h1 data-orderly-field="title">${escapeHtml(category.label)}</h1>
            <p data-orderly-field="description">${escapeHtml(category.metadata.description)}</p>
          </div>
        </header>
        ${renderProducts(products, "orderly-ssr-product-grid", { priorityFirst: !category.metadata.heroImage })}
      </section>
`;
}

function renderBreadcrumb(category) {
  if (category.metadata.pathSegments.length <= 1) {
    return "";
  }
  const paths = category.metadata.pathSegments;
  return `
        <nav class="orderly-ssr-breadcrumb" aria-label="Brødkrumme">
          ${paths.map((segment, index) => {
            const id = paths.slice(0, index + 1).join("/");
            const label = categoryById.get(id)?.label ?? segment;
            return `<a href="${escapeAttribute(`${pageRoot}${id}/`)}">${escapeHtml(label)}</a>`;
          }).join("<span>/</span>")}
        </nav>`;
}

function categoryLabelPath(category) {
  return category.metadata.pathSegments.map((segment, index) => {
    const id = category.metadata.pathSegments.slice(0, index + 1).join("/");
    return categoryById.get(id)?.label ?? segment;
  });
}

function renderProducts(products, className, options = {}) {
  if (products.length === 0) {
    return "";
  }
  return `
            <div class="${className}" data-orderly-products>
              ${products.map((product, index) => renderProduct(product, { priority: Boolean(options.priorityFirst) && index === 0 })).join("\n")}
            </div>`;
}

function renderProduct(product, options = {}) {
  const title = product.Title || "Produkt";
  const brand = normalizedBrand(product.Brand);
  const image = firstImageUrl(product, { width: 500, height: 500 });
  const href = productHref(product);
  const description = product.Description?.trim();
  const sku = product.SKU?.Value?.trim();
  return `
              <article class="orderly-ssr-product" data-orderly-product itemscope itemtype="https://schema.org/Product">
                ${href ? `<link itemprop="url" href="${escapeAttribute(href)}">` : ""}
                ${image ? `<link itemprop="image" href="${escapeAttribute(image)}">` : ""}
                ${description ? `<meta itemprop="description" content="${escapeAttribute(description)}">` : ""}
                ${sku ? `<meta itemprop="sku" content="${escapeAttribute(sku)}">` : ""}
                ${href ? `<a class="orderly-ssr-product__link" href="${escapeAttribute(href)}">` : ""}
                  <div class="orderly-ssr-product__image">
                    ${image ? `<img src="${escapeAttribute(image)}" alt="${escapeAttribute(title)}" ${ssrImageAttributes(options.priority)}>` : ""}
                  </div>
                  <div class="orderly-ssr-product__text">
                    <h3 data-orderly-field="title" itemprop="name">${escapeHtml(title)}</h3>
                    ${brand ? `<p data-orderly-field="brand" itemprop="brand" itemscope itemtype="https://schema.org/Brand"><span itemprop="name">${escapeHtml(brand)}</span></p>` : ""}
                    ${renderOffer(product, href)}
                  </div>
                ${href ? "</a>" : ""}
              </article>`;
}

function ssrImageAttributes(priority) {
  return priority
    ? `loading="eager" fetchpriority="high" decoding="async"`
    : `loading="lazy" decoding="async"`;
}

function renderOffer(product, href) {
  const price = schemaPrice(product.Price);
  const currency = schemaCurrency(product.Price);
  const availability = product.Active ? "https://schema.org/InStock" : "";
  if (!price) {
    return `<p data-orderly-field="price">${escapeHtml(formatCredit(product.Price))}</p>`;
  }
  return `<p data-orderly-field="price" itemprop="offers" itemscope itemtype="https://schema.org/Offer">
                      ${href ? `<link itemprop="url" href="${escapeAttribute(href)}">` : ""}
                      ${currency ? `<meta itemprop="priceCurrency" content="${escapeAttribute(currency)}">` : ""}
                      <meta itemprop="price" content="${escapeAttribute(price)}">
                      ${availability ? `<link itemprop="availability" href="${escapeAttribute(availability)}">` : ""}
                      ${escapeHtml(formatCredit(product.Price))}
                    </p>`;
}

function productHref(product) {
  const shareUrl = compactProductShareUrl(product.ShareURL?.URL);
  if (!shareUrl) {
    return "";
  }
  if (productRoot) {
    return joinUrl(productRoot, shareUrl.replace(/\.html$/i, ""));
  }
  return `${productPage}#url=${encodeURIComponent(shareUrl)}`;
}

function compactProductShareUrl(url) {
  if (!url) {
    return "";
  }
  return url.replace(/^https:\/\/orderly\.shop\//i, "");
}

function firstImageUrl(product, options = {}) {
  return storedImageUrl(product.Images?.[0], options);
}

function storedImageUrl(image, options = {}) {
  if (!image) {
    return "";
  }
  const candidate = image;
  for (const key of ["URL", "Url", "url", "ThumbnailUrl", "thumbnailUrl"]) {
    const value = candidate[key];
    if (typeof value === "string" && value.length > 0) {
      return withImageSizeParams(value, options);
    }
  }
  if (image.ImageDataBase64) {
    return `data:image/jpeg;base64,${image.ImageDataBase64}`;
  }
  if (/^https?:\/\//i.test(image.Name) || image.Name.startsWith("/")) {
    return withImageSizeParams(image.Name, options);
  }
  if (!image.Name) {
    return "";
  }
  const imageName = image.Name.toLowerCase();
  const path = image.IsAsset || imageName.includes("cms") || imageName.startsWith("images")
    ? "objects"
    : "thumbnails";
  return withImageSizeParams(joinUrl(imageBaseUrl, path, image.Name), options);
}

function withImageSizeParams(url, options = {}) {
  if (/^data:/i.test(url)) {
    return url;
  }
  const width = imageUrlDimension(options.width);
  const height = imageUrlDimension(options.height);
  if (!width && !height) {
    return url;
  }
  const hashIndex = url.indexOf("#");
  const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
  const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
  const queryIndex = withoutHash.indexOf("?");
  const path = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
  const search = new URLSearchParams(queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : "");
  if (width) {
    search.set("width", String(width));
  }
  if (height) {
    search.set("height", String(height));
  }
  return `${path}?${search.toString()}${hash}`;
}

function imageUrlDimension(value) {
  return value !== undefined && Number.isFinite(value) && value > 0 ? Math.ceil(value) : undefined;
}

function formatCredit(credit) {
  if (!credit) {
    return "";
  }
  const amount = Number(credit.Amount ?? 0);
  const currency = credit.Currency || "";
  const prefix = currency.toUpperCase() === "DKK" ? "kr." : currency.trim();
  const value = amount.toLocaleString("da-DK", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  });
  return `${prefix ? `${prefix} ` : ""}${value}`.trim();
}

function schemaPrice(credit) {
  if (!credit || !Number.isFinite(credit.Amount)) {
    return "";
  }
  return credit.Amount.toFixed(2);
}

function schemaCurrency(credit) {
  const currency = credit?.Currency?.trim();
  return currency ? currency.toUpperCase() : "";
}

function normalizedBrand(brand) {
  const value = brand?.trim();
  if (!value || value.toLowerCase() === "ukendt" || value.toUpperCase() === "DVD") {
    return "";
  }
  return value;
}

function injectComponentFallback(html, tagName, fallback) {
  const cleaned = stripExistingFallback(html);
  const pattern = new RegExp(`<${escapeRegExp(tagName)}\\b([^>]*)>`, "i");
  if (!pattern.test(cleaned)) {
    return cleaned;
  }
  return cleaned.replace(pattern, (match, attributes) => {
    const hydratedAttributes = /\sdata-orderly-ssr(?:\s|=|$)/.test(attributes)
      ? attributes
      : `${attributes} data-orderly-ssr`;
    return `<${tagName}${hydratedAttributes}>\n      <!-- orderly-ssr-fallback-start -->${fallback}      <!-- orderly-ssr-fallback-end -->`;
  });
}

function stripExistingFallback(html) {
  return html.replace(/\s*<!-- orderly-ssr-fallback-start -->[\s\S]*?<!-- orderly-ssr-fallback-end -->\s*/g, "\n");
}

function injectDescription(html, description) {
  const meta = `<meta name="description" content="${escapeAttribute(description)}">`;
  if (/<meta\s+name=["']description["'][^>]*>/i.test(html)) {
    return html.replace(/<meta\s+name=["']description["'][^>]*>/i, meta);
  }
  if (html.includes("<!-- orderly-head-includes -->")) {
    return html.replace("<!-- orderly-head-includes -->", `<!-- orderly-head-includes -->\n    ${meta}`);
  }
  return html.replace("</head>", `    ${meta}\n  </head>`);
}

function injectCanonical(html, href) {
  if (!href) {
    return html;
  }
  const canonical = `<link rel="canonical" href="${escapeAttribute(href)}">`;
  if (/<link\s+rel=["']canonical["'][^>]*>/i.test(html)) {
    return html.replace(/<link\s+rel=["']canonical["'][^>]*>/i, canonical);
  }
  if (html.includes("<!-- orderly-head-includes -->")) {
    return html.replace("<!-- orderly-head-includes -->", `<!-- orderly-head-includes -->\n    ${canonical}`);
  }
  return html.replace("</head>", `    ${canonical}\n  </head>`);
}

function readAttribute(html, tagName, attribute) {
  const tag = html.match(new RegExp(`<${escapeRegExp(tagName)}\\b([^>]*)>`, "i"))?.[1];
  if (!tag) {
    return undefined;
  }
  const match = tag.match(new RegExp(`${escapeRegExp(attribute)}=["']([^"']*)["']`, "i"));
  return match?.[1] ? decodeHtml(match[1]) : undefined;
}

async function loadDefaultQuery(file, name) {
  const module = await loadModule(file);
  const query = module[name];
  if (query === undefined || query === null) {
    return undefined;
  }
  return clone(SearchQuerySchema, query);
}

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

async function loadTypeScriptModule(file) {
  const require = createRequire(resolve(root, "package.json"));
  let typescript;
  try {
    typescript = require("typescript");
  } catch (error) {
    throw new Error(`TypeScript 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 defaultShopQueryPath() {
  for (const candidate of ["src/shop-query.ts", "src/shop-query.js", "src/shop-query.mjs", "shop-query.ts", "shop-query.js", "shop-query.mjs"]) {
    if (existsSync(resolve(root, candidate))) {
      return candidate;
    }
  }
  return "src/shop-query.ts";
}

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

function pathRootFromPageRoot(value) {
  const normalized = String(value || "/categories/").replace(/^\/+|\/+$/g, "");
  return normalized || "categories";
}

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 positiveInteger(value, fallback) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

function joinUrl(baseUrl, ...parts) {
  const rootUrl = String(baseUrl).endsWith("/") ? String(baseUrl) : `${baseUrl}/`;
  return new URL(parts
    .filter(Boolean)
    .map((part) => String(part).replace(/^\/+|\/+$/g, ""))
    .join("/"), rootUrl).toString();
}

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 relativePath(file) {
  return relative(root, file) || ".";
}

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 decodeHtml(value) {
  return value
    .replace(/&quot;/g, "\"")
    .replace(/&gt;/g, ">")
    .replace(/&lt;/g, "<")
    .replace(/&amp;/g, "&");
}

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

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

Hydrate built static storefront pages with SEO-friendly category and homepage fallback HTML.

Options:
  --dist <dir>                 Build output directory. Defaults to dist.
  --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.
  --shop-query <file>          Optional shop-wide SearchQuery module. Defaults to src/shop-query.ts when present.
  --shop-query-export <name>   Export name for shop query. Defaults to defaultQuery.
  --page-root <path>           Category URL root. Defaults to /categories/.
  --dist-categories-dir <dir>  Category directory inside dist. Defaults to page-root without slashes.
  --site-title <title>         Site title suffix used in generated category <title> tags.
  --base-url <url>             Orderly backend URL used for product snapshots. Also reads VITE_ORDERLY_BASE_URL. Defaults to https://service.orderly.shop.
  --protocol <grpc|grpc-web>   Backend protocol for snapshots. Defaults to grpc-web.
  --products-per-category <n>  Product cards embedded per category page. Defaults to 12.
  --home-products-per-category <n>
                               Product cards embedded per homepage rail. Defaults to --products-per-category.
  --product-page <path>        Product page used for snapshot links. Defaults to /product.html.
  --product-root <path>        Optional path-style product root instead of product.html#url=...
  --image-base-url <url>       StoredImage CDN prefix. Defaults to Orderly production CDN.
  --skip-products              Hydrate category/home text only, without backend product calls.
  --strict-products            Fail when a product snapshot call fails.
  --strict-empty-categories    Fail when a category page or homepage rail query returns 0 products.
  --dry-run                    Print files that would change without writing.
  --help, -h                   Show this help.
`);
}
