import { generateSecretKey, getPublicKey as getPubKeyFromSecret } from "nostr-tools/pure";
import { nip19 } from "nostr-tools";

// Relays return events keyed by hex pubkeys and only accept hex in `authors`
// filters. Pubkeys may be stored/passed as npub (or nprofile), so normalize.
export function toHexPubkey(pubkey: string | null | undefined): string | null {
  if (!pubkey) return null;
  const trimmed = pubkey.trim();
  if (/^[0-9a-f]{64}$/i.test(trimmed)) return trimmed.toLowerCase();
  try {
    const decoded = nip19.decode(trimmed);
    if (decoded.type === "npub") return decoded.data as string;
    if (decoded.type === "nprofile") return (decoded.data as { pubkey: string }).pubkey;
  } catch {
    // fall through
  }
  return null;
}

// Relays that specialize in (or broadly aggregate) kind:0 profile metadata.
// Included alongside the site relays so profiles are found even when a user's
// metadata was never published to the site's configured relay set.
const PROFILE_METADATA_RELAYS = [
  "wss://purplepag.es",
  "wss://relay.nostr.band",
];

declare global {
  interface Window {
    nostr?: {
      getPublicKey(): Promise<string>;
      signEvent(event: any): Promise<any>;
      getRelays?(): Promise<Record<string, { read: boolean; write: boolean }>>;
    };
  }
}

export type BunkerSignerInterface = {
  getPublicKey(): Promise<string>;
  signEvent(event: any): Promise<any>;
  close(): Promise<void>;
};

export function hasNostrExtension(): boolean {
  return typeof window !== "undefined" && !!window.nostr;
}

export async function getPublicKey(): Promise<string> {
  if (!window.nostr) throw new Error("No Nostr extension found");
  return window.nostr.getPublicKey();
}

export async function signEvent(event: any): Promise<any> {
  if (!window.nostr) throw new Error("No Nostr extension found");
  return window.nostr.signEvent(event);
}

export function createAuthEvent(pubkey: string, challenge: string) {
  return {
    kind: 22242,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ["relay", ""],
      ["challenge", challenge],
    ],
    content: "",
    pubkey,
  };
}

export function shortenPubkey(pubkey: string): string {
  if (!pubkey) return "";
  return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
}

export interface NostrProfile {
  name?: string;
  picture?: string;
  about?: string;
  nip05?: string;
  displayName?: string;
}

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

let _siteRelaysCache: { relays: string[]; fetchedAt: number } | null = null;
const SITE_RELAY_TTL = 5 * 60 * 1000; // 5 minutes

export async function getSiteRelays(): Promise<string[]> {
  if (_siteRelaysCache && Date.now() - _siteRelaysCache.fetchedAt < SITE_RELAY_TTL) {
    return _siteRelaysCache.relays;
  }
  try {
    const res = await fetch("/api/relays/public");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    if (Array.isArray(data.relays) && data.relays.length > 0) {
      _siteRelaysCache = { relays: data.relays, fetchedAt: Date.now() };
      return data.relays;
    }
  } catch {}
  return FALLBACK_RELAYS;
}

export interface Nip65RelayList {
  write: string[];
  read: string[];
  all: string[];
}

const _nip65Cache = new Map<string, { data: Nip65RelayList; fetchedAt: number }>();
const NIP65_TTL = 10 * 60 * 1000; // 10 minutes

export async function fetchNip65RelayList(pubkey: string): Promise<Nip65RelayList> {
  const cached = _nip65Cache.get(pubkey);
  if (cached && Date.now() - cached.fetchedAt < NIP65_TTL) return cached.data;

  const { SimplePool } = await import("nostr-tools/pool");
  const siteRelays = await getSiteRelays();
  const pool = new SimplePool();

  try {
    const event = await pool.get(siteRelays, {
      kinds: [10002],
      authors: [pubkey],
    });

    const result: Nip65RelayList = { write: [], read: [], all: [] };
    if (!event) {
      _nip65Cache.set(pubkey, { data: result, fetchedAt: Date.now() });
      return result;
    }

    for (const tag of event.tags) {
      if (tag[0] !== "r" || !tag[1]) continue;
      const url = tag[1];
      const marker = tag[2];
      result.all.push(url);
      if (marker === "write") {
        result.write.push(url);
      } else if (marker === "read") {
        result.read.push(url);
      } else {
        result.write.push(url);
        result.read.push(url);
      }
    }

    _nip65Cache.set(pubkey, { data: result, fetchedAt: Date.now() });
    return result;
  } catch {
    return { write: [], read: [], all: [] };
  } finally {
    pool.close(siteRelays);
  }
}

let _userRelaysCache: { relays: { url: string; read: boolean; write: boolean }[]; fetchedAt: number } | null = null;
const USER_RELAY_TTL = 2 * 60 * 1000; // 2 minutes

export async function getUserStoredRelays(): Promise<{ url: string; read: boolean; write: boolean }[]> {
  if (_userRelaysCache && Date.now() - _userRelaysCache.fetchedAt < USER_RELAY_TTL) {
    return _userRelaysCache.relays;
  }
  try {
    const token = typeof window !== "undefined" ? localStorage.getItem("bbe_token") : null;
    if (!token) return [];
    const res = await fetch("/api/user-relays", {
      headers: { Authorization: `Bearer ${token}` },
    });
    if (!res.ok) return [];
    const data = await res.json();
    if (Array.isArray(data)) {
      _userRelaysCache = { relays: data, fetchedAt: Date.now() };
      return data;
    }
  } catch {}
  return [];
}

export function clearUserRelaysCache(): void {
  _userRelaysCache = null;
}

export async function publishEvent(signedEvent: any): Promise<void> {
  const { SimplePool } = await import("nostr-tools/pool");
  const siteRelays = await getSiteRelays();
  const allRelays = new Set<string>(siteRelays);

  // Merge user's stored write relays
  try {
    const stored = await getUserStoredRelays();
    stored
      .filter((r) => r.write)
      .forEach((r) => allRelays.add(r.url));
  } catch {}

  // Merge NIP-65 write relays for the event author
  if (signedEvent.pubkey) {
    try {
      const nip65 = await fetchNip65RelayList(signedEvent.pubkey);
      nip65.write.forEach((url) => allRelays.add(url));
    } catch {}
  }

  // Merge extension relays
  try {
    if (window.nostr?.getRelays) {
      const ext = await window.nostr.getRelays();
      Object.entries(ext)
        .filter(([, p]) => (p as any).write)
        .forEach(([url]) => allRelays.add(url));
    }
  } catch {}

  const relayUrls = [...allRelays];
  const pool = new SimplePool();
  try {
    await Promise.allSettled(pool.publish(relayUrls, signedEvent));
  } finally {
    pool.close(relayUrls);
  }
}

export async function fetchNostrProfile(
  pubkey: string,
  relayUrls?: string[]
): Promise<NostrProfile> {
  const hex = toHexPubkey(pubkey);
  if (!hex) return {};

  const { SimplePool } = await import("nostr-tools/pool");
  const allRelays = new Set<string>(relayUrls || await getSiteRelays());
  PROFILE_METADATA_RELAYS.forEach((url) => allRelays.add(url));

  try {
    const nip65 = await fetchNip65RelayList(hex);
    nip65.write.forEach((url) => allRelays.add(url));
  } catch {}

  const urls = [...allRelays];
  const pool = new SimplePool();

  try {
    const event = await pool.get(urls, {
      kinds: [0],
      authors: [hex],
    });

    if (!event?.content) return {};
    return parseProfileContent(event.content);
  } catch {
    return {};
  } finally {
    pool.close(urls);
  }
}

function parseProfileContent(content: string): NostrProfile {
  const meta = JSON.parse(content);
  return {
    name: meta.name || meta.display_name,
    displayName: meta.display_name,
    picture: meta.picture,
    about: meta.about,
    nip05: meta.nip05,
  };
}

const _profileCache = new Map<string, { profile: NostrProfile; fetchedAt: number; empty: boolean }>();
const PROFILE_TTL = 5 * 60 * 1000; // 5 minutes for resolved profiles
const PROFILE_EMPTY_TTL = 30 * 1000; // retry misses sooner

function isProfileEmpty(p: NostrProfile): boolean {
  return !p.name && !p.displayName && !p.picture && !p.about && !p.nip05;
}

function readProfileCache(pubkey: string, now: number): NostrProfile | null {
  const cached = _profileCache.get(pubkey);
  if (!cached) return null;
  const ttl = cached.empty ? PROFILE_EMPTY_TTL : PROFILE_TTL;
  return now - cached.fetchedAt < ttl ? cached.profile : null;
}

function writeProfileCache(pubkey: string, profile: NostrProfile, now: number): void {
  _profileCache.set(pubkey, { profile, fetchedAt: now, empty: isProfileEmpty(profile) });
}

// Batched profile fetch. Resolves kind:0 metadata for many pubkeys using a
// single SimplePool and one query, instead of opening a pool (plus a NIP-65
// lookup pool) per pubkey. Querying many profiles individually opens dozens of
// simultaneous websocket connections to the same relays, which get
// throttled/dropped and return empty results. Pubkeys are normalized to hex
// (relays reject npub/nprofile in `authors`).
export async function fetchNostrProfiles(
  pubkeys: string[],
  relayUrls?: string[]
): Promise<Record<string, NostrProfile>> {
  const result: Record<string, NostrProfile> = {};
  const now = Date.now();

  // Map each requested key to its hex form. Skip cached and un-decodable keys.
  const hexByKey = new Map<string, string>();
  for (const pk of pubkeys) {
    const cached = readProfileCache(pk, now);
    if (cached) {
      result[pk] = cached;
      continue;
    }
    const hex = toHexPubkey(pk);
    if (!hex) {
      result[pk] = {};
      continue;
    }
    hexByKey.set(pk, hex);
  }

  if (hexByKey.size === 0) return result;

  const { SimplePool } = await import("nostr-tools/pool");
  const base = relayUrls && relayUrls.length > 0 ? relayUrls : await getSiteRelays();
  const urls = [...new Set([...base, ...PROFILE_METADATA_RELAYS])];
  const pool = new SimplePool();

  try {
    const authors = [...new Set(hexByKey.values())];
    const events = await pool.querySync(
      urls,
      { kinds: [0], authors },
      { maxWait: 6000 }
    );

    // Keep only the most recent kind:0 event per hex author.
    const latest = new Map<string, { created_at: number; content: string }>();
    for (const event of events) {
      const prev = latest.get(event.pubkey);
      if (!prev || event.created_at > prev.created_at) {
        latest.set(event.pubkey, { created_at: event.created_at, content: event.content });
      }
    }

    for (const [key, hex] of hexByKey) {
      const ev = latest.get(hex);
      let profile: NostrProfile = {};
      if (ev?.content) {
        try {
          profile = parseProfileContent(ev.content);
        } catch {
          profile = {};
        }
      }
      result[key] = profile;
      writeProfileCache(key, profile, now);
    }

    return result;
  } catch {
    for (const key of hexByKey.keys()) {
      if (!(key in result)) result[key] = {};
    }
    return result;
  } finally {
    pool.close(urls);
  }
}

// DataLoader-style batching for single-pubkey requests. Component instances
// (e.g. <NostrAvatar />) each ask for one pubkey; this coalesces all requests
// made within a short window into a single batched relay query.
let _batchQueue = new Set<string>();
let _batchResolvers = new Map<string, Array<(p: NostrProfile) => void>>();
let _batchTimer: ReturnType<typeof setTimeout> | null = null;
const BATCH_WINDOW_MS = 60;

async function flushProfileBatch(): Promise<void> {
  const pubkeys = [..._batchQueue];
  const resolvers = _batchResolvers;
  _batchQueue = new Set();
  _batchResolvers = new Map();
  _batchTimer = null;

  let profiles: Record<string, NostrProfile> = {};
  try {
    profiles = await fetchNostrProfiles(pubkeys);
  } catch {
    profiles = {};
  }
  for (const pk of pubkeys) {
    const profile = profiles[pk] ?? {};
    resolvers.get(pk)?.forEach((resolve) => resolve(profile));
  }
}

// Resolve a single pubkey's profile, batching concurrent calls and reusing the
// shared profile cache.
export function loadNostrProfile(pubkey: string): Promise<NostrProfile> {
  const cached = readProfileCache(pubkey, Date.now());
  if (cached) {
    return Promise.resolve(cached);
  }
  return new Promise((resolve) => {
    const existing = _batchResolvers.get(pubkey);
    if (existing) {
      existing.push(resolve);
    } else {
      _batchResolvers.set(pubkey, [resolve]);
    }
    _batchQueue.add(pubkey);
    if (_batchTimer === null) {
      _batchTimer = setTimeout(() => void flushProfileBatch(), BATCH_WINDOW_MS);
    }
  });
}

export async function fetchEventFromRelays(eventId: string): Promise<any | null> {
  const { SimplePool } = await import("nostr-tools/pool");
  const siteRelays = await getSiteRelays();
  const pool = new SimplePool();

  try {
    const event = await pool.get(siteRelays, { ids: [eventId] });
    return event || null;
  } catch {
    return null;
  } finally {
    pool.close(siteRelays);
  }
}

export async function fetchLongformFromRelays(naddrStr: string): Promise<any | null> {
  const { SimplePool } = await import("nostr-tools/pool");
  const { nip19 } = await import("nostr-tools");

  let decoded: { kind: number; pubkey: string; identifier: string; relays?: string[] };
  try {
    const result = nip19.decode(naddrStr);
    if (result.type !== "naddr") return null;
    decoded = result.data;
  } catch {
    return null;
  }

  const siteRelays = await getSiteRelays();
  const naddrRelays = decoded.relays || [];
  const allRelays = new Set<string>([...naddrRelays, ...siteRelays]);

  try {
    const nip65 = await fetchNip65RelayList(decoded.pubkey);
    nip65.write.forEach((url) => allRelays.add(url));
  } catch {}

  const relayUrls = [...allRelays];
  const filter = {
    kinds: [decoded.kind],
    authors: [decoded.pubkey],
    "#d": [decoded.identifier],
  };

  const pool = new SimplePool();
  try {
    const event = await pool.get(relayUrls, filter);
    return event || null;
  } catch {
    return null;
  } finally {
    pool.close(relayUrls);
  }
}

// NIP-46: External signer (bunker:// URI)
export async function createBunkerSigner(
  input: string
): Promise<{ signer: BunkerSignerInterface; pubkey: string }> {
  const { BunkerSigner, parseBunkerInput } = await import("nostr-tools/nip46");
  const bp = await parseBunkerInput(input);
  if (!bp) throw new Error("Invalid bunker URI or NIP-05 identifier");
  const clientSecretKey = generateSecretKey();
  const signer = BunkerSigner.fromBunker(clientSecretKey, bp);
  await signer.connect();
  const pubkey = await signer.getPublicKey();
  return { signer, pubkey };
}

// NIP-46: Generate a nostrconnect:// URI for QR display
export async function generateNostrConnectSetup(
  relayUrls?: string[]
): Promise<{ uri: string; clientSecretKey: Uint8Array }> {
  if (!relayUrls) relayUrls = (await getSiteRelays()).slice(0, 2);
  const { createNostrConnectURI } = await import("nostr-tools/nip46");
  const clientSecretKey = generateSecretKey();
  const clientPubkey = getPubKeyFromSecret(clientSecretKey);
  const secretBytes = crypto.getRandomValues(new Uint8Array(8));
  const secret = Array.from(secretBytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  const uri = createNostrConnectURI({
    clientPubkey,
    relays: relayUrls,
    secret,
    name: "Belgian Bitcoin Embassy",
  });
  return { uri, clientSecretKey };
}

// NIP-46: Wait for a remote signer to connect via nostrconnect:// URI
export async function waitForNostrConnectSigner(
  clientSecretKey: Uint8Array,
  uri: string,
  signal?: AbortSignal
): Promise<{ signer: BunkerSignerInterface; pubkey: string }> {
  const { BunkerSigner } = await import("nostr-tools/nip46");
  const signer = await BunkerSigner.fromURI(clientSecretKey, uri, {}, signal);
  const pubkey = await signer.getPublicKey();
  return { signer, pubkey };
}
