import crypto, { BinaryLike } from 'crypto'
import { TextEncoder, promisify } from 'util'
import { DecryptOptions, generalDecrypt, GeneralJWE } from 'jose'
import base64Url from 'base64url'
import type { CypressRequestOptions } from './api'
import { deflateRaw as deflateRawCb, inflateRaw as inflateRawCb } from 'zlib'
import fs from 'fs'

const deflateRaw = promisify(deflateRawCb)
const inflateRaw = promisify(inflateRawCb)

// The `jose` library caps decompressed JWE payloads at ~250KB by default, which
// is too small for larger cy.prompt `/plan` responses (these carry cached
// selectors/chains that can inflate past that threshold). Override with a 5MB
// ceiling so decryption matches the server-side inflate limit configured in
// `@packages/encryption` in cypress-services.
const DECRYPT_OPTIONS: DecryptOptions = {
  inflateRaw: (data) => inflateRaw(data, { maxOutputLength: 1024 * 1024 * 5 }),
}

const CY_TEST = `LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFyZ0pGb2FuMFROTUxWSUNvZWF2WQpJVWtqNWJaM2QxTlVJdVU4WjM1b2hzcUFGWHY1eGhRRzd5MWdkeTR1SVZOSFU0a1RmUERBZEk1MnRWcGE5UG1yCm5OSDhsZE5kMjRwemVFNm1CeE91MlVDQ1d3VEY5eS9BUGZqNjVkczJSSTMwR09oZm95Q1pyQndxRU1zdWJ1MTUKVVNqbFBUcEFoWlFZN2Y2bHA4TTZMYU55SUhLMzYyMDgvZFp6aWs4Q1NLWmwya0E0TUN4eWxhUElWNVVWZG0rRQpkdjJhdm1Hcy9vQzVUV1VRNzlVSmJyMDllem1oZWExcS81VnpvajRvODlKSkNnelFhcllvL1QrNVlreWJ0Z2hkCjN3NnNPSjBCQ2NrUUV5MGpXVWJWUnhSa084VGJsckxXbC9Rd0Ryd1EvRERQaEZaSGhIbCtCL3JRTldsYTFXdEoKclFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`
const CY_STAGING = `LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZE1OazZYVkFhV3VlT0lXZ3V2aQpDTlhPRGVtMHRINmo0NnFTWUhJZFcyU1N5NHU0ZFpGd1VHQldlZjBEbDVmeGhZa1BFczBxUDJHUnlUZnY5YjNXCk9xWEJFQmFyNXMyYzJSMGd1RzdqNGtidTlZZklRQWpWejZndUtQMTIzd3VKSjFmcEU5T3pXWlZmUi9pQWl4b1gKTi82aEFhSHNMT1RlNXROdTVESzNOQnUxa3VJTTExcDZScEo5bGgvbWVFK3JObzRZVWUvZ2Jzam1mZmJiODRGeQpqWk1sQW9YSnYxU3lKK2phdTNMa3JkdzMybWYrczMyd2NLUnNpbmp1STgrZndDT2lEb2xnZW9NdEhta2tXVS8xCnJvUnVEcGd6d2FZemxLbUhRODNWQTlOTUhvNmMwVU40MzlBMnVtRFQ4ek94SjJjQUY0U0RiWTV3RnBnd3cvUVgKd1FJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`
const CY_DEVELOPMENT = `LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF3aEpiaG1pbHJsT012V3V4WHdWcAp2MWZBSUZWNGlrcS8vOGhNdFpCbVk1WDRaNjVwQ3pqZjJzYVNXSnJVWExMd1R4d3VpOGlFd3hCb2Jlelc0d1ZsCk9xYjlEWE5RbnFvd2hrcGZNUmlhTjM0ZEtYZHZLZi9RaFAvU2tJbVFCYi9EQzFISWdhYjRXZGpJOFlwZnhHQWoKdkJhbU1TcEErVGdDekVGaWRuR3p6OUk5SmY0bUVBTHdIT3NGaVh3NkpqMmlXeDZBT1JtR3FBL3FYWStPY25wdQo1dmxISEl3cllxU1hQZjU2OHRUV2dZOVNvK0ZmYVhlK2s0MnZGeDAyRjdGRXlCZmJxMnBNdjhJWWJnbDViaE0rClRjUFVEYnpBZ0FWNUhlRENKKzQ2RklLNW9JZHV5WjhXQUQxRGFJRTQyWHBGNjlhWjRkcE11YnR5VDBIbXBHeHIKM3dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`
const CY_PRODUCTION = `LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5dm5nWG1mdU51dnpnckJ3bHRLdApOZVQwYy9WYlZCWTRkZlZIaVBiQXF1Z3l2dndSUWFseVJ4SjNUdDgxL28vMXhyTXIyUUNtbXRkOVllTXA5dmVzClVqd1BuQnZucEIrWGo0ZXVoR2VVY0Q4Unhxb0cxdmFFd294ajFsSWFIckwrUVBBVGVPNFJ0Q3EweFM4TlduaGoKK1NYYm13NjNjWGtYZGthYzdlQVJmdmt2QjVMU1VqR290eXhPZGpOZmp2SmtvTU1GYXlqUGpSMGpFQldBL3NPZApFOENUTFRKdkYzakF0ODVpSDBHYWNpQVhJUG1uUnJ2eEh0SS9seDNWNkk0T1dNeG45SDRRWStoMnFmV2N2bWlQCmsrL1FNMjQ5cW4zKzgvQnNRWHNEVXZOMWdwZk9nRUIxRTFRaWJMZVV1YVducE05YWRWZENzWEVvRS8xUHduZTIKMFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`

const keys: Record<string, string> = {
  test: CY_TEST,
  development: CY_DEVELOPMENT,
  staging: CY_STAGING,
  production: CY_PRODUCTION,
}

const keyObjects: Record<string, crypto.KeyObject> = {}

function getPublicKey () {
  const env = process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'development'

  if (!keyObjects[env]) {
    keyObjects[env] = crypto.createPublicKey(Buffer.from(keys[env], 'base64').toString('utf8').trim())
  }

  return keyObjects[env]
}

interface EncryptRequestData {
  jwe: GeneralJWE
  secretKey: crypto.KeyObject
}

export function verifySignature (body: BinaryLike, signature: string, publicKey?: crypto.KeyObject) {
  const verify = crypto.createVerify('SHA256')

  verify.update(body)

  return verify.verify(publicKey || getPublicKey(), signature, 'base64')
}

export function createStreamingSignatureVerifier (publicKey?: crypto.KeyObject) {
  const verify = crypto.createVerify('SHA256')

  return {
    update (chunk: BinaryLike) {
      verify.update(chunk)
    },
    verify (signature: string): boolean {
      return verify.verify(publicKey || getPublicKey(), signature, 'base64')
    },
  }
}

// Implements the https://www.rfc-editor.org/rfc/rfc7516 spec
// Functionally equivalent to the behavior for AES-256-GCM encryption
// in the jose library (https://github.com/panva/jose/blob/main/src/jwe/general/encrypt.ts),
// but allows us to keep track of the encrypting key locally, to optionally use it for decryption
// of encrypted payloads coming back in the response body.
export async function encryptRequest (params: Pick<CypressRequestOptions, 'body'>, options: {
  publicKey?: crypto.KeyObject
  secretKey?: crypto.KeyObject
} = {}): Promise<EncryptRequestData> {
  const { publicKey = getPublicKey(), secretKey = crypto.createSecretKey(crypto.randomBytes(32)) } = options
  const header = base64Url(JSON.stringify({ alg: 'RSA-OAEP', enc: 'A256GCM', zip: 'DEF' }))
  const deflated = await deflateRaw(JSON.stringify(params.body))
  const iv = crypto.randomBytes(12)
  const cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv, { authTagLength: 16 })
  const aad = new TextEncoder().encode(header)

  cipher.setAAD(aad, {
    plaintextLength: deflated.length,
  })

  const encrypted = cipher.update(deflated)

  cipher.final()

  // Returns the payload in JWE format, as well as the secretKey so we can use it
  // for decrypting the payload sent in response
  return {
    secretKey,
    jwe: {
      iv: base64Url(iv),
      ciphertext: base64Url(encrypted),
      recipients: [
        {
          encrypted_key: base64Url(crypto.publicEncrypt(publicKey, secretKey.export())),
        },
      ],
      tag: base64Url(cipher.getAuthTag()),
      protected: header,
    },
  }
}

/**
 * Given the returned JWE and the symmetric symmetric key used in the original request,
 * decrypts the response payload, which is assumed to be JSON
 */
export async function decryptResponse (jwe: GeneralJWE, encryptionKey: crypto.KeyObject): Promise<any> {
  const result = await generalDecrypt(jwe, encryptionKey, DECRYPT_OPTIONS)
  const plaintext = Buffer.from(result.plaintext).toString('utf8')

  return JSON.parse(plaintext)
}
