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, create } from "@bufbuild/protobuf";
import { anyUnpack } from "@bufbuild/protobuf/wkt";

let coreClientPromise;
let coreNodeClientPromise;
let coreClientModule;

export const ORDERLY_SSR_VISIBLE_CRITICAL_CSS = `<style data-orderly-ssr-critical data-orderly-ssr-mode="visible">
[data-orderly-ssr-fallback], [data-orderly-ssr-fallback] * { box-sizing: border-box; }
.orderly-ssr-home, .orderly-ssr-category, .orderly-ssr-product-detail { color: var(--orderly-color-text, #242222); font-family: var(--orderly-font-family-body, ui-sans-serif, system-ui, sans-serif); }
.orderly-ssr-home, .orderly-ssr-category { display: grid; gap: clamp(22px, 4vw, 44px); width: 100%; max-width: 100%; margin: 0; padding: clamp(18px, 4vw, 44px); background: var(--orderly-color-page, #f4f4f4); }
.orderly-ssr-home__intro { display: grid; gap: 10px; width: min(var(--orderly-content-max-width, 1160px), 100%); margin: 0 auto; }
.orderly-ssr-home__intro h1 { margin: 0; font-size: clamp(2rem, 5vw, 4.4rem); line-height: 1; }
.orderly-ssr-home__intro p { margin: 0; }
.shop-utility-strip, [slot="utility"] { display: flex; align-items: center; justify-content: center; gap: clamp(16px, 10vw, 220px); min-height: 44px; padding: 10px 18px; background: var(--orderly-color-ink, #1f1f1f); color: var(--orderly-color-ink-contrast, #fff); font-weight: 700; }
.orderly-ssr-home__rails { display: grid; gap: clamp(26px, 5vw, 52px); }
.orderly-ssr-rail { display: grid; gap: 14px; width: min(var(--orderly-content-max-width, 1160px), 100%); margin: 0 auto; }
.orderly-ssr-rail__header { display: flex; align-items: end; justify-content: space-between; gap: 16px; }
.orderly-ssr-rail__header h2, .orderly-ssr-rail__header p { margin: 0; }
.orderly-ssr-rail__items { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(160px, 220px); gap: 14px; overflow-x: auto; padding-bottom: 8px; }
.orderly-ssr-category__hero { position: relative; display: grid; align-items: end; min-height: clamp(220px, 28vw, 360px); width: 100%; max-width: 100%; overflow: hidden; border-radius: var(--orderly-radius-card, 10px); background: var(--orderly-color-surface-muted, #e8e8e8); }
.orderly-ssr-category__hero > img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; }
.orderly-ssr-category__hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(0, 0, 0, 0.52), rgba(0, 0, 0, 0.18)); }
.orderly-ssr-category__text { position: relative; z-index: 1; display: grid; gap: 10px; width: min(var(--orderly-content-max-width, 1160px), 100%); margin: 0 auto; padding: clamp(18px, 4vw, 44px); color: #fff; }
.orderly-ssr-category__text h1 { margin: 0; font-size: clamp(2rem, 5vw, 4.4rem); line-height: 1; }
.orderly-ssr-category__text p, .orderly-ssr-product__text h3, .orderly-ssr-product__text p, .orderly-ssr-product-detail__text h1, .orderly-ssr-product-detail__text p { margin: 0; }
.orderly-ssr-category > :not(.orderly-ssr-category__hero), .orderly-ssr-product-grid { width: min(var(--orderly-content-max-width, 1160px), 100%); margin: 0 auto; }
.orderly-ssr-product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr)); gap: 14px; }
.orderly-ssr-product { min-width: 0; overflow: hidden; border: var(--orderly-border-card, 1px solid #dedede); border-radius: var(--orderly-radius-card, 10px); background: var(--orderly-color-surface, #fff); }
.orderly-ssr-product__link { display: grid; height: 100%; color: inherit; text-decoration: none; }
.orderly-ssr-product__image { display: grid; place-items: center; aspect-ratio: 1; background: var(--orderly-color-surface, #fff); }
.orderly-ssr-product__image img { display: block; width: 100%; height: 100%; object-fit: contain; }
.orderly-ssr-product__text { display: grid; gap: 6px; padding: 14px; }
.orderly-ssr-product-detail { display: grid; grid-template-columns: minmax(0, 0.95fr) minmax(280px, 0.75fr); align-items: start; gap: clamp(20px, 4vw, 48px); width: min(var(--orderly-content-max-width, 1160px), 100%); margin: 0 auto; padding: clamp(18px, 4vw, 44px); }
.orderly-ssr-product-detail__image { display: grid; place-items: center; aspect-ratio: 1; overflow: hidden; border: var(--orderly-product-page-image-border, 1px solid transparent); border-radius: var(--orderly-product-page-image-radius, 4px); background: var(--orderly-product-page-image-background, transparent); }
.orderly-ssr-product-detail__image img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }
.orderly-ssr-product-detail__text { display: grid; gap: 10px; }
.orderly-ssr-product-detail__text h1 { font-size: clamp(2rem, 4vw, 3.6rem); line-height: 1; }
@media (max-width: 767px) { .orderly-ssr-product-detail { grid-template-columns: 1fr; } .orderly-ssr-category { padding-inline: 0; } .orderly-ssr-category__hero { border-radius: 0; } .orderly-ssr-product-grid { padding-inline: 14px; } }
</style>`;

export const ORDERLY_SSR_HIDDEN_CRITICAL_CSS = `<style data-orderly-ssr-critical data-orderly-ssr-mode="hidden">
[data-orderly-ssr-fallback], [data-orderly-ssr-fallback] * { box-sizing: border-box; }
[data-orderly-ssr-fallback] { display: none !important; }
orderly-home-page[data-orderly-ssr]:not(:defined) > :not(template):not([data-orderly-ssr-fallback]), orderly-category-page[data-orderly-ssr]:not(:defined) > :not(template):not([data-orderly-ssr-fallback]), orderly-product-detail-page[data-orderly-ssr]:not(:defined) > :not(template):not([data-orderly-ssr-fallback]) { display: none !important; }
orderly-home-page[data-orderly-ssr], orderly-category-page[data-orderly-ssr], orderly-product-detail-page[data-orderly-ssr] { display: block; min-height: min(100vh, 720px); }
</style>
<noscript>${ORDERLY_SSR_VISIBLE_CRITICAL_CSS.replace("<style ", "<style data-orderly-ssr-noscript ")}</noscript>`;

export const ORDERLY_SSR_CRITICAL_CSS = ORDERLY_SSR_VISIBLE_CRITICAL_CSS;

export async function createOrderlySsrContext(options = {}) {
  const coreClient = await loadCoreClient();
  const { createOrderlyNodeClient } = await loadCoreNodeClient();
  const root = resolve(options.root ?? process.cwd());
  const navigationFile = resolve(root, options.navigation ?? defaultNavigationPath(root));
  const shopQueryFile = resolve(root, options.shopQuery ?? defaultShopQueryPath(root));
  const pageRoot = options.pageRoot ?? "/categories/";
  const productRoot = options.productRoot ?? "/products/";
  const productOrigin = options.productOrigin ?? "https://orderly.shop/";
  const siteTitle = options.siteTitle ?? defaultSiteTitle(root);
  const imageBaseUrl = options.imageBaseUrl ?? process.env.VITE_ORDERLY_IMAGE_BASE_URL ?? process.env.ORDERLY_IMAGE_BASE_URL ?? "https://orderlyproduction.azureedge.net/";
  const baseUrl = options.baseUrl ?? process.env.VITE_ORDERLY_BASE_URL ?? process.env.ORDERLY_BASE_URL ?? "https://service.orderly.shop";
  const protocol = options.protocol === "grpc" || process.env.ORDERLY_BACKEND_PROTOCOL === "grpc" ? "grpc" : "grpc-web";
  const productsPerCategory = positiveInteger(options.productsPerCategory, 24);
  const cacheTtlMs = positiveInteger(options.cacheTtlMs ?? process.env.ORDERLY_DEV_SSR_CACHE_TTL_MS, 10_000);
  const navigationModule = existsSync(navigationFile) ? await loadModule(root, navigationFile) : {};
  const definitions = navigationModule[options.navigationExport ?? "navigationDefinitions"] ?? navigationModule.categoryDefinitions ?? [];
  const defaultQuery = existsSync(shopQueryFile)
    ? await loadDefaultQuery(root, shopQueryFile, options.shopQueryExport ?? "defaultQuery")
    : undefined;
  const navigation = Array.isArray(definitions)
    ? createCategoryNavigation(definitions, { pageRoot, urlMode: "path" })
    : [];
  const categories = flattenCategoryNavigation(navigation);
  const client = options.client ?? createOrderlyNodeClient({ baseUrl, protocol });

  return {
    baseUrl,
    cache: new Map(),
    cacheTtlMs,
    categories,
    client,
    defaultQuery,
    imageBaseUrl,
    pageRoot,
    productOrigin,
    productRoot,
    productsPerCategory,
    root,
    schemas: {
      SearchQuerySchema: coreClient.SearchQuerySchema
    },
    siteTitle
  };
}

export async function renderOrderlySsrHtml(context, url, html, options = {}) {
  const target = typeof url === "string" ? new URL(url, "http://localhost") : url;
  const productShareUrl = productShareUrlFromPath(target, context);
  if (productShareUrl) {
    return renderProductHtml(context, html, productShareUrl, target, options);
  }
  const category = categoryFromPath(target, context);
  if (category) {
    return renderCategoryHtml(context, html, category, target, options);
  }
  return html;
}

export function sourceHtmlFileForUrl(root, url) {
  const requestPath = safeHtmlRequestPath(url);
  if (!requestPath) {
    return undefined;
  }
  const srcRoot = resolve(root, "src");
  const categoryTemplate = resolve(srcRoot, "category.html");
  if (requestPath.startsWith("categories/") && requestPath.endsWith("index.html") && existsSync(categoryTemplate)) {
    return categoryTemplate;
  }
  const productTemplate = resolve(srcRoot, "product.html");
  if (requestPath.startsWith("products/") && requestPath.endsWith("index.html") && existsSync(productTemplate)) {
    return productTemplate;
  }
  const file = resolve(srcRoot, requestPath);
  if (!file.startsWith(srcRoot) || isHtmlPartial(root, file) || !existsSync(file)) {
    return undefined;
  }
  return file;
}

export async function fetchProductByShareUrl(context, shareUrl) {
  return cached(context, `product:${shareUrl}`, async () => {
    const { SearchQuerySchema } = await loadCoreClient();
    const request = mergeShopSearchQuery(create(SearchQuerySchema, { ShareUrl: shareUrl }), context.defaultQuery);
    const result = await context.client.services.searchService.search(request);
    return unpackProducts(result)[0];
  });
}

export async function fetchProductsForCategory(context, category) {
  return cached(context, `category:${category.id}`, async () => {
    const { SearchQuerySchema } = await loadCoreClient();
    const request = mergeShopSearchQuery(clone(SearchQuerySchema, category.metadata.query), context.defaultQuery);
    request.Continuation = undefined;
    const result = await context.client.services.searchService.search(request);
    return unpackProducts(result).slice(0, context.productsPerCategory);
  });
}

export function renderProductFallback(product, shareUrl, context) {
  const title = product?.Title ?? titleFromShareUrl(shareUrl);
  const description = product?.Description ?? "";
  const brand = normalizedBrand(product?.Brand);
  const image = firstImageUrl(product, context);
  const price = product?.Price;
  const href = product?.ShareURL?.URL ?? shareUrl;
  return `
      <article slot="content-before" class="orderly-ssr-product-detail" data-orderly-ssr-fallback data-orderly-product ${dataAttr("orderly-share-url", href)} ${productDataAttrs(product, context)} 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)}">` : ""}
        ${product?.SKU?.Value ? `<meta itemprop="sku" content="${escapeAttribute(product.SKU.Value)}">` : ""}
        ${image ? `<div class="orderly-ssr-product-detail__image"><img src="${escapeAttribute(image)}" alt="${escapeAttribute(title)}" loading="eager"></div>` : ""}
        <section class="orderly-ssr-product-detail__text">
          ${brand ? `<p data-orderly-field="brand" itemprop="brand" itemscope itemtype="https://schema.org/Brand"><span itemprop="name">${escapeHtml(brand)}</span></p>` : ""}
          <h1 data-orderly-field="title" itemprop="name">${escapeHtml(title)}</h1>
          ${renderOffer(price, href)}
          ${description ? `<p data-orderly-field="description">${escapeHtml(description)}</p>` : ""}
        </section>
      </article>`;
}

export function renderCategoryFallback(category, products, context) {
  return `
      <section slot="content-before" class="orderly-ssr-category" data-orderly-ssr-fallback data-orderly-category="${escapeAttribute(category.id)}">
        ${renderBreadcrumb(category, context)}
        <header class="orderly-ssr-category__hero">
          ${category.metadata.heroImage ? `<img src="${escapeAttribute(category.metadata.heroImage)}" alt="" loading="eager">` : ""}
          <div class="orderly-ssr-category__text">
            <p class="orderly-ssr-category__eyebrow">${escapeHtml(categoryLabelPath(category, context).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", context)}
      </section>
`;
}

export function renderProducts(products, className, context) {
  if (products.length === 0) {
    return "";
  }
  return `
        <div class="${className}" data-orderly-products>
          ${products.map((product) => renderProductCard(product, context)).join("\n")}
        </div>`;
}

export function renderProductCard(product, context) {
  const title = product.Title || "Produkt";
  const brand = normalizedBrand(product.Brand);
  const image = firstImageUrl(product, context);
  const href = product.ShareURL?.URL ?? "";
  const description = product.Description?.trim();
  return `
          <article class="orderly-ssr-product" data-orderly-product ${dataAttr("orderly-share-url", href)} ${productDataAttrs(product, context)} 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)}">` : ""}
            ${product.SKU?.Value ? `<meta itemprop="sku" content="${escapeAttribute(product.SKU.Value)}">` : ""}
            ${href ? `<a class="orderly-ssr-product__link" href="${escapeAttribute(href)}">` : ""}
              <div class="orderly-ssr-product__image">
                ${image ? `<img src="${escapeAttribute(image)}" alt="${escapeAttribute(title)}" loading="lazy">` : ""}
              </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.Price, href)}
              </div>
            ${href ? "</a>" : ""}
          </article>`;
}

async function renderProductHtml(context, html, shareUrl, url, options) {
  const product = await fetchProductByShareUrl(context, shareUrl);
  const fallback = renderProductFallback(product, shareUrl, context);
  let next = injectComponentFallback(html, "orderly-product-detail-page", fallback, ` share-url="${escapeAttribute(shareUrl)}"`);
  const title = product?.Title ?? titleFromShareUrl(shareUrl);
  next = injectTitle(next, title);
  next = injectDescription(next, product?.Description ?? title);
  next = injectCanonical(next, new URL(url.pathname, url.origin).toString());
  next = injectOpenGraph(next, title, product?.Description, firstImageUrl(product, context), new URL(url.pathname, url.origin).toString());
  next = injectCriticalSsrStyle(next, options);
  return next;
}

async function renderCategoryHtml(context, html, category, url, options) {
  const products = await fetchProductsForCategory(context, category);
  const fallback = renderCategoryFallback(category, products, context);
  let next = injectComponentFallback(html, "orderly-category-page", fallback, ` slug="${escapeAttribute(category.id)}"`);
  next = injectTitle(next, `${category.label} | ${context.siteTitle}`);
  next = injectDescription(next, category.metadata.description);
  next = injectCanonical(next, new URL(url.pathname, url.origin).toString());
  next = injectCriticalSsrStyle(next, options);
  return next;
}

function productShareUrlFromPath(url, context) {
  const root = normalizedRoot(context.productRoot);
  if (!url.pathname.startsWith(root)) {
    return undefined;
  }
  const value = url.pathname.slice(root.length).replace(/^\/+|\/+$/g, "");
  if (!value) {
    return undefined;
  }
  const decoded = decodeURIComponent(value);
  if (/^https?:\/\//i.test(decoded)) {
    return decoded;
  }
  return new URL(decoded, context.productOrigin).toString();
}

function categoryFromPath(url, context) {
  const root = normalizedRoot(context.pageRoot);
  if (!url.pathname.startsWith(root)) {
    return undefined;
  }
  const id = url.pathname.slice(root.length).replace(/^\/+|\/+$/g, "");
  return context.categories.find((category) => category.id === id || category.metadata.slug === id);
}

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

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

export function injectCriticalSsrStyle(html, options = {}) {
  return stripExistingCriticalSsrStyle(html).replace("</head>", `    ${criticalSsrStyle(options)}\n  </head>`);
}

export function criticalSsrStyle(options = {}) {
  const mode = options.mode ?? (isGooglebotUserAgent(options.userAgent) ? "visible" : "hidden");
  return mode === "hidden" ? ORDERLY_SSR_HIDDEN_CRITICAL_CSS : ORDERLY_SSR_VISIBLE_CRITICAL_CSS;
}

export function isGooglebotUserAgent(userAgent) {
  return /Googlebot|Google-InspectionTool|AdsBot-Google|Mediapartners-Google|Storebot-Google/i.test(String(userAgent ?? ""));
}

function stripExistingCriticalSsrStyle(html) {
  return html
    .replace(/\s*<noscript>\s*<style\b[^>]*data-orderly-ssr-noscript[^>]*>[\s\S]*?<\/style>\s*<\/noscript>\s*/gi, "\n")
    .replace(/\s*<style\b[^>]*data-orderly-ssr-critical[^>]*>[\s\S]*?<\/style>\s*/gi, "\n");
}

function injectTitle(html, title) {
  return /<title>.*?<\/title>/is.test(html)
    ? html.replace(/<title>.*?<\/title>/is, `<title>${escapeHtml(title)}</title>`)
    : html.replace("</head>", `    <title>${escapeHtml(title)}</title>\n  </head>`);
}

function injectDescription(html, description) {
  return upsertHeadTag(html, /<meta\s+name=["']description["'][^>]*>/i, `<meta name="description" content="${escapeAttribute(description ?? "")}">`);
}

function injectCanonical(html, href) {
  return upsertHeadTag(html, /<link\s+rel=["']canonical["'][^>]*>/i, `<link rel="canonical" href="${escapeAttribute(href)}">`);
}

function injectOpenGraph(html, title, description, image, url) {
  let next = upsertHeadTag(html, /<meta\s+property=["']og:title["'][^>]*>/i, `<meta property="og:title" content="${escapeAttribute(title)}">`);
  next = upsertHeadTag(next, /<meta\s+property=["']og:url["'][^>]*>/i, `<meta property="og:url" content="${escapeAttribute(url)}">`);
  if (description) {
    next = upsertHeadTag(next, /<meta\s+property=["']og:description["'][^>]*>/i, `<meta property="og:description" content="${escapeAttribute(description)}">`);
  }
  if (image) {
    next = upsertHeadTag(next, /<meta\s+property=["']og:image["'][^>]*>/i, `<meta property="og:image" content="${escapeAttribute(image)}">`);
  }
  return next;
}

function upsertHeadTag(html, pattern, tag) {
  if (pattern.test(html)) {
    return html.replace(pattern, tag);
  }
  return html.replace("</head>", `    ${tag}\n  </head>`);
}

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

function categoryLabelPath(category, context) {
  return category.metadata.pathSegments.map((_, index) => {
    const id = category.metadata.pathSegments.slice(0, index + 1).join("/");
    return context.categories.find((candidate) => candidate.id === id)?.label ?? id;
  });
}

function renderOffer(price, href) {
  const amount = Number(price?.Amount);
  const currency = price?.Currency || "DKK";
  if (!Number.isFinite(amount)) {
    return "";
  }
  return `<p data-orderly-field="price" data-orderly-price-amount="${escapeAttribute(String(amount))}" data-orderly-price-currency="${escapeAttribute(currency)}" itemprop="offers" itemscope itemtype="https://schema.org/Offer">
                  ${href ? `<link itemprop="url" href="${escapeAttribute(href)}">` : ""}
                  <meta itemprop="priceCurrency" content="${escapeAttribute(currency)}">
                  <meta itemprop="price" content="${escapeAttribute(amount.toFixed(2))}">
                  <link itemprop="availability" href="https://schema.org/InStock">
                  ${escapeHtml(formatCredit(price))}
                </p>`;
}

function productDataAttrs(product, context) {
  const attrs = [];
  const id = product?.Target?.Id?.Value;
  const sku = product?.SKU?.Value;
  const image = firstImageUrl(product, context);
  if (id) {
    attrs.push(dataAttr("orderly-product-id", id));
  }
  if (sku) {
    attrs.push(dataAttr("orderly-sku-value", sku));
  }
  if (image) {
    attrs.push(dataAttr("orderly-image-url", image));
  }
  if (product?.Price) {
    attrs.push(dataAttr("orderly-price-amount", String(product.Price.Amount ?? 0)));
    attrs.push(dataAttr("orderly-price-currency", product.Price.Currency || "DKK"));
  }
  return attrs.filter(Boolean).join(" ");
}

function dataAttr(name, value) {
  return value ? `data-${name}="${escapeAttribute(value)}"` : "";
}

function unpackProducts(result) {
  const { SearchObjectSchema } = loadedCoreClient();
  return result.data
    .map((item) => anyUnpack(item, SearchObjectSchema))
    .filter(Boolean);
}

function createCategoryNavigation(definitions, options = {}) {
  const pageRoot = options.pageRoot ?? "/categories/";
  const urlMode = options.urlMode ?? "path";
  return definitions.map((definition) => createCategoryItem({
    definition,
    pageRoot,
    urlMode,
    pathLabels: [definition.label],
    pathSlugs: [definition.slug]
  }));
}

function createCategoryItem(input) {
  const { SearchQuerySchema } = loadedCoreClient();
  const id = input.pathSlugs.join("/");
  const query = categorySearchQuery(input.definition, input.parent, input.pathLabels);
  const searchText = categorySearchText(query, input.pathLabels);
  const metadata = {
    slug: input.pathSlugs[input.pathSlugs.length - 1],
    parentId: input.parentId,
    pathSegments: input.pathSlugs,
    query: clone(SearchQuerySchema, query),
    searchText,
    description: input.definition.description ?? `Selected products in ${input.pathLabels.join(" ").toLowerCase()}.`,
    heroImage: input.definition.heroImage ?? input.parent?.heroImage ?? "",
    pagePath: input.pageRoot
  };
  return {
    id,
    label: input.definition.label,
    href: categoryPagePath(id, { pageRoot: input.pageRoot, urlMode: input.urlMode }),
    query: clone(SearchQuerySchema, query),
    metadata,
    children: (input.definition.children ?? []).map((child) => createCategoryItem({
      definition: child,
      parent: input.definition,
      pageRoot: input.pageRoot,
      urlMode: input.urlMode,
      parentId: id,
      pathLabels: [...input.pathLabels, child.label],
      pathSlugs: [...input.pathSlugs, child.slug]
    }))
  };
}

function flattenCategoryNavigation(items) {
  return items.flatMap((item) => [item, ...flattenCategoryNavigation(item.children ?? [])]);
}

function categoryPagePath(id, options = {}) {
  const pageRoot = options.pageRoot ?? "/categories/";
  if (options.urlMode === "hash") {
    return `${pageRoot}#id=${encodeURIComponent(id)}`;
  }
  return `${pageRoot}${id}/`;
}

function categorySearchQuery(definition, parent, pathLabels) {
  const { SearchQuerySchema } = loadedCoreClient();
  if (definition.query) {
    return normalizeSearchQuery(definition.query);
  }
  const parentQuery = parent?.query ? normalizeSearchQuery(parent.query) : undefined;
  const inherited = parentQuery ? clone(SearchQuerySchema, parentQuery) : create(SearchQuerySchema);
  const parentText = parentQuery ? categorySearchText(parentQuery, pathLabels.slice(0, -1)) : parent?.label;
  inherited.Query = [definition.label, parentText].filter(Boolean).join(" ");
  return inherited;
}

function categorySearchText(query, pathLabels) {
  return query.Query
    || query.HiddenQuery
    || query.Tags.map((tag) => tag.Value).filter(Boolean).join(" ")
    || pathLabels.join(" ");
}

function normalizeSearchQuery(input = {}) {
  const { SearchQuerySchema, TagSchema } = loadedCoreClient();
  if (isCoreSearchQuery(input)) {
    return clone(SearchQuerySchema, input);
  }
  return create(SearchQuerySchema, {
    Query: input.query ?? input.keywords,
    HiddenQuery: input.hiddenQuery,
    Tags: (input.tags ?? []).map((tag) => create(TagSchema, { Value: tag })),
    OrderBy: input.orderBy ?? [],
    StoreId: input.storeId,
    Featured: input.featured ?? false
  });
}

function isCoreSearchQuery(input) {
  return "Query" in input
    || "HiddenQuery" in input
    || "Tags" in input
    || "OrderBy" in input
    || "StoreId" in input
    || "Featured" in input;
}

export function mergeShopSearchQuery(query, localShopQuery) {
  const { SearchQuerySchema } = loadedCoreClient();
  const specific = query ? clone(SearchQuerySchema, query) : create(SearchQuerySchema);
  const base = localShopQuery ? clone(SearchQuerySchema, localShopQuery) : undefined;
  if (!base) {
    return specific;
  }

  const merged = clone(SearchQuerySchema, base);
  const hiddenQuery = combineQueryText(
    base.HiddenQuery,
    specific.HiddenQuery,
    specific.Query !== undefined ? base.Query : undefined
  );

  merged.Query = specific.Query ?? base.Query;
  merged.HiddenQuery = hiddenQuery || undefined;
  merged.Tags = [...base.Tags, ...specific.Tags];
  merged.SourceAccountId = base.SourceAccountId ?? specific.SourceAccountId;
  merged.ExcludeAccountId = base.ExcludeAccountId ?? specific.ExcludeAccountId;
  merged.PriceFrom = specific.PriceFrom ?? base.PriceFrom;
  merged.PriceTo = specific.PriceTo ?? base.PriceTo;
  merged.OrderBy = specific.OrderBy.length ? [...specific.OrderBy] : [...base.OrderBy];
  merged.Facets = uniqueStrings([...base.Facets, ...specific.Facets]);
  merged.Barcode = specific.Barcode ?? base.Barcode;
  merged.ConditionAbove = specific.ConditionAbove ?? base.ConditionAbove;
  merged.SaveAsLatest = base.SaveAsLatest || specific.SaveAsLatest;
  merged.Featured = base.Featured || specific.Featured;
  merged.ShareUrl = specific.ShareUrl ?? base.ShareUrl;
  merged.StoreId = base.StoreId ?? specific.StoreId;
  merged.WithBadge = uniqueNumbers([...base.WithBadge, ...specific.WithBadge]);
  merged.RelatedUrl = specific.RelatedUrl ?? base.RelatedUrl;
  merged.Continuation = specific.Continuation;
  return merged;
}

function combineQueryText(...values) {
  return uniqueStrings(values
    .map((value) => value?.trim())
    .filter(Boolean))
    .join(" ");
}

function uniqueStrings(values) {
  return [...new Set(values)];
}

function uniqueNumbers(values) {
  return [...new Set(values)];
}

function cached(context, key, load) {
  if (context.cacheTtlMs > 0) {
    const cachedValue = context.cache.get(key);
    if (cachedValue && Date.now() - cachedValue.time <= context.cacheTtlMs) {
      return cachedValue.value;
    }
  }
  const value = Promise.resolve(load()).catch((error) => {
    context.cache.delete(key);
    throw error;
  });
  context.cache.set(key, { time: Date.now(), value });
  return value;
}

function firstImageUrl(product, context) {
  const image = product?.Images?.[0];
  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 value;
    }
  }
  if (image.ImageDataBase64) {
    return `data:image/jpeg;base64,${image.ImageDataBase64}`;
  }
  if (/^https?:\/\//i.test(image.Name) || image.Name.startsWith("/")) {
    return image.Name;
  }
  if (!image.Name) {
    return "";
  }
  const path = image.IsAsset || image.Name.toLowerCase().includes("cms") || image.Name.toLowerCase().startsWith("images")
    ? "objects"
    : "thumbnails";
  return joinUrl(context.imageBaseUrl, path, image.Name);
}

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 titleFromShareUrl(shareUrl) {
  const path = new URL(shareUrl || "https://orderly.shop/produkt").pathname;
  const name = basename(path, ".html").replace(/-[a-z0-9]{8,}$/i, "").replace(/[-_]/g, " ");
  return name ? name.replace(/\b\w/g, (value) => value.toUpperCase()) : "Produkt";
}

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

function safeHtmlRequestPath(url) {
  const [rawPath] = String(url).split(/[?#]/, 1);
  let path;
  try {
    path = decodeURIComponent(rawPath || "/");
  } catch {
    return undefined;
  }
  if (path.includes("..")) {
    return undefined;
  }
  if (path === "/") {
    return "index.html";
  }
  if (path.endsWith("/")) {
    return `${path.slice(1)}index.html`;
  }
  if (path.startsWith("/products/")) {
    return `${path.slice(1)}/index.html`;
  }
  if ((path.startsWith("/categories/") || path.startsWith("/products/")) && !path.includes(".")) {
    return `${path.slice(1)}/index.html`;
  }
  if (path.endsWith(".html")) {
    return path.slice(1);
  }
  return undefined;
}

function isHtmlPartial(root, file) {
  const path = relative(root, file).replace(/\\/g, "/");
  return path.startsWith("src/includes") || path.startsWith("src/templates");
}

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

async function loadCoreClient() {
  coreClientPromise ??= import("@orderlyshop/core-client").then((module) => {
    coreClientModule = module;
    return module;
  });
  return coreClientPromise;
}

async function loadCoreNodeClient() {
  coreNodeClientPromise ??= import("@orderlyshop/core-client/node");
  return coreNodeClientPromise;
}

function loadedCoreClient() {
  if (!coreClientModule) {
    throw new Error("Orderly SSR core-client schemas are not loaded. Call createOrderlySsrContext() before rendering SSR data.");
  }
  return coreClientModule;
}

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

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

function defaultShopQueryPath(root) {
  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 defaultSiteTitle(root) {
  try {
    const packageJson = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
    return packageJson.displayName ?? packageJson.description ?? packageJson.name ?? "Shop";
  } catch {
    return "Shop";
  }
}

function normalizedRoot(value) {
  const root = String(value || "/").startsWith("/") ? String(value || "/") : `/${value}`;
  return root.endsWith("/") ? root : `${root}/`;
}

function positiveInteger(value, fallback) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

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

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, "\\$&");
}
