// Vanday DAM — thin API client + helpers to merge server data into the
// existing mocked ALL_ASSETS/ASSETS arrays without rewriting downstream code.

(function () {
  const BASE = ""; // same-origin (Express serves this HTML)

  // If Clerk is loaded and the user has a session, attach a short-lived JWT
  // so the server's @clerk/express middleware can authenticate the request.
  // Falls back to the legacy cookie-based session when Clerk isn't available.
  async function clerkAuthHeader() {
    try {
      const session = window.Clerk && window.Clerk.session;
      if (!session) return null;
      const token = await session.getToken();
      return token ? `Bearer ${token}` : null;
    } catch { return null; }
  }

  async function jfetch(url, opts = {}) {
    const headers = new Headers(opts.headers || {});
    const auth = await clerkAuthHeader();
    if (auth && !headers.has("Authorization")) headers.set("Authorization", auth);
    // While an invitee is mid-signup we carry the invite id from the link so the
    // server can verify their signup email matches the invited email. Cleared
    // once they've joined (or chosen to start a fresh account instead).
    try {
      const inv = window.sessionStorage.getItem("vanday_invite_id");
      if (inv && !headers.has("X-Vanday-Invite")) headers.set("X-Vanday-Invite", inv);
    } catch {}
    const r = await fetch(BASE + url, { ...opts, headers });
    if (!r.ok) {
      const text = await r.text().catch(() => "");
      // Try to surface the server's structured { error, message } so callers
      // can show a friendly message (e.g. the beta seat limit) instead of raw
      // status text.
      let data = null;
      try { data = JSON.parse(text); } catch {}
      const err = new Error(
        (data && (data.message || data.error)) || `${r.status} ${r.statusText}: ${text.slice(0, 200)}`,
      );
      err.status = r.status;
      err.data = data;
      throw err;
    }
    return r.json();
  }

  const VandayAPI = {
    health: () => jfetch("/api/health"),
    listAssets: () => jfetch("/api/assets"),
    getAsset: (id) => jfetch(`/api/assets/${encodeURIComponent(id)}`),
    patchAsset: (id, patch) =>
      jfetch(`/api/assets/${encodeURIComponent(id)}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(patch),
      }),
    deleteAsset: (id) =>
      jfetch(`/api/assets/${encodeURIComponent(id)}`, { method: "DELETE" }),
    search: (q) => jfetch(`/api/search?q=${encodeURIComponent(q)}`),
    similar: (id) => jfetch(`/api/assets/${encodeURIComponent(id)}/similar`),
    uploadFiles: (files, { folder, uploader } = {}) => {
      const fd = new FormData();
      for (const f of files) fd.append("files", f, f.name);
      if (folder) fd.append("folder", folder);
      if (uploader) fd.append("uploader", uploader);
      return jfetch("/api/upload", { method: "POST", body: fd });
    },
    listFolders: () => jfetch("/api/folders"),
    createFolder: (name, parentId) =>
      jfetch("/api/folders", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, parentId: parentId || null }),
      }),
    createShare: (payload) =>
      jfetch("/api/share", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      }),
    viewShare: (token, password) =>
      jfetch(`/api/share/${encodeURIComponent(token)}/view`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ password: password || "" }),
      }),
    cropUrl: (id, ratio, w = 1080, opts = {}) => {
      const r = String(ratio || "1:1").replace(":", "x").toLowerCase();
      const dl = opts.download ? "&download=1" : "";
      return `/api/crop/${encodeURIComponent(id)}?ratio=${r}&w=${w}${dl}`;
    },

    // --- Workflow labels (per-org) ---
    listWorkflowLabels: () => jfetch("/api/workflow-labels"),
    createWorkflowLabel: ({ name, color, sortOrder }) =>
      jfetch("/api/workflow-labels", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, color, sortOrder }),
      }),
    updateWorkflowLabel: (id, patch) =>
      jfetch(`/api/workflow-labels/${encodeURIComponent(id)}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(patch),
      }),
    deleteWorkflowLabel: (id, migrateTo) => {
      const qs = migrateTo ? `?migrateTo=${encodeURIComponent(migrateTo)}` : "";
      return jfetch(`/api/workflow-labels/${encodeURIComponent(id)}${qs}`, {
        method: "DELETE",
      });
    },

    // --- Comments ---
    listComments: (assetId) =>
      jfetch(`/api/assets/${encodeURIComponent(assetId)}/comments`),
    postComment: (assetId, body, mentionedUserIds) =>
      jfetch(`/api/assets/${encodeURIComponent(assetId)}/comments`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ body, mentionedUserIds: mentionedUserIds || [] }),
      }),
    deleteComment: (assetId, commentId) =>
      jfetch(
        `/api/assets/${encodeURIComponent(assetId)}/comments/${encodeURIComponent(commentId)}`,
        { method: "DELETE" },
      ),

    // --- Org members (used by @mention autocomplete) ---
    listOrgMembers: () => jfetch("/api/org/members"),

    // Rename the current workspace (admin-only on the server).
    setOrgName: (name) =>
      jfetch("/api/org", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name }),
      }),

    // --- Workspace access management (admin Settings → Users & permissions) ---
    getOrgAccess: () => jfetch("/api/org/access"),
    setMemberRole: (userId, role) =>
      jfetch(`/api/org/members/${encodeURIComponent(userId)}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ role }),
      }),
    removeMember: (userId) =>
      jfetch(`/api/org/members/${encodeURIComponent(userId)}`, {
        method: "DELETE",
      }),
    setDomainRule: ({ enabled, domain, role }) =>
      jfetch("/api/org/domain-rule", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ enabled, domain, role }),
      }),
    inviteUser: (email, role) =>
      jfetch("/api/org/invites", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, role }),
      }),
    revokeInvite: (id) =>
      jfetch(`/api/org/invites/${encodeURIComponent(id)}`, { method: "DELETE" }),

    // --- Account deletion (Settings → Profile → Delete account) ---
    // Permanent + irreversible. Wipes the user, their workspace and files
    // server-side and deletes the Clerk login.
    deleteAccount: () => jfetch("/api/account", { method: "DELETE" }),

    // --- Per-org metadata fields (Settings → Fields) ---
    listFields: () => jfetch("/api/org/fields"),
    setFieldVisible: (key, visible) =>
      jfetch(`/api/org/fields/${encodeURIComponent(key)}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ visible }),
      }),
    createField: ({ label, type, category }) =>
      jfetch("/api/org/fields", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ label, type, category }),
      }),
    deleteField: (key) =>
      jfetch(`/api/org/fields/${encodeURIComponent(key)}`, { method: "DELETE" }),
  };

  // Trigger a real browser download for an asset. We fetch the bytes as a
  // blob (which works for same-origin /files/... and for CORS-enabled hosts
  // like images.unsplash.com), then save via a blob URL + `download` attr.
  // `format` is "original" (default), "jpg", or "png" — for jpg/png we
  // re-encode through a canvas.
  async function downloadAsset(asset, format = "original") {
    if (!asset || !asset.url) return;

    const originalName = asset.name || asset.filename || "download";
    const baseName = originalName.replace(/\.[^.]+$/, "") || "download";

    const triggerSave = (blob, filename) => {
      const blobUrl = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = blobUrl;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      setTimeout(() => {
        a.remove();
        URL.revokeObjectURL(blobUrl);
      }, 100);
    };

    try {
      const res = await fetch(asset.url, { mode: "cors" });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const srcBlob = await res.blob();

      if (format === "jpg" || format === "png") {
        const mime = format === "jpg" ? "image/jpeg" : "image/png";
        const ext = format === "jpg" ? ".jpg" : ".png";
        const out = await rasterToFormat(srcBlob, mime);
        triggerSave(out, baseName + ext);
      } else {
        triggerSave(srcBlob, originalName);
      }
    } catch (err) {
      // CORS blocked or network failure — fall back to opening the file so
      // the user can still right-click → Save As.
      console.warn("downloadAsset fetch failed, opening in new tab:", err);
      window.open(asset.url, "_blank", "noopener");
    }

    // Track the download for the admin dashboard. Fire-and-forget — a tracking
    // failure must never affect the actual download the user just did.
    try { jfetch("/api/events/download", { method: "POST" }).catch(() => {}); } catch {}
  }

  // Re-encode an image blob to the chosen MIME type via canvas. JPEG has
  // no alpha channel, so we paint a white background first.
  function rasterToFormat(srcBlob, mime) {
    return new Promise((resolve, reject) => {
      const url = URL.createObjectURL(srcBlob);
      const img = new Image();
      img.onload = () => {
        const canvas = document.createElement("canvas");
        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        const ctx = canvas.getContext("2d");
        if (mime === "image/jpeg") {
          ctx.fillStyle = "#ffffff";
          ctx.fillRect(0, 0, canvas.width, canvas.height);
        }
        ctx.drawImage(img, 0, 0);
        URL.revokeObjectURL(url);
        canvas.toBlob(
          (b) => (b ? resolve(b) : reject(new Error("toBlob returned null"))),
          mime,
          mime === "image/jpeg" ? 0.92 : undefined
        );
      };
      img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
      img.src = url;
    });
  }

  async function downloadMany(assets, format = "original") {
    for (const a of assets) {
      await downloadAsset(a, format);
      // Tiny pause so the browser doesn't merge them into one prompt
      await new Promise((r) => setTimeout(r, 250));
    }
  }

  // Convert a server asset row into the shape the prototype's UI expects.
  function serverToUI(a) {
    const sizeMB = (a.sizeBytes / (1024 * 1024)).toFixed(1) + " MB";
    return {
      id: a.id,
      name: a.name,
      folder: a.folder || "raw",
      size: sizeMB,
      sizeBytes: a.sizeBytes,
      uploader: a.uploader,
      date: new Date(a.createdAt).toLocaleDateString(undefined, {
        month: "short", day: "2-digit", year: "numeric",
      }),
      // Numeric sort key for the "Recently uploaded" view. Mock assets have
      // no createdAt and fall back to 0 (they sort after real uploads).
      createdAtMs: a.createdAt ? new Date(a.createdAt).getTime() : 0,
      w: a.w || 4000,
      h: a.h || 3000,
      tags: a.tags || [],
      caption: a.caption || "",
      publications: a.publications || [],
      aiTags: true,
      aiState: a.aiState,
      rating: 0,
      url: a.url,
      thumb: a.url,
      mime: a.mime || null,
      kind: "original",
      isServer: true,
      workflowLabelId: a.workflowLabelId || null,
    };
  }

  // Subscribers wake on every server-side change so React can re-render.
  const subs = new Set();
  let rev = 0;
  function notify() { rev += 1; subs.forEach((fn) => { try { fn(rev); } catch {} }); }

  // Build variant (crop) entries for a server asset. Mirrors the shape the
  // prototype's data.jsx produces for mocked assets, so the existing crop /
  // variants UI keeps working — but URLs now point to the real crop endpoint.
  function variantsForServer(parent) {
    if (!Array.isArray(window.UNIQUE_RATIOS)) return [];
    const slugOf = window.shortRatioSlug || ((r) => r.replace(":", "x"));
    return window.UNIQUE_RATIOS.map((r) => {
      const slug = slugOf(r.r);
      const baseName = parent.name.replace(/\.[^.]+$/, "");
      const id = `${parent.id}--${slug}`;
      // Generate a reasonable width so the crop endpoint serves something
      // proportional to the source.
      const w = Math.min(1600, Math.max(640, parent.w || 1080));
      return {
        id,
        parentId: parent.id,
        kind: "crop",
        name: `${baseName}—${slug}.jpg`,
        folder: parent.folder,
        uploader: parent.uploader,
        date: parent.date,
        size: "—",
        w: r.w, h: r.h,
        ratio: r.r,
        ratioName: r.name,
        sites: [...(r.sites || [])],
        tags: parent.tags,
        aiTags: true,
        rating: parent.rating || 0,
        url: VandayAPI.cropUrl(parent.id, r.r, w),
        thumb: VandayAPI.cropUrl(parent.id, r.r, 480),
        isServer: true,
        isServerVariant: true,
      };
    });
  }

  // Public: merge a fresh server list into the in-memory ASSETS/ALL_ASSETS arrays.
  // We mutate in place so existing code that reads these globals picks up changes.
  function applyServerAssets(serverList) {
    const incoming = serverList.map(serverToUI);
    // Link variantIds so existing UI ("View crops") finds them.
    for (const a of incoming) a.variantIds = [];
    const incomingVariants = [];
    for (const a of incoming) {
      // Videos get no auto-crops, so we generate no crop variants for them.
      if (window.assetIsVideo && window.assetIsVideo(a)) continue;
      const vs = variantsForServer(a);
      a.variantIds = vs.map((v) => v.id);
      incomingVariants.push(...vs);
    }

    if (Array.isArray(window.ASSETS)) {
      for (let i = window.ASSETS.length - 1; i >= 0; i--) {
        if (window.ASSETS[i].isServer && !window.ASSETS[i].isServerVariant) window.ASSETS.splice(i, 1);
      }
      window.ASSETS.unshift(...incoming);
    }
    if (Array.isArray(window.VARIANTS)) {
      for (let i = window.VARIANTS.length - 1; i >= 0; i--) {
        if (window.VARIANTS[i].isServerVariant) window.VARIANTS.splice(i, 1);
      }
      window.VARIANTS.unshift(...incomingVariants);
    }
    if (Array.isArray(window.ALL_ASSETS)) {
      for (let i = window.ALL_ASSETS.length - 1; i >= 0; i--) {
        if (window.ALL_ASSETS[i].isServer) window.ALL_ASSETS.splice(i, 1);
      }
      window.ALL_ASSETS.unshift(...incoming, ...incomingVariants);
    }
    if (Array.isArray(window.FOLDERS)) {
      const all = window.FOLDERS.find((f) => f.id === "all");
      if (all) all.count = (window.ASSETS || []).length;
      // Inbox = unfiled originals (no folder, or explicitly "raw"). Same
      // predicate the "Move from Inbox" button uses. Without this, the
      // sidebar badge stays stuck at the seed value of 0.
      const raw = window.FOLDERS.find((f) => f.id === "raw");
      if (raw) {
        raw.count = (window.ASSETS || []).filter(
          (a) => a.isServer && a.kind !== "crop" && (a.folder === "raw" || a.folder == null)
        ).length;
      }
    }

    window.__SERVER_ASSET_COUNT__ = incoming.length;
    notify();
  }

  // Merge server folders into the sidebar list. Server folders use ids like
  // "f_xxxx" so they don't collide with the prototype's seeded ones.
  function applyServerFolders(serverFolders) {
    if (!Array.isArray(window.FOLDERS)) return;
    for (let i = window.FOLDERS.length - 1; i >= 0; i--) {
      if (window.FOLDERS[i].isServer) window.FOLDERS.splice(i, 1);
    }
    for (const f of serverFolders) {
      window.FOLDERS.push({
        id: f.id, name: f.name, count: f.count, icon: "folder", isServer: true,
        // `parent` drives the sidebar's nested-folder tree (null = top level).
        parent: f.parentId || null,
      });
    }
    notify();
  }

  function subscribe(fn) { subs.add(fn); return () => subs.delete(fn); }
  function getRev() { return rev; }

  // React hook: re-render when server data changes.
  function useServerRev() {
    const [, setR] = React.useState(0);
    React.useEffect(() => subscribe((r) => setR(r)), []);
    return rev;
  }

  // App-level hook: load + poll the server asset list.
  function useServerAssets({ pollMs = 2000 } = {}) {
    const [aiEnabled, setAI] = React.useState(false);
    const [error, setError] = React.useState(null);

    React.useEffect(() => {
      let stop = false;
      let timer;
      async function tick() {
        try {
          const [h, list, folders] = await Promise.all([
            VandayAPI.health(),
            VandayAPI.listAssets(),
            VandayAPI.listFolders().catch(() => ({ folders: [] })),
          ]);
          if (stop) return;
          setAI(!!h.aiEnabled);
          applyServerAssets(list.assets || []);
          applyServerFolders(folders.folders || []);
          setError(null);
        } catch (e) {
          if (!stop) setError(e.message);
        }
        if (!stop) timer = setTimeout(tick, pollMs);
      }
      tick();
      return () => { stop = true; clearTimeout(timer); };
    }, [pollMs]);

    return { aiEnabled, error };
  }

  // Shared cache for workflow labels and org members — loaded once per
  // session and refreshed when a mutation happens. Hooks below subscribe.
  let labelsCache = null;
  let labelsLoading = null;
  const labelsSubs = new Set();
  function notifyLabels() { labelsSubs.forEach((fn) => { try { fn(labelsCache); } catch {} }); }

  async function loadLabels(force = false) {
    if (!force && labelsCache) return labelsCache;
    if (labelsLoading) return labelsLoading;
    labelsLoading = (async () => {
      try {
        const r = await VandayAPI.listWorkflowLabels();
        labelsCache = Array.isArray(r.labels) ? r.labels : (Array.isArray(r) ? r : []);
        notifyLabels();
        return labelsCache;
      } catch {
        labelsCache = labelsCache || [];
        return labelsCache;
      } finally {
        labelsLoading = null;
      }
    })();
    return labelsLoading;
  }

  function useWorkflowLabels() {
    const [labels, setLabels] = React.useState(labelsCache || []);
    React.useEffect(() => {
      const fn = (v) => setLabels(v || []);
      labelsSubs.add(fn);
      if (!labelsCache) loadLabels();
      return () => labelsSubs.delete(fn);
    }, []);
    return {
      labels,
      reload: () => loadLabels(true),
      byId: (id) => (labels || []).find((l) => l.id === id) || null,
    };
  }

  let membersCache = null;
  let membersLoading = null;
  async function loadMembers() {
    if (membersCache) return membersCache;
    if (membersLoading) return membersLoading;
    membersLoading = (async () => {
      try {
        const r = await VandayAPI.listOrgMembers();
        membersCache = Array.isArray(r.members) ? r.members : (Array.isArray(r) ? r : []);
        return membersCache;
      } catch {
        membersCache = [];
        return membersCache;
      } finally {
        membersLoading = null;
      }
    })();
    return membersLoading;
  }

  function useOrgMembers() {
    const [members, setMembers] = React.useState(membersCache || []);
    React.useEffect(() => {
      let cancelled = false;
      loadMembers().then((m) => { if (!cancelled) setMembers(m || []); });
      return () => { cancelled = true; };
    }, []);
    return members;
  }

  // Derive a display name from a member record. Backend currently only
  // exposes `email` and `role`, so we use the email local-part as the name.
  function memberDisplayName(m) {
    if (!m) return "Unknown";
    if (m.name) return m.name;
    if (m.email) return String(m.email).split("@")[0];
    return m.id ? m.id.slice(0, 6) : "Unknown";
  }

  window.VandayAPI = VandayAPI;
  window.VandayServer = {
    applyServerAssets, applyServerFolders, subscribe, getRev, serverToUI,
    useServerRev, useServerAssets,
    downloadAsset, downloadMany,
    variantsForServer,
    useWorkflowLabels, reloadLabels: () => loadLabels(true),
    useOrgMembers, memberDisplayName,
    // Lets non-server code (e.g. publish flow) trigger a re-render across
    // components that subscribed via useServerRev — used after mutating
    // asset.publications on mock assets so the Published view picks it up.
    bumpRev: notify,
  };
})();
