/**
 * In-process `manage` capability-token minter — the in-code equivalent of
 * relay-run/scripts/mint-manage-token.ts. Every proxied admin call mints a FRESH short-TTL token so
 * a leaked token's blast radius is ~60s.
 *
 * The token is a §4.3 PASETO v4.public capability token signed by the P5 PRIVATE Ed25519 key
 * (PKCS#8 PEM at CAPABILITY_SIGN_KEY_PATH): `aud = BASE_DOMAIN`, `sub = OPERATOR_ACCOUNT_ID`,
 * `rights = ['manage']`, `ttl = 60s`. `issueCapabilityToken` mandates a well-formed DPoP `cnf.jkt`,
 * so we stamp one from a throwaway ephemeral key (the CP admin API verifies signature+aud+rights but
 * does NOT require a live DPoP proof — see the mint script's header note).
 *
 * SECURITY: the signing-key material and the minted token are NEVER logged.
 */
import { readFile } from 'node:fs/promises'
import { issueCapabilityToken } from 'relay-auth'
import { generateEd25519KeyPair, exportEd25519PublicRaw } from 'relay-auth/src/crypto/ed25519.js'
import { jwkThumbprint } from 'relay-auth/src/crypto/thumbprint.js'

/** Manage tokens are `manage`-scoped, single-account, 60s TTL. `host` is a placeholder (issue() forbids '*'/''). */
const MANAGE_TOKEN_TTL_SEC = 60
const MANAGE_HOST_PLACEHOLDER = '_manage_'

/** Raised when the signing key cannot be loaded/imported. Message is safe (no key material). */
export class ManageTokenError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'ManageTokenError'
  }
}

export interface ManageTokenMinter {
  /** Mint a fresh manage token for the configured operator account. */
  mint(): Promise<string>
}

export interface ManageTokenConfig {
  readonly capabilitySignKeyPath: string
  readonly baseDomain: string
  readonly operatorAccountId: string
}

/** Import a PKCS#8 PEM Ed25519 private key into a non-extractable signing CryptoKey. */
async function importSigningKeyFromPem(pemPath: string): Promise<CryptoKey> {
  let pem: string
  try {
    pem = await readFile(pemPath, 'utf8')
  } catch {
    throw new ManageTokenError(`capability signing key not readable at ${pemPath}`)
  }
  const b64 = pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '')
  if (b64.length === 0) throw new ManageTokenError('capability signing key PEM is empty')
  let der: Uint8Array<ArrayBuffer>
  try {
    // Copy into a fresh ArrayBuffer-backed view: WebCrypto's BufferSource requires Uint8Array<ArrayBuffer>,
    // which Buffer (ArrayBufferLike) does not satisfy under strict lib types.
    const raw = Buffer.from(b64, 'base64')
    der = new Uint8Array(new ArrayBuffer(raw.byteLength))
    der.set(raw)
  } catch {
    throw new ManageTokenError('capability signing key PEM is not valid base64')
  }
  try {
    return await globalThis.crypto.subtle.importKey('pkcs8', der, { name: 'Ed25519' }, false, ['sign'])
  } catch {
    throw new ManageTokenError('capability signing key is not a valid PKCS#8 Ed25519 key')
  }
}

/**
 * Build a minter that lazily loads + memoizes the signing key (so the panel boots even if the key
 * file is briefly unavailable — a missing key surfaces as an upstream error on the first proxy call,
 * not a boot crash). Clock is injectable for tests.
 */
export function createManageTokenMinter(config: ManageTokenConfig, now: () => number = () => Date.now()): ManageTokenMinter {
  let keyPromise: Promise<CryptoKey> | undefined

  const signingKey = (): Promise<CryptoKey> => {
    if (keyPromise === undefined) {
      keyPromise = importSigningKeyFromPem(config.capabilitySignKeyPath).catch((err: unknown) => {
        keyPromise = undefined // allow a later retry after the operator fixes the key
        throw err
      })
    }
    return keyPromise
  }

  return {
    async mint(): Promise<string> {
      const key = await signingKey()
      const eph = await generateEd25519KeyPair()
      const cnfJkt = await jwkThumbprint(await exportEd25519PublicRaw(eph.publicKey))
      return issueCapabilityToken(
        {
          principal: { accountId: config.operatorAccountId } as never, // runtime reads only accountId
          aud: config.baseDomain,
          host: MANAGE_HOST_PLACEHOLDER,
          rights: ['manage'],
          ttlSeconds: MANAGE_TOKEN_TTL_SEC,
          cnfJkt,
        },
        key,
        Math.floor(now() / 1000),
      )
    },
  }
}
