const { contextBridge, ipcRenderer } = require("electron");

// Keep SDK route strings in sync with src/plugin-sdk-routes.ts. The desktop
// conformance check extracts call()/callSync()/subscription() route literals
// from this preload and compares them to the canonical route table.

const tokenArg = process.argv.find((arg) => arg.startsWith("--openpets-plugin-token="));
const channel = tokenArg ? `openpets:plugin-sdk:${tokenArg.slice("--openpets-plugin-token=".length)}` : "";
let callbackId = 0;
const callbacks = new Map();

async function call(path, args) {
  if (!channel) throw new Error("OpenPets plugin SDK is unavailable.");
  return ipcRenderer.invoke(channel, path, normalizeForIpc(args));
}

function callSync(path, args) {
  if (!channel) throw new Error("OpenPets plugin SDK is unavailable.");
  const result = ipcRenderer.sendSync(channel, path, normalizeForIpc(args));
  if (result && typeof result === "object" && typeof result.__openPetsError === "string") throw new Error(result.__openPetsError);
  return result;
}

function normalizeForIpc(value, depth = 0) {
  if (depth > 20) return null;
  if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
  if (typeof value === "bigint") return Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
  if (typeof value === "function" || typeof value === "symbol") return undefined;
  if (value instanceof Uint8Array) return value;
  if (value instanceof Error) return { name: value.name, message: value.message };
  if (Array.isArray(value)) return value.slice(0, 100).map((item) => normalizeForIpc(item, depth + 1));
  if (typeof value === "object") {
    const output = {};
    for (const [key, nested] of Object.entries(value).slice(0, 100)) {
      const normalized = normalizeForIpc(nested, depth + 1);
      if (normalized !== undefined) output[key] = normalized;
    }
    return output;
  }
  return String(value);
}

function registerCallback(fn) {
  if (typeof fn !== "function") return undefined;
  const id = `cb-${++callbackId}`;
  callbacks.set(id, fn);
  return id;
}

// Subscription helper: fire-and-forget subscribe that resolves to a
// subscription id; returns a disposer that unsubscribes once resolved.
function subscription(onPath, offPath, args, fn) {
  const id = registerCallback(fn);
  const pending = call(onPath, [...args, id]).catch(() => undefined);
  return () => {
    callbacks.delete(id);
    void pending.then((result) => {
      const subscriptionId = result && result.subscriptionId;
      if (subscriptionId) void call(offPath, [subscriptionId]).catch(() => undefined);
    });
  };
}

function makeBubbleHandle(result) {
  const bubbleId = result && result.bubbleId;
  return {
    id: String(bubbleId),
    update: (patch) => call("ui.bubbleUpdate", [bubbleId, patch]),
    dismiss: () => call("ui.bubbleDismiss", [bubbleId]),
    pin: () => call("ui.bubblePin", [bubbleId]),
    unpin: () => call("ui.bubbleUnpin", [bubbleId]),
    onAction: (fn) => { void call("ui.bubbleSubscribe", [bubbleId, "action", registerCallback(fn)]).catch(() => undefined); },
    onSubmit: (fn) => { void call("ui.bubbleSubscribe", [bubbleId, "submit", registerCallback(fn)]).catch(() => undefined); },
    onDismiss: (fn) => { void call("ui.bubbleSubscribe", [bubbleId, "dismiss", registerCallback(fn)]).catch(() => undefined); },
  };
}

function makePanelHandle(result) {
  const panelId = result && result.panelId;
  return {
    id: String(panelId),
    show: () => call("ui.panelShow", [panelId]),
    hide: () => call("ui.panelHide", [panelId]),
    postMessage: (msg) => call("ui.panelPost", [panelId, msg]),
    onMessage: (fn) => { void call("ui.panelOnMessage", [panelId, registerCallback(fn)]).catch(() => undefined); },
    close: () => call("ui.panelClose", [panelId]),
  };
}

function wrapPickedFile(file) {
  return {
    name: String(file.name),
    sizeBytes: Number(file.sizeBytes),
    readText: () => call("files.read", [file.fileId, "text"]),
    readBytes: () => call("files.read", [file.fileId, "bytes"]),
  };
}

function wrapEventPayload(event, payload) {
  if (event !== "pet:drop" || !payload || !Array.isArray(payload.files)) return payload;
  return {
    ...payload,
    files: payload.files.map((file) => ({
      name: String(file.name),
      sizeBytes: Number(file.sizeBytes),
      readText: () => call("files.read", [file.fileId, "text"]),
    })),
  };
}

function makePetHandle(petId) {
  return {
    id: String(petId),
    speak: (spec) => call("pet.speak", [petId, spec]).then(makeBubbleHandle),
    react: (reaction, options) => call("pet.react", [petId, reaction, options]),
    setAnimation: (state) => call("pet.setAnimation", [petId, state]),
    setScale: (scale) => call("pet.setScale", [petId, scale]),
    setStatusReaction: (reaction) => call("pet.setStatusReaction", [petId, reaction]),
    moveBy: (options) => call("pet.moveBy", [petId, options]),
    wander: (options) => call("pet.wander", [petId, options]),
    moveToHome: () => call("pet.moveToHome", [petId]),
    moveTo: (point, opts) => call("pet.moveTo", [petId, point, opts]),
    followCursor: (opts) => call("pet.followCursor", [petId, opts]),
    physics: (opts) => call("pet.physics", [petId, opts]),
    onTick: (fn) => subscription("pet.onTick", "pet.offTick", [petId], fn),
    getState: () => call("pet.getState", [petId]),
    show: () => call("pet.show", [petId]),
    hide: () => call("pet.hide", [petId]),
    close: () => call("pet.close", [petId]),
  };
}

const defaultPet = makePetHandle("default");

const sdk = {
  pet: defaultPet,
  pets: {
    default: defaultPet,
    list: () => call("pets.list", []),
    get: (petId) => makePetHandle(petId),
    spawn: (spec) => call("pets.spawn", [spec]).then((result) => makePetHandle(result && result.petHandleId)),
    onChange: (fn) => subscription("pets.onChange", "pets.offChange", [], fn),
  },
  ui: {
    bubble: (spec) => call("ui.bubble", [spec]).then(makeBubbleHandle),
    alert: (spec) => call("ui.alert", [spec]).then((handle) => {
      const bubble = makeBubbleHandle(handle);
      bubble.acknowledge = () => call("ui.bubbleDismiss", [handle.bubbleId]);
      return bubble;
    }),
    toast: (spec) => call("ui.toast", [spec]),
    panel: (spec) => call("ui.panel", [spec]).then(makePanelHandle),
    menu: {
      setItems: (items) => call("ui.menuSetItems", [items]),
      onSelect: (fn) => subscription("ui.menuOnSelect", "ui.menuOffSelect", [], fn),
    },
  },
  audio: {
    play: (sound, options) => call("audio.play", [sound, options]),
    importUserSound: (file, opts) => call("audio.importUserSound", [file && file.fileId ? file.fileId : file, opts]),
    forgetUserSound: (ref) => call("audio.forgetUserSound", [ref]),
    stop: (handle) => call("audio.stop", [handle]),
  },
  events: {
    on: (event, fn) => subscription("events.on", "events.off", [event], (payload) => fn(wrapEventPayload(event, payload))),
  },
  assets: {
    icon: (name) => ({ kind: "icon", name: String(name) }),
    image: (name) => ({ kind: "image", name: String(name) }),
    svg: (name) => ({ kind: "svg", name: String(name) }),
    sprite: (name) => ({ kind: "sprite", name: String(name) }),
    sound: (name) => ({ kind: "sound", name: String(name) }),
  },
  bus: {
    publish: (topic, payload) => call("bus.publish", [topic, payload]),
    subscribe: (topic, fn) => subscription("bus.subscribe", "bus.unsubscribe", [topic], fn),
  },
  schedule: {
    once: (id, delayMs, callback) => call("schedule.once", [id, delayMs, registerCallback(callback)]),
    every: (id, intervalMs, callback) => call("schedule.every", [id, intervalMs, registerCallback(callback)]),
    daily: (id, spec, callback) => call("schedule.daily", [id, spec, registerCallback(callback)]),
    cron: (id, expr, callback) => call("schedule.cron", [id, expr, registerCallback(callback)]),
    at: (id, isoTimestamp, callback) => call("schedule.at", [id, isoTimestamp, registerCallback(callback)]),
    list: () => call("schedule.list", []),
    cancel: (id) => call("schedule.cancel", [id]),
    cancelAll: () => call("schedule.cancelAll", []),
  },
  storage: {
    get: (key) => call("storage.get", [key]),
    set: (key, value) => call("storage.set", [key, value]),
    delete: (key) => call("storage.delete", [key]),
    keys: () => call("storage.keys", []),
    subscribe: (key, fn) => subscription("storage.subscribe", "storage.unsubscribe", [key], fn),
  },
  config: {
    get: () => call("config.get", []),
    onChange: (listener) => {
      const id = registerCallback(listener);
      void call("config.onChange", [id]);
      return () => { callbacks.delete(id); void call("config.offChange", [id]); };
    },
  },
  net: {
    fetch: (url, options) => call("net.fetch", [url, options]),
    stream: (url, options, onChunk) => call("net.stream", [url, options, registerCallback(onChunk)]),
  },
  notify: {
    notify: (spec) => call("notify.notify", [spec]),
  },
  ai: {
    available: () => call("ai.available", []),
    complete: (req) => call("ai.complete", [req]),
    stream: (req, onToken) => call("ai.stream", [req, registerCallback(onToken)]),
  },
  secrets: {
    set: (key, value) => call("secrets.set", [key, value]),
    get: (key) => call("secrets.get", [key]),
    delete: (key) => call("secrets.delete", [key]),
    has: (key) => call("secrets.has", [key]),
  },
  voice: {
    speak: (text, opts) => call("voice.speak", [text, opts]),
    listen: (opts) => call("voice.listen", [opts]),
  },
  auth: {
    oauth: (config) => call("auth.oauth", [config]),
    refresh: (provider) => call("auth.refresh", [provider]),
    signOut: (provider) => call("auth.signOut", [provider]),
  },
  files: {
    pick: (opts) => call("files.pick", [opts]).then((files) => (Array.isArray(files) ? files.map(wrapPickedFile) : [])),
    save: (opts) => call("files.save", [opts]),
  },
  system: {
    info: () => call("system.info", []),
    metrics: () => call("system.metrics", []),
    openExternal: (url) => call("system.openExternal", [url]),
    readClipboardText: () => call("system.readClipboardText", []),
    writeClipboardText: (text) => call("system.writeClipboardText", [text]),
  },
  commands: {
    register: (command, handler) => call("commands.register", [command, registerCallback(handler)]),
    unregister: (id) => call("commands.unregister", [id]),
  },
  status: {
    set: (status) => call("status.set", [status]),
    clear: () => call("status.clear", []),
  },
  http: {
    fetch: (url, options) => call("http.fetch", [url, options]),
  },
  log: Object.fromEntries(["debug", "info", "warn", "error"].map((level) => [level, (...args) => call(`log.${level}`, args)])),
  t: (key, vars) => callSync("i18n.t", [key, vars]),
};

Object.defineProperty(sdk, "locale", {
  enumerable: true,
  get: () => callSync("i18n.locale", []),
});

contextBridge.exposeInMainWorld("__openPetsSdk", sdk);
contextBridge.exposeInMainWorld("__openPetsRunCallback", async (id, args) => {
  const callback = callbacks.get(id);
  if (callback) return callback(...(Array.isArray(args) ? args : []));
  return undefined;
});
