/**
 * NIP-46 Bunker Signer with Cashu Token Authentication
 *
 * Implements remote signing via NIP-46 protocol with Cashu access tokens
 * for authorization. The signer connects to a bunker WebSocket and
 * requests signing operations.
 *
 * Token flow:
 * 1. Connect to bunker with X-Cashu-Token header
 * 2. Send NIP-46 requests encrypted with NIP-04
 * 3. Receive signed events from bunker
 */

import { ISigner, TDraftEvent } from '@/types'
import cashuTokenService, { TCashuToken, TokenScope } from '@/services/cashu-token.service'
import * as utils from '@noble/curves/abstract/utils'
import { secp256k1 } from '@noble/curves/secp256k1'
import { Event, VerifiedEvent, getPublicKey as nGetPublicKey, nip04, finalizeEvent } from 'nostr-tools'

// NIP-46 methods
const NIP46_METHOD = {
  CONNECT: 'connect',
  GET_PUBLIC_KEY: 'get_public_key',
  SIGN_EVENT: 'sign_event',
  NIP04_ENCRYPT: 'nip04_encrypt',
  NIP04_DECRYPT: 'nip04_decrypt',
  PING: 'ping'
} as const

type NIP46Method = (typeof NIP46_METHOD)[keyof typeof NIP46_METHOD]

// NIP-46 request format
interface NIP46Request {
  id: string
  method: NIP46Method
  params: string[]
}

// NIP-46 response format
interface NIP46Response {
  id: string
  result?: string
  error?: string
}

// Pending request tracker
interface PendingRequest {
  resolve: (value: string) => void
  reject: (error: Error) => void
  timeout: ReturnType<typeof setTimeout>
}

/**
 * Generate a random request ID.
 */
function generateRequestId(): string {
  const bytes = crypto.getRandomValues(new Uint8Array(16))
  return utils.bytesToHex(bytes)
}

/**
 * Parse a bunker URL (bunker://<pubkey>?relay=<url>&secret=<secret>&cat=<token>).
 */
export function parseBunkerUrl(url: string): {
  pubkey: string
  relays: string[]
  secret?: string
  catToken?: string
} {
  if (!url.startsWith('bunker://')) {
    throw new Error('Invalid bunker URL: must start with bunker://')
  }

  const withoutPrefix = url.slice('bunker://'.length)
  const [pubkeyPart, queryPart] = withoutPrefix.split('?')

  if (!pubkeyPart || pubkeyPart.length !== 64) {
    throw new Error('Invalid bunker URL: missing or invalid pubkey')
  }

  const params = new URLSearchParams(queryPart || '')
  const relays = params.getAll('relay')
  const secret = params.get('secret') || undefined
  const catToken = params.get('cat') || undefined

  if (relays.length === 0) {
    throw new Error('Invalid bunker URL: no relay specified')
  }

  return {
    pubkey: pubkeyPart,
    relays,
    secret,
    catToken
  }
}

/**
 * Parse a nostr+connect URL (nostr+connect://<relay-url>?pubkey=<client-pubkey>&secret=<secret>).
 * This is the format that signers (like Amber) scan to connect to a client.
 */
export function parseNostrConnectUrl(url: string): {
  relay: string
  pubkey?: string
  secret?: string
} {
  if (!url.startsWith('nostr+connect://')) {
    throw new Error('Invalid nostr+connect URL: must start with nostr+connect://')
  }

  const withoutPrefix = url.slice('nostr+connect://'.length)
  const [relayPart, queryPart] = withoutPrefix.split('?')

  if (!relayPart) {
    throw new Error('Invalid nostr+connect URL: missing relay')
  }

  const params = new URLSearchParams(queryPart || '')
  const pubkey = params.get('pubkey') || undefined
  const secret = params.get('secret') || undefined

  return {
    relay: relayPart,
    pubkey,
    secret
  }
}

/**
 * Build a nostr+connect URL for signers to scan.
 * @param relay - The relay URL (without ws:// prefix, will be added)
 * @param pubkey - The client's ephemeral pubkey for this session
 * @param secret - Optional secret for the handshake
 */
export function buildNostrConnectUrl(relay: string, pubkey: string, secret?: string): string {
  // Ensure relay URL uses the relay host without protocol
  let relayHost = relay
    .replace('wss://', '')
    .replace('ws://', '')
    .replace('https://', '')
    .replace('http://', '')
    .replace(/\/$/, '')

  const params = new URLSearchParams()
  params.set('pubkey', pubkey)
  if (secret) {
    params.set('secret', secret)
  }
  return `nostr+connect://${relayHost}?${params.toString()}`
}

/**
 * Build a bunker URL from components.
 */
export function buildBunkerUrl(pubkey: string, relays: string[], secret?: string): string {
  const params = new URLSearchParams()
  relays.forEach((relay) => params.append('relay', relay))
  if (secret) {
    params.set('secret', secret)
  }
  return `bunker://${pubkey}?${params.toString()}`
}

export class BunkerSigner implements ISigner {
  private bunkerPubkey: string
  private relayUrls: string[]
  private connectionSecret?: string
  private localPrivkey: Uint8Array
  private localPubkey: string
  private remotePubkey: string | null = null
  private ws: WebSocket | null = null
  private pendingRequests = new Map<string, PendingRequest>()
  private connected = false
  private token: TCashuToken | null = null
  private mintUrl: string | null = null
  private requestTimeout = 30000 // 30 seconds

  // Whether we're waiting for signer to connect (reverse flow)
  private awaitingConnection = false
  private connectionResolve: ((pubkey: string) => void) | null = null

  /**
   * Create a BunkerSigner.
   * @param bunkerPubkey - The bunker's public key (hex)
   * @param relayUrls - Relay URLs to connect to
   * @param connectionSecret - Optional connection secret for initial handshake
   * @param catToken - Optional CAT token (encoded string) for authorization
   */
  constructor(bunkerPubkey: string, relayUrls: string[], connectionSecret?: string, catToken?: string) {
    this.bunkerPubkey = bunkerPubkey
    this.relayUrls = relayUrls
    this.connectionSecret = connectionSecret

    // Decode CAT token if provided
    if (catToken) {
      try {
        this.token = cashuTokenService.decodeToken(catToken)
      } catch (err) {
        console.warn('Failed to decode CAT token from URL:', err)
      }
    }

    // Generate local ephemeral keypair for NIP-46 communication
    this.localPrivkey = secp256k1.utils.randomPrivateKey()
    this.localPubkey = nGetPublicKey(this.localPrivkey)
  }

  /**
   * Create a BunkerSigner that waits for a signer (like Amber) to connect.
   * Returns the nostr+connect URL to display as QR code and a promise for the connected signer.
   *
   * @param relayUrl - The relay URL for the connection
   * @param secret - Optional secret for the handshake
   * @param timeout - Connection timeout in ms (default 120000 = 2 minutes)
   */
  static async awaitSignerConnection(
    relayUrl: string,
    secret?: string,
    timeout = 120000
  ): Promise<{ connectUrl: string; signer: Promise<BunkerSigner> }> {
    // Generate ephemeral keypair for this session
    const localPrivkey = secp256k1.utils.randomPrivateKey()
    const localPubkey = nGetPublicKey(localPrivkey)

    // Generate secret if not provided
    const connectionSecret = secret || generateRequestId()

    // Build the nostr+connect URL for signer to scan
    const connectUrl = buildNostrConnectUrl(relayUrl, localPubkey, connectionSecret)

    // Create signer instance (bunkerPubkey will be set when signer connects)
    const signer = new BunkerSigner('', [relayUrl], connectionSecret)
    signer.localPrivkey = localPrivkey
    signer.localPubkey = localPubkey
    signer.awaitingConnection = true

    // Return URL immediately, signer promise resolves when connected
    const signerPromise = new Promise<BunkerSigner>((resolve, reject) => {
      signer.connectionResolve = (signerPubkey: string) => {
        signer.bunkerPubkey = signerPubkey
        signer.remotePubkey = signerPubkey
        signer.awaitingConnection = false
        resolve(signer)
      }
      // Set timeout
      setTimeout(() => {
        if (signer.awaitingConnection) {
          signer.disconnect()
          reject(new Error('Connection timeout waiting for signer'))
        }
      }, timeout)

      // Connect to relay and wait
      signer.connectAndWait(relayUrl).catch(reject)
    })

    return { connectUrl, signer: signerPromise }
  }

  /**
   * Connect to relay and wait for signer to initiate connection.
   */
  private async connectAndWait(relayUrl: string): Promise<void> {
    await this.acquireTokenIfNeeded(relayUrl)
    await this.connectToRelayAndListen(relayUrl)
  }

  /**
   * Connect to relay and listen for incoming connect requests.
   */
  private async connectToRelayAndListen(relayUrl: string): Promise<void> {
    return new Promise((resolve, reject) => {
      let wsUrl = relayUrl
      if (relayUrl.startsWith('http://')) {
        wsUrl = 'ws://' + relayUrl.slice(7)
      } else if (relayUrl.startsWith('https://')) {
        wsUrl = 'wss://' + relayUrl.slice(8)
      } else if (!relayUrl.startsWith('ws://') && !relayUrl.startsWith('wss://')) {
        wsUrl = 'wss://' + relayUrl
      }

      // Add token if available
      if (this.token) {
        const tokenEncoded = cashuTokenService.encodeToken(this.token)
        const url = new URL(wsUrl)
        url.searchParams.set('token', tokenEncoded)
        wsUrl = url.toString()
      }

      const ws = new WebSocket(wsUrl)

      const timeout = setTimeout(() => {
        ws.close()
        reject(new Error('Connection timeout'))
      }, 10000)

      ws.onopen = () => {
        clearTimeout(timeout)
        this.ws = ws
        this.connected = true

        // Subscribe to events for our local pubkey
        const subId = generateRequestId()
        ws.send(
          JSON.stringify([
            'REQ',
            subId,
            {
              kinds: [24133],
              '#p': [this.localPubkey],
              since: Math.floor(Date.now() / 1000) - 60
            }
          ])
        )

        resolve()
      }

      ws.onerror = () => {
        clearTimeout(timeout)
        reject(new Error('WebSocket error'))
      }

      ws.onclose = () => {
        this.connected = false
        this.ws = null
      }

      ws.onmessage = (event) => {
        this.handleMessage(event.data)
      }
    })
  }

  /**
   * Get the local public key (for displaying in nostr+connect URL).
   */
  getLocalPubkey(): string {
    return this.localPubkey
  }

  /**
   * Set the Cashu token for authentication.
   */
  setToken(token: TCashuToken) {
    this.token = token
  }

  /**
   * Set the mint URL for token refresh.
   */
  setMintUrl(url: string) {
    this.mintUrl = url
    cashuTokenService.setMint(url)
  }

  /**
   * Initialize connection to the bunker.
   */
  async init(): Promise<void> {
    // Check for stored token
    const stored = cashuTokenService.loadTokens(this.bunkerPubkey)
    if (stored?.current && !cashuTokenService.needsRefresh(stored.current)) {
      this.token = stored.current
    }

    // Try to acquire token for each relay if we don't have one
    if (!this.token) {
      for (const relayUrl of this.relayUrls) {
        try {
          await this.acquireTokenIfNeeded(relayUrl)
          if (this.token) break
        } catch (err) {
          console.warn(`Failed to acquire token for ${relayUrl}:`, err)
        }
      }
    }

    // Connect to first available relay
    for (const relayUrl of this.relayUrls) {
      try {
        await this.connectToRelay(relayUrl)
        break
      } catch (err) {
        console.warn(`Failed to connect to ${relayUrl}:`, err)
      }
    }

    if (!this.connected) {
      throw new Error('Failed to connect to any bunker relay')
    }

    // Perform NIP-46 connect handshake
    await this.connect()
  }

  /**
   * Check if relay requires Cashu token and acquire one if needed.
   */
  private async acquireTokenIfNeeded(relayUrl: string): Promise<void> {
    // Convert to HTTP URL for mint endpoints
    let mintUrl = relayUrl
    if (relayUrl.startsWith('ws://')) {
      mintUrl = 'http://' + relayUrl.slice(5)
    } else if (relayUrl.startsWith('wss://')) {
      mintUrl = 'https://' + relayUrl.slice(6)
    } else if (!relayUrl.startsWith('http://') && !relayUrl.startsWith('https://')) {
      mintUrl = 'https://' + relayUrl
    }
    mintUrl = mintUrl.replace(/\/$/, '')

    try {
      // Check if relay has Cashu mint endpoints
      const infoResponse = await fetch(`${mintUrl}/cashu/info`)
      if (!infoResponse.ok) {
        console.log(`Relay ${relayUrl} does not support Cashu tokens`)
        return
      }

      await infoResponse.json() // Validate JSON response
      console.log(`Relay ${relayUrl} requires Cashu token, acquiring...`)

      // Configure the mint
      this.mintUrl = mintUrl
      cashuTokenService.setMint(mintUrl)

      // Create NIP-98 auth signer using our local ephemeral key
      const signHttpAuth = async (url: string, method: string): Promise<string> => {
        const authEvent: TDraftEvent = {
          kind: 27235,
          created_at: Math.floor(Date.now() / 1000),
          content: '',
          tags: [
            ['u', url],
            ['method', method]
          ]
        }
        const signedAuth = finalizeEvent(authEvent, this.localPrivkey)
        // Encode as base64url for NIP-98 header
        const eventJson = JSON.stringify(signedAuth)
        const base64 = btoa(eventJson)
          .replace(/\+/g, '-')
          .replace(/\//g, '_')
          .replace(/=+$/, '')
        return `Nostr ${base64}`
      }

      // Request token with NIP-46 scope
      const token = await cashuTokenService.requestToken(
        TokenScope.NIP46,
        utils.hexToBytes(this.localPubkey),
        signHttpAuth,
        [24133] // NIP-46 kind
      )

      this.token = token
      cashuTokenService.storeTokens(this.bunkerPubkey, token)
      console.log(`Acquired Cashu token for ${relayUrl}, expires: ${new Date(token.expiry * 1000).toISOString()}`)
    } catch (err) {
      // Relay doesn't support Cashu or request failed - continue without token
      console.warn(`Could not acquire Cashu token for ${relayUrl}:`, err)
    }
  }

  /**
   * Connect to a relay WebSocket.
   */
  private async connectToRelay(relayUrl: string): Promise<void> {
    return new Promise((resolve, reject) => {
      // Convert ws:// or wss:// URL
      let wsUrl = relayUrl
      if (relayUrl.startsWith('http://')) {
        wsUrl = 'ws://' + relayUrl.slice(7)
      } else if (relayUrl.startsWith('https://')) {
        wsUrl = 'wss://' + relayUrl.slice(8)
      } else if (!relayUrl.startsWith('ws://') && !relayUrl.startsWith('wss://')) {
        wsUrl = 'wss://' + relayUrl
      }

      // Add Cashu token header if available
      // Note: WebSocket API doesn't support custom headers directly,
      // so we'll need to pass token as a subprotocol or query param
      if (this.token) {
        const tokenEncoded = cashuTokenService.encodeToken(this.token)
        const url = new URL(wsUrl)
        url.searchParams.set('token', tokenEncoded)
        wsUrl = url.toString()
      }

      const ws = new WebSocket(wsUrl)

      const timeout = setTimeout(() => {
        ws.close()
        reject(new Error('Connection timeout'))
      }, 10000)

      ws.onopen = () => {
        clearTimeout(timeout)
        this.ws = ws
        this.connected = true

        // Subscribe to responses for our local pubkey
        const subId = generateRequestId()
        ws.send(
          JSON.stringify([
            'REQ',
            subId,
            {
              kinds: [24133], // NIP-46 response kind
              '#p': [this.localPubkey],
              since: Math.floor(Date.now() / 1000) - 60
            }
          ])
        )

        resolve()
      }

      ws.onerror = () => {
        clearTimeout(timeout)
        reject(new Error('WebSocket error'))
      }

      ws.onclose = () => {
        this.connected = false
        this.ws = null
      }

      ws.onmessage = (event) => {
        this.handleMessage(event.data)
      }
    })
  }

  /**
   * Handle incoming WebSocket messages.
   */
  private async handleMessage(data: string): Promise<void> {
    try {
      const msg = JSON.parse(data)
      if (!Array.isArray(msg)) return

      const [type, ...rest] = msg

      if (type === 'EVENT') {
        const [, event] = rest as [string, Event]
        if (event.kind === 24133) {
          await this.handleNIP46Response(event)
        }
      } else if (type === 'OK') {
        // Event published confirmation
      } else if (type === 'NOTICE') {
        console.warn('Relay notice:', rest[0])
      }
    } catch (err) {
      console.error('Failed to parse message:', err)
    }
  }

  /**
   * Handle NIP-46 response event.
   */
  private async handleNIP46Response(event: Event): Promise<void> {
    try {
      // Decrypt the content with NIP-04
      const decrypted = await nip04.decrypt(this.localPrivkey, event.pubkey, event.content)
      const parsed = JSON.parse(decrypted)

      // Check if this is an incoming connect request (signer initiating connection)
      if (this.awaitingConnection && parsed.method === 'connect') {
        const request = parsed as NIP46Request
        console.log('Received connect request from signer:', event.pubkey)

        // Verify secret if we have one
        if (this.connectionSecret) {
          const providedSecret = request.params[1] // Second param is the secret
          if (providedSecret !== this.connectionSecret) {
            console.warn('Connect request has wrong secret, ignoring')
            return
          }
        }

        // Send ack response
        const response: NIP46Response = {
          id: request.id,
          result: 'ack'
        }
        const encrypted = await nip04.encrypt(this.localPrivkey, event.pubkey, JSON.stringify(response))
        const responseEvent: TDraftEvent = {
          kind: 24133,
          created_at: Math.floor(Date.now() / 1000),
          content: encrypted,
          tags: [['p', event.pubkey]]
        }
        const signedResponse = finalizeEvent(responseEvent, this.localPrivkey)
        this.ws?.send(JSON.stringify(['EVENT', signedResponse]))

        // Resolve the connection promise
        if (this.connectionResolve) {
          this.connectionResolve(event.pubkey)
        }
        return
      }

      // Handle as normal response
      const response = parsed as NIP46Response
      const pending = this.pendingRequests.get(response.id)

      if (pending) {
        clearTimeout(pending.timeout)
        this.pendingRequests.delete(response.id)

        if (response.error) {
          pending.reject(new Error(response.error))
        } else if (response.result !== undefined) {
          pending.resolve(response.result)
        } else {
          pending.reject(new Error('Empty response'))
        }
      }
    } catch (err) {
      console.error('Failed to handle NIP-46 response:', err)
    }
  }

  /**
   * Send a NIP-46 request and wait for response.
   */
  private async sendRequest(method: NIP46Method, params: string[] = []): Promise<string> {
    if (!this.ws || !this.connected) {
      throw new Error('Not connected to bunker')
    }

    const request: NIP46Request = {
      id: generateRequestId(),
      method,
      params
    }

    // Encrypt with NIP-04 to the bunker's pubkey
    const encrypted = await nip04.encrypt(this.localPrivkey, this.bunkerPubkey, JSON.stringify(request))

    // Create NIP-46 request event
    const draftEvent: TDraftEvent = {
      kind: 24133,
      created_at: Math.floor(Date.now() / 1000),
      content: encrypted,
      tags: [['p', this.bunkerPubkey]]
    }

    const signedEvent = finalizeEvent(draftEvent, this.localPrivkey)

    // Send to relay
    this.ws.send(JSON.stringify(['EVENT', signedEvent]))

    // Wait for response
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pendingRequests.delete(request.id)
        reject(new Error('Request timeout'))
      }, this.requestTimeout)

      this.pendingRequests.set(request.id, { resolve, reject, timeout })
    })
  }

  /**
   * Perform NIP-46 connect handshake.
   */
  private async connect(): Promise<void> {
    const params: string[] = [this.localPubkey]
    if (this.connectionSecret) {
      params.push(this.connectionSecret)
    }

    const result = await this.sendRequest(NIP46_METHOD.CONNECT, params)
    if (result !== 'ack') {
      throw new Error(`Connect failed: ${result}`)
    }
  }

  /**
   * Get the public key of the user (from the bunker).
   */
  async getPublicKey(): Promise<string> {
    if (this.remotePubkey) {
      return this.remotePubkey
    }

    const pubkey = await this.sendRequest(NIP46_METHOD.GET_PUBLIC_KEY)
    this.remotePubkey = pubkey
    return pubkey
  }

  /**
   * Sign an event via the bunker.
   */
  async signEvent(draftEvent: TDraftEvent): Promise<VerifiedEvent> {
    const eventJson = JSON.stringify({
      ...draftEvent,
      pubkey: await this.getPublicKey()
    })

    const signedEventJson = await this.sendRequest(NIP46_METHOD.SIGN_EVENT, [eventJson])
    const signedEvent = JSON.parse(signedEventJson) as VerifiedEvent

    return signedEvent
  }

  /**
   * Encrypt a message with NIP-04 via the bunker.
   */
  async nip04Encrypt(pubkey: string, plainText: string): Promise<string> {
    return this.sendRequest(NIP46_METHOD.NIP04_ENCRYPT, [pubkey, plainText])
  }

  /**
   * Decrypt a message with NIP-04 via the bunker.
   */
  async nip04Decrypt(pubkey: string, cipherText: string): Promise<string> {
    return this.sendRequest(NIP46_METHOD.NIP04_DECRYPT, [pubkey, cipherText])
  }

  /**
   * Check if connected to the bunker.
   */
  isConnected(): boolean {
    return this.connected
  }

  /**
   * Get the current token.
   */
  getToken(): TCashuToken | null {
    return this.token
  }

  /**
   * Request a new token from the mint.
   * Requires a signing function for NIP-98 auth.
   */
  async refreshToken(
    signHttpAuth: (url: string, method: string) => Promise<string>,
    userPubkey: Uint8Array
  ): Promise<TCashuToken> {
    if (!this.mintUrl) {
      throw new Error('Mint URL not configured')
    }

    const token = await cashuTokenService.requestToken(
      TokenScope.NIP46,
      userPubkey,
      signHttpAuth,
      [24133] // NIP-46 kind
    )

    this.token = token

    // Store the new token
    const existing = cashuTokenService.loadTokens(this.bunkerPubkey)
    if (existing?.current && cashuTokenService.verifyToken(existing.current)) {
      // Current still valid, store new as next
      cashuTokenService.storeTokens(this.bunkerPubkey, existing.current, token)
    } else {
      // Current expired or invalid, use new as current
      cashuTokenService.storeTokens(this.bunkerPubkey, token)
    }

    return token
  }

  /**
   * Disconnect from the bunker.
   */
  disconnect(): void {
    if (this.ws) {
      this.ws.close()
      this.ws = null
    }
    this.connected = false
    this.pendingRequests.forEach((pending) => {
      clearTimeout(pending.timeout)
      pending.reject(new Error('Disconnected'))
    })
    this.pendingRequests.clear()
  }

  /**
   * Get the bunker's public key.
   */
  getBunkerPubkey(): string {
    return this.bunkerPubkey
  }

  /**
   * Get the relay URLs.
   */
  getRelayUrls(): string[] {
    return this.relayUrls
  }

  /**
   * Get the bunker URL for sharing.
   */
  getBunkerUrl(): string {
    return buildBunkerUrl(this.bunkerPubkey, this.relayUrls)
  }
}
