import { Script } from '@ts-bitcoin/core';
import { HashInfo, HashOutput } from '../../../lib/types.js';
import { Envelope } from 'univrse';
import { Key } from 'univrse';

import { hex, sha256, sha512, generateSaltToDeriveKey } from '../../utils.js';

/**
 * This uses the UNIVRSE protocol, hash type is distinguished by the header { meta: { type: 'hash }}
 */

// TODO:
// - Return op_hex instead of envelope.

/**
 * @category Envelopes
 */
export class Hash {
  public readonly info: HashInfo;
  private envelope: Envelope | null = null;

  private constructor({ info }: { info: HashInfo }) {
    this.info = info;
  }

  public static async create(options: HashOutput): Promise<Hash> {
    if ((options.sign || options.encrypt) && !options.key) {
      throw new Error('Invalid options. Please provide a key to sign and/or encrypt.');
    }

    const digest = options.algorithm === 'SHA-256' ? await sha256(options.data) : await sha512(options.data);
    const digestHex = hex.bufToHex(digest);

    const hash = new Hash({
      info: {
        type: 'hash',
        algorithm: options.algorithm,
        digestHex,
        digest,
      },
    });

    await hash.createEnvelope(options);

    return hash;
  }

  private async createEnvelope({ sign, encrypt, key }: HashOutput): Promise<void> {
    const nonce = generateSaltToDeriveKey(16);
    const headers = {
      proto: 'mintBlue',
      meta: { type: 'hash' },
    };
    // Use Uint8Array digest only
    const data = { digest: this.info.digest, algorithm: this.info.algorithm };
    const env = Envelope.wrap(data, headers);

    if (sign) {
      if (!key) throw new Error('sign is true but no key provided');
      await env.sign(key, { alg: 'ES256K' });
    }
    if (encrypt) {
      if (!key) throw new Error('encrypt is true but no key provided');
      await env.encrypt(key.toPublic(), { alg: 'ECDH-ES+A128GCM', nonce });
    }

    this.envelope = env;
  }

  toOpReturnScriptHex(): string {
    return this.envelope?.toScript().toHex();
  }

  public static async fromOpReturnScript(script: Script, key?: Key | string): Promise<Hash> {
    let envelope = Envelope.fromScript(script);
    const info: Pick<HashInfo, 'encrypt' | 'sign'> = {};
    if (envelope.recipient) {
      if (!key) {
        throw new Error('Cannot decrypt Script without a key.');
      }
      envelope = await envelope.decrypt(key as Key);
      info.encrypt = true;
    }
    if (envelope.signature) {
      info.sign = true;
    }

    return new Hash({
      info: {
        type: 'hash',
        ...envelope.payload, // digest, algorithm
        digestHex: hex.bufToHex(envelope.payload.digest),
        ...info, // sign, encrypt
      },
    });
  }
}
