import WebSocket from 'ws';
import { SimplePool, nip19 } from 'nostr-tools';
import { prisma } from '../db/prisma';

// nostr-tools' SimplePool relies on a global WebSocket. Node only exposes one
// from v22 onward, so on older runtimes (this backend runs Node 20) every relay
// query silently fails without this polyfill. Set before any pool connects.
if (typeof (globalThis as { WebSocket?: unknown }).WebSocket === 'undefined') {
  (globalThis as { WebSocket?: unknown }).WebSocket = WebSocket;
}

const pool = new SimplePool();

async function getRelayUrls(): Promise<string[]> {
  const relays = await prisma.relay.findMany({
    where: { active: true },
    orderBy: { priority: 'asc' },
  });
  return relays.map((r) => r.url);
}

function dedupeRelays(...lists: string[][]): string[] {
  return [...new Set(lists.flat().filter(Boolean))];
}

async function cacheEvent(event: any) {
  await prisma.nostrEventCache.upsert({
    where: { eventId: event.id },
    update: {
      content: event.content,
      tags: JSON.stringify(event.tags),
    },
    create: {
      eventId: event.id,
      kind: event.kind,
      pubkey: event.pubkey,
      content: event.content,
      tags: JSON.stringify(event.tags),
      createdAt: event.created_at,
    },
  });
}

export const nostrService = {
  async fetchNip65Relays(pubkey: string): Promise<{ write: string[]; read: string[] }> {
    const siteRelays = await getRelayUrls();
    if (siteRelays.length === 0) return { write: [], read: [] };

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

      const write: string[] = [];
      const read: string[] = [];
      for (const tag of event.tags) {
        if (tag[0] !== 'r' || !tag[1]) continue;
        const url = tag[1];
        const marker = tag[2];
        if (marker === 'write') {
          write.push(url);
        } else if (marker === 'read') {
          read.push(url);
        } else {
          write.push(url);
          read.push(url);
        }
      }
      return { write, read };
    } catch (err) {
      console.error('Failed to fetch NIP-65 relays for', pubkey, err);
      return { write: [], read: [] };
    }
  },

  async fetchEvent(eventId: string, skipCache = false) {
    if (!skipCache) {
      const cached = await prisma.nostrEventCache.findUnique({
        where: { eventId },
      });
      if (cached) {
        return {
          id: cached.eventId,
          kind: cached.kind,
          pubkey: cached.pubkey,
          content: cached.content,
          tags: JSON.parse(cached.tags),
          created_at: cached.createdAt,
        };
      }
    }

    const siteRelays = await getRelayUrls();
    if (siteRelays.length === 0) return null;

    try {
      const event = await pool.get(siteRelays, { ids: [eventId] });
      if (event) {
        await cacheEvent(event);
        return event;
      }
    } catch (err) {
      console.error('Failed to fetch event from site relays:', err);
    }

    return null;
  },

  async fetchLongformEvent(naddrStr: string) {
    let decoded: nip19.AddressPointer;
    try {
      const result = nip19.decode(naddrStr);
      if (result.type !== 'naddr') return null;
      decoded = result.data;
    } catch {
      return null;
    }

    // Cache-first: addressable events are keyed by kind+pubkey+d-tag, not by a
    // stable event id, so look them up among cached events of the same author.
    // Avoids re-querying relays on every repeat visit to the same naddr.
    const cached = await prisma.nostrEventCache.findMany({
      where: { kind: decoded.kind, pubkey: decoded.pubkey },
    });
    for (const c of cached) {
      let tags: string[][] = [];
      try {
        tags = JSON.parse(c.tags);
      } catch {
        continue;
      }
      const dTag = tags.find((t) => t[0] === 'd');
      if (dTag?.[1] === decoded.identifier) {
        return {
          id: c.eventId,
          kind: c.kind,
          pubkey: c.pubkey,
          content: c.content,
          tags,
          created_at: c.createdAt,
        };
      }
    }

    const siteRelays = await getRelayUrls();
    const naddrRelays = decoded.relays || [];
    const filter = {
      kinds: [decoded.kind],
      authors: [decoded.pubkey],
      '#d': [decoded.identifier],
    };

    // Try naddr-embedded relays first, then site relays
    const firstTry = dedupeRelays(naddrRelays, siteRelays);
    if (firstTry.length > 0) {
      try {
        const event = await pool.get(firstTry, filter);
        if (event) {
          await cacheEvent(event);
          return event;
        }
      } catch (err) {
        console.error('Failed to fetch longform event (first pass):', err);
      }
    }

    // Fallback: try author's NIP-65 write relays
    const nip65 = await nostrService.fetchNip65Relays(decoded.pubkey);
    if (nip65.write.length > 0) {
      const nip65Relays = dedupeRelays(nip65.write);
      try {
        const event = await pool.get(nip65Relays, filter);
        if (event) {
          await cacheEvent(event);
          return event;
        }
      } catch (err) {
        console.error('Failed to fetch longform event (NIP-65 fallback):', err);
      }
    }

    return null;
  },

  // Resolves any NIP-19 reference (naddr / nevent / note) or a raw 64-char hex
  // event id to the underlying Nostr event, reusing the cache-aware fetchers.
  // Returns null if the string can't be decoded or the event isn't found.
  async resolveEventByIdentifier(identifier: string) {
    const trimmed = identifier.trim();

    if (/^[0-9a-f]{64}$/i.test(trimmed)) {
      return nostrService.fetchEvent(trimmed.toLowerCase());
    }

    let decoded;
    try {
      decoded = nip19.decode(trimmed);
    } catch {
      return null;
    }

    switch (decoded.type) {
      case 'naddr':
        return nostrService.fetchLongformEvent(trimmed);
      case 'nevent':
        return nostrService.fetchEvent((decoded.data as nip19.EventPointer).id);
      case 'note':
        return nostrService.fetchEvent(decoded.data as string);
      default:
        return null;
    }
  },

  async fetchReactions(eventId: string) {
    const relays = await getRelayUrls();
    if (relays.length === 0) return [];

    try {
      const events = await pool.querySync(relays, {
        kinds: [7],
        '#e': [eventId],
      });
      return events;
    } catch (err) {
      console.error('Failed to fetch reactions:', err);
      return [];
    }
  },

  async fetchReplies(eventId: string) {
    const relays = await getRelayUrls();
    if (relays.length === 0) return [];

    try {
      const events = await pool.querySync(relays, {
        kinds: [1],
        '#e': [eventId],
      });
      return events;
    } catch (err) {
      console.error('Failed to fetch replies:', err);
      return [];
    }
  },

  async getRelays() {
    return prisma.relay.findMany({
      where: { active: true },
      orderBy: { priority: 'asc' },
    });
  },
};
