/* global React, L */
// ===========================================================
// 스킨 런타임 — catalog · 설치 · CSS 주입 · redeem · 오프라인 영수증
// Rust(Tauri) 다중 소스 로더와 동일 경로. dev: DINGLE_DEV_SKINS
// ===========================================================

const SKIN_STYLE_ID_PREFIX = "skin-installed-css-";
const DEFAULT_REDEEM_BASE = "https://skins.todoary.app";

let _catalog = null;
let _installed = [];
let _patchById = Object.create(null);
function skinDefaultId() {
  return window.SKIN_DEFAULT_ID || "default";
}

let _unlocked = new Set([skinDefaultId()]);
let _ready = null;

function tauriInvoke(cmd, args) {
  const T = window.__TAURI__;
  if (!T?.core?.invoke) return Promise.reject(new Error("not tauri"));
  return T.core.invoke(cmd, args || {});
}

/** Tauri 주입 전역 `isTauri` 와 이름 충돌 방지 (todoary.html 주석 참고) */
function isTauriApp() {
  return !!(window.__TAURI__ || window.__TAURI_INTERNALS__);
}

async function fetchCatalogFallback() {
  try {
    const r = await fetch("catalog.json", { cache: "no-store" });
    if (r.ok) return r.json();
  } catch (_) {}
  return {
    schemaVersion: 1,
    skins: [
      { id: "default", name: "Default", nameKey: "skin.default", descKey: "skin.defaultDesc",
        price: 0, locked: false, version: "1.0.0", preview: "previews/default.svg", bundleId: "default" },
      { id: "notion", name: "Black & white", nameKey: "skin.notion", descKey: "skin.notionDesc",
        price: 4, locked: true, version: "1.0.0", preview: "previews/notion_blur.svg" },
      { id: "excel", name: "Spreadsheet", nameKey: "skin.excel", descKey: "skin.excelDesc",
        price: 4, locked: true, version: "1.0.0", preview: "previews/excel_blur.svg" },
    ],
  };
}

function applyManifestTokens(tokens) {
  if (!tokens || typeof document === "undefined") return;
  const root = document.documentElement;
  for (const [key, val] of Object.entries(tokens)) {
    if (key === "data-skin") continue;
    if (key.startsWith("color.")) {
      const cssVar = "--" + key.slice(6).replace(/\./g, "-");
      root.style.setProperty(cssVar, val);
    }
  }
}

function injectSkinCss(skinId, css) {
  if (typeof document === "undefined") return;
  const id = SKIN_STYLE_ID_PREFIX + skinId;
  let el = document.getElementById(id);
  if (!css) {
    if (el) el.remove();
    return;
  }
  if (!el) {
    el = document.createElement("style");
    el.id = id;
    document.head.appendChild(el);
  }
  el.textContent = css;
}

async function loadManifestPatch(skinId) {
  if (_patchById[skinId]) return _patchById[skinId];
  if (isTauriApp()) {
    try {
      const m = await tauriInvoke("skins_get_manifest", { skinId });
      const patch = m.patch && typeof m.patch === "object" ? m.patch : {};
      _patchById[skinId] = patch;
      return patch;
    } catch (_) {}
  }
  if (skinId === skinDefaultId() && window.PATCH_KAWAII) {
    _patchById[skinId] = window.PATCH_KAWAII;
    return window.PATCH_KAWAII;
  }
  return null;
}

async function loadAndInjectCss(skinId) {
  if (skinId === skinDefaultId()) {
    injectSkinCss(skinId, "");
    return;
  }
  if (!isTauriApp()) return;
  try {
    const { css } = await tauriInvoke("skins_get_styles", { skinId });
    injectSkinCss(skinId, css || "");
  } catch (e) {
    console.warn("[skin] styles load failed", skinId, e);
  }
}

async function refreshUnlockState() {
  _unlocked = new Set([skinDefaultId()]);
  if (!_catalog?.skins) return;
  for (const entry of _catalog.skins) {
    if (!entry.locked) {
      _unlocked.add(entry.id);
      continue;
    }
    if (isTauriApp()) {
      try {
        const ok = await tauriInvoke("skins_verify_receipt", { skinId: entry.id });
        if (ok) _unlocked.add(entry.id);
      } catch (_) {}
    } else {
      try {
        const raw = localStorage.getItem(`todoary.skinReceipt.${entry.id}`);
        if (raw) _unlocked.add(entry.id);
      } catch (_) {}
    }
  }
  if (isTauriApp() && _installed?.length) {
    for (const row of _installed) {
      if (row.receiptValid || row.source === "dev") _unlocked.add(row.id);
    }
  }
}

async function init() {
  if (_ready) return _ready;
  _ready = (async () => {
    if (isTauriApp()) {
      try {
        _catalog = await tauriInvoke("skins_get_catalog");
        _installed = await tauriInvoke("skins_list_installed") || [];
      } catch (e) {
        console.warn("[skin] tauri catalog", e);
        _catalog = await fetchCatalogFallback();
      }
    } else {
      _catalog = await fetchCatalogFallback();
    }
    await refreshUnlockState();
    for (const id of _unlocked) {
      await loadManifestPatch(id);
      if (id !== skinDefaultId()) await loadAndInjectCss(id);
    }
  })();
  return _ready;
}

function getCatalog() {
  return _catalog;
}

function getGalleryEntries() {
  if (!_catalog?.skins) return [];
  return _catalog.skins.map((entry) => ({
    id: entry.id,
    nameKey: entry.nameKey || entry.name,
    descKey: entry.descKey || "",
    locked: !!entry.locked,
    unlocked: _unlocked.has(entry.id),
    price: entry.price ?? 0,
    preview: entry.preview,
    checkoutUrl: entry.checkoutUrl || "",
    version: entry.version,
    previewColors: entry.previewColors,
  }));
}

function isUnlocked(skinId) {
  return _unlocked.has(skinId);
}

function getPatch(skinId) {
  return _patchById[skinId] ? { ..._patchById[skinId] } : null;
}

async function applySkinFull(skinId, setTweak) {
  await init();
  if (!_unlocked.has(skinId)) {
    throw new Error(L("skin.locked") || "Skin is locked");
  }
  const patch = await loadManifestPatch(skinId);
  if (!patch) throw new Error("manifest missing");
  await loadAndInjectCss(skinId);
  const m = isTauriApp() ? await tauriInvoke("skins_get_manifest", { skinId }).catch(() => null) : null;
  if (m?.tokens) applyManifestTokens(m.tokens);
  // data-skin 은 isExcelSkin() 등이 DOM에서 읽음 — setTweak 리렌더 전에 맞춰야 1회 클릭으로 동기화됨
  if (window.applySkinRuntime) window.applySkinRuntime(patch);
  else if (window.applySkinDom) window.applySkinDom(patch.appSkin || skinId);
  if (setTweak) setTweak(patch);
  return patch;
}

function redeemServerBase() {
  try {
    return localStorage.getItem("todoary.skinRedeemBase") || DEFAULT_REDEEM_BASE;
  } catch (_) {
    return DEFAULT_REDEEM_BASE;
  }
}

async function redeemLicense(licenseKey, skinId) {
  await init();
  const key = String(licenseKey || "").trim();
  if (!key) throw new Error(L("skin.keyRequired") || "License key required");

  const bridge = window.skinLicensesBridge;
  if (!bridge?.callRedeemSkin) {
    throw new Error(L("skin.licensesConfigMissing") || "License redeem is not configured");
  }

  const receipt = await bridge.callRedeemSkin(key, skinId);

  if (isTauriApp()) {
    try {
      await tauriInvoke("skins_save_receipt", { skinId, receipt });
    } catch (e) {
      const msg = String(e?.message || e || "");
      if (msg.includes("bundle not found")) {
        throw new Error(L("skin.redeemBundleMissing") || "Skin bundle is not installed in this app build.");
      }
      if (msg.includes("invalid receipt") || msg.includes("signature")) {
        throw new Error(L("skin.redeemReceiptInvalid") || "License verification failed (invalid receipt).");
      }
      throw e;
    }
    _installed = await tauriInvoke("skins_list_installed");
  } else {
    try {
      localStorage.setItem(`todoary.skinReceipt.${skinId}`, JSON.stringify(receipt));
    } catch (_) {}
    _unlocked.add(skinId);
  }

  await refreshUnlockState();
  await loadManifestPatch(skinId);
  await loadAndInjectCss(skinId);
}

function SkinLicenseRedeem({ skinId, onDone }) {
  const [key, setKey] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState("");
  const entry = (_catalog?.skins || []).find((s) => s.id === skinId);

  const submit = async () => {
    setBusy(true);
    setErr("");
    try {
      await redeemLicense(key, skinId);
      onDone && onDone();
    } catch (e) {
      setErr(e?.message || String(e));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div style={{ marginTop: 8, padding: 8, border: "1px solid var(--ink-soft)", borderRadius: 8, background: "var(--paper)" }}>
      <div className="sk-label" style={{ marginBottom: 6 }}>{L("skin.enterKey")}</div>
      <input
        type="text"
        value={key}
        onChange={(e) => setKey(e.target.value)}
        placeholder={L("skin.keyPlaceholder")}
        style={{
          width: "100%", boxSizing: "border-box", marginBottom: 6,
          border: "1.1px solid var(--ink)", borderRadius: 6, padding: "6px 8px",
          fontFamily: "var(--mono)", fontSize: 12,
        }}
      />
      {entry?.checkoutUrl ? (
        <a href={entry.checkoutUrl} target="_blank" rel="noopener noreferrer"
          style={{ fontFamily: "var(--hand)", fontSize: 11, color: "var(--ink)" }}>
          {L("skin.buy")}
        </a>
      ) : null}
      <button type="button" disabled={busy} onClick={submit} style={{
        display: "block", marginTop: 8, width: "100%",
        border: "1.1px solid var(--ink)", borderRadius: 6, padding: "6px 10px",
        fontFamily: "var(--hand)", fontWeight: 700, cursor: busy ? "wait" : "pointer",
        background: "linear-gradient(180deg, var(--point-soft), var(--point))",
      }}>
        {busy ? "…" : L("skin.redeem")}
      </button>
      {err ? <div className="sk-cap" style={{ marginTop: 6, color: "#b03030", fontSize: 11 }}>{err}</div> : null}
    </div>
  );
}

window.SkinRuntime = {
  init,
  getCatalog,
  getGalleryEntries,
  isUnlocked,
  getPatch,
  applySkinFull,
  redeemLicense,
  redeemServerBase,
  SkinLicenseRedeem,
  refreshUnlockState,
};

if (typeof document !== "undefined") {
  init().catch((e) => console.warn("[skin-runtime] init", e));
}
