(() => {
  const SURFACES = [
    { key: "leftDoor", label: "Left Door", inputId: "numberplateTemplateLeftBackground", baseId: "numberplateTemplateLeftBase", statusId: "numberplateTemplateLeftBackgroundStatus", suffix: "left" },
    { key: "rightDoor", label: "Right Door", inputId: "numberplateTemplateRightBackground", baseId: "numberplateTemplateRightBase", statusId: "numberplateTemplateRightBackgroundStatus", suffix: "right" },
    { key: "hood", label: "Hood", inputId: "numberplateTemplateHoodBackground", baseId: "numberplateTemplateHoodBase", statusId: "numberplateTemplateHoodBackgroundStatus", suffix: "hood" },
  ];
  const DEFAULT_BACKGROUND_BASE = "content\\cars\\common_assets\\numberplates\\backgrounds\\nls_1\\numberplate_nls.texture";
  const DEFAULT_FONT_BASE = "content\\cars\\common_assets\\numberplates\\fonts\\alfa_2\\numbers_guilia_2.texture";
  const TEMPLATE_PLACEHOLDER_PREVIEW = "assets/placeholder_numberplate.png";
  const DEFAULT_TEXT_LAYER = { x: 0, y: 0, scale: 0.3, spacing: 0.02 };
  const DEFAULT_TEXT_COLOR = [1, 1, 1, 1];
  // Disabled legacy game-export path. Normal Numberplate Studio templates use
  // local PNG sources and export baked PNGs for manual placement on liveries.
  const GAME_EXPORT_ENABLED = false;

  const state = {
    assets: { backgrounds: [], fonts: [] },
    builtins: { backgrounds: [], fonts: [] },
    templates: [],
    selectedId: "",
    activeSurface: "leftDoor",
    optionMap: new Map(),
    previewCache: new Map(),
    previewRenderId: 0,
    dirty: false,
    loading: false,
    refreshRequestId: 0,
  };

  const $ = (id) => document.getElementById(id);
  const api = () => window.pywebview?.api || {};

  function status(message) {
    const target = $("numberplateStudioStatus");
    if (target) target.textContent = message || "";
  }

  function setLoading(loading) {
    state.loading = Boolean(loading);
    const root = document.querySelector('[data-loading-surface="numberplates"]');
    const overlay = document.querySelector('[data-loading-overlay="numberplates"]');
    root?.classList.toggle("is-loading", state.loading);
    if (overlay) {
      overlay.hidden = !state.loading;
      overlay.setAttribute("aria-hidden", state.loading ? "false" : "true");
    }
  }

  function packageImportError(message, callName) {
    if (typeof window.LiveryLabFormatImportError === "function") {
      return window.LiveryLabFormatImportError(message, callName);
    }
    const text = String(message || "Package import failed.").trim();
    return { summary: text, original: text, details: "" };
  }

  function showPackageImportError(title, message, callName) {
    const formatted = packageImportError(message, callName);
    status(formatted.summary);
    if (typeof window.showStatusModal === "function") {
      window.showStatusModal({
        type: "error",
        title,
        subtitle: "Action failed",
        error: formatted.summary,
        details: formatted.details,
      });
    }
  }

  function rootPath() {
    return $("rootPath")?.value.trim() || "";
  }

  function normalizeRel(value) {
    return String(value || "").replace(/\//g, "\\");
  }

  function cleanName(value, fallback = "numberplate_template") {
    return String(value || fallback).trim().replace(/[^a-zA-Z0-9_.-]+/g, "_").replace(/^[_\-.]+|[_\-.]+$/g, "").toLowerCase() || fallback;
  }

  function assetLabel(asset) {
    if (asset && typeof asset === "object") return String(asset.name || asset.id || asset.label || asset.rel || "").trim() || "Asset";
    const parts = String(asset || "").split(/[\\/]/).filter(Boolean);
    const file = parts[parts.length - 1] || String(asset || "");
    const group = parts[parts.length - 2] || "";
    return group ? `${group} / ${file.replace(/\.texture$/i, "")}` : file.replace(/\.texture$/i, "");
  }

  function fileName(value) {
    const parts = String(value || "").split(/[\\/]/).filter(Boolean);
    return parts[parts.length - 1] || "";
  }

  function isPendingRef(ref) {
    return Boolean(ref?.pending || ref?.source === "pending" || ref?.pendingPngPath);
  }

  function pendingAssetRef(type, png, label = "") {
    return {
      type,
      assetId: "",
      rel: "",
      label: label || fileName(png) || "Selected PNG",
      sourcePngPath: String(png || ""),
      pendingPngPath: String(png || ""),
      source: "pending",
      active: false,
      pending: true,
    };
  }

  function customAssetRel(asset) {
    return normalizeRel(asset?.assetRel || asset?.gameTextureRel || asset?.textureRel || asset?.texture || asset?.baseTemplate || "");
  }

  function customAssetRef(asset, type) {
    return {
      type,
      assetId: String(asset?.id || "").trim(),
      rel: customAssetRel(asset),
      label: assetLabel(asset),
      sourcePngPath: String(asset?.sourcePngPath || ""),
      source: String(asset?.source || ""),
      sourceBaseTemplate: normalizeRel(asset?.sourceBaseTemplate || asset?.baseTemplate || ""),
      active: Boolean(asset?.active),
    };
  }

  function builtinAssetRef(rel, type) {
    return {
      type,
      assetId: "",
      rel: normalizeRel(rel),
      label: assetLabel(rel),
      sourcePngPath: "",
      active: true,
    };
  }

  function assetPreviewPath(ref, type) {
    const assetId = String(ref?.assetId || "").trim().toLowerCase();
    const normalizedRel = normalizeRel(ref?.rel).toLowerCase();
    const bucket = type === "font" ? state.assets.fonts : state.assets.backgrounds;
    const match = bucket.find((asset) => {
      if (assetId && String(asset?.id || "").trim().toLowerCase() === assetId) return true;
      return normalizedRel && customAssetRel(asset).toLowerCase() === normalizedRel;
    });
    const managedPath = String(match?.sourcePngPath || "").trim();
    if (managedPath) return managedPath;
    return String(ref?.sourcePngPath || ref?.pendingPngPath || "").trim();
  }

  function currentTemplate() {
    return state.templates.find((template) => String(template.id || "") === state.selectedId) || null;
  }

  function createDraftTemplate() {
    const emptyBackground = { type: "background", assetId: "", rel: "", label: "", sourcePngPath: "", active: false };
    const emptyFont = { type: "font", assetId: "", rel: "", label: "", sourcePngPath: "", active: false };
    return {
      formatVersion: 1,
      type: "template",
      id: "",
      name: "new_template",
      backgrounds: {
        leftDoor: { ...emptyBackground },
        rightDoor: { ...emptyBackground },
        hood: { ...emptyBackground },
      },
      font: emptyFont,
      textLayer: { ...DEFAULT_TEXT_LAYER },
      textColor: [...DEFAULT_TEXT_COLOR],
      surfaceTextLayers: {},
      testNumber: "250",
      draft: true,
    };
  }

  function selectTemplate(id) {
    state.selectedId = String(id || "");
    render();
  }

  function selectedRefFromSelect(selectId, fallbackRef = {}) {
    const value = $(selectId)?.value || "";
    if (value && state.optionMap.has(value)) return state.optionMap.get(value);
    if (isPendingRef(fallbackRef)) return fallbackRef;
    return { type: "", assetId: "", rel: "", label: "", sourcePngPath: "" };
  }

  function selectedBaseSource(type, surfaceKey = "") {
    const id = type === "font"
      ? "numberplateTemplateFontBase"
      : (SURFACES.find((surface) => surface.key === surfaceKey)?.baseId || SURFACES[0].baseId);
    return normalizeRel($(id)?.value || defaultBaseSource(type));
  }

  function defaultBaseSource(type) {
    return type === "font" ? DEFAULT_FONT_BASE : DEFAULT_BACKGROUND_BASE;
  }

  function templateAssetName(template, type, surfaceKey = "") {
    const suffix = type === "font" ? "font" : (SURFACES.find((surface) => surface.key === surfaceKey)?.suffix || "background");
    return `${cleanName(template?.name || template?.id || "numberplate_template")}_${suffix}`;
  }

  function customAssetById(type, assetId = "") {
    const bucket = type === "font" ? state.assets.fonts : state.assets.backgrounds;
    const needle = String(assetId || "").trim().toLowerCase();
    return bucket.find((asset) => String(asset?.id || "").trim().toLowerCase() === needle) || null;
  }

  function upsertTemplateInState(template) {
    if (!template) return;
    state.templates = [template, ...state.templates.filter((row) => row.id && row.id !== template.id)];
    state.selectedId = template.id || "";
  }

  function templatePublishLabel(template) {
    if (!GAME_EXPORT_ENABLED) return "PNG generator";
    const published = template?.publishedFont || template?.gameFont;
    if (!published || typeof published !== "object") return "Not published to game";
    const date = String(published.publishedAt || template?.publishedAt || "").trim();
    return date ? `Published to game: ${date}` : "Published to game";
  }

  function templateReadiness(template) {
    const pending = pendingTemplateRefs(template).length;
    if (pending) return GAME_EXPORT_ENABLED ? `${pending} PNG${pending === 1 ? "" : "s"} pending save` : "PNG source ready";
    const missing = localTemplateProblems(template);
    if (missing.length) return GAME_EXPORT_ENABLED ? `${missing.length} local asset${missing.length === 1 ? "" : "s"} missing` : "PNG source missing";
    return GAME_EXPORT_ENABLED ? "Local assets ready" : "Ready for PNG export";
  }

  function fontSourceLabel(template) {
    if (!GAME_EXPORT_ENABLED) return "Local font PNG";
    return fontSourceFromTemplate(template) === "evo" ? "AC EVO font baked locally" : "Local font PNG";
  }

  function updateSaveButtonState() {
    const button = $("numberplateTemplateSave");
    if (!button) return;
    button.classList.toggle("numberplate-template-save-pulse", Boolean(state.dirty));
    button.setAttribute("aria-label", state.dirty ? "Save template changes" : "Save template");
  }

  function setFieldStatus(id, message, tone = "muted") {
    const target = $(id);
    if (!target) return;
    target.textContent = message;
    target.className = `numberplate-field-status is-${tone}`;
  }

  function assetStatusLabel(ref, fallbackName = "PNG") {
    if (isPendingRef(ref)) return fileName(ref.pendingPngPath || ref.sourcePngPath) || fallbackName;
    const label = String(ref?.label || ref?.assetId || ref?.rel || "").trim();
    return label || fallbackName;
  }

  function renderAssetImportStatuses(template) {
    const source = template || currentTemplate() || createDraftTemplate();
    SURFACES.forEach((surface) => {
      const ref = source.backgrounds?.[surface.key] || {};
      if (isPendingRef(ref)) {
        setFieldStatus(
          surface.statusId,
          GAME_EXPORT_ENABLED
            ? `PNG selected, save template to create local asset: ${assetStatusLabel(ref)}`
            : `PNG selected for export: ${assetStatusLabel(ref)}`,
          "warning"
        );
        return;
      }
      if (!String(ref.assetId || "").trim()) {
        setFieldStatus(surface.statusId, assetPreviewPath(ref, "background") ? `Saved PNG source: ${assetStatusLabel(ref)}` : "No square PNG selected", "muted");
        return;
      }
      setFieldStatus(
        surface.statusId,
        `Saved local PNG: ${assetStatusLabel(ref)}`,
        "ok"
      );
    });
    const fontSource = fontSourceFromTemplate(source);
    if (fontSource === "evo") {
      setFieldStatus("numberplateTemplateFontStatus", GAME_EXPORT_ENABLED ? "AC EVO font selected; LiveryLab bakes it locally on save." : "AC EVO font export is disabled. Choose a local font PNG.", GAME_EXPORT_ENABLED ? "ok" : "warning");
      return;
    }
    if (isPendingRef(source.font)) {
      setFieldStatus(
        "numberplateTemplateFontStatus",
        GAME_EXPORT_ENABLED
          ? `Font PNG selected, save template to create local asset: ${assetStatusLabel(source.font, "Font PNG")}`
          : `Font PNG selected for export: ${assetStatusLabel(source.font, "Font PNG")}`,
        "warning"
      );
      return;
    }
    if (!String(source.font?.assetId || "").trim()) {
      setFieldStatus("numberplateTemplateFontStatus", assetPreviewPath(source.font, "font") ? `Saved font PNG source: ${assetStatusLabel(source.font, "Font")}` : "No 1024x512 font PNG selected", "muted");
      return;
    }
    setFieldStatus(
      "numberplateTemplateFontStatus",
      `Saved local font PNG: ${assetStatusLabel(source.font, "Font")}`,
      "ok"
    );
  }

  function fontSourceFromTemplate(template) {
    const source = String(template?.fontSource || "").trim().toLowerCase();
    if (source === "evo" || source === "local") return source;
    return String(template?.evoFontRel || "").trim() ? "evo" : "local";
  }

  function updateFontSourceUi() {
    if (!GAME_EXPORT_ENABLED && $("numberplateTemplateFontSource")) $("numberplateTemplateFontSource").value = "local";
    const source = $("numberplateTemplateFontSource")?.value || "local";
    const evo = source === "evo";
    $("numberplateTemplateFont")?.toggleAttribute("hidden", evo);
    $("numberplateTemplateImportFont")?.toggleAttribute("hidden", evo);
    $("numberplateTemplateEvoFont")?.toggleAttribute("hidden", !evo || !GAME_EXPORT_ENABLED);
    Array.from($("numberplateTemplateFontSource")?.options || []).forEach((option) => {
      if (option.value === "evo") option.disabled = !GAME_EXPORT_ENABLED;
    });
  }

  function textLayerFromInputs() {
    const numeric = (id, fallback, lo, hi) => {
      const value = Number($(id)?.value);
      return Number.isFinite(value) ? Math.max(lo, Math.min(hi, value)) : fallback;
    };
    return {
      x: numeric("numberplateTemplateTextX", 0, -1, 1),
      y: numeric("numberplateTemplateTextY", 0, -1, 1),
      scale: numeric("numberplateTemplateTextScale", 0.3, 0.05, 1),
      spacing: numeric("numberplateTemplateTextSpacing", 0.02, -0.1, 0.5),
    };
  }

  function linearChannelToSrgb(value) {
    const clamped = Math.max(0, Math.min(1, Number(value) || 0));
    return clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
  }

  function srgbChannelToLinear(value) {
    const clamped = Math.max(0, Math.min(1, Number(value) || 0));
    return clamped <= 0.04045 ? clamped / 12.92 : Math.pow((clamped + 0.055) / 1.055, 2.4);
  }

  function linearRgbaToHex(color) {
    const rgba = Array.isArray(color) && color.length >= 3 ? color : DEFAULT_TEXT_COLOR;
    const channels = rgba.slice(0, 3).map((value) => Math.round(linearChannelToSrgb(value) * 255));
    return `#${channels.map((value) => Math.max(0, Math.min(255, value)).toString(16).padStart(2, "0")).join("")}`;
  }

  function hexAlphaToLinearRgba(hex, alpha = 1) {
    const normalized = String(hex || "").trim().replace(/^#/, "");
    if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return [...DEFAULT_TEXT_COLOR];
    const channels = [0, 2, 4].map((index) => parseInt(normalized.slice(index, index + 2), 16) / 255);
    const numericAlpha = Number(alpha);
    return [
      srgbChannelToLinear(channels[0]),
      srgbChannelToLinear(channels[1]),
      srgbChannelToLinear(channels[2]),
      Number.isFinite(numericAlpha) ? Math.max(0, Math.min(1, numericAlpha)) : 1,
    ];
  }

  function textColorFromInputs() {
    return hexAlphaToLinearRgba($("numberplateTemplateTextColor")?.value || "#ffffff", $("numberplateTemplateTextAlpha")?.value ?? 1);
  }

  function textColorCss(color) {
    const rgba = Array.isArray(color) && color.length >= 4 ? color : DEFAULT_TEXT_COLOR;
    const srgb = rgba.slice(0, 3).map((value) => Math.round(linearChannelToSrgb(value) * 255));
    return `rgba(${srgb[0]}, ${srgb[1]}, ${srgb[2]}, ${Math.max(0, Math.min(1, Number(rgba[3]) || 0))})`;
  }

  function applyTemplateFromInputs(template) {
    if (!template) return null;
    const existingBackgrounds = template.backgrounds || {};
    template.name = $("numberplateTemplateName")?.value.trim() || template.name || "numberplate_template";
    template.testNumber = String($("numberplateTemplateTestNumber")?.value || "250").replace(/\D/g, "").slice(0, 3) || "250";
    template.backgrounds = {
      leftDoor: selectedRefFromSelect("numberplateTemplateLeftBackground", existingBackgrounds.leftDoor),
      rightDoor: selectedRefFromSelect("numberplateTemplateRightBackground", existingBackgrounds.rightDoor),
      hood: selectedRefFromSelect("numberplateTemplateHoodBackground", existingBackgrounds.hood),
    };
    template.font = selectedRefFromSelect("numberplateTemplateFont", template.font);
    template.fontSource = GAME_EXPORT_ENABLED ? ($("numberplateTemplateFontSource")?.value || template.fontSource || "local") : "local";
    template.evoFontRel = GAME_EXPORT_ENABLED ? normalizeRel($("numberplateTemplateEvoFont")?.value || template.evoFontRel || "") : "";
    template.textLayer = textLayerFromInputs();
    template.textColor = textColorFromInputs();
    template.text_color = template.textColor;
    return template;
  }

  function setSelectToRef(select, ref, type) {
    if (!select) return;
    const normalizedRel = normalizeRel(ref?.rel).toLowerCase();
    const assetId = String(ref?.assetId || "").toLowerCase();
    const pendingPath = String(ref?.pendingPngPath || ref?.sourcePngPath || "").trim().toLowerCase();
    let match = "";
    for (const [key, optionRef] of state.optionMap.entries()) {
      if (optionRef.type !== type) continue;
      const optionPendingPath = String(optionRef.pendingPngPath || optionRef.sourcePngPath || "").trim().toLowerCase();
      if (isPendingRef(ref) && isPendingRef(optionRef) && pendingPath && optionPendingPath === pendingPath) {
        match = key;
        break;
      }
      if (!isPendingRef(ref) && pendingPath && optionPendingPath === pendingPath) {
        match = key;
        break;
      }
      if (assetId && String(optionRef.assetId || "").toLowerCase() === assetId) {
        match = key;
        break;
      }
      if (normalizedRel && normalizeRel(optionRef.rel).toLowerCase() === normalizedRel) {
        match = key;
        break;
      }
    }
    select.value = match || "";
  }

  function populateAssetSelect(selectId, type, currentRef) {
    const select = $(selectId);
    if (!select) return;
    select.innerHTML = "";
    const empty = document.createElement("option");
    empty.value = "";
    empty.textContent = type === "font" ? "No local font selected" : "No local background selected";
    select.append(empty);

    const customBucket = type === "font" ? state.assets.fonts : state.assets.backgrounds;
    customBucket.forEach((asset) => {
      const ref = customAssetRef(asset, type);
      const key = `custom:${type}:${ref.assetId}`;
      state.optionMap.set(key, ref);
      const option = document.createElement("option");
      option.value = key;
      option.textContent = `${ref.label}${ref.active ? "" : " (local)"}`;
      option.title = ref.rel || ref.sourcePngPath;
      select.append(option);
    });
    if (isPendingRef(currentRef)) {
      const key = `pending:${selectId}`;
      const option = document.createElement("option");
      state.optionMap.set(key, currentRef);
      option.value = key;
      option.textContent = `Pending: ${assetStatusLabel(currentRef, type === "font" ? "Font PNG" : "PNG")}`;
      option.title = currentRef.pendingPngPath || currentRef.sourcePngPath || "";
      select.append(option);
    } else if (!String(currentRef?.assetId || "").trim() && assetPreviewPath(currentRef, type)) {
      const key = `source:${selectId}`;
      const option = document.createElement("option");
      state.optionMap.set(key, currentRef);
      option.value = key;
      option.textContent = `Saved PNG: ${assetStatusLabel(currentRef, type === "font" ? "Font PNG" : "PNG")}`;
      option.title = currentRef.sourcePngPath || "";
      select.append(option);
    }
    setSelectToRef(select, currentRef || {}, type);
  }

  function populateBaseSelect(selectId, type, currentRel = "") {
    const select = $(selectId);
    if (!select) return;
    const bucket = type === "font" ? state.builtins.fonts : state.builtins.backgrounds;
    select.innerHTML = "";
    const empty = document.createElement("option");
    empty.value = "";
    empty.textContent = type === "font" ? "AC EVO base font" : "AC EVO base background";
    select.append(empty);
    bucket.forEach((rel) => {
      const value = normalizeRel(rel);
      const option = document.createElement("option");
      option.value = value;
      option.textContent = assetLabel(value);
      option.title = value;
      select.append(option);
    });
    const normalized = normalizeRel(currentRel).toLowerCase();
    const target = normalized || normalizeRel(defaultBaseSource(type)).toLowerCase();
    if (target) {
      const match = Array.from(select.options).find((option) => normalizeRel(option.value).toLowerCase() === target);
      if (match) select.value = match.value;
    }
  }

  function renderTemplateList() {
    const list = $("numberplateTemplateList");
    if (!list) return;
    list.innerHTML = "";
    if (!state.templates.length) {
      const empty = document.createElement("div");
      empty.className = "numberplate-template-empty";
      empty.textContent = "No templates yet";
      list.append(empty);
      return;
    }
    state.templates.forEach((template) => {
      const button = document.createElement("button");
      button.type = "button";
      button.className = "numberplate-template-item";
      button.classList.toggle("active", String(template.id || "") === state.selectedId);
      const preview = document.createElement("img");
      preview.className = "numberplate-template-thumb";
      preview.alt = "";
      preview.loading = "lazy";
      preview.src = TEMPLATE_PLACEHOLDER_PREVIEW;
      applyTemplatePreviewImage(preview, template);
      const copy = document.createElement("span");
      copy.className = "numberplate-template-copy";
      const name = document.createElement("strong");
      name.textContent = template.name || template.id || "Template";
      const meta = document.createElement("span");
      meta.textContent = `${templateReadiness(template)} - ${templatePublishLabel(template)}`;
      const font = document.createElement("span");
      font.textContent = template.font?.label || template.font?.rel || "No font";
      copy.append(name, meta, font);
      button.append(preview, copy);
      button.addEventListener("click", () => selectTemplate(template.id || ""));
      list.append(button);
    });
  }

  function applyTemplatePreviewImage(image, template) {
    const previewPath = String(template?.previewPngPath || "").trim();
    if (!previewPath) return;
    previewDataUrl(previewPath).then((src) => {
      if (src) image.src = src;
    }).catch(() => {});
  }

  function renderEditor() {
    const template = currentTemplate() || createDraftTemplate();
    if (!currentTemplate() && !state.templates.length) {
      state.templates = [template];
      state.selectedId = "";
    }
    if ($("numberplateTemplateName")) $("numberplateTemplateName").value = template.name || "";
    if ($("numberplateTemplateTestNumber")) $("numberplateTemplateTestNumber").value = template.testNumber || "250";
    state.optionMap.clear();
    populateAssetSelect("numberplateTemplateLeftBackground", "background", template.backgrounds?.leftDoor);
    populateAssetSelect("numberplateTemplateRightBackground", "background", template.backgrounds?.rightDoor);
    populateAssetSelect("numberplateTemplateHoodBackground", "background", template.backgrounds?.hood);
    populateAssetSelect("numberplateTemplateFont", "font", template.font);
    populateBaseSelect("numberplateTemplateLeftBase", "background", template.backgrounds?.leftDoor?.rel);
    populateBaseSelect("numberplateTemplateRightBase", "background", template.backgrounds?.rightDoor?.rel);
    populateBaseSelect("numberplateTemplateHoodBase", "background", template.backgrounds?.hood?.rel);
    populateBaseSelect("numberplateTemplateFontBase", "font", template.font?.rel);
    populateBaseSelect("numberplateTemplateEvoFont", "font", template.evoFontRel || template.font?.sourceBaseTemplate || template.font?.rel);
    if ($("numberplateTemplateFontSource")) $("numberplateTemplateFontSource").value = fontSourceFromTemplate(template);
    updateFontSourceUi();
    const layer = { ...DEFAULT_TEXT_LAYER, ...(template.textLayer || {}) };
    if ($("numberplateTemplateTextX")) $("numberplateTemplateTextX").value = String(layer.x);
    if ($("numberplateTemplateTextY")) $("numberplateTemplateTextY").value = String(layer.y);
    if ($("numberplateTemplateTextScale")) $("numberplateTemplateTextScale").value = String(layer.scale);
    if ($("numberplateTemplateTextSpacing")) $("numberplateTemplateTextSpacing").value = String(layer.spacing);
    const color = Array.isArray(template.textColor || template.text_color) ? (template.textColor || template.text_color) : DEFAULT_TEXT_COLOR;
    if ($("numberplateTemplateTextColor")) $("numberplateTemplateTextColor").value = linearRgbaToHex(color);
    if ($("numberplateTemplateTextAlpha")) $("numberplateTemplateTextAlpha").value = String(Math.round((Number(color[3]) || 1) * 1000) / 1000);
    renderAssetImportStatuses(template);
    renderSummary(template);
  }

  function renderSummary(template) {
    const target = $("numberplateTemplateSummary");
    if (!target || !template) return;
    updateSaveButtonState();
    const backgrounds = template.backgrounds || {};
    const problems = localTemplateProblems(template);
    const nextStep = problems.length
      ? `Choose PNGs for: ${problems.map((problem) => problem.replace(" is missing a local asset.", "").replace(" PNG is missing.", "")).join(", ")}`
      : (state.dirty ? "Save template to keep the current changes." : (GAME_EXPORT_ENABLED ? "Ready. Use Publish to Game only when the in-game shared font target should be updated." : "Ready. Export PNGs and place them on the livery texture."));
    target.textContent = [
      `Template: ${template.name || "-"}`,
      `Status: ${template.id ? (state.dirty ? "Unsaved local changes" : "Saved locally") : "Draft not saved yet"}`,
      `Assets: ${templateReadiness(template)}`,
      `Game: ${templatePublishLabel(template)}`,
      `Left door: ${backgrounds.leftDoor?.label || backgrounds.leftDoor?.rel || "-"}`,
      `Right door: ${backgrounds.rightDoor?.label || backgrounds.rightDoor?.rel || "-"}`,
      `Hood: ${backgrounds.hood?.label || backgrounds.hood?.rel || "-"}`,
      `Font source: ${fontSourceLabel(template)}`,
      `Font: ${template.font?.label || template.font?.rel || "-"}`,
      `Next: ${nextStep}`,
    ].join("\n");
  }

  function selectImportedAsset(type, surfaceKey, ref) {
    const selectId = type === "font"
      ? "numberplateTemplateFont"
      : (SURFACES.find((surface) => surface.key === surfaceKey)?.inputId || "");
    if (!selectId) return;
    const select = $(selectId);
    if (!select) return;
    setSelectToRef(select, ref || {}, type);
  }

  function loadImage(src) {
    return new Promise((resolve, reject) => {
      const image = new Image();
      image.onload = () => resolve(image);
      image.onerror = reject;
      image.src = src;
    });
  }

  async function previewDataUrl(path) {
    const key = String(path || "").trim();
    if (!key || !api().load_image_preview) return "";
    if (state.previewCache.has(key)) return state.previewCache.get(key);
    const result = await api().load_image_preview(key, "numberplates");
    const src = result?.ok && result.data?.data_url ? result.data.data_url : "";
    state.previewCache.set(key, src);
    return src;
  }

  function alphaRuns(counts, threshold) {
    const runs = [];
    let start = -1;
    for (let index = 0; index < counts.length; index += 1) {
      if (counts[index] > threshold) {
        if (start < 0) start = index;
      } else if (start >= 0) {
        runs.push({ start, end: index - 1, size: index - start });
        start = -1;
      }
    }
    if (start >= 0) runs.push({ start, end: counts.length - 1, size: counts.length - start });
    return runs;
  }

  function fallbackGlyphBoxes(image) {
    const width = image.naturalWidth || image.width || 1024;
    const height = image.naturalHeight || image.height || 512;
    const cellWidth = width / 8;
    const cellHeight = height / 2;
    return Array.from({ length: 10 }, (_item, index) => {
      const row = index < 8 ? 0 : 1;
      const column = index < 8 ? index : index - 8;
      return { x: Math.round(column * cellWidth), y: Math.round(row * cellHeight), w: Math.round(cellWidth), h: Math.round(cellHeight) };
    });
  }

  function detectGlyphBoxes(image) {
    const width = image.naturalWidth || image.width || 1024;
    const height = image.naturalHeight || image.height || 512;
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const context = canvas.getContext("2d", { willReadFrequently: true });
    context.drawImage(image, 0, 0, width, height);
    const data = context.getImageData(0, 0, width, height).data;
    const rowCounts = new Array(height).fill(0);
    for (let y = 0; y < height; y += 1) {
      let count = 0;
      for (let x = 0; x < width; x += 1) {
        if (data[(y * width + x) * 4 + 3] > 16) count += 1;
      }
      rowCounts[y] = count;
    }
    const rows = alphaRuns(rowCounts, Math.max(4, Math.round(width * 0.006))).filter((row) => row.size > height * 0.08).slice(0, 2);
    if (rows.length < 2) return fallbackGlyphBoxes(image);
    const boxes = [];
    rows.forEach((row, rowIndex) => {
      const colCounts = new Array(width).fill(0);
      for (let y = row.start; y <= row.end; y += 1) {
        for (let x = 0; x < width; x += 1) {
          if (data[(y * width + x) * 4 + 3] > 16) colCounts[x] += 1;
        }
      }
      const expected = rowIndex === 0 ? 8 : 2;
      alphaRuns(colCounts, Math.max(3, Math.round(row.size * 0.025)))
        .filter((col) => col.size > width * 0.03)
        .slice(0, expected)
        .forEach((col) => boxes.push({ x: col.start, y: row.start, w: col.end - col.start + 1, h: row.end - row.start + 1 }));
    });
    return boxes.length === 10 ? boxes : fallbackGlyphBoxes(image);
  }

  function drawTintedGlyph(context, image, glyph, x, y, width, height, fillStyle) {
    const buffer = document.createElement("canvas");
    buffer.width = Math.max(1, Math.ceil(width));
    buffer.height = Math.max(1, Math.ceil(height));
    const bufferContext = buffer.getContext("2d");
    bufferContext.imageSmoothingEnabled = true;
    bufferContext.imageSmoothingQuality = "high";
    bufferContext.drawImage(image, glyph.x, glyph.y, glyph.w, glyph.h, 0, 0, buffer.width, buffer.height);
    bufferContext.globalCompositeOperation = "source-in";
    bufferContext.fillStyle = fillStyle;
    bufferContext.fillRect(0, 0, buffer.width, buffer.height);
    bufferContext.globalCompositeOperation = "source-over";
    context.drawImage(buffer, x, y, width, height);
  }

  function drawAtlasText(context, canvas, image, text, layer, textColor) {
    const digits = String(text || "").replace(/\D/g, "").slice(0, 3);
    if (!digits) return false;
    const glyphs = digits.split("").map((digit) => detectGlyphBoxes(image)[Number(digit)]).filter(Boolean);
    if (glyphs.length !== digits.length) return false;
    const fillStyle = textColorCss(textColor);
    const maxGlyphHeight = Math.max(...glyphs.map((glyph) => glyph.h || 1), 1);
    const targetHeight = canvas.height * layer.scale;
    const scale = targetHeight / maxGlyphHeight;
    const spacing = canvas.height * layer.spacing;
    const totalWidth = glyphs.reduce((sum, glyph) => sum + glyph.w * scale, 0) + spacing * Math.max(0, glyphs.length - 1);
    let x = canvas.width * (0.5 + layer.x) - totalWidth * 0.5;
    const centerY = canvas.height * (0.53 + layer.y);
    glyphs.forEach((glyph) => {
      const width = glyph.w * scale;
      const height = glyph.h * scale;
      drawTintedGlyph(context, image, glyph, x, centerY - height * 0.5, width, height, fillStyle);
      x += width + spacing;
    });
    return true;
  }

  async function drawSurfacePreview(renderId, canvas, template, surface, font, layer, textColor) {
    const context = canvas.getContext("2d");
    const background = template.backgrounds?.[surface.key] || {};
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.fillStyle = "#0c0f14";
    context.fillRect(0, 0, canvas.width, canvas.height);
    context.strokeStyle = "rgba(255,255,255,0.18)";
    context.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);

    const backgroundSrc = await previewDataUrl(assetPreviewPath(background, "background"));
    if (renderId !== state.previewRenderId) return;
    if (backgroundSrc) {
      const image = await loadImage(backgroundSrc);
      if (renderId !== state.previewRenderId) return;
      context.drawImage(image, 0, 0, canvas.width, canvas.height);
    } else {
      context.fillStyle = "rgba(255,255,255,0.08)";
      context.fillRect(44, 166, 424, 180);
      context.fillStyle = "rgba(255,255,255,0.64)";
      context.font = "20px Arial, sans-serif";
      context.textAlign = "center";
      context.fillText(background.label || background.rel || `No ${surface.label} preview`, canvas.width * 0.5, 260);
    }

    const fontSrc = await previewDataUrl(assetPreviewPath(font, "font"));
    if (renderId !== state.previewRenderId) return;
    let rendered = false;
    if (fontSrc) {
      try {
        rendered = drawAtlasText(context, canvas, await loadImage(fontSrc), template.testNumber || "250", layer, textColor);
        if (renderId !== state.previewRenderId) return;
      } catch (_error) {
        rendered = false;
      }
    }
    if (!rendered) {
      context.font = `700 ${Math.round(canvas.height * layer.scale)}px Arial, sans-serif`;
      context.textAlign = "center";
      context.textBaseline = "middle";
      context.fillStyle = textColorCss(textColor);
      context.strokeStyle = "rgba(255,255,255,0.85)";
      context.lineWidth = 6;
      const x = canvas.width * (0.5 + layer.x);
      const y = canvas.height * (0.53 + layer.y);
      context.strokeText(template.testNumber || "250", x, y);
      context.fillText(template.testNumber || "250", x, y);
    }
  }

  async function drawPreview() {
    const renderId = ++state.previewRenderId;
    const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
    const font = template.font || {};
    const layer = template.textLayer || DEFAULT_TEXT_LAYER;
    const textColor = Array.isArray(template.textColor || template.text_color) ? (template.textColor || template.text_color) : DEFAULT_TEXT_COLOR;
    const jobs = SURFACES.map(async (surface) => {
      const canvas = document.querySelector(`[data-numberplate-preview="${surface.key}"]`);
      if (!canvas) return;
      await drawSurfacePreview(renderId, canvas, template, surface, font, layer, textColor);
    });
    await Promise.all(jobs);
    if (renderId !== state.previewRenderId) return;
    renderSummary(template);
  }

  function preferredPreviewCanvas(template) {
    const backgrounds = template?.backgrounds || {};
    const preferred = SURFACES.find((surface) => {
      const ref = backgrounds[surface.key] || {};
      return String(ref.assetId || ref.rel || "").trim();
    }) || SURFACES[0];
    return document.querySelector(`[data-numberplate-preview="${preferred.key}"]`)
      || document.querySelector(`[data-numberplate-preview="${SURFACES[0].key}"]`);
  }

  async function templatePreviewDataUrl(template) {
    try {
      await drawPreview();
      const canvas = preferredPreviewCanvas(template);
      return canvas?.toDataURL ? canvas.toDataURL("image/png") : "";
    } catch (_error) {
      return "";
    }
  }

  async function renderSurfaceExportPng(template, surface) {
    const background = template.backgrounds?.[surface.key] || {};
    const backgroundSrc = await previewDataUrl(assetPreviewPath(background, "background"));
    if (!backgroundSrc) return null;
    const backgroundImage = await loadImage(backgroundSrc);
    const size = Math.max(512, backgroundImage.naturalWidth || backgroundImage.width || 512, backgroundImage.naturalHeight || backgroundImage.height || 512);
    const canvas = document.createElement("canvas");
    canvas.width = size;
    canvas.height = size;
    const context = canvas.getContext("2d");
    context.clearRect(0, 0, size, size);
    context.drawImage(backgroundImage, 0, 0, size, size);
    const layer = template.textLayer || DEFAULT_TEXT_LAYER;
    const textColor = Array.isArray(template.textColor || template.text_color) ? (template.textColor || template.text_color) : DEFAULT_TEXT_COLOR;
    const fontSrc = await previewDataUrl(assetPreviewPath(template.font || {}, "font"));
    let rendered = false;
    if (fontSrc) {
      try {
        rendered = drawAtlasText(context, canvas, await loadImage(fontSrc), template.testNumber || "250", layer, textColor);
      } catch (_error) {
        rendered = false;
      }
    }
    if (!rendered) {
      context.font = `700 ${Math.round(canvas.height * layer.scale)}px Arial, sans-serif`;
      context.textAlign = "center";
      context.textBaseline = "middle";
      context.fillStyle = textColorCss(textColor);
      context.strokeStyle = "rgba(255,255,255,0.85)";
      context.lineWidth = Math.max(2, Math.round(canvas.height * 0.012));
      const x = canvas.width * (0.5 + layer.x);
      const y = canvas.height * (0.53 + layer.y);
      context.strokeText(template.testNumber || "250", x, y);
      context.fillText(template.testNumber || "250", x, y);
    }
    return canvas.toDataURL("image/png");
  }

  function exportFilename(template, surface) {
    const name = cleanName(template.name || template.id || "numberplate");
    return `${name}_${surface.suffix || surface.key}.png`;
  }

  async function exportTemplatePngs() {
    const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
    const problems = localTemplateProblems(template);
    if (problems.length) {
      statusValidationProblems(problems);
      return;
    }
    const outDir = await chooseFolder(await numberplatePngExportRoot());
    if (!outDir) return;
    status("Baking PNGs...");
    const surfaces = [];
    for (const surface of SURFACES) {
      const dataUrl = await renderSurfaceExportPng(template, surface);
      if (!dataUrl) continue;
      surfaces.push({
        key: surface.key,
        surface: surface.key,
        filename: exportFilename(template, surface),
        dataUrl,
      });
    }
    if (!surfaces.length) {
      status("No numberplate PNGs could be baked.");
      return;
    }
    const result = await api().export_numberplate_generator_pngs({
      outDir,
      root: rootPath(),
      templateName: template.name || template.id || "numberplate",
      surfaces,
    });
    if (!result?.ok) {
      status(result?.message || "PNG export failed");
      return;
    }
    if (typeof window.showStatusModal === "function") {
      window.showStatusModal({
        type: "success",
        title: "Numberplate PNGs exported",
        subtitle: `${result.data?.writtenCount || surfaces.length} PNG files`,
        message: "The baked numberplate PNGs are ready for placement on the livery texture.",
        details: result.message || "",
        openFolderPath: result.data?.outDir || outDir,
      });
    }
    status(result.message || "Numberplate PNGs exported.");
  }

  function render() {
    renderTemplateList();
    renderEditor();
    updateSaveButtonState();
    drawPreview().catch((error) => status(`Preview failed: ${error?.message || error}`));
  }

  async function loadBuiltins() {
    if (!GAME_EXPORT_ENABLED) {
      state.builtins = { backgrounds: [], fonts: [] };
      return;
    }
    const root = rootPath();
    if (!root || !api().list_numberplate_templates) {
      state.builtins = { backgrounds: [], fonts: [] };
      return;
    }
    const result = await api().list_numberplate_templates(root);
    state.builtins = result?.ok ? { backgrounds: result.data?.backgrounds || [], fonts: result.data?.fonts || [] } : { backgrounds: [], fonts: [] };
  }

  async function loadAssets() {
    if (!api().list_custom_numberplates) {
      state.assets = { backgrounds: [], fonts: [] };
      state.previewCache.clear();
      return;
    }
    const result = await api().list_custom_numberplates(rootPath());
    state.assets = result?.ok ? result.data || { backgrounds: [], fonts: [] } : { backgrounds: [], fonts: [] };
    state.previewCache.clear();
  }

  async function loadTemplates() {
    if (!api().list_numberplate_studio_templates) {
      state.templates = [];
      return;
    }
    const result = await api().list_numberplate_studio_templates();
    state.templates = result?.ok ? result.data?.templates || [] : [];
    if (!state.templates.some((template) => String(template.id || "") === state.selectedId)) {
      state.selectedId = state.templates[0]?.id || "";
    }
  }

  async function refresh() {
    const requestId = ++state.refreshRequestId;
    setLoading(true);
    status("Loading templates...");
    try {
      await Promise.all([loadBuiltins(), loadAssets(), loadTemplates()]);
      if (requestId !== state.refreshRequestId) return;
      state.previewCache.clear();
      render();
      status("Ready");
    } finally {
      if (requestId === state.refreshRequestId) setLoading(false);
    }
  }

  function markDirty() {
    state.dirty = true;
    updateSaveButtonState();
    drawPreview().catch(() => {});
  }

  function pendingTemplateRefs(template) {
    const refs = [];
    SURFACES.forEach((surface) => {
      const ref = template?.backgrounds?.[surface.key] || {};
      if (isPendingRef(ref)) refs.push({ type: "background", surfaceKey: surface.key, ref });
    });
    if (isPendingRef(template?.font)) refs.push({ type: "font", surfaceKey: "", ref: template.font });
    return refs;
  }

  function hasTemplatePngSource(ref, type) {
    return Boolean(String(ref?.assetId || "").trim() || isPendingRef(ref) || assetPreviewPath(ref, type));
  }

  function localTemplateProblems(template) {
    const problems = [];
    SURFACES.forEach((surface) => {
      const ref = template.backgrounds?.[surface.key] || {};
      if (GAME_EXPORT_ENABLED) {
        if (!String(ref.assetId || "").trim() && !isPendingRef(ref)) problems.push(`${surface.label} background is missing a local asset.`);
      } else if (!hasTemplatePngSource(ref, "background")) {
        problems.push(`${surface.label} background PNG is missing.`);
      }
    });
    if (GAME_EXPORT_ENABLED && !String(template.font?.assetId || "").trim() && !isPendingRef(template.font)) problems.push("Font is missing a local asset.");
    return problems;
  }

  function statusValidationProblems(problems) {
    status(problems.length ? problems.join(" ") : "");
  }

  async function ensureDefaultFontAsset(template) {
    if (!GAME_EXPORT_ENABLED) return true;
    if (String(template.font?.assetId || "").trim()) return true;
    const root = rootPath();
    if (!root || !api().create_custom_numberplate_asset_from_game) return false;
    const name = templateAssetName(template, "font");
    status("Preparing default local font asset...");
    const result = await api().create_custom_numberplate_asset_from_game({
      root,
      type: "font",
      baseTemplate: selectedBaseSource("font"),
      name,
      overwrite: true,
    });
    if (!result?.ok) {
      status(result?.message || "Default font asset could not be prepared.");
      return false;
    }
    await loadAssets();
    const asset = customAssetById("font", result.data?.id || name);
    template.font = asset ? customAssetRef(asset, "font") : customAssetRef(result.data || {}, "font");
    return true;
  }

  async function ensureEvoFontAsset(template, rel = "") {
    if (!GAME_EXPORT_ENABLED) {
      status("AC EVO font export is disabled. Choose a local font PNG.");
      return false;
    }
    const root = rootPath();
    const baseTemplate = normalizeRel(rel || template?.evoFontRel || selectedBaseSource("font"));
    if (!root || !baseTemplate || !api().create_custom_numberplate_asset_from_game) return false;
    const name = templateAssetName(template, "font");
    status("Preparing AC EVO font for baked numbers...");
    const result = await api().create_custom_numberplate_asset_from_game({
      root,
      type: "font",
      baseTemplate,
      name,
      overwrite: true,
    });
    if (!result?.ok) {
      status(result?.message || "AC EVO font could not be prepared.");
      return false;
    }
    await loadAssets();
    const asset = customAssetById("font", result.data?.id || name);
    template.font = asset ? customAssetRef(asset, "font") : customAssetRef(result.data || {}, "font");
    template.fontSource = "evo";
    template.evoFontRel = baseTemplate;
    upsertTemplateInState(template);
    return true;
  }

  async function validateChosenPng(type, png, statusId = "") {
    if (!api().inspect_preview_image) return true;
    const result = await api().inspect_preview_image(png);
    if (!result?.ok) {
      const message = result?.message || "PNG could not be inspected.";
      if (statusId) setFieldStatus(statusId, message, "error");
      status(message);
      return false;
    }
    const width = Number(result.data?.width || 0);
    const height = Number(result.data?.height || 0);
    const valid = type === "font" ? width === 1024 && height === 512 : width > 0 && width === height;
    if (!valid) {
      const message = type === "font"
        ? `Font PNG must be 1024x512. Selected image is ${width}x${height}.`
        : `Background PNG must be square. Selected image is ${width}x${height}.`;
      if (statusId) setFieldStatus(statusId, message, "error");
      status(message);
      return false;
    }
    return true;
  }

  async function confirmAssetOverwrite(type, name) {
    const existing = customAssetById(type, name);
    if (!existing) return true;
    const message = `A local ${type === "font" ? "font" : "background"} asset named "${name}" already exists. Replace it on save?`;
    if (typeof window.showConfirmModal === "function") {
      return window.showConfirmModal({
        type: "warning",
        title: "Replace numberplate asset",
        message,
        details: "This can happen when you save a template again with the same name. Only the local Numberplates asset is replaced.",
        confirmLabel: "Replace asset",
        cancelLabel: "Cancel",
      });
    }
    return window.confirm(message);
  }

  async function materializePendingAssets(template) {
    if (!GAME_EXPORT_ENABLED) return true;
    const pending = pendingTemplateRefs(template);
    if (!pending.length) return true;
    const root = rootPath();
    if (!root || !api().create_custom_numberplate_asset) {
      status("Set the document root before saving selected PNGs.");
      return false;
    }
    const created = [];
    for (const item of pending) {
      const surface = SURFACES.find((row) => row.key === item.surfaceKey);
      const png = item.ref.pendingPngPath || item.ref.sourcePngPath || "";
      const name = templateAssetName(template, item.type, item.surfaceKey);
      const statusId = item.type === "font" ? "numberplateTemplateFontStatus" : surface?.statusId || "";
      if (!(await validateChosenPng(item.type, png, statusId))) return false;
      const overwrite = Boolean(customAssetById(item.type, name));
      if (overwrite && !(await confirmAssetOverwrite(item.type, name))) {
        status("Save cancelled.");
        return false;
      }
      status(`Creating local ${item.type === "font" ? "font" : surface?.label || "background"} asset...`);
      const result = await api().create_custom_numberplate_asset({
        root,
        type: item.type,
        baseTemplate: selectedBaseSource(item.type, item.surfaceKey),
        name,
        png,
        overwrite,
      });
      if (!result?.ok) {
        const message = result?.message || "Selected PNG could not be saved as a local asset.";
        if (statusId) setFieldStatus(statusId, message, "error");
        status(message);
        return false;
      }
      created.push({ ...item, id: result.data?.id || name, fallback: result.data || {} });
    }
    await loadAssets();
    created.forEach((item) => {
      const asset = customAssetById(item.type, item.id);
      const nextRef = asset ? customAssetRef(asset, item.type) : customAssetRef(item.fallback, item.type);
      if (item.type === "font") {
        template.font = nextRef;
        template.fontSource = "local";
        template.evoFontRel = "";
      } else {
        template.backgrounds[item.surfaceKey] = nextRef;
      }
    });
    state.previewCache.clear();
    upsertTemplateInState(template);
    renderEditor();
    return true;
  }

  async function prepareTemplateForSave(template) {
    if (!template) return false;
    if (!String(template.backgrounds?.hood?.assetId || "").trim() && !isPendingRef(template.backgrounds?.hood)) {
      const fallback = (String(template.backgrounds?.leftDoor?.assetId || "").trim() || isPendingRef(template.backgrounds?.leftDoor))
        ? template.backgrounds.leftDoor
        : template.backgrounds?.rightDoor;
      if (fallback?.assetId || isPendingRef(fallback)) template.backgrounds.hood = { ...fallback, label: `${fallback.label || "Background"} (hood fallback)` };
    }
    if (!GAME_EXPORT_ENABLED) {
      template.fontSource = "local";
      template.evoFontRel = "";
      upsertTemplateInState(template);
      return true;
    }
    if (fontSourceFromTemplate(template) === "evo") {
      const ok = await ensureEvoFontAsset(template, template.evoFontRel || selectedBaseSource("font"));
      if (!ok) return false;
    } else if (!String(template.font?.assetId || "").trim() && !isPendingRef(template.font)) {
      const ok = await ensureDefaultFontAsset(template);
      if (!ok) return false;
    }
    upsertTemplateInState(template);
    return true;
  }

  async function saveTemplate(options = {}) {
    const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
    if (!(await prepareTemplateForSave(template))) return null;
    if (!(await materializePendingAssets(template))) return null;
    const problems = localTemplateProblems(template);
    if (problems.length) {
      statusValidationProblems(problems);
      return null;
    }
    const root = rootPath();
    const publishToGame = GAME_EXPORT_ENABLED && Boolean(options.publishToGame);
    const payload = {
      ...template,
      id: template.id || cleanName(template.name),
      name: template.name || cleanName(template.id),
      root,
      publishToGame,
      previewDataUrl: await templatePreviewDataUrl(template),
    };
    status("Saving template...");
    const result = await api().save_numberplate_studio_template(payload);
    if (!result?.ok) {
      status(result?.message || "Save failed");
      return null;
    }
    state.dirty = false;
    state.selectedId = result.data?.id || payload.id;
    await refresh();
    if (typeof window.loadMySkins === "function") {
      await window.loadMySkins();
    }
    window.dispatchEvent(new CustomEvent("liverylab-numberplate-templates-updated"));
    const affected = result.data?.affectedSkins;
    const affectedCount = Number(affected?.updatedCount || 0);
    status(affectedCount ? `${result.message || "Template saved"} (${affectedCount} skin${affectedCount === 1 ? "" : "s"} need Update in Game)` : (result.message || "Template saved"));
    return result;
  }

  async function publishTemplate() {
    if (!GAME_EXPORT_ENABLED) {
      status("Publish to Game is disabled. Use Export PNGs and place them directly on the livery texture.");
      return;
    }
    const root = rootPath();
    if (!root) {
      status("Set the document root before publishing to game.");
      return;
    }
    const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
    if (!(await prepareTemplateForSave(template))) return;
    const problems = localTemplateProblems(template);
    if (problems.length) {
      statusValidationProblems(problems);
      return;
    }
    const confirmPublish = typeof window.showConfirmModal === "function" ? window.showConfirmModal : null;
    const proceed = confirmPublish
      ? await confirmPublish({
        type: "warning",
        title: "Publish numberplate template",
        message: "Publish this template font into the AC EVO common numberplate fonts?",
        details: [
          `Template: ${template.name || template.id || "Template"}`,
          `Root: ${root}`,
          "",
          "Writes:",
          "- Create or update one shared template font under content\\cars\\common_assets\\numberplates\\fonts.",
          "- Convert the configured local font PNG to .texturemips.",
          "- Back up existing target files before overwrite.",
        ].join("\n"),
        confirmLabel: "Publish to Game",
        cancelLabel: "Cancel",
      })
      : window.confirm(`Publish template "${template.name || template.id || "Template"}" to game?`);
    if (!proceed) {
      status("Publish cancelled.");
      return;
    }
    await saveTemplate({ publishToGame: true });
  }

  async function loadTemplateUsage(templateId) {
    if (!api().get_numberplate_studio_template_usage) return null;
    const result = await api().get_numberplate_studio_template_usage({ id: templateId });
    return result?.ok ? result.data || null : null;
  }

  function templateDeleteDetails(template, usage) {
    const skins = Array.isArray(usage?.usedBySkins) ? usage.usedBySkins : [];
    const skinCount = Number(usage?.skinCount || skins.length || 0);
    const assetCount = Number(usage?.assetCount || 0);
    const lines = [
      `Template: ${template.name || template.id}`,
      "",
      usage
        ? (skinCount
          ? `This template is currently used by ${skinCount} skin${skinCount === 1 ? "" : "s"}.`
          : "No skin currently references this template.")
        : "Template usage could not be checked in this build.",
    ];
    if (skinCount) {
      skins.slice(0, 12).forEach((skin) => {
        const label = [skin.label || skin.skinId || skin.skinKey || "Skin", skin.carModel || ""].filter(Boolean).join(" - ");
        lines.push(`- ${label}`);
      });
      if (skinCount > 12) lines.push(`- ...and ${skinCount - 12} more`);
      lines.push("");
      lines.push("Affected skins keep their current baked files, but they cannot be rebaked from this template after deletion.");
    }
    lines.push("");
    lines.push(`Referenced local asset${assetCount === 1 ? "" : "s"} kept: ${assetCount}`);
    lines.push("Backgrounds and fonts are not deleted because they can be shared by other templates.");
    return lines.join("\n");
  }

  async function deleteTemplate() {
    const template = currentTemplate();
    if (!template?.id) return;
    const usage = await loadTemplateUsage(template.id);
    const skinCount = Number(usage?.skinCount || 0);
    const confirmDelete = typeof window.showConfirmModal === "function" ? window.showConfirmModal : null;
    const proceed = confirmDelete
      ? await confirmDelete({
        type: skinCount ? "warning" : "danger",
        title: skinCount ? "Delete used template?" : "Delete numberplate template?",
        message: skinCount
          ? "This template is still referenced by one or more skins."
          : "This removes the template from Numberplate Studio.",
        details: templateDeleteDetails(template, usage),
        confirmLabel: skinCount ? "Delete template anyway" : "Delete template",
        cancelLabel: "Cancel",
      })
      : window.confirm(`Delete template "${template.name || template.id}"?`);
    if (!proceed) {
      status("Delete cancelled.");
      return;
    }
    status("Deleting template...");
    const result = await api().delete_numberplate_studio_template({ id: template.id });
    if (!result?.ok) {
      status(result?.message || "Delete failed");
      return;
    }
    state.selectedId = "";
    await refresh();
    window.dispatchEvent(new CustomEvent("liverylab-numberplate-templates-updated"));
    status(result.message || "Template deleted");
  }

  function newTemplate() {
    const draft = createDraftTemplate();
    draft.id = "";
    draft.name = "new_template";
    state.templates = [draft, ...state.templates.filter((template) => template.id)];
    state.selectedId = "";
    state.dirty = true;
    render();
    status("New template draft");
  }

  async function choosePng() {
    if (!api().choose_path) return "";
    const picked = await api().choose_path("file", ["PNG (*.png)", "All files (*.*)"], "");
    return picked?.ok && picked.data?.path ? picked.data.path : "";
  }

  async function chooseFolder(initialDir = "") {
    if (!api().choose_path) return "";
    const picked = await api().choose_path("folder", ["All files (*.*)"], "", initialDir || "");
    return picked?.ok && picked.data?.path ? picked.data.path : "";
  }

  async function chooseZip(initialDir = "") {
    if (!api().choose_path) return "";
    const picked = await api().choose_path("file", ["ZIP (*.zip)", "All files (*.*)"], "", initialDir || "");
    return picked?.ok && picked.data?.path ? picked.data.path : "";
  }

  async function numberplatePackageRoot() {
    if (!api().get_numberplate_template_export_root) return "";
    const result = await api().get_numberplate_template_export_root(rootPath());
    return result?.ok && result.data?.path ? result.data.path : "";
  }

  async function numberplatePngExportRoot() {
    if (!api().get_numberplate_png_export_root) return "";
    const result = await api().get_numberplate_png_export_root();
    return result?.ok && result.data?.path ? result.data.path : "";
  }

  async function importAsset(type, surfaceKey = "") {
    const root = rootPath();
    if (!root && GAME_EXPORT_ENABLED) {
      status("Set the document root first.");
      return;
    }
    const targetSurface = surfaceKey || state.activeSurface;
    const surface = SURFACES.find((row) => row.key === targetSurface);
    const png = await choosePng();
    if (!png) return;
    const targetStatusId = type === "font" ? "numberplateTemplateFontStatus" : surface?.statusId || "";
    if (!(await validateChosenPng(type, png, targetStatusId))) return;
    const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
    const nextRef = pendingAssetRef(type, png, fileName(png));
    if (type === "font") {
      template.font = nextRef;
      template.fontSource = "local";
      template.evoFontRel = "";
    } else {
      state.activeSurface = targetSurface;
      template.backgrounds[targetSurface] = nextRef;
    }
    upsertTemplateInState(template);
    state.dirty = true;
    state.previewCache.clear();
    render();
    selectImportedAsset(type, targetSurface, nextRef);
    renderAssetImportStatuses(template);
    status(`${type === "font" ? "Font" : surface?.label || "Background"} PNG selected. ${GAME_EXPORT_ENABLED ? "Save template to create the local asset." : "Save the template or export baked PNGs."}`);
  }

  async function createAssetFromGame(type, surfaceKey = "") {
    if (!GAME_EXPORT_ENABLED) {
      status("Create from AC EVO is disabled. Choose PNG files and export baked PNGs instead.");
      return;
    }
    const root = rootPath();
    if (!root) {
      status("Set the document root first.");
      return;
    }
    if (!api().create_custom_numberplate_asset_from_game) {
      status("Create from AC EVO is not available.");
      return;
    }
    const targetSurface = surfaceKey || state.activeSurface;
    const baseTemplate = selectedBaseSource(type, targetSurface);
    if (!baseTemplate) {
      status(`Choose an AC EVO base ${type} first.`);
      return;
    }
    const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
    const suffix = type === "font" ? "font" : (SURFACES.find((surface) => surface.key === targetSurface)?.suffix || "background");
    const name = window.prompt(`Local ${type} asset name`, `${cleanName(template.name)}_${suffix}`);
    if (!name) return;
    status(`Creating local ${type} asset from AC EVO...`);
    const result = await api().create_custom_numberplate_asset_from_game({ root, type, baseTemplate, name, overwrite: false });
    if (!result?.ok) {
      status(result?.message || "Create from AC EVO failed");
      return;
    }
    await loadAssets();
    const createdId = result.data?.id || "";
    const asset = (type === "font" ? state.assets.fonts : state.assets.backgrounds).find((item) => item.id === createdId);
    const nextRef = asset ? customAssetRef(asset, type) : customAssetRef(result.data || {}, type);
    if (type === "font") {
      template.font = nextRef;
      template.fontSource = "local";
      template.evoFontRel = "";
    } else {
      state.activeSurface = targetSurface;
      template.backgrounds[targetSurface] = nextRef;
    }
    upsertTemplateInState(template);
    state.dirty = true;
    state.previewCache.clear();
    render();
    status(result.message || `Local ${type} asset created. Save the template to keep this change.`);
  }

  async function exportTemplatePackage() {
    const template = currentTemplate();
    if (!template?.id) {
      status("Save the template before exporting a package.");
      return;
    }
    if (state.dirty) {
      status("Save pending template changes before exporting.");
      return;
    }
    if (!api().export_numberplate_template_package) {
      status("Template package export is not available.");
      return;
    }
    const outDir = await chooseFolder(await numberplatePackageRoot());
    if (!outDir) return;
    status("Exporting template package...");
    const result = await api().export_numberplate_template_package({
      id: template.id,
      outDir,
      root: rootPath(),
    });
    if (!result?.ok) {
      status(result?.message || "Package export failed");
      return;
    }
    if (typeof window.showStatusModal === "function") {
      window.showStatusModal({
        type: "success",
        title: "Numberplate template exported",
        subtitle: "Package ready",
        message: `${template.name || template.id || "Template"} was exported successfully.`,
        details: result.message || `Package: ${result.data?.packagePath || result.data?.packageRoot || "-"}`,
        openFolderPath: result.data?.packagePath || result.data?.packageRoot || "",
      });
    }
    status(result.message || `Package exported: ${result.data?.packagePath || result.data?.packageRoot || template.id}`);
  }

  async function importTemplatePackage() {
    if (!api().inspect_numberplate_template_package || !api().import_numberplate_template_package) {
      status("Template package import is not available.");
      return;
    }
    const packageDir = await chooseZip(await numberplatePackageRoot());
    if (!packageDir) return;
    status("Inspecting template package...");
    const inspected = await api().inspect_numberplate_template_package(packageDir);
    if (!inspected?.ok) {
      showPackageImportError("Numberplate package", inspected?.message || "Package inspection failed", "inspect_numberplate_template_package");
      return;
    }
    const template = inspected.data?.template || {};
    const assetCount = Number(inspected.data?.assetCount || 0);
    const missingCount = Number(inspected.data?.missingCount || 0);
    if (missingCount) {
      status(`Package is incomplete: ${missingCount} missing entries.`);
      return;
    }
    const confirmImport = typeof window.showConfirmModal === "function" ? window.showConfirmModal : null;
    const proceed = confirmImport
      ? await confirmImport({
        type: "info",
        title: "Import numberplate package",
        message: "Import this numberplate template package into your local library?",
        details: [
          `Template: ${template.name || template.id || "-"}`,
          `Package: ${packageDir}`,
          `Assets: ${assetCount}`,
          "",
          "Existing templates or assets with the same id will be imported with a new id.",
        ].join("\n"),
        confirmLabel: "Import Package",
        cancelLabel: "Cancel",
      })
      : window.confirm(`Import numberplate package "${template.name || template.id || packageDir}"?`);
    if (!proceed) {
      status("Package import cancelled.");
      return;
    }
    status("Importing template package...");
    const result = await api().import_numberplate_template_package({
      packageDir,
      conflictMode: "rename",
    });
    if (!result?.ok) {
      showPackageImportError("Numberplate package", result?.message || "Package import failed", "import_numberplate_template_package");
      return;
    }
    state.selectedId = result.data?.templateId || "";
    await refresh();
    if (typeof window.loadMySkins === "function") {
      await window.loadMySkins();
    }
    window.dispatchEvent(new CustomEvent("liverylab-numberplate-templates-updated"));
    status(result.message || "Numberplate template package imported.");
  }

  async function downloadNumberplateTemplate() {
    if (!api().download_numberplate_templates) {
      status("Numberplate template download is not available.");
      return;
    }
    const button = $("numberplateTemplateDownload");
    if (button) button.disabled = true;
    status("Preparing numberplate template PNGs...");
    try {
      const result = await api().download_numberplate_templates();
      if (!result?.ok) {
        status(result?.message || "Numberplate template download failed.");
        return;
      }
      const folder = result.data?.folder || "";
      if (folder && api().open_folder) {
        await api().open_folder(folder);
      }
      const items = Array.isArray(result.data?.items) ? result.data.items : [];
      if (typeof window.showStatusModal === "function") {
        window.showStatusModal({
          type: "success",
          title: "Numberplate template ready",
          subtitle: "Folder opened",
          message: "Numberplate template PNGs are ready for editing.",
          details: items.map((item) => `${item.status || "ready"}: ${item.name || item.target || "-"}`).join("\n") || result.message || "",
          openFolderPath: folder,
        });
      }
      status(result.message || "Numberplate template PNGs are ready.");
    } finally {
      if (button) button.disabled = false;
    }
  }

  function bind() {
    $("numberplateTemplateNew")?.addEventListener("click", newTemplate);
    $("numberplateTemplateDelete")?.addEventListener("click", deleteTemplate);
    $("numberplateTemplateImportPackage")?.addEventListener("click", importTemplatePackage);
    $("numberplateTemplateExportPackage")?.addEventListener("click", exportTemplatePackage);
    $("numberplateTemplateDownload")?.addEventListener("click", downloadNumberplateTemplate);
    $("numberplateTemplateSave")?.addEventListener("click", () => saveTemplate());
    $("numberplateTemplateExportPng")?.addEventListener("click", exportTemplatePngs);
    $("numberplateTemplatePublish")?.addEventListener("click", publishTemplate);
    $("numberplateTemplateImportFont")?.addEventListener("click", () => importAsset("font"));
    $("numberplateTemplateFontSource")?.addEventListener("change", async () => {
      updateFontSourceUi();
      const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
      if ($("numberplateTemplateFontSource")?.value === "evo") {
        if (!GAME_EXPORT_ENABLED) {
          status("AC EVO font export is disabled. Choose a local font PNG.");
          return;
        }
        await ensureEvoFontAsset(template, $("numberplateTemplateEvoFont")?.value || selectedBaseSource("font"));
        state.dirty = true;
        render();
        status("AC EVO font prepared locally. Save the template to keep this font source.");
      } else {
        markDirty();
      }
    });
    $("numberplateTemplateEvoFont")?.addEventListener("change", async () => {
      if (!GAME_EXPORT_ENABLED) {
        status("AC EVO font export is disabled. Choose a local font PNG.");
        return;
      }
      const template = applyTemplateFromInputs(currentTemplate() || createDraftTemplate());
      await ensureEvoFontAsset(template, $("numberplateTemplateEvoFont")?.value || selectedBaseSource("font"));
      state.previewCache.clear();
      state.dirty = true;
      render();
      status("AC EVO font prepared locally. Save the template to keep this font source.");
    });
    document.querySelectorAll("[data-numberplate-import-background]").forEach((button) => {
      button.addEventListener("click", () => importAsset("background", button.dataset.numberplateImportBackground || "leftDoor"));
    });
    document.querySelectorAll("[data-numberplate-create-from-game]").forEach((button) => {
      button.addEventListener("click", () => createAssetFromGame("background", button.dataset.numberplateCreateFromGame || "leftDoor"));
    });
    [
      "numberplateTemplateName",
      "numberplateTemplateTestNumber",
      "numberplateTemplateLeftBackground",
      "numberplateTemplateRightBackground",
      "numberplateTemplateHoodBackground",
      "numberplateTemplateFont",
      "numberplateTemplateFontBase",
      "numberplateTemplateTextX",
      "numberplateTemplateTextY",
      "numberplateTemplateTextScale",
      "numberplateTemplateTextSpacing",
      "numberplateTemplateTextColor",
      "numberplateTemplateTextAlpha",
    ].forEach((id) => {
      $(id)?.addEventListener("input", markDirty);
      $(id)?.addEventListener("change", markDirty);
    });
  }

  document.addEventListener("DOMContentLoaded", () => {
    bind();
  });

  window.LiveryLabNumberplateStudio = {
    refresh,
    render,
    assetPreviewPath,
    state,
  };
})();
