/**
 * DM Service - Direct Message handling with NIP-04 and NIP-17 encryption support
 *
 * NIP-04: Kind 4 encrypted direct messages (legacy)
 * NIP-17: Kind 14 private direct messages with NIP-59 gift wrapping (modern)
 */

import { ARCHIVE_RELAY_URL, BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { TConversation, TDirectMessage, TDMDeletedState, TDMEncryptionType, TDraftEvent } from '@/types'
import { Event, kinds, VerifiedEvent } from 'nostr-tools'
import client from './client.service'
import indexedDb from './indexed-db.service'

// In-memory plaintext cache for fast access (avoids async IndexedDB lookups on re-render)
const plaintextCache = new Map<string, string>()
const MAX_CACHE_SIZE = 1000

/**
 * Get plaintext from in-memory cache
 */
export function getCachedPlaintext(eventId: string): string | undefined {
  return plaintextCache.get(eventId)
}

/**
 * Set plaintext in in-memory cache (with LRU eviction)
 */
export function setCachedPlaintext(eventId: string, plaintext: string): void {
  // Simple LRU: if cache is full, delete oldest entries
  if (plaintextCache.size >= MAX_CACHE_SIZE) {
    const keysToDelete = Array.from(plaintextCache.keys()).slice(0, 100)
    keysToDelete.forEach(k => plaintextCache.delete(k))
  }
  plaintextCache.set(eventId, plaintext)
}

/**
 * Clear the plaintext cache (e.g., on logout)
 */
export function clearPlaintextCache(): void {
  plaintextCache.clear()
}

/**
 * Decrypt messages in batches to avoid blocking the UI
 * Yields control back to the event loop between batches
 */
export async function decryptMessagesInBatches(
  events: Event[],
  encryption: IDMEncryption,
  myPubkey: string,
  batchSize: number = 10,
  onBatchComplete?: (messages: TDirectMessage[], progress: number) => void
): Promise<TDirectMessage[]> {
  const allMessages: TDirectMessage[] = []
  const total = events.length

  for (let i = 0; i < events.length; i += batchSize) {
    const batch = events.slice(i, i + batchSize)

    // Process batch
    const batchResults = await Promise.all(
      batch.map((event) => dmService.decryptMessage(event, encryption, myPubkey))
    )

    const validMessages = batchResults.filter((m): m is TDirectMessage => m !== null)
    allMessages.push(...validMessages)

    // Report progress
    const progress = Math.min((i + batchSize) / total, 1)
    onBatchComplete?.(validMessages, progress)

    // Yield to event loop between batches (prevents UI blocking)
    if (i + batchSize < events.length) {
      await new Promise(resolve => setTimeout(resolve, 0))
    }
  }

  return allMessages
}

/**
 * Create and publish a kind 5 delete request for own messages
 * This requests relays to delete the original event
 */
export async function publishDeleteRequest(
  eventIds: string[],
  eventKind: number,
  encryption: IDMEncryption,
  relayUrls: string[]
): Promise<void> {
  if (eventIds.length === 0) return

  const draftEvent: TDraftEvent = {
    kind: kinds.EventDeletion, // 5
    created_at: Math.floor(Date.now() / 1000),
    content: 'Deleted by sender',
    tags: [
      ['k', eventKind.toString()],
      ...eventIds.map((id) => ['e', id])
    ]
  }

  const signedEvent = await encryption.signEvent(draftEvent)
  await client.publishEvent(relayUrls, signedEvent)
}

/**
 * Encryption methods interface for DM operations
 */
export interface IDMEncryption {
  nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
  nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
  nip44Encrypt?: (pubkey: string, plainText: string) => Promise<string>
  nip44Decrypt?: (pubkey: string, cipherText: string) => Promise<string>
  signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
  getPublicKey: () => string
}

// NIP-04 uses kind 4
const KIND_ENCRYPTED_DM = kinds.EncryptedDirectMessage // 4

// NIP-17 uses kind 14 for chat messages, wrapped in gift wraps
const KIND_PRIVATE_DM = ExtendedKind.PRIVATE_DM // 14
const KIND_SEAL = ExtendedKind.SEAL // 13
const KIND_GIFT_WRAP = ExtendedKind.GIFT_WRAP // 1059
const KIND_REACTION = kinds.Reaction // 7

// 15 second timeout for DM fetches - if relays are dead, don't wait forever
const DM_FETCH_TIMEOUT_MS = 15000

/**
 * Wrap a promise with a timeout that returns empty array on timeout or error
 */
function withTimeout<T>(promise: Promise<T[]>, ms: number): Promise<T[]> {
  const timeoutPromise = new Promise<T[]>((resolve) => {
    setTimeout(() => resolve([]), ms)
  })
  const safePromise = promise.catch(() => [] as T[])
  return Promise.race([safePromise, timeoutPromise])
}

class DMService {
  /**
   * Fetch all DM events for a user from relays
   */
  async fetchDMEvents(pubkey: string, relayUrls: string[], limit = 500): Promise<Event[]> {
    const allRelays = [...new Set([...relayUrls, ...BIG_RELAY_URLS])]

    // Fetch NIP-04 DMs (kind 4) and NIP-17 gift wraps in parallel
    const nip04Filter = {
      kinds: [KIND_ENCRYPTED_DM],
      limit
    }

    const [incomingNip04, outgoingNip04, giftWraps] = await Promise.all([
      // Fetch messages sent TO the user
      withTimeout(
        client.fetchEvents(allRelays, {
          ...nip04Filter,
          '#p': [pubkey]
        }),
        DM_FETCH_TIMEOUT_MS
      ),
      // Fetch messages sent BY the user
      withTimeout(
        client.fetchEvents(allRelays, {
          ...nip04Filter,
          authors: [pubkey]
        }),
        DM_FETCH_TIMEOUT_MS
      ),
      // Fetch NIP-17 gift wraps (kind 1059) - these are addressed to the user
      withTimeout(
        client.fetchEvents(allRelays, {
          kinds: [KIND_GIFT_WRAP],
          '#p': [pubkey],
          limit
        }),
        DM_FETCH_TIMEOUT_MS
      )
    ])

    // Combine all events
    const allEvents = [...incomingNip04, ...outgoingNip04, ...giftWraps]

    // Store in IndexedDB for caching
    await Promise.all(allEvents.map((event) => indexedDb.putDMEvent(event)))

    return allEvents
  }

  /**
   * Fetch recent DM events (limited) for building conversation list
   * Returns only most recent events to quickly show conversations
   */
  async fetchRecentDMEvents(pubkey: string, relayUrls: string[]): Promise<Event[]> {
    // Fetch with smaller limit for faster initial load
    return this.fetchDMEvents(pubkey, relayUrls, 100)
  }

  /**
   * Fetch all DM events for a specific conversation partner
   */
  async fetchConversationEvents(
    pubkey: string,
    partnerPubkey: string,
    relayUrls: string[]
  ): Promise<Event[]> {
    const allRelays = [...new Set([...relayUrls, ...BIG_RELAY_URLS])]

    // Get partner's inbox relays for better NIP-17 discovery
    const partnerInboxRelays = await this.fetchPartnerInboxRelays(partnerPubkey)
    const inboxRelays = [...new Set([...relayUrls, ...partnerInboxRelays, ...BIG_RELAY_URLS])]

    // Fetch NIP-04 messages between user and partner (with timeout)
    const [incomingNip04, outgoingNip04, giftWraps] = await Promise.all([
      // Messages FROM partner TO user
      withTimeout(
        client.fetchEvents(allRelays, {
          kinds: [KIND_ENCRYPTED_DM],
          authors: [partnerPubkey],
          '#p': [pubkey],
          limit: 500
        }),
        DM_FETCH_TIMEOUT_MS
      ),
      // Messages FROM user TO partner
      withTimeout(
        client.fetchEvents(allRelays, {
          kinds: [KIND_ENCRYPTED_DM],
          authors: [pubkey],
          '#p': [partnerPubkey],
          limit: 500
        }),
        DM_FETCH_TIMEOUT_MS
      ),
      // Gift wraps addressed to user - check both regular relays and inbox relays
      withTimeout(
        client.fetchEvents(inboxRelays, {
          kinds: [KIND_GIFT_WRAP],
          '#p': [pubkey],
          limit: 500
        }),
        DM_FETCH_TIMEOUT_MS
      )
    ])

    const allEvents = [...incomingNip04, ...outgoingNip04, ...giftWraps]

    // Store in IndexedDB for caching
    await Promise.all(allEvents.map((event) => indexedDb.putDMEvent(event)))

    return allEvents
  }

  /**
   * Decrypt a DM event and return a TDirectMessage
   */
  async decryptMessage(
    event: Event,
    encryption: IDMEncryption,
    myPubkey: string
  ): Promise<TDirectMessage | null> {
    try {
      if (event.kind === KIND_ENCRYPTED_DM) {
        // NIP-04 decryption - check in-memory cache first (fastest)
        const memCached = getCachedPlaintext(event.id)
        if (memCached) {
          return this.buildDirectMessage(event, memCached, myPubkey, 'nip04')
        }

        // Check IndexedDB cache (slower but persistent)
        const dbCached = await indexedDb.getDecryptedContent(event.id)
        if (dbCached) {
          // Populate in-memory cache for next access
          setCachedPlaintext(event.id, dbCached)
          return this.buildDirectMessage(event, dbCached, myPubkey, 'nip04')
        }

        const otherPubkey = this.getOtherPartyPubkey(event, myPubkey)
        if (!otherPubkey) return null

        const decryptedContent = await encryption.nip04Decrypt(otherPubkey, event.content)

        // Cache in both layers
        setCachedPlaintext(event.id, decryptedContent)
        indexedDb.putDecryptedContent(event.id, decryptedContent).catch(() => {})

        return this.buildDirectMessage(event, decryptedContent, myPubkey, 'nip04')
      } else if (event.kind === KIND_GIFT_WRAP) {
        // NIP-17 - check in-memory cache first
        const memCached = getCachedPlaintext(event.id)
        if (memCached) {
          // We stored "pubkey|recipient|content" format in memory for NIP-17
          const parts = memCached.split('|', 3)
          if (parts.length === 3) {
            const [senderPubkey, recipientPubkey, content] = parts
            if (recipientPubkey === '__reaction__') return null
            const seenOnRelays = client.getSeenEventRelayUrls(event.id)
            return {
              id: event.id,
              senderPubkey,
              recipientPubkey,
              content,
              createdAt: event.created_at,
              encryptionType: 'nip17',
              event,
              decryptedContent: content,
              seenOnRelays: seenOnRelays.length > 0 ? seenOnRelays : undefined
            }
          }
        }

        // Check IndexedDB cache (includes sender info)
        const cachedUnwrapped = await indexedDb.getUnwrappedGiftWrap(event.id)
        if (cachedUnwrapped) {
          // Skip reactions in cache for now (they're stored but not returned as messages)
          if (cachedUnwrapped.recipientPubkey === '__reaction__') {
            return null
          }
          // Populate in-memory cache
          setCachedPlaintext(event.id, `${cachedUnwrapped.pubkey}|${cachedUnwrapped.recipientPubkey}|${cachedUnwrapped.content}`)
          const seenOnRelays = client.getSeenEventRelayUrls(event.id)
          return {
            id: event.id,
            senderPubkey: cachedUnwrapped.pubkey,
            recipientPubkey: cachedUnwrapped.recipientPubkey,
            content: cachedUnwrapped.content,
            createdAt: cachedUnwrapped.createdAt,
            encryptionType: 'nip17',
            event,
            decryptedContent: cachedUnwrapped.content,
            seenOnRelays: seenOnRelays.length > 0 ? seenOnRelays : undefined
          }
        }

        // Decrypt (unwrap gift wrap -> unseal -> decrypt)
        const unwrapped = await this.unwrapGiftWrap(event, encryption)
        if (!unwrapped) return null

        const innerEvent = unwrapped.innerEvent

        // Handle reactions - cache them but don't return as messages
        if (unwrapped.type === 'reaction') {
          // Cache the reaction for later display
          // TODO: Store reaction separately and associate with target message via 'e' tag
          indexedDb
            .putUnwrappedGiftWrap(event.id, {
              pubkey: innerEvent.pubkey,
              recipientPubkey: '__reaction__', // Marker for reactions
              content: unwrapped.content, // The emoji
              createdAt: innerEvent.created_at
            })
            .catch(() => {})
          // For now, just skip reactions (they're cached for future use)
          return null
        }

        const recipientPubkey = this.getRecipientFromTags(innerEvent.tags) || myPubkey

        // Cache in both layers
        setCachedPlaintext(event.id, `${innerEvent.pubkey}|${recipientPubkey}|${unwrapped.content}`)
        indexedDb
          .putUnwrappedGiftWrap(event.id, {
            pubkey: innerEvent.pubkey,
            recipientPubkey,
            content: unwrapped.content,
            createdAt: innerEvent.created_at
          })
          .catch(() => {})

        const seenOnRelays = client.getSeenEventRelayUrls(event.id)
        return {
          id: event.id,
          senderPubkey: innerEvent.pubkey,
          recipientPubkey,
          content: unwrapped.content,
          createdAt: innerEvent.created_at,
          encryptionType: 'nip17',
          event,
          decryptedContent: unwrapped.content,
          seenOnRelays: seenOnRelays.length > 0 ? seenOnRelays : undefined
        }
      } else {
        return null
      }
    } catch {
      return null
    }
  }

  /**
   * Unwrap a NIP-59 gift wrap to get the inner message or reaction
   */
  private async unwrapGiftWrap(
    giftWrap: Event,
    encryption: IDMEncryption
  ): Promise<{ content: string; innerEvent: Event; type: 'dm' | 'reaction' } | null> {
    try {
      // Step 1: Decrypt the gift wrap content using NIP-44
      if (!encryption.nip44Decrypt) {
        return null
      }

      const sealJson = await encryption.nip44Decrypt(giftWrap.pubkey, giftWrap.content)
      const seal = JSON.parse(sealJson) as Event

      if (seal.kind !== KIND_SEAL) {
        return null
      }

      // Step 2: Decrypt the seal content using NIP-44
      const innerEventJson = await encryption.nip44Decrypt(seal.pubkey, seal.content)
      const innerEvent = JSON.parse(innerEventJson) as Event

      if (innerEvent.kind === KIND_PRIVATE_DM) {
        return {
          content: innerEvent.content,
          innerEvent,
          type: 'dm'
        }
      } else if (innerEvent.kind === KIND_REACTION) {
        return {
          content: innerEvent.content, // The emoji
          innerEvent,
          type: 'reaction'
        }
      } else {
        // Silently ignore other event types (e.g., read receipts)
        return null
      }
    } catch {
      return null
    }
  }

  /**
   * Build a TDirectMessage from an event
   */
  private buildDirectMessage(
    event: Event,
    decryptedContent: string,
    myPubkey: string,
    encryptionType: TDMEncryptionType = 'nip04'
  ): TDirectMessage {
    const recipient = this.getRecipientFromTags(event.tags)
    const isSender = event.pubkey === myPubkey
    const seenOnRelays = client.getSeenEventRelayUrls(event.id)

    return {
      id: event.id,
      senderPubkey: event.pubkey,
      recipientPubkey: recipient || (isSender ? '' : myPubkey),
      content: decryptedContent,
      createdAt: event.created_at,
      encryptionType,
      event,
      decryptedContent,
      seenOnRelays: seenOnRelays.length > 0 ? seenOnRelays : undefined
    }
  }

  /**
   * Send a DM to a recipient
   * When no existing conversation, sends in BOTH formats (NIP-04 and NIP-17)
   */
  async sendDM(
    recipientPubkey: string,
    content: string,
    encryption: IDMEncryption,
    relayUrls: string[],
    _preferNip44: boolean,
    existingEncryption: TDMEncryptionType | null
  ): Promise<Event[]> {
    const sentEvents: Event[] = []

    // Get recipient's relays for better delivery
    // Use inbox relays for NIP-17 (where recipient receives messages)
    // Use write relays for NIP-04 (where recipient publishes from)
    const [recipientInboxRelays, recipientWriteRelays] = await Promise.all([
      this.fetchPartnerInboxRelays(recipientPubkey),
      this.fetchPartnerRelays(recipientPubkey)
    ])
    const allRelays = [...new Set([...relayUrls, ...recipientWriteRelays])]
    const inboxRelays = [...new Set([...relayUrls, ...recipientInboxRelays])]

    if (existingEncryption === null) {
      // No existing conversation - send in BOTH formats
      try {
        const nip04Event = await this.createAndPublishNip04DM(
          recipientPubkey,
          content,
          encryption,
          allRelays
        )
        sentEvents.push(nip04Event)
      } catch (error) {
        console.error('Failed to send NIP-04 DM:', error)
      }

      try {
        if (encryption.nip44Encrypt) {
          // Use inbox relays for NIP-17 delivery
          const nip17Event = await this.createAndPublishNip17DM(
            recipientPubkey,
            content,
            encryption,
            inboxRelays
          )
          sentEvents.push(nip17Event)
        }
      } catch (error) {
        console.error('Failed to send NIP-17 DM:', error)
      }
    } else if (existingEncryption === 'nip04') {
      // Match existing NIP-04 encryption
      try {
        const nip04Event = await this.createAndPublishNip04DM(
          recipientPubkey,
          content,
          encryption,
          allRelays
        )
        sentEvents.push(nip04Event)
      } catch (error) {
        console.error('Failed to send NIP-04 DM:', error)
        throw error // Re-throw so caller knows it failed
      }
    } else if (existingEncryption === 'nip17') {
      // Match existing NIP-17 encryption - use inbox relays
      if (!encryption.nip44Encrypt) {
        throw new Error('Encryption does not support NIP-44')
      }
      try {
        const nip17Event = await this.createAndPublishNip17DM(
          recipientPubkey,
          content,
          encryption,
          inboxRelays
        )
        sentEvents.push(nip17Event)
      } catch (error) {
        console.error('Failed to send NIP-17 DM:', error)
        throw error // Re-throw so caller knows it failed
      }
    }

    return sentEvents
  }

  /**
   * Create and publish a NIP-04 DM (kind 4)
   */
  private async createAndPublishNip04DM(
    recipientPubkey: string,
    content: string,
    encryption: IDMEncryption,
    relayUrls: string[]
  ): Promise<VerifiedEvent> {
    const encryptedContent = await encryption.nip04Encrypt(recipientPubkey, content)

    const draftEvent: TDraftEvent = {
      kind: KIND_ENCRYPTED_DM,
      created_at: Math.floor(Date.now() / 1000),
      content: encryptedContent,
      tags: [['p', recipientPubkey]]
    }

    const signedEvent = await encryption.signEvent(draftEvent)
    await client.publishEvent(relayUrls, signedEvent)
    await indexedDb.putDMEvent(signedEvent)
    await indexedDb.putDecryptedContent(signedEvent.id, content)

    return signedEvent
  }

  /**
   * Create and publish a NIP-17 DM with gift wrapping (kind 14 -> 13 -> 1059)
   */
  private async createAndPublishNip17DM(
    recipientPubkey: string,
    content: string,
    encryption: IDMEncryption,
    relayUrls: string[]
  ): Promise<VerifiedEvent> {
    if (!encryption.nip44Encrypt) {
      throw new Error('Encryption does not support NIP-44')
    }

    // Note: senderPubkey is determined by the signer when signing the event

    // Step 1: Create the inner chat message (kind 14)
    const chatMessage: TDraftEvent = {
      kind: KIND_PRIVATE_DM,
      created_at: Math.floor(Date.now() / 1000),
      content,
      tags: [['p', recipientPubkey]]
    }

    // Step 2: Sign the chat message
    const signedChat = await encryption.signEvent(chatMessage)

    // Step 3: Create a seal (kind 13) containing the encrypted chat message
    const sealContent = await encryption.nip44Encrypt(recipientPubkey, JSON.stringify(signedChat))
    const seal: TDraftEvent = {
      kind: KIND_SEAL,
      created_at: this.randomizeTimestamp(signedChat.created_at),
      content: sealContent,
      tags: []
    }
    const signedSeal = await encryption.signEvent(seal)

    // Step 4: Create a gift wrap (kind 1059) with random sender key
    // For simplicity, we'll use the same encryption but in production you'd use a random key
    const giftWrapContent = await encryption.nip44Encrypt(recipientPubkey, JSON.stringify(signedSeal))
    const giftWrap: TDraftEvent = {
      kind: KIND_GIFT_WRAP,
      created_at: this.randomizeTimestamp(signedSeal.created_at),
      content: giftWrapContent,
      tags: [['p', recipientPubkey]]
    }
    const signedGiftWrap = await encryption.signEvent(giftWrap)

    // Publish the gift wrap
    await client.publishEvent(relayUrls, signedGiftWrap)
    await indexedDb.putDMEvent(signedGiftWrap)
    await indexedDb.putDecryptedContent(signedGiftWrap.id, content)

    return signedGiftWrap
  }

  /**
   * Randomize timestamp for privacy (NIP-59)
   */
  private randomizeTimestamp(baseTime: number): number {
    // Add random offset between -2 days and +2 days
    const offset = Math.floor(Math.random() * 4 * 24 * 60 * 60) - 2 * 24 * 60 * 60
    return baseTime + offset
  }

  /**
   * Fetch partner's write relays for better DM delivery
   */
  async fetchPartnerRelays(pubkey: string): Promise<string[]> {
    try {
      // Try to get relay list from IndexedDB first
      const cachedEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.RelayList)
      if (cachedEvent) {
        return this.parseWriteRelays(cachedEvent)
      }

      // Fetch from relays
      const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
        kinds: [kinds.RelayList],
        authors: [pubkey],
        limit: 1
      })

      if (relayListEvents.length > 0) {
        const event = relayListEvents[0]
        await indexedDb.putReplaceableEvent(event)
        return this.parseWriteRelays(event)
      }

      // Fallback to archive relay
      return [ARCHIVE_RELAY_URL]
    } catch {
      return [ARCHIVE_RELAY_URL]
    }
  }

  /**
   * Fetch partner's inbox (read) relays for NIP-17 DM delivery
   * NIP-65: Inbox relays are where a user receives messages
   */
  async fetchPartnerInboxRelays(pubkey: string): Promise<string[]> {
    try {
      // Try to get relay list from IndexedDB first
      const cachedEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.RelayList)
      if (cachedEvent) {
        return this.parseInboxRelays(cachedEvent)
      }

      // Fetch from relays
      const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
        kinds: [kinds.RelayList],
        authors: [pubkey],
        limit: 1
      })

      if (relayListEvents.length > 0) {
        const event = relayListEvents[0]
        await indexedDb.putReplaceableEvent(event)
        return this.parseInboxRelays(event)
      }

      // Fallback to big relays
      return BIG_RELAY_URLS
    } catch {
      return BIG_RELAY_URLS
    }
  }

  /**
   * Parse write (outbox) relays from kind 10002 event
   */
  private parseWriteRelays(event: Event): string[] {
    const writeRelays: string[] = []

    for (const tag of event.tags) {
      if (tag[0] === 'r') {
        const url = tag[1]
        const scope = tag[2]
        // Include if it's a write relay or has no scope (both)
        if (!scope || scope === 'write') {
          writeRelays.push(url)
        }
      }
    }

    return writeRelays.length > 0 ? writeRelays : [ARCHIVE_RELAY_URL]
  }

  /**
   * Parse inbox (read) relays from kind 10002 event
   * These are where the user receives DMs
   */
  private parseInboxRelays(event: Event): string[] {
    const inboxRelays: string[] = []

    for (const tag of event.tags) {
      if (tag[0] === 'r') {
        const url = tag[1]
        const scope = tag[2]
        // Include if it's a read relay or has no scope (both)
        if (!scope || scope === 'read') {
          inboxRelays.push(url)
        }
      }
    }

    return inboxRelays.length > 0 ? inboxRelays : BIG_RELAY_URLS
  }

  /**
   * Check other relays for an event and return which ones have it
   */
  async checkOtherRelaysForEvent(
    eventId: string,
    knownRelays: string[]
  ): Promise<string[]> {
    const knownSet = new Set(knownRelays.map((r) => r.replace(/\/$/, '')))
    const relaysToCheck = BIG_RELAY_URLS.filter(
      (url) => !knownSet.has(url.replace(/\/$/, ''))
    )

    const foundOnRelays: string[] = []

    // Check each relay individually
    await Promise.all(
      relaysToCheck.map(async (relayUrl) => {
        try {
          const events = await client.fetchEvents([relayUrl], {
            ids: [eventId],
            limit: 1
          })
          if (events.length > 0) {
            foundOnRelays.push(relayUrl)
            // Track the event as seen on this relay
            client.trackEventSeenOn(eventId, { url: relayUrl } as any)
          }
        } catch {
          // Relay unreachable, ignore
        }
      })
    )

    return foundOnRelays
  }

  /**
   * Group messages into conversations
   */
  groupMessagesIntoConversations(
    messages: TDirectMessage[],
    myPubkey: string
  ): Map<string, TConversation> {
    const conversations = new Map<string, TConversation>()

    for (const message of messages) {
      const partnerPubkey =
        message.senderPubkey === myPubkey ? message.recipientPubkey : message.senderPubkey

      if (!partnerPubkey) continue

      const existing = conversations.get(partnerPubkey)
      if (!existing || message.createdAt > existing.lastMessageAt) {
        conversations.set(partnerPubkey, {
          partnerPubkey,
          lastMessageAt: message.createdAt,
          lastMessagePreview: message.content.substring(0, 100),
          unreadCount: 0,
          preferredEncryption: message.encryptionType
        })
      }
    }

    return conversations
  }

  /**
   * Build conversation list from raw events WITHOUT decryption (fast)
   * Only works for NIP-04 events - NIP-17 gift wraps need decryption
   */
  groupEventsIntoConversations(events: Event[], myPubkey: string): Map<string, TConversation> {
    const conversations = new Map<string, TConversation>()

    for (const event of events) {
      // Only process NIP-04 events (kind 4) - we can get metadata without decryption
      if (event.kind !== KIND_ENCRYPTED_DM) continue

      const recipient = this.getRecipientFromTags(event.tags)
      const partnerPubkey = event.pubkey === myPubkey ? recipient : event.pubkey

      if (!partnerPubkey) continue

      const existing = conversations.get(partnerPubkey)
      if (!existing || event.created_at > existing.lastMessageAt) {
        conversations.set(partnerPubkey, {
          partnerPubkey,
          lastMessageAt: event.created_at,
          lastMessagePreview: '', // Skip preview for speed - will be filled on conversation open
          unreadCount: 0,
          preferredEncryption: 'nip04'
        })
      }
    }

    return conversations
  }

  /**
   * Get messages for a specific conversation
   */
  getMessagesForConversation(
    messages: TDirectMessage[],
    partnerPubkey: string,
    myPubkey: string
  ): TDirectMessage[] {
    return messages
      .filter(
        (m) =>
          (m.senderPubkey === partnerPubkey && m.recipientPubkey === myPubkey) ||
          (m.senderPubkey === myPubkey && m.recipientPubkey === partnerPubkey)
      )
      .sort((a, b) => a.createdAt - b.createdAt)
  }

  /**
   * Get the other party's pubkey from a DM event
   */
  private getOtherPartyPubkey(event: Event, myPubkey: string): string | null {
    if (event.pubkey === myPubkey) {
      // I'm the sender, get recipient from tags
      return this.getRecipientFromTags(event.tags)
    } else {
      // I'm the recipient, sender is the pubkey
      return event.pubkey
    }
  }

  /**
   * Get recipient pubkey from event tags
   */
  private getRecipientFromTags(tags: string[][]): string | null {
    const pTag = tags.find((t) => t[0] === 'p')
    return pTag ? pTag[1] : null
  }

  /**
   * Subscribe to incoming DMs in real-time
   * Returns a close function to stop the subscription
   */
  subscribeToDMs(
    pubkey: string,
    relayUrls: string[],
    onEvent: (event: Event) => void
  ): { close: () => void } {
    const allRelays = [...new Set([...relayUrls, ...BIG_RELAY_URLS])]
    const since = Math.floor(Date.now() / 1000) - 60 // Start from 1 minute ago to catch recent

    // Subscribe to NIP-04 DMs (kind 4) addressed to user
    const nip04Sub = client.subscribe(
      allRelays,
      [
        { kinds: [KIND_ENCRYPTED_DM], '#p': [pubkey], since },
        { kinds: [KIND_ENCRYPTED_DM], authors: [pubkey], since }
      ],
      {
        onevent: (event) => {
          indexedDb.putDMEvent(event).catch(() => {})
          onEvent(event)
        }
      }
    )

    // Subscribe to NIP-17 gift wraps (kind 1059) addressed to user
    const giftWrapSub = client.subscribe(
      allRelays,
      { kinds: [KIND_GIFT_WRAP], '#p': [pubkey], since },
      {
        onevent: (event) => {
          indexedDb.putDMEvent(event).catch(() => {})
          onEvent(event)
        }
      }
    )

    return {
      close: async () => {
        const [nip04, giftWrap] = await Promise.all([nip04Sub, giftWrapSub])
        nip04.close()
        giftWrap.close()
      }
    }
  }
}

const dmService = new DMService()
export default dmService

/**
 * Check if a message should be treated as deleted based on the deleted state
 * @param messageId - The event ID of the message
 * @param partnerPubkey - The conversation partner's pubkey
 * @param timestamp - The message timestamp (created_at)
 * @param deletedState - The user's deleted messages state
 * @returns true if the message should be hidden
 */
export function isMessageDeleted(
  messageId: string,
  partnerPubkey: string,
  timestamp: number,
  deletedState: TDMDeletedState | null
): boolean {
  if (!deletedState) return false

  // Check if message ID is explicitly deleted
  if (deletedState.deletedIds.includes(messageId)) {
    return true
  }

  // Check if timestamp falls within any deleted range for this conversation
  const ranges = deletedState.deletedRanges[partnerPubkey]
  if (ranges) {
    for (const range of ranges) {
      if (timestamp >= range.start && timestamp <= range.end) {
        return true
      }
    }
  }

  return false
}

/**
 * Check if a conversation should be hidden based on its last message timestamp
 * A conversation is deleted if its lastMessageAt falls within any deleted range
 * @param partnerPubkey - The conversation partner's pubkey
 * @param lastMessageAt - The timestamp of the last message in the conversation
 * @param deletedState - The user's deleted messages state
 * @returns true if the conversation should be hidden
 */
export function isConversationDeleted(
  partnerPubkey: string,
  lastMessageAt: number,
  deletedState: TDMDeletedState | null
): boolean {
  if (!deletedState) return false

  const ranges = deletedState.deletedRanges[partnerPubkey]
  if (!ranges || ranges.length === 0) return false

  // Check if lastMessageAt falls within any deleted range
  for (const range of ranges) {
    if (lastMessageAt >= range.start && lastMessageAt <= range.end) {
      return true
    }
  }

  return false
}
