/* =========================================================================
   Nostr helpers — wraps nostr-tools (window.NostrTools) and window.nostr (NIP-07)
   ========================================================================= */

const NT = window.NostrTools;
const { SimplePool, nip19 } = NT;

const DEFAULT_RELAYS = [
  "wss://relay.damus.io",
  "wss://nos.lol",
  "wss://relay.nostr.band",
  "wss://relay.primal.net",
];

const pool = new SimplePool();

// --- key helpers ---------------------------------------------------------
function npubEncode(hex) {
  try { return nip19.npubEncode(hex); } catch { return hex; }
}
function decodeAny(input) {
  // accepts npub1.., nprofile1.., hex, nip-05 (returns null for nip-05; caller resolves)
  input = (input || "").trim();
  if (!input) return null;
  if (/^[0-9a-f]{64}$/i.test(input)) return { type: "hex", pubkey: input.toLowerCase() };
  try {
    const d = nip19.decode(input);
    if (d.type === "npub") return { type: "hex", pubkey: d.data };
    if (d.type === "nprofile") return { type: "hex", pubkey: d.data.pubkey, relays: d.data.relays };
  } catch (_) {}
  if (input.includes("@") || input.includes(".")) return { type: "nip05", value: input };
  return null;
}
async function resolveNip05(addr) {
  let [name, domain] = addr.includes("@") ? addr.split("@") : ["_", addr];
  if (!domain) return null;
  try {
    const r = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(name)}`);
    if (!r.ok) return null;
    const j = await r.json();
    const pk = j.names && j.names[name];
    return pk ? pk.toLowerCase() : null;
  } catch { return null; }
}

// --- NIP-07 wrappers -----------------------------------------------------
function hasNip07() { return typeof window !== "undefined" && !!window.nostr; }
async function getPubkey() {
  if (!hasNip07()) throw new Error("NIP-07 extension not detected. Install Alby, nos2x, or similar.");
  return await window.nostr.getPublicKey();
}
async function signEvent(evt) {
  return await window.nostr.signEvent(evt);
}
async function nip04Decrypt(otherPk, ciphertext) {
  if (!window.nostr?.nip04?.decrypt) throw new Error("Extension does not expose nip04.decrypt");
  return await window.nostr.nip04.decrypt(otherPk, ciphertext);
}
async function nip04Encrypt(otherPk, plaintext) {
  if (!window.nostr?.nip04?.encrypt) throw new Error("Extension does not expose nip04.encrypt");
  return await window.nostr.nip04.encrypt(otherPk, plaintext);
}

// --- fetch ---------------------------------------------------------------
async function fetchEvents(filter, relays = DEFAULT_RELAYS, ms = 4000) {
  return new Promise((resolve) => {
    const events = [];
    const sub = pool.subscribeMany(relays, [filter], {
      onevent(e) { events.push(e); },
      oneose() { setTimeout(() => { sub.close(); resolve(events); }, 200); },
    });
    setTimeout(() => { sub.close(); resolve(events); }, ms);
  });
}

// kind 3 — contact list
async function fetchContactList(pubkey) {
  const evs = await fetchEvents({ kinds: [3], authors: [pubkey], limit: 1 });
  evs.sort((a, b) => b.created_at - a.created_at);
  return evs[0] || null;
}

// kind 30000 — categorized people lists (follow sets)
async function fetchFollowSets(pubkey) {
  const evs = await fetchEvents({ kinds: [30000], authors: [pubkey] });
  // dedupe by d-tag, keep newest
  const byD = new Map();
  for (const e of evs) {
    const d = (e.tags.find(t => t[0] === "d") || [])[1] || "";
    const prev = byD.get(d);
    if (!prev || prev.created_at < e.created_at) byD.set(d, e);
  }
  return Array.from(byD.values()).sort((a, b) => b.created_at - a.created_at);
}

// kind 0 — profile metadata, batched
async function fetchProfiles(pubkeys) {
  if (!pubkeys.length) return {};
  const evs = await fetchEvents({ kinds: [0], authors: pubkeys, limit: pubkeys.length * 2 }, DEFAULT_RELAYS, 5000);
  const out = {};
  for (const e of evs) {
    const prev = out[e.pubkey];
    if (!prev || prev._at < e.created_at) {
      try {
        const c = JSON.parse(e.content);
        out[e.pubkey] = { ...c, _at: e.created_at, pubkey: e.pubkey };
      } catch {}
    }
  }
  return out;
}

// last seen — most recent kind 1 from each
async function fetchLastSeen(pubkeys) {
  if (!pubkeys.length) return {};
  const evs = await fetchEvents({ kinds: [1], authors: pubkeys, limit: pubkeys.length * 3 }, DEFAULT_RELAYS, 4000);
  const out = {};
  for (const e of evs) {
    if (!out[e.pubkey] || out[e.pubkey] < e.created_at) out[e.pubkey] = e.created_at;
  }
  return out;
}

// global user search via NIP-50 (relay.nostr.band supports it)
async function searchProfiles(query, limit = 12) {
  return await fetchEvents(
    { kinds: [0], search: query, limit },
    ["wss://relay.nostr.band", "wss://search.nos.today"],
    3500
  );
}

// --- publish -------------------------------------------------------------
async function publish(event, relays = DEFAULT_RELAYS) {
  const pubs = pool.publish(relays, event);
  // wait for at least one to settle
  return Promise.allSettled(pubs);
}

// --- parsing follow sets -------------------------------------------------
function tagsOfList(evt) {
  return evt.tags.filter(t => t[0] === "p" && t[1]).map(t => t[1]);
}
function dTagOf(evt) {
  return (evt.tags.find(t => t[0] === "d") || [])[1] || "";
}
function titleOf(evt) {
  const t = evt.tags.find(t => t[0] === "title");
  return (t && t[1]) || dTagOf(evt) || "Untitled set";
}
function descriptionOf(evt) {
  const t = evt.tags.find(t => t[0] === "description");
  return (t && t[1]) || "";
}
function imageOf(evt) {
  const t = evt.tags.find(t => t[0] === "image");
  return t && t[1];
}

// decrypt private content if present; returns array of pubkeys (p tags)
async function decryptPrivate(evt, ownPubkey) {
  if (!evt.content) return [];
  try {
    const plaintext = await nip04Decrypt(ownPubkey, evt.content);
    const arr = JSON.parse(plaintext);
    if (!Array.isArray(arr)) return [];
    return arr.filter(t => Array.isArray(t) && t[0] === "p" && t[1]).map(t => t[1]);
  } catch (err) {
    console.warn("decrypt failed", err);
    return [];
  }
}

// --- build event templates ----------------------------------------------
function buildFollowSet({ d, title, description, image, publicPubkeys, privateCiphertext }) {
  const tags = [["d", d]];
  if (title) tags.push(["title", title]);
  if (description) tags.push(["description", description]);
  if (image) tags.push(["image", image]);
  for (const pk of publicPubkeys) tags.push(["p", pk]);
  return {
    kind: 30000,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content: privateCiphertext || "",
  };
}

function buildContactList(pubkeys, originalTags = []) {
  // preserve non-p tags from existing list if any
  const keep = originalTags.filter(t => t[0] !== "p");
  const ps = pubkeys.map(pk => ["p", pk]);
  return {
    kind: 3,
    created_at: Math.floor(Date.now() / 1000),
    tags: [...keep, ...ps],
    content: "",
  };
}

// --- recent activity for a profile ---------------------------------------
const ACTIVITY_KINDS = [1, 6, 7, 9802, 1063, 30023, 31922, 31923, 31925, 39701, 30315, 10003, 1985];
async function fetchActivity(pubkey, limit = 50) {
  if (!pubkey) return [];
  const [own, zapsIn, zapsOut] = await Promise.all([
    fetchEvents({ authors: [pubkey], kinds: ACTIVITY_KINDS, limit }, DEFAULT_RELAYS, 5000),
    fetchEvents({ kinds: [9735], "#p": [pubkey], limit: 20 }, DEFAULT_RELAYS, 5000),
    fetchEvents({ kinds: [9735], "#P": [pubkey], limit: 20 }, DEFAULT_RELAYS, 5000),
  ]);
  const tagged = [
    ...own,
    ...zapsIn.map(e => ({ ...e, _zapDir: "in" })),
    ...zapsOut.map(e => ({ ...e, _zapDir: "out" })),
  ];
  const seen = new Set();
  const unique = tagged.filter(e => seen.has(e.id) ? false : (seen.add(e.id), true));
  unique.sort((a, b) => b.created_at - a.created_at);
  return unique.slice(0, 10);
}

// --- NIP-57 zaps ---------------------------------------------------------
async function fetchLnurlPay(lud) {
  if (!lud) throw new Error("No lightning address");
  if (!lud.includes("@")) throw new Error("Lightning address must be name@domain");
  const [name, domain] = lud.split("@");
  const r = await fetch(`https://${domain}/.well-known/lnurlp/${encodeURIComponent(name)}`);
  if (!r.ok) throw new Error(`Lightning host returned ${r.status}`);
  const j = await r.json();
  if (j.tag && j.tag !== "payRequest") throw new Error("Not a pay endpoint");
  return j;
}
function buildZapRequest({ recipientPk, amountMsats, comment = "", relays }) {
  return {
    kind: 9734,
    created_at: Math.floor(Date.now() / 1000),
    content: comment || "",
    tags: [
      ["relays", ...(relays || DEFAULT_RELAYS)],
      ["amount", String(amountMsats)],
      ["p", recipientPk],
    ],
  };
}
async function fetchZapInvoice(callback, amountMsats, signedZapRequest) {
  const u = new URL(callback);
  u.searchParams.set("amount", String(amountMsats));
  u.searchParams.set("nostr", JSON.stringify(signedZapRequest));
  const r = await fetch(u.toString());
  if (!r.ok) throw new Error(`Invoice request failed (${r.status})`);
  const j = await r.json();
  if (!j.pr) throw new Error(j.reason || "No invoice returned");
  return j.pr;
}
async function payInvoice(invoice) {
  if (typeof window !== "undefined" && window.webln) {
    try { await window.webln.enable(); } catch {}
    return await window.webln.sendPayment(invoice);
  }
  // fallback: open lightning: URI in user's wallet
  window.open(`lightning:${invoice}`, "_blank");
  return null;
}

// export everything globally for other Babel scripts
Object.assign(window, {
  Nostr: {
    DEFAULT_RELAYS, pool,
    npubEncode, decodeAny, resolveNip05,
    hasNip07, getPubkey, signEvent, nip04Decrypt, nip04Encrypt,
    fetchContactList, fetchFollowSets, fetchProfiles, fetchLastSeen, searchProfiles,
    publish,
    tagsOfList, dTagOf, titleOf, descriptionOf, imageOf,
    decryptPrivate, buildFollowSet, buildContactList,
    fetchLnurlPay, buildZapRequest, fetchZapInvoice, payInvoice,
    fetchActivity,
  }
});
