import { SimplePool } from 'nostr-tools/pool'

const NOSTR_CONNECT_RELAYS = ['wss://relay.primal.net']
const NOSTR_CONNECT_TIMEOUT_MS = 120_000
const NOSTR_CONNECT_PENDING_KEY = 'l484.nostrconnect.pending'
const NOSTR_CONNECT_PENDING_SESSION_KEY = 'l484.nostrconnect.pending.session'
const SIGNER_SESSION_KEY = 'l484.signer.session'
const HEX_PUBKEY_PATTERN = /^[0-9a-f]{64}$/i
const pool = new SimplePool()

let activeSigner = null

export const clearSigner = () => {
  activeSigner = null
  localStorage.removeItem(SIGNER_SESSION_KEY)
}

export const getActiveSignerPublicKey = async () => {
  if (!activeSigner) return ''
  return activeSigner.getPublicKey()
}

export const loginWithExtension = async () => {
  if (!window.nostr?.getPublicKey) {
    throw new Error('No NIP-07 extension found. Try Alby, nos2x, or Primal extension.')
  }

  const pubkey = await window.nostr.getPublicKey()
  activeSigner = {
    kind: 'extension',
    getPublicKey: async () => pubkey,
    signEvent: (template) => window.nostr.signEvent(template),
  }
  saveSignerSession({ kind: 'extension', pubkey })
  return pubkey
}

export const loginWithRemoteApp = async () => {
  clearPendingRemoteLogin()
  return connectRemoteApp({ openApp: true })
}

export const resumeRemoteAppLogin = () => connectRemoteApp({ openApp: false })

export const hasPendingRemoteAppLogin = () => !!loadPendingRemoteLogin()

export const cancelPendingRemoteAppLogin = () => {
  clearPendingRemoteLogin()
}

export const restoreSavedSigner = async () => {
  if (activeSigner) return true

  const saved = loadSavedSignerSession()
  if (!saved) return false

  if (saved.kind === 'extension') {
    if (!window.nostr?.getPublicKey) return false
    const pubkey = await window.nostr.getPublicKey()
    if (saved.pubkey !== pubkey) {
      clearSigner()
      return false
    }
    activeSigner = {
      kind: 'extension',
      getPublicKey: async () => pubkey,
      signEvent: (template) => window.nostr.signEvent(template),
    }
    return true
  }

  const { NostrConnectSigner, PrivateKeySigner } = await loadNostrConnect()
  const signer = new NostrConnectSigner({
    relays: saved.relays,
    signer: new PrivateKeySigner(new Uint8Array(saved.key)),
    secret: saved.secret,
    remote: saved.remote,
    pubkey: saved.pubkey,
    subscriptionMethod: subscribeToRelays,
    publishMethod: publishToRelays,
  })

  activeSigner = {
    kind: 'remote',
    getPublicKey: async () => saved.pubkey,
    signEvent: (template) => signer.signEvent(template),
  }
  return true
}

const connectRemoteApp = async ({ openApp }) => {
  const { NostrConnectSigner, PrivateKeySigner } = await loadNostrConnect()
  const pending = loadPendingRemoteLogin()
  const clientSigner = pending
    ? new PrivateKeySigner(new Uint8Array(pending.key))
    : new PrivateKeySigner()

  const signer = new NostrConnectSigner({
    relays: pending?.relays ?? NOSTR_CONNECT_RELAYS,
    signer: clientSigner,
    ...(pending ? { secret: pending.secret } : {}),
    subscriptionMethod: subscribeToRelays,
    publishMethod: publishToRelays,
  })

  const permissions = [
    ...(NostrConnectSigner.buildSigningPermissions?.([27235, 4]) ?? ['sign_event:27235', 'sign_event:4']),
    'nip44_encrypt',
    'nip44_decrypt',
  ]

  if (!pending) {
    savePendingRemoteLogin({
      key: Array.from(clientSigner.key),
      secret: signer.secret,
      relays: NOSTR_CONNECT_RELAYS,
    })
  }

  if (openApp) {
    openSignerApp(withCallback(signer.getNostrConnectURI({
      name: 'L484',
      url: window.location.origin,
      permissions,
    })))
  }

  const abort = new AbortController()
  const timeout = window.setTimeout(() => abort.abort(), NOSTR_CONNECT_TIMEOUT_MS)
  try {
    await signer.waitForSigner(abort.signal)
    if (!signer.remote) throw new Error('Remote signer did not complete the connection.')
    const pubkey = await signer.getPublicKey()
    if (!HEX_PUBKEY_PATTERN.test(pubkey)) throw new Error('Remote signer returned an invalid public key.')
    activeSigner = {
      kind: 'remote',
      getPublicKey: async () => pubkey,
      signEvent: (template) => signer.signEvent(template),
    }
    saveSignerSession({
      kind: 'remote',
      key: Array.from(clientSigner.key),
      secret: signer.secret,
      relays: signer.relays,
      remote: signer.remote,
      pubkey,
    })
    clearPendingRemoteLogin()
    return pubkey
  } catch (error) {
    clearPendingRemoteLogin()
    if (error instanceof Error && /aborted/i.test(error.message)) {
      throw new Error('Signer approval timed out. Open signer app again and approve the L484 connection.')
    }
    throw error
  } finally {
    window.clearTimeout(timeout)
    if (!signer.isConnected) await signer.close().catch(() => {})
  }
}

const loadNostrConnect = async () => {
  const mod = await import('applesauce-signers')
  const NostrConnectSigner = mod.NostrConnectSigner ?? mod.default?.NostrConnectSigner
  const PrivateKeySigner = mod.PrivateKeySigner ?? mod.default?.PrivateKeySigner
  if (!NostrConnectSigner || !PrivateKeySigner) {
    throw new Error('Nostr Connect signer support is unavailable.')
  }
  return { NostrConnectSigner, PrivateKeySigner }
}

const withCallback = (uri) => {
  const separator = uri.includes('?') ? '&' : '?'
  return `${uri}${separator}callback=${encodeURIComponent(`${window.location.origin}/auth/nostr-callback`)}`
}

const openSignerApp = (uri) => {
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
  if (isMobile) {
    window.location.href = uri
    return
  }
  const opened = window.open(uri, '_blank', 'noopener,noreferrer')
  if (!opened) window.location.href = uri
}

const loadPendingRemoteLogin = () => {
  try {
    const raw = localStorage.getItem(NOSTR_CONNECT_PENDING_KEY) || sessionStorage.getItem(NOSTR_CONNECT_PENDING_SESSION_KEY)
    const parsed = JSON.parse(raw || 'null')
    if (!Array.isArray(parsed?.key) || !parsed.secret || !Array.isArray(parsed.relays)) return null
    return parsed
  } catch {
    return null
  }
}

const savePendingRemoteLogin = (pending) => {
  const serialized = JSON.stringify({ ...pending, startedAt: Date.now() })
  localStorage.setItem(NOSTR_CONNECT_PENDING_KEY, serialized)
  sessionStorage.setItem(NOSTR_CONNECT_PENDING_SESSION_KEY, serialized)
}

const clearPendingRemoteLogin = () => {
  localStorage.removeItem(NOSTR_CONNECT_PENDING_KEY)
  sessionStorage.removeItem(NOSTR_CONNECT_PENDING_KEY)
  sessionStorage.removeItem(NOSTR_CONNECT_PENDING_SESSION_KEY)
}

const loadSavedSignerSession = () => {
  try {
    const parsed = JSON.parse(localStorage.getItem(SIGNER_SESSION_KEY) || 'null')
    if (parsed?.kind === 'extension' && parsed.pubkey) return parsed
    if (
      parsed?.kind === 'remote' &&
      Array.isArray(parsed.key) &&
      parsed.secret &&
      Array.isArray(parsed.relays) &&
      parsed.remote &&
      parsed.pubkey
    ) {
      return parsed
    }
    return null
  } catch {
    return null
  }
}

const saveSignerSession = (session) => {
  localStorage.setItem(SIGNER_SESSION_KEY, JSON.stringify(session))
}

const subscribeToRelays = (relays, filters) => ({
  [Symbol.asyncIterator]() {
    const queue = []
    let wake = null
    let closed = false
    const sub = pool.subscribeMany(relays, filters, {
      onevent(event) {
        queue.push(event)
        wake?.()
      },
      onclose(reasons) {
        for (const reason of reasons) queue.push(reason)
        closed = true
        wake?.()
      },
    })

    return {
      async next() {
        while (!queue.length && !closed) {
          await new Promise((resolve) => {
            wake = resolve
          })
          wake = null
        }
        const value = queue.shift()
        if (value) return { value, done: false }
        return { value: undefined, done: true }
      },
      async return() {
        closed = true
        sub.close()
        wake?.()
        return { value: undefined, done: true }
      },
    }
  },
})

const publishToRelays = async (relays, event) => {
  const results = await Promise.allSettled(pool.publish(relays, event))
  if (!results.some((result) => result.status === 'fulfilled')) {
    throw new Error('Could not publish Nostr Connect request to relay.')
  }
}
