import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'

/**
 * eBay Digital Signatures for APIs (RFC 9421 HTTP Message Signatures).
 *
 * eBay requires every Finances API request (and a few other "compliant" APIs,
 * mandatory in the EU/UK) to carry a message signature, otherwise it answers
 * 403 "Missing x-ebay-signature-key header".
 *
 * The scheme:
 *  1. Create an Ed25519 signing key once via the Key Management API
 *     (POST /developer/key_management/v1/signing_key) using an APPLICATION token
 *     (client_credentials grant). eBay returns { privateKey, publicKey, jwe,
 *     signingKeyId }. The `jwe` is the value echoed back in the
 *     `x-ebay-signature-key` header.
 *  2. For each request, build the RFC 9421 signature base over the covered
 *     components, sign it with the Ed25519 private key, and send:
 *       x-ebay-signature-key: <jwe>
 *       Signature-Input: sig1=(...);created=<unix-ts>
 *       Signature: sig1=:<base64>:
 *       Content-Digest: sha-256=:<base64>:   (only when there is a request body)
 *
 * The signing key is cached in-memory and persisted to data/ebay-signing-keys.json
 * (keyed per environment + client id) so we don't mint a new key every request.
 * READ side-effects only — no iDempiere writes.
 */

const EBAY_TOKEN_URL = {
  production: 'https://api.ebay.com/identity/v1/oauth2/token',
  sandbox: 'https://api.sandbox.ebay.com/identity/v1/oauth2/token'
}
const EBAY_API_BASE = {
  production: 'https://apiz.ebay.com',
  sandbox: 'https://apiz.sandbox.ebay.com'
}

interface SigningKeyEntry {
  jwe: string
  privateKey: string
  publicKey?: string
  signingKeyId?: string
  expirationTime?: number // epoch seconds, if eBay supplied one
  createdAt?: string
}

const memCache = new Map<string, SigningKeyEntry>()

const storePath = () => path.join(process.cwd(), 'data', 'ebay-signing-keys.json')

const loadStore = (): Record<string, SigningKeyEntry> => {
  try {
    const p = storePath()
    if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf8')) || {}
  } catch { /* fall through to empty store */ }
  return {}
}

const saveStore = (store: Record<string, SigningKeyEntry>) => {
  try {
    const p = storePath()
    fs.mkdirSync(path.dirname(p), { recursive: true })
    fs.writeFileSync(p, JSON.stringify(store, null, 2), 'utf8')
  } catch { /* non-fatal: in-memory cache still works for this process */ }
}

// Application access token (client_credentials grant) — used only to create the
// signing key. Distinct from the per-user OAuth token used for data calls.
const getAppAccessToken = async (clientId: string, clientSecret: string, environment: string): Promise<string> => {
  const url = environment === 'sandbox' ? EBAY_TOKEN_URL.sandbox : EBAY_TOKEN_URL.production
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')

  const response = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${credentials}` },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      scope: 'https://api.ebay.com/oauth/api_scope'
    }).toString()
  })

  const text = await response.text()
  let json: any = {}
  try { json = text ? JSON.parse(text) : {} } catch { json = { raw: text } }

  if (!response.ok || !json.access_token) {
    const msg = json?.error_description || json?.error || text || response.statusText
    const err: any = new Error(`eBay application token request failed (${response.status}): ${msg}`)
    err.status = response.status
    err.ebay = json
    throw err
  }
  return json.access_token
}

// Create a fresh Ed25519 signing key via the Key Management API. This endpoint
// itself does NOT require a signature.
const createSigningKey = async (appToken: string, environment: string): Promise<SigningKeyEntry> => {
  const base = environment === 'sandbox' ? EBAY_API_BASE.sandbox : EBAY_API_BASE.production
  const response = await fetch(`${base}/developer/key_management/v1/signing_key`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${appToken}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ signingKeyCipher: 'ED25519' })
  })

  const text = await response.text()
  let json: any = {}
  try { json = text ? JSON.parse(text) : {} } catch { json = { raw: text } }

  if (!response.ok || !json.jwe || !json.privateKey) {
    const msg = json?.errors?.[0]?.message || json?.errors?.[0]?.longMessage ||
      json?.error_description || text || response.statusText
    const err: any = new Error(`eBay signing key creation failed (${response.status}): ${msg}`)
    err.status = response.status
    err.ebay = json
    throw err
  }

  return {
    jwe: json.jwe,
    privateKey: json.privateKey,
    publicKey: json.publicKey,
    signingKeyId: json.signingKeyId,
    expirationTime: json.expirationTime ? Number(json.expirationTime) : undefined,
    createdAt: new Date().toISOString()
  }
}

const isExpired = (entry: SigningKeyEntry): boolean => {
  if (!entry?.expirationTime) return false
  // 1-day safety buffer
  return Math.floor(Date.now() / 1000) > entry.expirationTime - 86400
}

// Get a usable signing key (cache → file → create), keyed per env + client id.
export const getOrCreateSigningKey = async (
  clientId: string,
  clientSecret: string,
  environment: string
): Promise<SigningKeyEntry> => {
  const cacheKey = `${environment}:${clientId}`

  const cached = memCache.get(cacheKey)
  if (cached && cached.jwe && cached.privateKey && !isExpired(cached)) return cached

  const store = loadStore()
  const persisted = store[cacheKey]
  if (persisted && persisted.jwe && persisted.privateKey && !isExpired(persisted)) {
    memCache.set(cacheKey, persisted)
    return persisted
  }

  const appToken = await getAppAccessToken(clientId, clientSecret, environment)
  const entry = await createSigningKey(appToken, environment)
  store[cacheKey] = entry
  saveStore(store)
  memCache.set(cacheKey, entry)
  return entry
}

// Wrap a base64 DER (PKCS#8) key as PEM so node:crypto can read it.
const pemFromBase64 = (b64: string, label = 'PRIVATE KEY'): string => {
  const body = (b64.match(/.{1,64}/g) || [b64]).join('\n')
  return `-----BEGIN ${label}-----\n${body}\n-----END ${label}-----\n`
}

// Build the RFC 9421 signature base + the signature-params serialization.
const buildSignatureBase = (opts: {
  method: string
  pathName: string
  authority: string
  jwe: string
  contentDigest?: string
  created: number
}): { base: string; params: string } => {
  const components: string[] = []
  if (opts.contentDigest) components.push('content-digest')
  components.push('x-ebay-signature-key', '@method', '@path', '@authority')

  const lines: string[] = []
  for (const c of components) {
    let value = ''
    if (c === 'content-digest') value = opts.contentDigest as string
    else if (c === 'x-ebay-signature-key') value = opts.jwe
    else if (c === '@method') value = opts.method.toUpperCase()
    else if (c === '@path') value = opts.pathName
    else if (c === '@authority') value = opts.authority
    lines.push(`"${c}": ${value}`)
  }

  const inner = components.map(c => `"${c}"`).join(' ')
  const params = `(${inner});created=${opts.created}`
  lines.push(`"@signature-params": ${params}`)

  return { base: lines.join('\n'), params }
}

// Produce the eBay digital-signature headers for one request.
// For GET requests pass no body (no Content-Digest needed).
export const buildSigningHeaders = async (opts: {
  method: string
  url: string
  clientId: string
  clientSecret: string
  environment: string
  body?: string | null
}): Promise<Record<string, string>> => {
  if (!opts.clientId || !opts.clientSecret) {
    const err: any = new Error('eBay Client ID / Secret are required to sign Finances API requests.')
    err.status = 400
    throw err
  }

  const { jwe, privateKey } = await getOrCreateSigningKey(opts.clientId, opts.clientSecret, opts.environment)

  const u = new URL(opts.url)
  const created = Math.floor(Date.now() / 1000)

  let contentDigest: string | undefined
  if (opts.body) {
    const hash = crypto.createHash('sha256').update(opts.body).digest('base64')
    contentDigest = `sha-256=:${hash}:`
  }

  const { base, params } = buildSignatureBase({
    method: opts.method,
    pathName: u.pathname,
    authority: u.host,
    jwe,
    contentDigest,
    created
  })

  const keyObj = crypto.createPrivateKey({ key: pemFromBase64(privateKey), format: 'pem' })
  const signature = crypto.sign(null, Buffer.from(base, 'utf8'), keyObj).toString('base64')

  const headers: Record<string, string> = {
    'x-ebay-signature-key': jwe,
    'Signature-Input': `sig1=${params}`,
    Signature: `sig1=:${signature}:`
  }
  if (contentDigest) headers['Content-Digest'] = contentDigest
  return headers
}
