import base64 from '@root/encoding/base64.js';
import { Script } from '@ts-bitcoin/core';
import cbor from 'cbor';
import * as jose from 'jose';

import { cbor2jose, jose2cbor } from '../../cbor-jose.js';
import * as lzma from '../../lzma.js';
import { JWKOrHexKeyToCryptoKeyAndJWK, JWKToUncompressedHex } from '../../utils.js';

export interface PeppolInfo {
  type: 'peppol';
  version: 1;
  value: Buffer;
  pubKey: string;
  options?: {
    extra: any;
  };
  encryptedData: Buffer;
}

export interface PeppolOutput {
  type: 'peppol';
  /** Protocol version */
  version: 1;
  /** Content (AS4 payload) to publish encrypted to blockchain. */
  value: Buffer;
  /** Uncompressed public key in hex format or JWK format */
  pubKey: string | jose.JWK;
  /** Optional key to add digital signature in JWK format */
  signingKey?: jose.JWK;
  /** Optional extra data to publish in blockchain transaction */
  options?: {
    extra: Buffer;
  };
}

export interface PeppolParseOptions {
  data: Buffer;
  privateKey: jose.JWK;
  pubKey: string;
  verifyKey?: jose.JWK;
}

export class PeppolV1 {
  static PROTOCOL_ID = 'mB:peppol';

  public readonly createOptions: PeppolOutput | null;
  public readonly parseOptions: PeppolParseOptions | null;
  public readonly info: PeppolInfo;

  private constructor(options: { create: PeppolOutput | null; parse: PeppolParseOptions | null; info: PeppolInfo }) {
    this.createOptions = options.create;
    this.parseOptions = options.parse;
    this.info = options.info;
  }

  public static async create(options: PeppolOutput): Promise<PeppolV1> {
    const { jwk: pubKeyJWK } = await JWKOrHexKeyToCryptoKeyAndJWK(options.pubKey, 'ECDH', 'p256');
    const importedPubKey = await jose.importJWK(pubKeyJWK, 'ECDH-ES');

    const payload = await cbor.encodeAsync([options.value, options.options?.extra]);

    const compressed = await lzma.compress(payload, 9);

    const l0 = await new jose.GeneralEncrypt(compressed)
      .setProtectedHeader({ enc: 'A256GCM' })
      .addRecipient(importedPubKey)
      .setUnprotectedHeader({ alg: 'ECDH-ES+A256KW' })
      .encrypt();

    const cl0 = await jose2cbor(l0);

    let final = cl0;

    if (options.signingKey) {
      const _l1 = await new jose.GeneralSign(cl0);

      const { jwk, cryptoKey } = await JWKOrHexKeyToCryptoKeyAndJWK(options.signingKey, 'ECDSA', 'p256');
      const kid = await jose.calculateJwkThumbprint(jwk);
      _l1.addSignature(cryptoKey).setProtectedHeader({ alg: 'ES256', kid: kid });

      const l1 = await _l1.sign();
      final = await jose2cbor(l1);
    }

    const uncompressedPublicKey = JWKToUncompressedHex(pubKeyJWK);

    const info: PeppolInfo = {
      type: 'peppol',
      version: 1,
      options: {
        extra: options.options?.extra,
      },
      pubKey: uncompressedPublicKey,
      value: options.value,
      encryptedData: final,
    };

    return new PeppolV1({ create: options, parse: null, info: info });
  }

  public static async parse(options: PeppolParseOptions): Promise<PeppolV1> {
    const { data, privateKey, verifyKey } = options;
    const importedPrivateKey = await jose.importJWK(privateKey, 'ECDH-ES');

    const l1 = await cbor2jose(data);

    let l0;

    if ('ciphertext' in l1) {
      l0 = l1;
    } else if ('signatures' in l1) {
      // Optionally verify
      if (verifyKey) {
        const l1verify = await jose.generalVerify(l1, await jose.importJWK(verifyKey, 'ES256'));
        l0 = await cbor2jose(l1verify.payload as Buffer);
      } else {
        // Signature present but not choosing to verify
        l0 = await cbor2jose(base64.base64ToBuf(l1.payload));
      }
    }

    const l0decrypted = await jose.generalDecrypt(l0, importedPrivateKey, {});

    const l0decrypteddata = l0decrypted.plaintext;

    const decompressed = await lzma.decompress(l0decrypteddata as Buffer);

    const [originaldata, extra] = cbor.decode(decompressed);

    const info: PeppolInfo = {
      type: 'peppol',
      encryptedData: data,
      options: {
        extra: extra,
      },
      pubKey: options.pubKey,
      value: originaldata,
      version: 1,
    };

    return new PeppolV1({ create: null, parse: options, info: info });
  }

  public static async fromOpReturnScriptHex(script: string, options: Omit<PeppolParseOptions, 'data' | 'pubKey'>) {
    const s = Script.fromHex(script);

    return PeppolV1.parse({
      data: s.chunks[s.chunks.length - 2].buf!,
      privateKey: options.privateKey,
      pubKey: s.chunks[s.chunks.length - 1].buf!.toString('hex'),
      verifyKey: options.verifyKey,
    });
  }

  public toOpReturnScriptHex(): string {
    const s = Script.fromSafeDataArray([
      Buffer.from(PeppolV1.PROTOCOL_ID),
      this.info.encryptedData,
      Buffer.from(this.info.pubKey),
    ]);

    return s.toHex();
  }
}
