/**
 * Signing JSON Web Signature (JWS) in Flattened JSON Serialization
 *
 * @module
 */

import type * as types from '../../types.d.ts'
import { encode as b64u } from '../../util/base64url.ts'
import { sign } from '../../lib/signing.ts'

import { isDisjoint } from '../../lib/type_checks.ts'
import { JWSInvalid } from '../../util/errors.ts'
import { concat, encode } from '../../lib/buffer_utils.ts'
import { checkKeyType } from '../../lib/check_key_type.ts'
import { validateCrit } from '../../lib/validate_crit.ts'
import { normalizeKey } from '../../lib/normalize_key.ts'
import { assertNotSet } from '../../lib/helpers.ts'

/**
 * The FlattenedSign class is used to build and sign Flattened JWS objects.
 *
 * This class is exported (as a named export) from the main `'jose'` module entry point as well as
 * from its subpath export `'jose/jws/flattened/sign'`.
 *
 */
export class FlattenedSign {
  #payload: Uint8Array

  #protectedHeader!: types.JWSHeaderParameters

  #unprotectedHeader!: types.JWSHeaderParameters

  /**
   * {@link FlattenedSign} constructor
   *
   * @param payload Binary representation of the payload to sign.
   */
  constructor(payload: Uint8Array) {
    if (!(payload instanceof Uint8Array)) {
      throw new TypeError('payload must be an instance of Uint8Array')
    }
    this.#payload = payload
  }

  /**
   * Sets the JWS Protected Header on the FlattenedSign object.
   *
   * @param protectedHeader JWS Protected Header.
   */
  setProtectedHeader(protectedHeader: types.JWSHeaderParameters): this {
    assertNotSet(this.#protectedHeader, 'setProtectedHeader')
    this.#protectedHeader = protectedHeader
    return this
  }

  /**
   * Sets the JWS Unprotected Header on the FlattenedSign object.
   *
   * @param unprotectedHeader JWS Unprotected Header.
   */
  setUnprotectedHeader(unprotectedHeader: types.JWSHeaderParameters): this {
    assertNotSet(this.#unprotectedHeader, 'setUnprotectedHeader')
    this.#unprotectedHeader = unprotectedHeader
    return this
  }

  /**
   * Signs and resolves the value of the Flattened JWS object.
   *
   * @param key Private Key or Secret to sign the JWS with. See
   *   {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}.
   * @param options JWS Sign options.
   */
  async sign(
    key: types.CryptoKey | types.KeyObject | types.JWK | Uint8Array,
    options?: types.SignOptions,
  ): Promise<types.FlattenedJWS> {
    if (!this.#protectedHeader && !this.#unprotectedHeader) {
      throw new JWSInvalid(
        'either setProtectedHeader or setUnprotectedHeader must be called before #sign()',
      )
    }

    if (!isDisjoint(this.#protectedHeader, this.#unprotectedHeader)) {
      throw new JWSInvalid(
        'JWS Protected and JWS Unprotected Header Parameter names must be disjoint',
      )
    }

    const joseHeader: types.JWSHeaderParameters = {
      ...this.#protectedHeader,
      ...this.#unprotectedHeader,
    }

    const extensions = validateCrit(
      JWSInvalid,
      new Map([['b64', true]]),
      options?.crit,
      this.#protectedHeader,
      joseHeader,
    )

    let b64 = true
    if (extensions.has('b64')) {
      b64 = this.#protectedHeader.b64!
      if (typeof b64 !== 'boolean') {
        throw new JWSInvalid(
          'The "b64" (base64url-encode payload) Header Parameter must be a boolean',
        )
      }
    }

    const { alg } = joseHeader

    if (typeof alg !== 'string' || !alg) {
      throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid')
    }

    checkKeyType(alg, key, 'sign')

    let payloadS: string
    let payloadB: Uint8Array
    if (b64) {
      payloadS = b64u(this.#payload)
      payloadB = encode(payloadS)
    } else {
      payloadB = this.#payload
      payloadS = ''
    }

    let protectedHeaderString: string
    let protectedHeaderBytes: Uint8Array
    if (this.#protectedHeader) {
      protectedHeaderString = b64u(JSON.stringify(this.#protectedHeader))
      protectedHeaderBytes = encode(protectedHeaderString)
    } else {
      protectedHeaderString = ''
      protectedHeaderBytes = new Uint8Array()
    }

    const data = concat(protectedHeaderBytes, encode('.'), payloadB)

    const k = await normalizeKey(key, alg)
    const signature = await sign(alg, k, data)

    const jws: types.FlattenedJWS = {
      signature: b64u(signature),
      payload: payloadS,
    }

    if (this.#unprotectedHeader) {
      jws.header = this.#unprotectedHeader
    }

    if (this.#protectedHeader) {
      jws.protected = protectedHeaderString
    }

    return jws
  }
}
