import { SimplePool } from 'nostr-tools/pool';
import { EventStore } from 'applesauce-core';
import { PrivateKeySigner } from 'applesauce-signers';
import { getDefaultRelays, FALLBACK_RELAYS } from "./constants.js";

// Dedicated pool for fallback relay queries (separate from main pool to avoid conflicts)
let fallbackPool = null;

function getFallbackPool() {
  if (!fallbackPool) {
    fallbackPool = new SimplePool();
  }
  return fallbackPool;
}

// Nostr client wrapper using nostr-tools
class NostrClient {
  constructor() {
    this.pool = new SimplePool();
    this.eventStore = new EventStore();
    this.isConnected = false;
    this.signer = null;
    // Use dynamic relay list (supports standalone mode)
    this.relays = [...getDefaultRelays()];
  }

  // Refresh relay list from config (call when relay URL changes)
  refreshRelays() {
    const newRelays = getDefaultRelays();
    if (JSON.stringify(this.relays) !== JSON.stringify(newRelays)) {
      console.log("Relay list updated:", newRelays);
      this.relays = [...newRelays];
    }
  }

  // Reset client for new relay (close old connections, refresh relay list, create new pool)
  reset() {
    console.log("[NostrClient] Resetting for new relay...");
    // Close ALL existing connections by destroying the pool
    if (this.pool) {
      try {
        // Close connections to old relays first
        this.pool.close(this.relays);
      } catch (e) {
        console.warn("[NostrClient] Error closing old relay connections:", e);
      }
      // Destroy the pool reference completely
      this.pool = null;
    }
    // Create completely fresh pool
    this.pool = new SimplePool();
    this.isConnected = false;
    // Refresh relay list
    this.relays = [...getDefaultRelays()];
    console.log("[NostrClient] Reset complete, new relays:", this.relays);
  }

  async connect() {
    console.log("Starting connection to", this.relays.length, "relays...");
    
    try {
      // SimplePool doesn't require explicit connect
      this.isConnected = true;
      console.log("✓ Successfully initialized relay pool");
      
      // Wait a bit for connections to stabilize
      await new Promise((resolve) => setTimeout(resolve, 1000));
    } catch (error) {
      console.error("✗ Connection failed:", error);
      throw error;
    }
  }

  async connectToRelay(relayUrl) {
    console.log(`Adding relay: ${relayUrl}`);
    
    try {
      if (!this.relays.includes(relayUrl)) {
        this.relays.push(relayUrl);
      }
      console.log(`✓ Successfully added relay ${relayUrl}`);
      return true;
    } catch (error) {
      console.error(`✗ Failed to add relay ${relayUrl}:`, error);
      return false;
    }
  }

  subscribe(filters, callback) {
    console.log("Creating subscription with filters:", filters);
    
    const sub = this.pool.subscribeMany(
      this.relays,
      filters,
      {
        onevent(event) {
          console.log("Event received:", event);
          callback(event);
        },
        oneose() {
          console.log("EOSE received");
          window.dispatchEvent(new CustomEvent('nostr-eose', {
            detail: { subscriptionId: sub.id }
          }));
        }
      }
    );

    return sub;
  }

  unsubscribe(subscription) {
    console.log(`Closing subscription`);
    if (subscription && subscription.close) {
      subscription.close();
    }
  }

  disconnect() {
    console.log("Disconnecting relay pool");
    if (this.pool) {
      this.pool.close(this.relays);
    }
    this.isConnected = false;
  }

  // Publish an event
  async publish(event, specificRelays = null) {
    if (!this.isConnected) {
      console.warn("Not connected to any relays, attempting to connect first");
      await this.connect();
    }
    
    try {
      const relaysToUse = specificRelays || this.relays;
      const promises = this.pool.publish(relaysToUse, event);
      await Promise.allSettled(promises);
      console.log("✓ Event published successfully");
      // Store the published event in IndexedDB
      await putEvents([event]);
      console.log("Event stored in IndexedDB");
      return { success: true, okCount: 1, errorCount: 0 };
    } catch (error) {
      console.error("✗ Failed to publish event:", error);
      throw error;
    }
  }

  // Get pool for advanced usage
  getPool() {
    return this.pool;
  }

  // Get event store
  getEventStore() {
    return this.eventStore;
  }

  // Get signer
  getSigner() {
    return this.signer;
  }

  // Set signer
  setSigner(signer) {
    this.signer = signer;
  }
}

// Create a global client instance
export const nostrClient = new NostrClient();

// Export the class for creating new instances
export { NostrClient };

// Export signer classes
export { PrivateKeySigner };

// Export NIP-07 helper
export class Nip07Signer {
  async getPublicKey() {
    if (window.nostr) {
      return await window.nostr.getPublicKey();
    }
    throw new Error('NIP-07 extension not found');
  }

  async signEvent(event) {
    if (window.nostr) {
      return await window.nostr.signEvent(event);
    }
    throw new Error('NIP-07 extension not found');
  }

  async nip04Encrypt(pubkey, plaintext) {
    if (window.nostr && window.nostr.nip04) {
      return await window.nostr.nip04.encrypt(pubkey, plaintext);
    }
    throw new Error('NIP-07 extension does not support NIP-04');
  }

  async nip04Decrypt(pubkey, ciphertext) {
    if (window.nostr && window.nostr.nip04) {
      return await window.nostr.nip04.decrypt(pubkey, ciphertext);
    }
    throw new Error('NIP-07 extension does not support NIP-04');
  }

  async nip44Encrypt(pubkey, plaintext) {
    if (window.nostr && window.nostr.nip44) {
      return await window.nostr.nip44.encrypt(pubkey, plaintext);
    }
    throw new Error('NIP-07 extension does not support NIP-44');
  }

  async nip44Decrypt(pubkey, ciphertext) {
    if (window.nostr && window.nostr.nip44) {
      return await window.nostr.nip44.decrypt(pubkey, ciphertext);
    }
    throw new Error('NIP-07 extension does not support NIP-44');
  }
}

// Merge two event arrays, deduplicating by event id
// Newer events (by created_at) take precedence for same id
function mergeAndDeduplicateEvents(cached, relay) {
  const eventMap = new Map();

  // Add cached events first
  for (const event of cached) {
    eventMap.set(event.id, event);
  }

  // Add/update with relay events (they may be newer)
  for (const event of relay) {
    const existing = eventMap.get(event.id);
    if (!existing || event.created_at >= existing.created_at) {
      eventMap.set(event.id, event);
    }
  }

  // Return sorted by created_at descending (newest first)
  return Array.from(eventMap.values()).sort((a, b) => b.created_at - a.created_at);
}

// IndexedDB helpers for unified event storage
// This provides a local cache that all components can access
const DB_NAME = "nostrCache";
const DB_VERSION = 2; // Incremented for new indexes
const STORE_EVENTS = "events";

function openDB() {
  return new Promise((resolve, reject) => {
    try {
      const req = indexedDB.open(DB_NAME, DB_VERSION);
      req.onupgradeneeded = (event) => {
        const db = req.result;
        const oldVersion = event.oldVersion;
        
        // Create or update the events store
        let store;
        if (!db.objectStoreNames.contains(STORE_EVENTS)) {
          store = db.createObjectStore(STORE_EVENTS, { keyPath: "id" });
        } else {
          // Get existing store during upgrade
          store = req.transaction.objectStore(STORE_EVENTS);
        }
        
        // Create indexes if they don't exist
        if (!store.indexNames.contains("byKindAuthor")) {
          store.createIndex("byKindAuthor", ["kind", "pubkey"], {
            unique: false,
          });
        }
        if (!store.indexNames.contains("byKindAuthorCreated")) {
          store.createIndex(
            "byKindAuthorCreated",
            ["kind", "pubkey", "created_at"],
            { unique: false },
          );
        }
        if (!store.indexNames.contains("byKind")) {
          store.createIndex("byKind", "kind", { unique: false });
        }
        if (!store.indexNames.contains("byAuthor")) {
          store.createIndex("byAuthor", "pubkey", { unique: false });
        }
        if (!store.indexNames.contains("byCreatedAt")) {
          store.createIndex("byCreatedAt", "created_at", { unique: false });
        }
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    } catch (e) {
      console.error("Failed to open IndexedDB", e);
      reject(e);
    }
  });
}

async function getLatestProfileEvent(pubkey) {
  try {
    const db = await openDB();
    return await new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_EVENTS, "readonly");
      const idx = tx.objectStore(STORE_EVENTS).index("byKindAuthorCreated");
      const range = IDBKeyRange.bound(
        [0, pubkey, -Infinity],
        [0, pubkey, Infinity],
      );
      const req = idx.openCursor(range, "prev"); // newest first
      req.onsuccess = () => {
        const cursor = req.result;
        resolve(cursor ? cursor.value : null);
      };
      req.onerror = () => reject(req.error);
    });
  } catch (e) {
    console.warn("IDB getLatestProfileEvent failed", e);
    return null;
  }
}

async function putEvent(event) {
  try {
    const db = await openDB();
    await new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_EVENTS, "readwrite");
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
      tx.objectStore(STORE_EVENTS).put(event);
    });
  } catch (e) {
    console.warn("IDB putEvent failed", e);
  }
}

// Store multiple events in IndexedDB
async function putEvents(events) {
  if (!events || events.length === 0) return;
  
  try {
    const db = await openDB();
    await new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_EVENTS, "readwrite");
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
      
      const store = tx.objectStore(STORE_EVENTS);
      for (const event of events) {
        store.put(event);
      }
    });
    console.log(`Stored ${events.length} events in IndexedDB`);
  } catch (e) {
    console.warn("IDB putEvents failed", e);
  }
}

// Query events from IndexedDB by filters
async function queryEventsFromDB(filters) {
  try {
    const db = await openDB();
    const results = [];
    
    console.log("QueryEventsFromDB: Starting query with filters:", filters);
    
    for (const filter of filters) {
      console.log("QueryEventsFromDB: Processing filter:", filter);
      
      const events = await new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_EVENTS, "readonly");
        const store = tx.objectStore(STORE_EVENTS);
        const allEvents = [];
        
        // Determine which index to use based on filter
        let req;
        if (filter.kinds && filter.kinds.length > 0 && filter.authors && filter.authors.length > 0) {
          // Use byKindAuthor index for the most specific query
          const kind = filter.kinds[0];
          const author = filter.authors[0];
          console.log(`QueryEventsFromDB: Using byKindAuthorCreated index for kind=${kind}, author=${author.substring(0, 8)}...`);
          
          const idx = store.index("byKindAuthorCreated");
          const range = IDBKeyRange.bound(
            [kind, author, -Infinity],
            [kind, author, Infinity]
          );
          req = idx.openCursor(range, "prev"); // newest first
        } else if (filter.kinds && filter.kinds.length > 0) {
          // Use byKind index
          console.log(`QueryEventsFromDB: Using byKind index for kind=${filter.kinds[0]}`);
          const idx = store.index("byKind");
          req = idx.openCursor(IDBKeyRange.only(filter.kinds[0]));
        } else if (filter.authors && filter.authors.length > 0) {
          // Use byAuthor index
          console.log(`QueryEventsFromDB: Using byAuthor index for author=${filter.authors[0].substring(0, 8)}...`);
          const idx = store.index("byAuthor");
          req = idx.openCursor(IDBKeyRange.only(filter.authors[0]));
        } else {
          // Scan all events
          console.log("QueryEventsFromDB: Scanning all events (no specific index)");
          req = store.openCursor();
        }
        
        req.onsuccess = (event) => {
          const cursor = event.target.result;
          if (cursor) {
            const evt = cursor.value;
            
            // Apply additional filters
            let matches = true;
            
            // Filter by kinds
            if (filter.kinds && filter.kinds.length > 0 && !filter.kinds.includes(evt.kind)) {
              matches = false;
            }
            
            // Filter by authors
            if (filter.authors && filter.authors.length > 0 && !filter.authors.includes(evt.pubkey)) {
              matches = false;
            }
            
            // Filter by since
            if (filter.since && evt.created_at < filter.since) {
              matches = false;
            }
            
            // Filter by until
            if (filter.until && evt.created_at > filter.until) {
              matches = false;
            }
            
            // Filter by IDs
            if (filter.ids && filter.ids.length > 0 && !filter.ids.includes(evt.id)) {
              matches = false;
            }
            
            if (matches) {
              allEvents.push(evt);
            }
            
            // Apply limit
            if (filter.limit && allEvents.length >= filter.limit) {
              console.log(`QueryEventsFromDB: Reached limit of ${filter.limit}, found ${allEvents.length} matching events`);
              resolve(allEvents);
              return;
            }
            
            cursor.continue();
          } else {
            console.log(`QueryEventsFromDB: Cursor exhausted, found ${allEvents.length} matching events`);
            resolve(allEvents);
          }
        };
        
        req.onerror = () => {
          console.error("QueryEventsFromDB: Cursor error:", req.error);
          reject(req.error);
        };
      });
      
      console.log(`QueryEventsFromDB: Found ${events.length} events for this filter`);
      results.push(...events);
    }
    
    // Sort by created_at (newest first) and apply global limit
    results.sort((a, b) => b.created_at - a.created_at);
    
    console.log(`QueryEventsFromDB: Returning ${results.length} total events`);
    return results;
  } catch (e) {
    console.error("QueryEventsFromDB failed:", e);
    return [];
  }
}

function parseProfileFromEvent(event) {
  try {
    const profile = JSON.parse(event.content || "{}");
    return {
      name: profile.name || profile.display_name || "",
      picture: profile.picture || "",
      banner: profile.banner || "",
      about: profile.about || "",
      nip05: profile.nip05 || "",
      lud16: profile.lud16 || profile.lud06 || "",
    };
  } catch (e) {
    return {
      name: "",
      picture: "",
      banner: "",
      about: "",
      nip05: "",
      lud16: "",
    };
  }
}

// Fetch user profile metadata (kind 0)
export async function fetchUserProfile(pubkey) {
  console.log(`Starting profile fetch for pubkey: ${pubkey}`);
  console.log(`[fetchUserProfile] Current relay list:`, nostrClient.relays);

  // 1) Try cached profile first and resolve immediately if present
  try {
    const cachedEvent = await getLatestProfileEvent(pubkey);
    if (cachedEvent) {
      console.log("Using cached profile event");
      const profile = parseProfileFromEvent(cachedEvent);
      return profile;
    }
  } catch (e) {
    console.warn("Failed to load cached profile", e);
  }

  const filters = [{
    kinds: [0],
    authors: [pubkey],
    limit: 1
  }];

  // 2) Fetch profile from local relay first
  try {
    const events = await fetchEvents(filters, { timeout: 10000 });

    if (events.length > 0) {
      const profileEvent = events[0];
      console.log("Profile fetched from local relay:", profileEvent);
      return processProfileEvent(profileEvent, pubkey);
    }
  } catch (error) {
    console.warn("Failed to fetch profile from local relay:", error);
  }

  // 3) Try fallback relays if local relay doesn't have the profile
  console.log("Profile not found on local relay, trying fallback relays:", FALLBACK_RELAYS);
  try {
    const profileEvent = await fetchProfileFromFallbackRelays(pubkey, filters);
    if (profileEvent) {
      return processProfileEvent(profileEvent, pubkey);
    }
  } catch (error) {
    console.warn("Failed to fetch profile from fallback relays:", error);
  }

  // 4) No profile found anywhere
  console.log("No profile found for pubkey:", pubkey);
  return null;
}

// Helper to fetch profile from fallback relays
async function fetchProfileFromFallbackRelays(pubkey, filters) {
  console.log(`[fetchProfileFromFallbackRelays] Querying fallback relays:`, FALLBACK_RELAYS);
  console.log(`[fetchProfileFromFallbackRelays] Using filters:`, JSON.stringify(filters));
  return new Promise((resolve) => {
    const events = [];
    const pool = getFallbackPool();
    let sub;

    const timeoutId = setTimeout(() => {
      if (sub) sub.close();
      // Return the most recent profile event
      if (events.length > 0) {
        events.sort((a, b) => b.created_at - a.created_at);
        resolve(events[0]);
      } else {
        resolve(null);
      }
    }, 5000);

    sub = pool.subscribeMany(
      FALLBACK_RELAYS,
      filters,
      {
        onevent(event) {
          console.log("[fetchProfileFromFallbackRelays] Event received:", event.id?.substring(0, 8), "kind:", event.kind, "pubkey:", event.pubkey?.substring(0, 8));
          events.push(event);
        },
        oneose() {
          console.log(`[fetchProfileFromFallbackRelays] EOSE received, got ${events.length} events`);
          clearTimeout(timeoutId);
          if (sub) sub.close();
          if (events.length > 0) {
            events.sort((a, b) => b.created_at - a.created_at);
            console.log("[fetchProfileFromFallbackRelays] Returning best event:", events[0].id?.substring(0, 8));
            resolve(events[0]);
          } else {
            console.log("[fetchProfileFromFallbackRelays] No events found");
            resolve(null);
          }
        }
      }
    );
  });
}

// Helper to process and cache a profile event
async function processProfileEvent(profileEvent, pubkey) {
  // Cache the event
  await putEvent(profileEvent);

  // Publish the profile event to the local relay
  try {
    console.log("Publishing profile event to local relay:", profileEvent.id);
    await nostrClient.publish(profileEvent);
    console.log("Profile event successfully saved to local relay");
  } catch (publishError) {
    console.warn("Failed to publish profile to local relay:", publishError);
  }

  // Parse profile data
  const profile = parseProfileFromEvent(profileEvent);

  // Notify listeners that an updated profile is available
  try {
    if (typeof window !== "undefined" && window.dispatchEvent) {
      window.dispatchEvent(
        new CustomEvent("profile-updated", {
          detail: { pubkey, profile, event: profileEvent },
        }),
      );
    }
  } catch (e) {
    console.warn("Failed to dispatch profile-updated event", e);
  }

  return profile;
}

// Fetch user's relay list (NIP-65 kind 10002)
export async function fetchUserRelayList(pubkey) {
  console.log(`[nostr] Fetching relay list for pubkey: ${pubkey?.substring(0, 8)}...`);

  const filters = [{
    kinds: [10002],
    authors: [pubkey],
    limit: 1
  }];

  // Try local relay first
  try {
    const events = await fetchEvents(filters, { timeout: 10000, useCache: true });
    if (events.length > 0) {
      const relayListEvent = events.sort((a, b) => b.created_at - a.created_at)[0];
      console.log("[nostr] Relay list found on local relay");
      return parseRelayListFromEvent(relayListEvent);
    }
  } catch (error) {
    console.warn("[nostr] Failed to fetch relay list from local relay:", error);
  }

  // Try fallback relays
  console.log("[nostr] Relay list not found locally, trying fallback relays...");
  try {
    const relayListEvent = await fetchFromFallbackRelays(filters);
    if (relayListEvent) {
      // Cache and publish to local relay
      await putEvent(relayListEvent);
      try {
        await nostrClient.publish(relayListEvent);
      } catch (e) {
        console.warn("[nostr] Failed to publish relay list to local relay:", e);
      }
      return parseRelayListFromEvent(relayListEvent);
    }
  } catch (error) {
    console.warn("[nostr] Failed to fetch relay list from fallback relays:", error);
  }

  console.log("[nostr] No relay list found for pubkey");
  return null;
}

// Parse relay list from kind 10002 event
function parseRelayListFromEvent(event) {
  if (!event || event.kind !== 10002) return null;

  const relays = {
    read: [],
    write: [],
    all: []
  };

  for (const tag of event.tags) {
    if (tag[0] === 'r' && tag[1]) {
      const url = tag[1];
      const marker = tag[2]; // 'read', 'write', or undefined (both)

      if (marker === 'read') {
        relays.read.push(url);
      } else if (marker === 'write') {
        relays.write.push(url);
      } else {
        // No marker means both read and write
        relays.read.push(url);
        relays.write.push(url);
      }
      relays.all.push({ url, read: marker !== 'write', write: marker !== 'read' });
    }
  }

  console.log(`[nostr] Parsed relay list: ${relays.all.length} relays`);
  return relays;
}

// Generic helper to fetch from fallback relays
async function fetchFromFallbackRelays(filters) {
  return new Promise((resolve) => {
    const events = [];
    const pool = getFallbackPool();
    let sub;

    const timeoutId = setTimeout(() => {
      if (sub) sub.close();
      if (events.length > 0) {
        events.sort((a, b) => b.created_at - a.created_at);
        resolve(events[0]);
      } else {
        resolve(null);
      }
    }, 5000);

    sub = pool.subscribeMany(
      FALLBACK_RELAYS,
      filters,
      {
        onevent(event) {
          events.push(event);
        },
        oneose() {
          clearTimeout(timeoutId);
          if (sub) sub.close();
          if (events.length > 0) {
            events.sort((a, b) => b.created_at - a.created_at);
            resolve(events[0]);
          } else {
            resolve(null);
          }
        }
      }
    );
  });
}

// Fetch user's contact list (kind 3) - includes follows and may have relay hints
export async function fetchUserContactList(pubkey) {
  console.log(`[nostr] Fetching contact list for pubkey: ${pubkey?.substring(0, 8)}...`);

  const filters = [{
    kinds: [3],
    authors: [pubkey],
    limit: 1
  }];

  // Try local relay first
  try {
    const events = await fetchEvents(filters, { timeout: 10000, useCache: true });
    if (events.length > 0) {
      const contactEvent = events.sort((a, b) => b.created_at - a.created_at)[0];
      console.log("[nostr] Contact list found on local relay");
      return parseContactListFromEvent(contactEvent);
    }
  } catch (error) {
    console.warn("[nostr] Failed to fetch contact list from local relay:", error);
  }

  // Try fallback relays
  console.log("[nostr] Contact list not found locally, trying fallback relays...");
  try {
    const contactEvent = await fetchFromFallbackRelays(filters);
    if (contactEvent) {
      await putEvent(contactEvent);
      try {
        await nostrClient.publish(contactEvent);
      } catch (e) {
        console.warn("[nostr] Failed to publish contact list to local relay:", e);
      }
      return parseContactListFromEvent(contactEvent);
    }
  } catch (error) {
    console.warn("[nostr] Failed to fetch contact list from fallback relays:", error);
  }

  console.log("[nostr] No contact list found for pubkey");
  return null;
}

// Parse contact list from kind 3 event
function parseContactListFromEvent(event) {
  if (!event || event.kind !== 3) return null;

  const follows = [];
  const relayHints = {};

  for (const tag of event.tags) {
    if (tag[0] === 'p' && tag[1]) {
      const pubkey = tag[1];
      const relayUrl = tag[2] || null;
      const petname = tag[3] || null;

      follows.push({ pubkey, relayUrl, petname });

      if (relayUrl) {
        if (!relayHints[relayUrl]) {
          relayHints[relayUrl] = [];
        }
        relayHints[relayUrl].push(pubkey);
      }
    }
  }

  // Also parse the content field which may contain relay preferences (legacy format)
  let legacyRelays = {};
  try {
    if (event.content) {
      legacyRelays = JSON.parse(event.content);
    }
  } catch (e) {
    // Content is not JSON, ignore
  }

  console.log(`[nostr] Parsed contact list: ${follows.length} follows, ${Object.keys(relayHints).length} relay hints`);
  return { follows, relayHints, legacyRelays, event };
}

// Fetch events
export async function fetchEvents(filters, options = {}) {
  console.log(`Starting event fetch with filters:`, JSON.stringify(filters, null, 2));
  console.log(`Current relays:`, nostrClient.relays);

  // Ensure client is connected
  if (!nostrClient.isConnected || nostrClient.relays.length === 0) {
    console.warn("Client not connected, initializing...");
    await initializeNostrClient();
  }

  const {
    timeout = 30000,
    useCache = true, // Option to query from cache first
  } = options;

  // Try to get cached events first if requested
  let cachedEvents = [];
  if (useCache) {
    try {
      cachedEvents = await queryEventsFromDB(filters);
      if (cachedEvents.length > 0) {
        console.log(`Found ${cachedEvents.length} cached events in IndexedDB`);
      }
    } catch (e) {
      console.warn("Failed to query cached events", e);
    }
  }

  return new Promise((resolve, reject) => {
    const relayEvents = [];
    let sub = null;

    const timeoutId = setTimeout(() => {
      console.log(`Timeout reached after ${timeout}ms, returning ${relayEvents.length} relay events`);
      if (sub) sub.close();

      // Store all received events in IndexedDB before resolving
      if (relayEvents.length > 0) {
        putEvents(relayEvents).catch(e => console.warn("Failed to cache events", e));
      }

      // Merge cached events with relay events, deduplicate by id
      const mergedEvents = mergeAndDeduplicateEvents(cachedEvents, relayEvents);
      resolve(mergedEvents);
    }, timeout);

    try {
      // Generate a subscription ID for logging
      const subId = Math.random().toString(36).substring(7);

      // Validate filters before sending
      if (!Array.isArray(filters) || filters.length === 0) {
        console.error(`❌ Invalid filters: not an array or empty`, filters);
        resolve(cachedEvents);
        return;
      }

      // Ensure each filter is a valid object
      const validFilters = filters.filter(f => f && typeof f === 'object' && !Array.isArray(f));
      if (validFilters.length !== filters.length) {
        console.warn(`⚠️ Some filters were invalid, filtered ${filters.length} -> ${validFilters.length}`, filters);
      }

      if (validFilters.length === 0) {
        console.error(`❌ No valid filters remaining`);
        resolve(cachedEvents);
        return;
      }

      console.log(`📤 REQ [${subId}] to ${nostrClient.relays.join(', ')}:`, JSON.stringify(["REQ", subId, ...validFilters], null, 2));

      sub = nostrClient.pool.subscribeMany(
        nostrClient.relays,
        validFilters,
        {
          onevent(event) {
            console.log(`📥 EVENT received for REQ [${subId}]:`, {
              id: event.id?.substring(0, 8) + '...',
              kind: event.kind,
              pubkey: event.pubkey?.substring(0, 8) + '...',
              created_at: event.created_at,
              content_preview: event.content?.substring(0, 50)
            });
            relayEvents.push(event);

            // Store event immediately in IndexedDB
            putEvent(event).catch(e => console.warn("Failed to cache event", e));
          },
          oneose() {
            console.log(`✅ EOSE received for REQ [${subId}], got ${relayEvents.length} relay events`);
            clearTimeout(timeoutId);
            if (sub) sub.close();

            // Store all events in IndexedDB before resolving
            if (relayEvents.length > 0) {
              putEvents(relayEvents).catch(e => console.warn("Failed to cache events", e));
            }

            // Merge cached events with relay events, deduplicate by id
            const mergedEvents = mergeAndDeduplicateEvents(cachedEvents, relayEvents);
            console.log(`Merged ${cachedEvents.length} cached + ${relayEvents.length} relay = ${mergedEvents.length} total events`);
            resolve(mergedEvents);
          }
        }
      );
    } catch (error) {
      clearTimeout(timeoutId);
      console.error("Failed to fetch events:", error);
      reject(error);
    }
  });
}

// Fetch all events with timestamp-based pagination (including delete events)
export async function fetchAllEvents(options = {}) {
  const {
    limit = 100,
    since = null,
    until = null,
    authors = null,
    kinds = null,
    ...rest
  } = options;

  const now = Math.floor(Date.now() / 1000);
  const thirtyDaysAgo = now - (30 * 24 * 60 * 60);
  const sixMonthsAgo = now - (180 * 24 * 60 * 60);

  // Start with 30 days if no since specified
  const initialSince = since || thirtyDaysAgo;

  const filters = [{ ...rest }];
  filters[0].since = initialSince;
  if (until) filters[0].until = until;
  if (authors) filters[0].authors = authors;
  if (kinds) filters[0].kinds = kinds;
  if (limit) filters[0].limit = limit;

  let events = await fetchEvents(filters, {
    timeout: 30000
  });

  // If we got few results and weren't already using a longer window, retry with 6 months
  const fewResultsThreshold = Math.min(20, limit / 2);
  if (events.length < fewResultsThreshold && initialSince > sixMonthsAgo && !since) {
    console.log(`[fetchAllEvents] Only got ${events.length} events, retrying with 6-month window...`);
    filters[0].since = sixMonthsAgo;
    events = await fetchEvents(filters, {
      timeout: 30000
    });
    console.log(`[fetchAllEvents] 6-month window returned ${events.length} events`);
  }

  return events;
}

// Fetch user's events with timestamp-based pagination
export async function fetchUserEvents(pubkey, options = {}) {
  const {
    limit = 100,
    since = null,
    until = null
  } = options;

  const filters = [{
    authors: [pubkey]
  }];
  
  if (since) filters[0].since = since;
  if (until) filters[0].until = until;
  if (limit) filters[0].limit = limit;
  
  const events = await fetchEvents(filters, { 
    timeout: 30000 
  });
  
  return events;
}

// NIP-50 search function
export async function searchEvents(searchQuery, options = {}) {
  const {
    limit = 100,
    since = null,
    until = null,
    kinds = null
  } = options;

  const filters = [{
    search: searchQuery
  }];
  
  if (since) filters[0].since = since;
  if (until) filters[0].until = until;
  if (kinds) filters[0].kinds = kinds;
  if (limit) filters[0].limit = limit;
  
  const events = await fetchEvents(filters, { 
    timeout: 30000 
  });
  
  return events;
}

// Fetch a specific event by ID
export async function fetchEventById(eventId, options = {}) {
  const {
    timeout = 10000,
  } = options;

  console.log(`Fetching event by ID: ${eventId}`);

  try {
    const filters = [{
      ids: [eventId]
    }];

    console.log('Fetching event with filters:', filters);

    const events = await fetchEvents(filters, { timeout });

    console.log(`Fetched ${events.length} events`);
    
    // Return the first event if found, null otherwise
    return events.length > 0 ? events[0] : null;
  } catch (error) {
    console.error("Failed to fetch event by ID:", error);
    throw error;
  }
}

// Fetch delete events that target a specific event ID
export async function fetchDeleteEventsByTarget(eventId, options = {}) {
  const {
    timeout = 10000
  } = options;

  console.log(`Fetching delete events for target: ${eventId}`);

  try {
    const filters = [{
      kinds: [5], // Kind 5 is deletion
      '#e': [eventId] // e-tag referencing the target event
    }];

    console.log('Fetching delete events with filters:', filters);

    const events = await fetchEvents(filters, { timeout });

    console.log(`Fetched ${events.length} delete events`);
    
    return events;
  } catch (error) {
    console.error("Failed to fetch delete events:", error);
    throw error;
  }
}

// Initialize client connection
export async function initializeNostrClient() {
  // Refresh relay list to pick up any changes (important for standalone mode)
  nostrClient.refreshRelays();
  await nostrClient.connect();
}

// Query events from cache and relay combined
// This is the main function components should use
export async function queryEvents(filters, options = {}) {
  const {
    timeout = 30000,
    cacheFirst = true, // Try cache first before hitting relay
    cacheOnly = false,  // Only use cache, don't query relay
  } = options;
  
  let cachedEvents = [];
  
  // Try cache first
  if (cacheFirst || cacheOnly) {
    try {
      cachedEvents = await queryEventsFromDB(filters);
      console.log(`Found ${cachedEvents.length} events in cache`);
      
      if (cacheOnly || cachedEvents.length > 0) {
        return cachedEvents;
      }
    } catch (e) {
      console.warn("Failed to query cache", e);
    }
  }
  
  // If cache didn't have results and we're not cache-only, query relay
  if (!cacheOnly) {
    const relayEvents = await fetchEvents(filters, { timeout, useCache: false });
    console.log(`Fetched ${relayEvents.length} events from relay`);
    return relayEvents;
  }
  
  return cachedEvents;
}

// Export cache query function for direct access
export { queryEventsFromDB };

// Clear the IndexedDB cache (call when switching relays)
export async function clearIndexedDBCache() {
  console.log("[nostr] Clearing IndexedDB cache...");
  try {
    const db = await openDB();
    const tx = db.transaction(STORE_EVENTS, "readwrite");
    const store = tx.objectStore(STORE_EVENTS);
    await new Promise((resolve, reject) => {
      const req = store.clear();
      req.onsuccess = () => resolve();
      req.onerror = () => reject(req.error);
    });
    console.log("[nostr] IndexedDB cache cleared");
  } catch (e) {
    console.warn("[nostr] Failed to clear IndexedDB cache", e);
  }
}

// Debug function to check database contents
export async function debugIndexedDB() {
  try {
    const db = await openDB();
    const tx = db.transaction(STORE_EVENTS, "readonly");
    const store = tx.objectStore(STORE_EVENTS);
    
    const allEvents = await new Promise((resolve, reject) => {
      const req = store.getAll();
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
    
    const byKind = allEvents.reduce((acc, e) => {
      acc[e.kind] = (acc[e.kind] || 0) + 1;
      return acc;
    }, {});
    
    console.log("===== IndexedDB Contents =====");
    console.log(`Total events: ${allEvents.length}`);
    console.log("Events by kind:", byKind);
    console.log("Kind 0 events:", allEvents.filter(e => e.kind === 0));
    console.log("All event IDs:", allEvents.map(e => ({ id: e.id.substring(0, 8), kind: e.kind, pubkey: e.pubkey.substring(0, 8) })));
    console.log("==============================");
    
    return {
      total: allEvents.length,
      byKind,
      events: allEvents
    };
  } catch (e) {
    console.error("Failed to debug IndexedDB:", e);
    return null;
  }
}
