import bytes from '@root/encoding/bytes.js';
import hex from '@root/encoding/hex.js';
import { Address, Script, Tx, TxOut } from '@ts-bitcoin/core';
import { AxiosInstance } from 'axios';
import axios from 'axios';
import WS from 'isomorphic-ws';
import { Envelope, Key, util } from 'univrse';
import Queue from 'p-queue';

const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

// nature restoration fixed values
const NATURE_SATS_PER_TREE = 23500000;
export const NATURE_ADDRESS = '1co2d2DkpSHd8eHCL8X1kcD2vKt5yAYwx';

import {
  ApiResponse,
  CreateAccesstokenOptions,
  CreatedTransaction,
  CreateEventListenerOptions,
  CreateProjectOptions,
  CreateTransactionOptions,
  DataOutput,
  EventListener,
  experimentalStreamProjectOptions,
  GetEventListenerOptions,
  GetProjectOptions,
  GetTransactionOptions,
  Keys,
  ListTransactionsOptions,
  ListTransactionsResult,
  LoginOptions,
  MB_DERIVING_KEY,
  MB_SIGNING_KEY,
  MintblueConfig,
  MintblueOptions,
  Output,
  OutputType,
  PigiAPIResponse,
  Project,
  QueryTransactionOptions,
  Transaction,
  TransactionsAfterTxidOptions,
  TransactionWithParsedOutputs,
  TransactionWithRawTX,
  UpdateProjectOptions,
  UserKeyAttributes,
} from './types.js';

import { DocV1 } from './envelopes/doc/v1.js';
import { pigiV2 } from './envelopes/envelopes.js';
import { Hash } from './envelopes/hash/v1.js';
import { PeppolV1 } from './envelopes/peppol/v1.js';
import * as utils from './utils.js';
import { APIDerivationService, KeysSDKModule, Secp256k1Key, SupportedKeyDerivationTypes } from './keys/index.js';

const DEFAULT_OPTIONS = {
  url: 'https://api.mintblue.com',
  version: '4',
  SDKTokenPrefix: 'secret-token:mintBlue.com/sdk/',
  ServerTokenPrefix: 'secret-token:mintBlue.com/api/',
};

/**
 * The main SDK class. Instantiate this class using the Mintblue.create() function
 *
 * ```ts
 *   const token = 'YOUR SDK TOKEN';
 *   const project_id = 'YOUR PROJECT ID*
 *   const client = await Mintblue.create({ token: token *
 *   const outputs = [
 *     {
 *       type: 'data',
 *       value: 'Hello world',
 *       sign: true,
 *       encrypt: true,
 *     },
 *   ];
 *
 *   const { txid, rawtx } = await client.createTransaction({ project_id, outputs });
 *   ```
 * @category SDK
 */
export class Mintblue {
  /**
   * @ignore
   */
  private config: MintblueConfig;
  /**
   * @ignore
   */
  private api: AxiosInstance;

  public keys: KeysSDKModule;

  // Supply token as 'server-token'
  /**
   * @ignore
   */
  constructor(config: MintblueConfig) {
    this.config = config;
    this.api =
      (this.config as any).mockApi ||
      axios.create({
        headers: {
          Authorization: 'Bearer ' + this.config.apiToken,
        },
        baseURL: `${this.config.url}/api/v${this.config.version}/`,
      });

    // The key derivation service requires the masterKey to be able to encrypt and send data back to the server.
    if (!this.config.keys) {
      throw new Error(
        'User attribute keys are required to be able to use the SDK. Please provide keys in the config object.',
      );
    }

    this.keys = new KeysSDKModule(new APIDerivationService(this.api, this.config.keys));
  }

  /**
   * Instantiate client with accesstoken (SDK-token)
   *
   * @param options needs to contain an SDK token
   * @returns MintBlue SDK instance
   */
  static async create(options: MintblueOptions): Promise<Mintblue> {
    const opts = { ...DEFAULT_OPTIONS, ...options };

    const { tokenId, tokenSecret, bearerToken } = await utils.parseSDKToken(
      opts.token,
      opts.SDKTokenPrefix,
      opts.ServerTokenPrefix,
    );

    const userKeyAttributes =
      opts.userKeyAttributes ||
      (await axios
        .get(`${opts.url}/api/v${opts.version}/accesstoken/${tokenId}`, {
          headers: {
            authorization: `Bearer ${bearerToken}`,
          },
        })
        .then((res) => {
          return res.data.data.userKeyAttributes;
        }));
    const keys = await utils.getKeysFromUserKeyAttributes(tokenSecret, userKeyAttributes);

    const config: MintblueConfig & { mockApi: any } = {
      apiToken: bearerToken,
      url: opts.url || DEFAULT_OPTIONS.url,
      version: opts.version || DEFAULT_OPTIONS.version,
      mockApi: (options as any).mockApi || null,
      keys,
    };

    const mb = new Mintblue(config);
    console.log('Creating Mintblue class, migrating keys', { mb });
    // Initialise keys from Server.
    config.keys = await mb.initialiseDerivationKeys(keys);

    return mb;
  }

  /**
   * Instantiate client with login (e-mail and password)
   *
   * @param options LoginOptions
   * @returns MintBlue SDK instance
   */
  static async login(options: LoginOptions): Promise<Mintblue> {
    options = { ...DEFAULT_OPTIONS, ...options };

    const hashedPassword = await utils.sha256s(options.password);

    const res = await axios
      .post(`${options.url}/api/v${options.version}/auth`, {
        email: options.email,
        password: hashedPassword,
      })
      .then((res) => res.data);

    const userKeyAttributes: UserKeyAttributes = res.userKeyAttributes;
    const keys: Keys = await utils.getKeysFromUserKeyAttributes(options.password, userKeyAttributes);

    const o: any = options;
    delete o.password;
    delete o.email;

    return new Mintblue({ token: res.token, userKeyAttributes, keys, ...o });
  }

  // this function takes any additional keys from the userKeyAttributes and migrates them to the
  // new key derivation service module.
  async migrateKeys(keys: Keys): Promise<Omit<Keys, 'additionalKeys'>> {
    const [mb_deriving_key, mb_signing_key, peppol_key, kvk_key, bitcoin_key] = await this.keys.getDefaultKeys();
    const additionalKeys = await utils.generateAdditionalKeys(keys.masterKey);

    console.log('Migrating keys', {
      additionalKeys,
      mb_deriving_key,
      mb_signing_key,
      peppol_key,
      kvk_key,
      bitcoin_key,
    });

    const migrateKey = async (
      keyName: string,
      alg: SupportedKeyDerivationTypes = SupportedKeyDerivationTypes.P256_ECDH,
    ) => {
      const key = additionalKeys?.find((x) => x.info === keyName)!.jwk;

      const rawKey = utils.base64.base64ToBuf(key.d!);

      await this.keys.import(keyName, rawKey, alg);
    };

    if (!mb_deriving_key) {
      await migrateKey(MB_DERIVING_KEY);
    }

    if (!mb_signing_key) {
      await migrateKey(MB_SIGNING_KEY, SupportedKeyDerivationTypes.P256_ECDSA);
    }

    if (!peppol_key) {
      await migrateKey('peppol');
    }

    if (!kvk_key) {
      await migrateKey('kvk');
    }

    if (!bitcoin_key) {
      const bitcoinKeyBytes = keys.privateKey.toBuffer();
      await this.keys.import('bitcoin', bitcoinKeyBytes, SupportedKeyDerivationTypes.SECP256K1);

      console.log('Migrated bitcoin key');
    }

    return keys;
  }

  public async initialiseDerivationKeys(keys: Keys) {
    await this.keys.getKeys();
    if (await this.keys.verifyKeyMigrationNeeded()) {
      return await this.migrateKeys(keys);
    }
    return keys;
  }

  /**
   * Create access token
   *
   * @param options
   * @returns SDK token & API token
   */
  public async createAccesstoken(options: CreateAccesstokenOptions): Promise<{ sdkToken: string; apiToken: string }> {
    const secret = (await import('nanoid')).customAlphabet(alphabet, 20)();
    const hashedSecret = await utils.sha256s(secret);

    const { userKeyAttributes } = await utils.generateUserKeyAttributes(secret, this.config.keys);

    const body = {
      ...options,
      secret: hashedSecret,
      userKeyAttributes,
    };

    const res = await this.api.post('accesstoken', body);

    const id = res.data.data.accesstoken.id;

    // TODO: return sdkToken and apiToken
    return {
      sdkToken: `${DEFAULT_OPTIONS.SDKTokenPrefix}${id}.${secret}`,
      apiToken: `${DEFAULT_OPTIONS.ServerTokenPrefix}${id}.${hashedSecret}`,
    };
  }

  /**
   * @ignore
   */
  public async createPigi(fileContent: Uint8Array): Promise<PigiAPIResponse> {
    return this.api
      .postForm('pigi/upload', {
        file: new Blob([fileContent]),
      })
      .then((res) => res.data);
  }

  /**
   * @ignore
   */
  private async createAPIOptions(options: CreateTransactionOptions): Promise<any> {
    const bitcoinKey = (await this.keys.findKeyByName('bitcoin')) as Secp256k1Key | undefined;
    if (!bitcoinKey) {
      throw new Error('Bitcoin key not found');
    }

    const convertedKey = util.fromBsvPrivKey(bitcoinKey.toBitcoinKey());
    const apiOptions: any = {
      ...options,
      outputs: [],
    };
    apiOptions.outputs = await Promise.all(
      options.outputs.map(async (output) => {
        // If encrypt or sign is not explicitly false, set it to true
        if (output.type in ['hash', 'data', 'file'] && (output as any).encrypt !== false) {
          (output as any).encrypt == true;
        }
        if (output.type in ['hash', 'data', 'file'] && (output as any).sign !== false) {
          (output as any).sign == true;
        }
        if (output.type === 'data' || output.type === 'file') {
          const nonce = await utils.generateSaltToDeriveKey(16);
          const headers =
            output.type === 'file'
              ? {
                  proto: 'mintBlue',
                  meta: { type: 'file', contentType: output.value.contentType },
                }
              : { proto: 'mintBlue', meta: {} };
          const env = Envelope.wrap(output.value, headers);

          // TODO: determine algos
          if (output.sign) {
            await env.sign(convertedKey, { alg: 'ES256K' });
          }
          if (output.encrypt) {
            // TODO: derive key
            await env.encrypt(convertedKey.toPublic(), {
              alg: 'ECDH-ES+A128GCM',
              nonce,
            });
          }

          delete output.sign;
          delete output.encrypt;

          return { type: 'env', value: env.toString() };
        } else if (output.type === OutputType.pigi) {
          if (output.version === 1) {
            //
          } else if (output.version === 2) {
            const signingKey = await utils.crypto.subtle.importKey(
              'jwk',
              output.signingKey!,
              {
                name: 'ECDSA',
                namedCurve: 'P-256',
              },
              true,
              ['sign'],
            );

            const p = await pigiV2.create(output.value, [signingKey], output.options);

            // Todo remove @bitcoin-ts/core in favor of bsv wasm or nimble to get rid of these buffers
            const script = Script.fromSafeDataArray([
              Buffer.from(bytes.strToBuf(pigiV2.PROTOCOL_ID)),
              Buffer.from(p.encryptedData),
            ]);

            return { type: 'script', encoding: 'hex', value: script.toHex() };
          }
        } else if (output.type == OutputType.peppol) {
          if (output.version !== 1) {
            throw new Error('Peppol unsupported version. Try version 1');
          }
          const p = await PeppolV1.create(output);
          return {
            type: 'script',
            encoding: 'hex',
            value: p.toOpReturnScriptHex(),
          };
        } else if (output.type == OutputType.doc) {
          const docv1 = await DocV1.create(output);
          return {
            type: 'script',
            encoding: 'hex',
            value: docv1.toOpReturnScriptHex(),
          };
        } else if (output.type === OutputType.hash) {
          const hashed = await Hash.create({ ...output, key: convertedKey });
          return {
            type: 'script',
            encoding: 'hex',
            value: hashed.toOpReturnScriptHex(),
          };
        } else if (output.type === OutputType.tree) {
          if (output.trees < 0.01) {
            throw new Error('Must be at least 0.01 trees.');
          }
          return {
            type: 'payment',
            to: NATURE_ADDRESS,
            satoshis: Math.ceil(output.trees * NATURE_SATS_PER_TREE),
          };
        } else if (output.type === OutputType.script) {
          if (!output.encoding) {
            output.encoding = 'hex';
          }
          return {
            type: 'script',
            encoding: output.encoding,
            value: output.value,
          };
        } else {
          if (!(<any>Object).values(OutputType).includes(output.type)) {
            throw new Error('Unsupported output type');
          }
        }
        return output;
      }),
    );

    return apiOptions;
  }

  /**
   * Create a blockchain transaction
   *
   * @param options
   * @returns Published transaction
   */
  public async createTransaction(options: CreateTransactionOptions): Promise<CreatedTransaction> {
    const apiOptions = await this.createAPIOptions(options);
    return this.api.post('transaction', apiOptions).then((res) => res.data.data);
  }

  /**
   * Fetch a blockchain transaction
   *
   * @param txid Transaction ID of the blockchain transaction
   * @param parse When true, will automatically parse all outputs of the transaction and return it in the outputs field of the result
   * @param secret Optional secret to decrypt content (Unnecessary when encrypted with your keys, decryption will automatically run)
   * @returns A transaction
   */
  public async getTransaction(options: {
    txid: string;
    parse: true;
    secret?: string;
  }): Promise<TransactionWithParsedOutputs>;
  public async getTransaction(options: { txid: string; parse: false; secret?: string }): Promise<TransactionWithRawTX>;
  public async getTransaction(
    options: GetTransactionOptions,
  ): Promise<TransactionWithParsedOutputs | TransactionWithRawTX> {
    const res = await this.api.get(`transaction/${options.txid}`);
    const tx = res.data.data;

    // Even when default options now points to version >1 there's nothing that stops the user to request v1 data, hence a compatibility check.
    const rawtx = this.config.version === '1' ? hex.bufToHex(new Uint8Array(tx.rawtx.data)) : tx.rawtx;

    if (options.parse) {
      tx.outputs = await this.parseTransaction(options.txid, rawtx, options.secret);
    }

    return tx;
  }

  /**
   * @ignore
   */
  public async parseTransaction(txid: string, rawtx: string, secret?: string): Promise<Output[]> {
    const convertedKey = util.fromBsvPrivKey(this.config.keys!.privateKey);
    const bsvTx = Tx.fromHex(rawtx);
    const outputs: any = [];

    for (const txOut of bsvTx.txOuts) {
      outputs.push(await this.parseOutput(txid, txOut, secret ? secret : convertedKey));
    }
    return outputs as Output[];
  }

  /**
   * List transactions
   *
   * @param options
   * @returns Array of transactions (without payload)
   */
  public async listTransactions(options: ListTransactionsOptions): Promise<ListTransactionsResult> {
    const params = {
      ...options,
      start_date: options.startDate?.toISOString(),
      end_date: options.startDate?.toISOString(),
    };

    return this.api.get('transaction', { params }).then((res) => res.data.data);
  }

  /**
   * List transactions in a project that happend after a specific txid
   *
   * @param options
   * @returns Array of transactions (without payload)
   */
  public async transactionsAfterTxid(options: TransactionsAfterTxidOptions): Promise<TransactionWithRawTX[]> {
    return this.api.get(`transaction/from/${options.project_id}/${options.txid}`).then((res) => res.data.data);
  }

  /**
   * Stream Project (experimental)
   *
   * This function allows you to stream experimental data from a project.
   *
   * @param {experimentalStreamProjectOptions} options - The options for streaming experimental data.
   * @returns {Promise<{ close: () => void }>} A promise that resolves with an object containing a `close` method.
   * The `close` method can be used to gracefully close the streaming connection.
   *
   * @experimental This function is experimental and subject to change. Use it with caution.
   */
  public async experimentalStreamProject(options: experimentalStreamProjectOptions): Promise<{ close: () => void }> {
    const queue = new ((Queue as any).default ? (Queue as any).default : Queue)({ concurrency: 1 }); // ESM <> CommonJS hack
    const url = new URL(this.config.url!);

    const token = (this.api.defaults.headers.Authorization as string).split(DEFAULT_OPTIONS.ServerTokenPrefix!)[1];

    let params = new URLSearchParams({
      project_id: options.project_id,
      ...(options.history ? { history: '1' } : {}),
      ...(options.last_event_id ? { last_event_id: options.last_event_id } : {}),
    });

    const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
    let ws: WebSocket | null = null;

    const connect = () => {
      ws = new WS(`${protocol}//${url.host}/ws/project?${params.toString()}`, token);

      ws!.onopen = () => {};

      ws!.onerror = (e: Event) => {
        // As clients also could handle a reconnect mechanism, we inform them that is already being attempted
        if (e.target && (e.target as any)._closeCode === 1006) {
          // Node
          options.onError(new Error(`Server disconnected. Reconnecting...`));
        } else if (e.type === 'error' && e.target && (e.target as any).readyState === WebSocket.CLOSED) {
          // browser
          options.onError(new Error(`Server disconnected. Reconnecting...`));
        } else {
          options.onError(e as any);
        }
      };

      ws!.onmessage = async (ev: MessageEvent) => {
        const tx = JSON.parse(ev.data);

        await queue.add(() => options.onTransaction(tx));

        // Update the last event id
        params = new URLSearchParams({
          project_id: options.project_id,
          last_event_id: tx.txid,
          history: '1',
        });
      };

      ws!.onclose = (_reason: CloseEvent) => {
        connect();
      };
    };

    connect();

    return {
      close: () => {
        if (ws) {
          // Remove the onclose handler so it does not reconnect.
          ws.onclose = null;
          ws.close();
          ws = null;
        }
      },
    };
  }

  /**
   * Create a project
   *
   * @param options
   * @returns Array of transactions (without payload)
   */
  public async createProject(options: CreateProjectOptions): Promise<Project> {
    const response = await this.api.post<ApiResponse<Project>>('project', options);
    return response.data.data;
  }

  /**
   * Retrieves project data with the specified ID.
   * @param {Object} params - The parameters for retrieving the project.
   * @param {string} params.id - The ID of the project to retrieve.
   * @returns {Promise<Project>} A promise that resolves with the retrieved project data.
   */
  public async getProject(options: GetProjectOptions): Promise<Project> {
    const response = await this.api.get<ApiResponse<Project>>(`project/${options.id}`);
    return response.data.data;
  }

  /**
   * Updates a project with the specified ID using the provided data.
   * @param {Object} params - The parameters for updating the project.
   * @param {string} params.id - The ID of the project to update.
   * @param {UpdateProjectOptions} params.data - The data to update the project with.
   * @returns {Promise<Project>} A promise that resolves with the updated project data.
   */
  public async updateProject(options: UpdateProjectOptions): Promise<Project> {
    const response = await this.api.patch<ApiResponse<Project>>(`project/${options.id}`, options.data);
    return response.data.data;
  }

  /**
   * Retrieves a list of projects.
   * @returns {Promise<Project[]>} A promise that resolves with an array of project data.
   */
  public async listProjects(): Promise<Project[]> {
    const response = await this.api.get<ApiResponse<Project[]>>('project');
    return response.data.data;
  }

  /**
   * Deletes a project with the specified ID.
   * @param {Object} params - The parameters for deleting the project.
   * @param {string} params.id - The ID of the project to delete.
   * @returns {Promise<void>} A promise that resolves when the project is deleted.
   */
  public async destroyProject(options: GetProjectOptions): Promise<void> {
    await this.api.delete(`project/${options.id}`);
  }

  /**
   * Retrieves a list of event listeners.
   * @returns {Promise<EventListener[]>} A promise that resolves with an array of event listener data.
   */
  public async listEventListeners(): Promise<EventListener[]> {
    const response = await this.api.get<ApiResponse<EventListener[]>>('subscription');
    return response.data.data;
  }

  /**
   * Retrieves event listener data with the specified ID.
   * @param {string} id - The ID of the event listener to retrieve.
   * @returns {Promise<EventListener>} A promise that resolves with the retrieved event listener data.
   */
  public async getEventListener(options: GetEventListenerOptions): Promise<EventListener> {
    const response = await this.api.get<ApiResponse<EventListener>>(`subscription/${options.id}`);
    return response.data.data;
  }

  /**
   * Creates a new event listener.
   * @param {EventListener} data - The data for creating the event listener.
   * @returns {Promise<EventListener>} A promise that resolves with the created event listener data.
   */
  public async createEventListener(data: CreateEventListenerOptions): Promise<EventListener> {
    const response = await this.api.post<ApiResponse<EventListener>>('subscription', data);
    return response.data.data;
  }

  /**
   * @experimental
   * Get Peppol Key information
   * @returns Object containing uncompressed public key in hex format. \
   * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
   */
  async experimentalGetPeppolKeyInfo() {
    const key = await this.keys.findKeyByName('peppol');
    const peppolJWK = await key!.toJWK({ includePrivate: true });

    const uncompressedPublicKey = utils.JWKToUncompressedHex(peppolJWK);

    return {
      uncompressedPublicKey,
      compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
      jwk: peppolJWK,
      kid: peppolJWK!.kid,
      info: 'peppol',
    };
  }

  /**
   * @experimental
   * Get Peppol Key information
   * @returns Object containing uncompressed public key in hex format. \
   * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
   */
  async experimentalGetKVKKeyInfo() {
    const key = await this.keys.findKeyByName('kvk');
    if (!key) {
      throw new Error('KVK key not found');
    }
    const kvkJWK = await key.toJWK({ includePrivate: true });

    const uncompressedPublicKey = utils.JWKToUncompressedHex(kvkJWK);

    return {
      uncompressedPublicKey,
      compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
      jwk: kvkJWK,
      kid: kvkJWK.kid,
      info: 'kvk',
      SHA1Fingerprint: await utils.getSHA1Fingerprint(kvkJWK),
    };
  }

  /**
   * @experimental
   * Get Signing Key information
   * @returns Object containing uncompressed public key in hex format. \
   * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
   */
  async experimentalGetSigningKeyInfo() {
    const key = await this.keys.findKeyByName(MB_SIGNING_KEY);

    if (!key) {
      throw new Error('Signing key not found');
    }
    const signingJWK = await key.toJWK({ includePrivate: true });

    const uncompressedPublicKey = utils.JWKToUncompressedHex(signingJWK);

    return {
      uncompressedPublicKey,
      compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
      jwk: signingJWK,
      kid: signingJWK.kid,
      info: MB_SIGNING_KEY,
      SHA1Fingerprint: await utils.getSHA1Fingerprint(signingJWK),
    };
  }

  /**
   * @experimental
   * Get Derive Key information
   * @returns Object containing uncompressed public key in hex format. \
   * Read more about compressed and decompressed keys [here](https://medium.com/asecuritysite-when-bob-met-alice/02-03-or-04-so-what-are-compressed-and-uncompressed-public-keys-6abcb57efeb6)
   */
  async experimentalGetDeriveKeyInfo() {
    const key = await this.keys.findKeyByName(MB_DERIVING_KEY);
    // const key = await this.keys.findKeyByName(MB_DERIVING_KEY);
    if (!key) {
      throw new Error('Derive key not found');
    }
    const deriveJWK = await key.toJWK({ includePrivate: true });

    const uncompressedPublicKey = utils.JWKToUncompressedHex(deriveJWK);

    return {
      uncompressedPublicKey,
      compressedPublicKey: utils.compressPublicKey(uncompressedPublicKey),
      jwk: deriveJWK,
      kid: deriveJWK.kid,
      info: MB_DERIVING_KEY,
      SHA1Fingerprint: await utils.getSHA1Fingerprint(deriveJWK),
    };
  }

  /**
   * Queries transactions based on the provided options.
   *
   * @remarks
   * This method can return transactions with or without the raw transaction data based on the `rawtx` option.
   *
   * @param options - The options to query transactions. If `rawtx` is set to true, the result will include raw transaction data.
   * @returns A promise that resolves to an array of transactions. If `rawtx` is true, the transactions will include raw data.
   *
   * @example
   * ```typescript
   * const transactions = await queryTransactions({ where: { metadata: { myfield: { $like: 'prefix%' }}}});
   * const transactionsWithRaw = await queryTransactions({ where: { txid: '0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9' }, rawtx: true });
   * ```
   */
  public async queryTransactions(options: QueryTransactionOptions & { rawtx: true }): Promise<TransactionWithRawTX[]>;
  public async queryTransactions(options: QueryTransactionOptions): Promise<Transaction[]>;
  public async queryTransactions(options: QueryTransactionOptions): Promise<Transaction[] | TransactionWithRawTX[]> {
    const response = await this.api.post<ApiResponse<Transaction[] | TransactionWithRawTX[]>>(
      'transaction/query',
      options,
    );
    return response.data.data;
  }

  /**
   * @ignore
   */
  private async parseOutput(_txid: string, txOut: TxOut, secret: Key | string) {
    const outputType = utils.identifyOutput(txOut);
    let output: Partial<Output>;
    if (outputType === OutputType.data || outputType === OutputType.file) {
      let envelope = Envelope.fromScript(txOut.script);
      output = {
        type: OutputType[outputType],
        encrypt: false,
        sign: false,
      } as DataOutput;
      if (envelope.recipient) {
        envelope = await envelope.decrypt(secret as Key);
        output.encrypt = true;
      }
      if (envelope.signature) {
        // TODO: verify
        output.sign = true;
      }
      output.value = envelope.payload;
    } else if (outputType === OutputType.payment) {
      const address = Address.fromTxOutScript(txOut.script).toString();
      output = {
        type: OutputType[outputType],
        to: address,
        satoshis: txOut.valueBn.toNumber(),
      };
    } else if (outputType === OutputType.peppol) {
      const key = await this.keys.findKeyByName('peppol');
      if (!key) {
        throw new Error('Peppol key not found');
      }
      const p = await PeppolV1.fromOpReturnScriptHex(txOut.script.toHex(), {
        privateKey: await key.toJWK({ includePrivate: true }),
      });

      output = p.info;
    } else if (outputType === OutputType.doc) {
      const data = txOut.script.chunks[3].buf;
      if (!data) {
        throw new Error('Could not get script chunk [3] from output');
      }

      const key = await this.keys.findKeyByName(MB_DERIVING_KEY);
      if (!key) {
        throw new Error('Could not key MB_DERIVING_KEY');
      }

      const payload = await DocV1.parse({
        encryptedData: data,
        receiverKey: await this.keys.asJoseGeneralDecryptGetKey(),
        secret1: null as any,
        secret2: null as any,
        verifyKey: null as any,
      });

      return payload.info;
    } else if (outputType === OutputType.hash) {
      const hash = await Hash.fromOpReturnScript(txOut.script, secret);
      return hash.info;
    } else if (outputType === OutputType.tree) {
      output = {
        type: OutputType[outputType],
        trees: txOut.valueBn.toNumber() / NATURE_SATS_PER_TREE,
      };
    } else {
      throw new Error('Could not automatically parse output');
    }

    return output;
  }
}
