/**
 * Cashu Token Service
 *
 * Manages Cashu access tokens for bunker authentication.
 * Handles token issuance, storage, and two-token rotation (current + next).
 *
 * Token flow:
 * 1. Generate random secret and blinding factor
 * 2. Compute blinded message B_ = hash_to_curve(secret) + r*G
 * 3. Submit B_ to mint with NIP-98 auth
 * 4. Receive blinded signature C_
 * 5. Unblind: C = C_ - r*K (where K is mint's pubkey)
 * 6. Store token {secret, C, keysetId, expiry, ...}
 */

import * as utils from '@noble/curves/abstract/utils'
import { secp256k1 } from '@noble/curves/secp256k1'
import { sha256 } from '@noble/hashes/sha256'

// Token scopes
export const TokenScope = {
  RELAY: 'relay',
  NIP46: 'nip46',
  BLOSSOM: 'blossom',
  API: 'api'
} as const

export type TTokenScope = (typeof TokenScope)[keyof typeof TokenScope]

// Token format matching ORLY's token.Token
export type TCashuToken = {
  keysetId: string // k - keyset identifier
  secret: Uint8Array // s - 32-byte random secret
  signature: Uint8Array // c - 33-byte signature point (compressed)
  pubkey: Uint8Array // p - 32-byte user pubkey
  expiry: number // e - Unix timestamp
  scope: TTokenScope // sc - token scope
  kinds?: number[] // kinds - permitted event kinds
  kindRanges?: [number, number][] // kind_ranges - permitted ranges
}

// Keyset info from mint
export type TKeysetInfo = {
  id: string
  publicKey: string // hex-encoded mint public key
  active: boolean
  expiresAt?: number
}

// Mint info
export type TMintInfo = {
  name: string
  version: string
  pubkey: string
  keysets: TKeysetInfo[]
}

// Blinding result
type BlindResult = {
  B_: Uint8Array // Blinded point (33 bytes compressed)
  secret: Uint8Array // Original secret
  r: Uint8Array // Blinding factor scalar
}

// Storage key prefix
const STORAGE_PREFIX = 'cashu_token_'

/**
 * Hash a message to a point on secp256k1 using try-and-increment.
 * Algorithm matches ORLY's Go implementation exactly:
 * 1. msgHash = SHA256(domain_separator || message)
 * 2. For counter in 0..65535:
 *    a. counterBytes = counter as 4-byte little-endian
 *    b. hash = SHA256(msgHash || counterBytes)
 *    c. compressed = 0x02 || hash
 *    d. If valid secp256k1 point, return compressed
 */
function hashToCurve(message: Uint8Array): Uint8Array {
  const domainSeparator = new TextEncoder().encode('Secp256k1_HashToCurve_Cashu_')
  const msgHash = sha256(new Uint8Array([...domainSeparator, ...message]))

  // Try incrementing counter until we get a valid point
  for (let counter = 0; counter < 65536; counter++) {
    // 4-byte little-endian counter (matching ORLY's binary.LittleEndian.PutUint32)
    const counterBytes = new Uint8Array(4)
    new DataView(counterBytes.buffer).setUint32(0, counter, true) // true = little-endian

    // msgHash THEN counterBytes (matching ORLY's append order)
    const toHash = new Uint8Array([...msgHash, ...counterBytes])
    const hash = sha256(toHash)

    // Only try 0x02 prefix (even Y coordinate)
    const compressed = new Uint8Array([0x02, ...hash])
    try {
      // Validate this is a valid point
      const point = secp256k1.ProjectivePoint.fromHex(compressed)
      if (!point.equals(secp256k1.ProjectivePoint.ZERO)) {
        return compressed
      }
    } catch {
      // Not a valid point, continue
    }
  }

  throw new Error('Failed to hash to curve after 65536 attempts')
}

/**
 * Create a blinded message from a secret.
 * B_ = Y + r*G where Y = hash_to_curve(secret)
 */
function blind(secret: Uint8Array): BlindResult {
  // Generate random blinding factor r
  const r = secp256k1.utils.randomPrivateKey()

  // Y = hash_to_curve(secret)
  const Y = secp256k1.ProjectivePoint.fromHex(hashToCurve(secret))

  // r*G
  const rG = secp256k1.ProjectivePoint.BASE.multiply(utils.bytesToNumberBE(r))

  // B_ = Y + r*G
  const B_ = Y.add(rG)

  return {
    B_: B_.toRawBytes(true), // Compressed format
    secret,
    r
  }
}

/**
 * Unblind the signature to get the final signature.
 * C = C_ - r*K where K is the mint's public key
 */
function unblind(C_: Uint8Array, r: Uint8Array, K: Uint8Array): Uint8Array {
  const C_point = secp256k1.ProjectivePoint.fromHex(C_)
  const K_point = secp256k1.ProjectivePoint.fromHex(K)

  // r*K
  const rK = K_point.multiply(utils.bytesToNumberBE(r))

  // C = C_ - r*K
  const C = C_point.subtract(rK)

  return C.toRawBytes(true)
}

/**
 * Verify a token signature locally.
 * Checks that C = k*Y where Y = hash_to_curve(secret) and k is unknown.
 * We verify using DLEQ proof or by checking C matches our expectations.
 */
function verifyToken(token: TCashuToken, _mintPubkey: Uint8Array): boolean {
  try {
    // Basic validation
    if (token.expiry < Date.now() / 1000) {
      return false
    }

    // Verify signature is a valid point
    secp256k1.ProjectivePoint.fromHex(token.signature)

    // Could implement full DLEQ verification here if needed
    return true
  } catch {
    return false
  }
}

/**
 * Encode a token to the Cashu format (cashuA prefix + base64url).
 */
function encodeToken(token: TCashuToken): string {
  const tokenData = {
    k: token.keysetId,
    s: utils.bytesToHex(token.secret),
    c: utils.bytesToHex(token.signature),
    p: utils.bytesToHex(token.pubkey),
    e: token.expiry,
    sc: token.scope,
    kinds: token.kinds,
    kind_ranges: token.kindRanges
  }

  const json = JSON.stringify(tokenData)
  // Use base64url encoding
  const base64 = btoa(json)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '')

  return 'cashuA' + base64
}

/**
 * Decode a token from the Cashu format.
 */
function decodeToken(encoded: string): TCashuToken {
  if (!encoded.startsWith('cashuA')) {
    throw new Error('Invalid token prefix, expected cashuA')
  }

  const base64url = encoded.slice(6)
  // Convert base64url to base64
  let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/')
  // Add padding if needed
  while (base64.length % 4 !== 0) {
    base64 += '='
  }

  const json = atob(base64)
  const data = JSON.parse(json)

  return {
    keysetId: data.k,
    secret: utils.hexToBytes(data.s),
    signature: utils.hexToBytes(data.c),
    pubkey: utils.hexToBytes(data.p),
    expiry: data.e,
    scope: data.sc,
    kinds: data.kinds,
    kindRanges: data.kind_ranges
  }
}

/**
 * Cashu Token Service - manages token lifecycle for bunker auth.
 */
class CashuTokenService {
  private mintUrl: string | null = null
  private mintPubkey: Uint8Array | null = null
  private activeKeysetId: string | null = null

  /**
   * Configure the mint endpoint.
   */
  setMint(url: string) {
    this.mintUrl = url.replace(/\/$/, '')
  }

  /**
   * Fetch mint info and keysets.
   */
  async fetchMintInfo(): Promise<TMintInfo> {
    if (!this.mintUrl) {
      throw new Error('Mint URL not configured')
    }

    const response = await fetch(`${this.mintUrl}/cashu/info`)
    if (!response.ok) {
      throw new Error(`Failed to fetch mint info: ${response.statusText}`)
    }

    const info = await response.json()
    this.mintPubkey = utils.hexToBytes(info.pubkey)

    // Also fetch keysets
    const keysetsResponse = await fetch(`${this.mintUrl}/cashu/keysets`)
    if (keysetsResponse.ok) {
      const keysetsData = await keysetsResponse.json()
      info.keysets = keysetsData.keysets

      // Find active keyset
      const active = keysetsData.keysets.find((k: TKeysetInfo) => k.active)
      if (active) {
        this.activeKeysetId = active.id
      }
    }

    return info
  }

  /**
   * Request a new token from the mint.
   * Requires NIP-98 auth via the signHttpAuth function.
   */
  async requestToken(
    scope: TTokenScope,
    userPubkey: Uint8Array,
    signHttpAuth: (url: string, method: string) => Promise<string>,
    kinds?: number[],
    kindRanges?: [number, number][]
  ): Promise<TCashuToken> {
    if (!this.mintUrl) {
      throw new Error('Mint URL not configured')
    }

    // Generate secret and blind it
    const secret = crypto.getRandomValues(new Uint8Array(32))
    const blindResult = blind(secret)

    // Create request
    const requestBody = {
      blinded_message: utils.bytesToHex(blindResult.B_),
      scope,
      kinds,
      kind_ranges: kindRanges
    }

    // Get NIP-98 auth header
    const authUrl = `${this.mintUrl}/cashu/mint`
    const authHeader = await signHttpAuth(authUrl, 'POST')

    // Submit to mint
    const response = await fetch(authUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: authHeader
      },
      body: JSON.stringify(requestBody)
    })

    if (!response.ok) {
      const error = await response.text()
      throw new Error(`Mint request failed: ${error}`)
    }

    const result = await response.json()

    // Unblind the signature
    const C_ = utils.hexToBytes(result.blinded_signature)
    const K = utils.hexToBytes(result.mint_pubkey)
    const signature = unblind(C_, blindResult.r, K)

    const token: TCashuToken = {
      keysetId: result.keyset_id,
      secret: blindResult.secret,
      signature,
      pubkey: userPubkey,
      expiry: result.expiry,
      scope,
      kinds,
      kindRanges
    }

    return token
  }

  /**
   * Store tokens for a specific bunker.
   * Maintains current and next token for rotation.
   */
  storeTokens(bunkerPubkey: string, current: TCashuToken, next?: TCashuToken) {
    const key = `${STORAGE_PREFIX}${bunkerPubkey}`
    const data = {
      current: encodeToken(current),
      next: next ? encodeToken(next) : undefined,
      storedAt: Date.now()
    }
    localStorage.setItem(key, JSON.stringify(data))
  }

  /**
   * Load tokens for a specific bunker.
   */
  loadTokens(bunkerPubkey: string): { current?: TCashuToken; next?: TCashuToken } | null {
    const key = `${STORAGE_PREFIX}${bunkerPubkey}`
    const stored = localStorage.getItem(key)
    if (!stored) {
      return null
    }

    try {
      const data = JSON.parse(stored)
      return {
        current: data.current ? decodeToken(data.current) : undefined,
        next: data.next ? decodeToken(data.next) : undefined
      }
    } catch {
      return null
    }
  }

  /**
   * Get the active token for a bunker, handling rotation if needed.
   */
  getActiveToken(bunkerPubkey: string): TCashuToken | null {
    const tokens = this.loadTokens(bunkerPubkey)
    if (!tokens) {
      return null
    }

    const now = Date.now() / 1000

    // If current is valid, use it
    if (tokens.current && tokens.current.expiry > now) {
      return tokens.current
    }

    // Current expired, try to promote next
    if (tokens.next && tokens.next.expiry > now) {
      // Promote next to current
      this.storeTokens(bunkerPubkey, tokens.next)
      return tokens.next
    }

    // Both expired
    return null
  }

  /**
   * Check if token needs refresh (< 1 day until expiry).
   */
  needsRefresh(token: TCashuToken): boolean {
    const now = Date.now() / 1000
    const oneDaySeconds = 24 * 60 * 60
    return token.expiry - now < oneDaySeconds
  }

  /**
   * Clear tokens for a bunker.
   */
  clearTokens(bunkerPubkey: string) {
    const key = `${STORAGE_PREFIX}${bunkerPubkey}`
    localStorage.removeItem(key)
  }

  /**
   * Encode a token for transmission (e.g., in headers).
   */
  encodeToken(token: TCashuToken): string {
    return encodeToken(token)
  }

  /**
   * Decode a token string.
   */
  decodeToken(encoded: string): TCashuToken {
    return decodeToken(encoded)
  }

  /**
   * Verify a token is valid.
   */
  verifyToken(token: TCashuToken): boolean {
    if (!this.mintPubkey) {
      // Can't verify without mint pubkey, assume valid if not expired
      return token.expiry > Date.now() / 1000
    }
    return verifyToken(token, this.mintPubkey)
  }

  /**
   * Get the active keyset ID.
   */
  getActiveKeysetId(): string | null {
    return this.activeKeysetId
  }
}

// Export singleton instance
const cashuTokenService = new CashuTokenService()
export default cashuTokenService

// Export utilities
export { encodeToken, decodeToken, hashToCurve, blind, unblind }
