import * as logger from 'winston';
import * as bitcoinjs from 'bitcoinjs-lib';
import * as readline from 'readline';
import * as stream from 'stream';
import * as fs from 'fs';
import * as blockstack from 'blockstack';
import {
  getTypeString,
  ClarityAbiType,
  isClarityAbiPrimitive,
  isClarityAbiStringAscii,
  isClarityAbiStringUtf8,
  isClarityAbiBuffer,
  isClarityAbiResponse,
  isClarityAbiOptional,
  isClarityAbiTuple,
  isClarityAbiList,
  ClarityValue,
  intCV,
  uintCV,
  bufferCVFromString,
  stringAsciiCV,
  stringUtf8CV,
  someCV,
  trueCV,
  falseCV,
  standardPrincipalCV,
  TransactionVersion,
} from '@stacks/transactions';

import { StacksNetwork } from '@stacks/network';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const ZoneFile = require('zone-file');

import {
  PRIVATE_KEY_NOSIGN_PATTERN,
  PRIVATE_KEY_PATTERN,
  PRIVATE_KEY_MULTISIG_PATTERN,
  PRIVATE_KEY_SEGWIT_P2SH_PATTERN,
  ID_ADDRESS_PATTERN,
} from './argparse';

import { CLITransactionSigner, isCLITransactionSigner } from './common';

import { decryptBackupPhrase } from './encrypt';

import { getOwnerKeyInfo, getApplicationKeyInfo, extractAppKey } from './keys';

import { NameInfoType, CLINetworkAdapter } from './network';

export interface UTXO {
  value?: number;
  confirmations?: number;
  tx_hash: string;
  tx_output_n: number;
}

export class NullSigner extends CLITransactionSigner {}

export class MultiSigKeySigner extends CLITransactionSigner {
  redeemScript: Buffer;
  privateKeys: string[];
  m: number;

  constructor(redeemScript: string, privateKeys: string[]) {
    super();
    this.redeemScript = Buffer.from(redeemScript, 'hex');
    this.privateKeys = privateKeys;
    this.isComplete = true;
    try {
      // try to deduce m (as in m-of-n)
      const chunks = bitcoinjs.script.decompile(this.redeemScript);
      const firstOp = chunks![0];
      this.m = parseInt(bitcoinjs.script.toASM([firstOp]).slice(3), 10);
      this.address = bitcoinjs.address.toBase58Check(
        bitcoinjs.crypto.hash160(this.redeemScript),
        blockstack.config.network.layer1.scriptHash
      );
    } catch (e) {
      logger.error(e);
      throw new Error('Improper redeem script for multi-sig input.');
    }
  }

  getAddress(): Promise<string> {
    return Promise.resolve().then(() => this.address);
  }

  signTransaction(txIn: bitcoinjs.TransactionBuilder, signingIndex: number): Promise<void> {
    return Promise.resolve().then(() => {
      const keysToUse = this.privateKeys.slice(0, this.m);
      keysToUse.forEach(keyHex => {
        const ecPair = blockstack.hexStringToECPair(keyHex);
        txIn.sign(signingIndex, ecPair, this.redeemScript);
      });
    });
  }

  signerVersion(): number {
    return 0;
  }
}

export class SegwitP2SHKeySigner extends CLITransactionSigner {
  redeemScript: Buffer;
  witnessScript: Buffer;
  privateKeys: string[];
  m: number;

  constructor(redeemScript: string, witnessScript: string, m: number, privateKeys: string[]) {
    super();
    this.redeemScript = Buffer.from(redeemScript, 'hex');
    this.witnessScript = Buffer.from(witnessScript, 'hex');
    this.address = bitcoinjs.address.toBase58Check(
      bitcoinjs.crypto.hash160(this.redeemScript),
      blockstack.config.network.layer1.scriptHash
    );

    this.privateKeys = privateKeys;
    this.m = m;
    this.isComplete = true;
  }

  getAddress(): Promise<string> {
    return Promise.resolve().then(() => this.address);
  }

  findUTXO(txIn: bitcoinjs.TransactionBuilder, signingIndex: number, utxos: UTXO[]): UTXO {
    // NOTE: this is O(n*2) complexity for n UTXOs when signing an n-input transaction
    // NOTE: as of bitcoinjs-lib 4.x, the "tx" field is private
    const private_tx = (txIn as any).__TX;
    const txidBuf = new Buffer(private_tx.ins[signingIndex].hash.slice());
    const outpoint = private_tx.ins[signingIndex].index;

    txidBuf.reverse(); // NOTE: bitcoinjs encodes txid as big-endian
    const txid = txidBuf.toString('hex');

    for (let i = 0; i < utxos.length; i++) {
      if (utxos[i].tx_hash === txid && utxos[i].tx_output_n === outpoint) {
        if (!utxos[i].value) {
          throw new Error(`UTXO for hash=${txid} vout=${outpoint} has no value`);
        }
        return utxos[i];
      }
    }
    throw new Error(`No UTXO for input hash=${txid} vout=${outpoint}`);
  }

  signTransaction(txIn: bitcoinjs.TransactionBuilder, signingIndex: number): Promise<void> {
    // This is an interface issue more than anything else.  Basically, in order to
    // form the segwit sighash, we need the UTXOs.  If we knew better, we would have
    // blockstack.js simply pass the consumed UTXO into this method.  But alas, we do
    // not.  Therefore, we need to re-query them.  This is probably fine, since we're
    // not pressured for time when it comes to generating transactions.
    return Promise.resolve()
      .then(() => {
        return this.getAddress();
      })
      .then(address => {
        return blockstack.config.network.getUTXOs(address);
      })
      .then(utxos => {
        const utxo = this.findUTXO(txIn, signingIndex, utxos);
        if (this.m === 1) {
          // p2sh-p2wpkh
          const ecPair = blockstack.hexStringToECPair(this.privateKeys[0]);
          txIn.sign(signingIndex, ecPair, this.redeemScript, undefined, utxo.value);
        } else {
          // p2sh-p2wsh
          const keysToUse = this.privateKeys.slice(0, this.m);
          keysToUse.forEach(keyHex => {
            const ecPair = blockstack.hexStringToECPair(keyHex);
            txIn.sign(
              signingIndex,
              ecPair,
              this.redeemScript,
              undefined,
              utxo.value,
              this.witnessScript
            );
          });
        }
      });
  }

  signerVersion(): number {
    return 0;
  }
}

export function hasKeys(signer: string | CLITransactionSigner): boolean {
  if (isCLITransactionSigner(signer)) {
    const s = signer;
    return s.isComplete;
  } else {
    return true;
  }
}

/*
 * Parse a string into a NullSigner
 * The string has the format "nosign:address"
 * @return a NullSigner instance
 */
export function parseNullSigner(addrString: string): NullSigner {
  if (!addrString.startsWith('nosign:')) {
    throw new Error('Invalid nosign string');
  }

  const addr = addrString.slice('nosign:'.length);
  return new NullSigner(addr);
}

/*
 * Parse a string into a MultiSigKeySigner.
 * The string has the format "m,pk1,pk2,...,pkn"
 * @serializedPrivateKeys (string) the above string
 * @return a MultiSigKeySigner instance
 */
export function parseMultiSigKeys(serializedPrivateKeys: string): MultiSigKeySigner {
  const matches = serializedPrivateKeys.match(PRIVATE_KEY_MULTISIG_PATTERN);
  if (!matches) {
    throw new Error('Invalid multisig private key string');
  }

  const m = parseInt(matches[1]);
  const parts = serializedPrivateKeys.split(',');
  const privkeys = [];
  for (let i = 1; i < 256; i++) {
    const pk = parts[i];
    if (!pk) {
      break;
    }

    if (!pk.match(PRIVATE_KEY_PATTERN)) {
      throw new Error('Invalid private key string');
    }

    privkeys.push(pk);
  }

  // generate public keys
  const pubkeys = privkeys.map(pk => {
    return Buffer.from(getPublicKeyFromPrivateKey(pk), 'hex');
  });

  // generate redeem script
  const multisigInfo = bitcoinjs.payments.p2ms({ m, pubkeys });
  return new MultiSigKeySigner(multisigInfo.output!.toString('hex'), privkeys);
}

/*
 * Parse a string into a SegwitP2SHKeySigner
 * The string has the format "segwit:p2sh:m,pk1,pk2,...,pkn"
 * @serializedPrivateKeys (string) the above string
 * @return a MultiSigKeySigner instance
 */
export function parseSegwitP2SHKeys(serializedPrivateKeys: string): SegwitP2SHKeySigner {
  const matches = serializedPrivateKeys.match(PRIVATE_KEY_SEGWIT_P2SH_PATTERN);
  if (!matches) {
    throw new Error('Invalid segwit p2sh private key string');
  }

  const m = parseInt(matches[1]);
  const parts = serializedPrivateKeys.split(',');
  const privkeys = [];
  for (let i = 1; i < 256; i++) {
    const pk = parts[i];
    if (!pk) {
      break;
    }

    if (!pk.match(PRIVATE_KEY_PATTERN)) {
      throw new Error('Invalid private key string');
    }

    privkeys.push(pk);
  }

  // generate public keys
  const pubkeys = privkeys.map(pk => {
    return Buffer.from(getPublicKeyFromPrivateKey(pk), 'hex');
  });

  // generate redeem script for p2wpkh or p2sh, depending on how many keys
  let redeemScript: string;
  let witnessScript = '';
  if (m === 1) {
    // p2wpkh
    const p2wpkh = bitcoinjs.payments.p2wpkh({ pubkey: pubkeys[0] });
    const p2sh = bitcoinjs.payments.p2sh({ redeem: p2wpkh });

    redeemScript = p2sh.redeem!.output!.toString('hex');
  } else {
    // p2wsh
    const p2ms = bitcoinjs.payments.p2ms({ m, pubkeys });
    const p2wsh = bitcoinjs.payments.p2wsh({ redeem: p2ms });
    const p2sh = bitcoinjs.payments.p2sh({ redeem: p2wsh });

    redeemScript = p2sh.redeem!.output!.toString('hex');
    witnessScript = p2wsh.redeem!.output!.toString('hex');
  }

  return new SegwitP2SHKeySigner(redeemScript, witnessScript, m, privkeys);
}

/*
 * Decode one or more private keys from a string.
 * Can be used to parse single private keys (as strings),
 * or multisig bundles (as CLITransactionSigners)
 * @serializedPrivateKey (string) the private key, encoded
 * @return a CLITransactionSigner or a String
 */
export function decodePrivateKey(serializedPrivateKey: string): string | CLITransactionSigner {
  const nosignMatches = serializedPrivateKey.match(PRIVATE_KEY_NOSIGN_PATTERN);
  if (!!nosignMatches) {
    // no private key
    return parseNullSigner(serializedPrivateKey);
  }

  const singleKeyMatches = serializedPrivateKey.match(PRIVATE_KEY_PATTERN);
  if (!!singleKeyMatches) {
    // one private key
    return serializedPrivateKey;
  }

  const multiKeyMatches = serializedPrivateKey.match(PRIVATE_KEY_MULTISIG_PATTERN);
  if (!!multiKeyMatches) {
    // multisig bundle
    return parseMultiSigKeys(serializedPrivateKey);
  }

  const segwitP2SHMatches = serializedPrivateKey.match(PRIVATE_KEY_SEGWIT_P2SH_PATTERN);
  if (!!segwitP2SHMatches) {
    // segwit p2sh bundle
    return parseSegwitP2SHKeys(serializedPrivateKey);
  }

  throw new Error('Unparseable private key');
}

type AnyJson = string | number | boolean | null | { [property: string]: AnyJson } | AnyJson[];

/*
 * JSON stringify helper
 * -- if stdout is a TTY, then pretty-format the JSON
 * -- otherwise, print it all on one line to make it easy for programs to consume
 */
export function JSONStringify(obj: AnyJson, stderr: boolean = false): string {
  if ((!stderr && process.stdout.isTTY) || (stderr && process.stderr.isTTY)) {
    return JSON.stringify(obj, null, 2);
  }
  return JSON.stringify(obj);
}

/*
 * Get a private key's public key, while honoring the 01 to compress it.
 * @privateKey (string) the hex-encoded private key
 */
export function getPublicKeyFromPrivateKey(privateKey: string): string {
  const ecKeyPair = blockstack.hexStringToECPair(privateKey);
  return ecKeyPair.publicKey.toString('hex');
}

/*
 * Get the canonical form of a hex-encoded private key
 * (i.e. strip the trailing '01' if present)
 */
export function canonicalPrivateKey(privkey: string): string {
  if (privkey.length == 66 && privkey.slice(-2) === '01') {
    return privkey.substring(0, 64);
  }
  return privkey;
}

/*
 * Hash160 function for zone files
 */
export function hash160(buff: Buffer): Buffer {
  return bitcoinjs.crypto.hash160(buff);
}

/*
 * Sign a profile into a JWT
 */
// eslint-disable-next-line @typescript-eslint/ban-types
export function makeProfileJWT(profileData: Object, privateKey: string): string {
  const signedToken = blockstack.signProfileToken(profileData, privateKey);
  const wrappedToken = blockstack.wrapProfileToken(signedToken);
  const tokenRecords = [wrappedToken];
  return JSONStringify(tokenRecords as unknown as AnyJson);
}

/*
 * Easier-to-use getNameInfo.  Returns null if the name does not exist.
 */
export function getNameInfoEasy(
  network: CLINetworkAdapter,
  name: string
): Promise<NameInfoType | null> {
  const nameInfoPromise = network
    .getNameInfo(name)
    .then((nameInfo: NameInfoType) => nameInfo)
    .catch((error: Error): null => {
      if (error.message === 'Name not found') {
        return null;
      } else {
        throw error;
      }
    });

  return nameInfoPromise;
}

/*
 * Look up a name's zone file, profile URL, and profile
 * Returns a Promise to the above, or throws an error.
 */
export async function nameLookup(
  network: CLINetworkAdapter,
  name: string,
  includeProfile: boolean = true
): Promise<{ profile: any; profileUrl?: string; zonefile?: string }> {
  const nameInfoPromise = getNameInfoEasy(network, name);
  const profilePromise = includeProfile
    ? blockstack.lookupProfile(name).catch(() => null)
    : Promise.resolve().then(() => null);

  const zonefilePromise = nameInfoPromise.then((nameInfo: NameInfoType | null) =>
    nameInfo ? nameInfo.zonefile : null
  );

  const [profile, zonefile, nameInfo] = await Promise.all([
    profilePromise,
    zonefilePromise,
    nameInfoPromise,
  ]);
  let profileObj = profile;

  if (!nameInfo) {
    throw new Error('Name not found');
  }
  if (nameInfo.hasOwnProperty('grace_period') && nameInfo.grace_period) {
    throw new Error(
      `Name is expired at block ${nameInfo.expire_block} ` +
        `and must be renewed by block ${nameInfo.renewal_deadline}`
    );
  }

  let profileUrl = null;
  try {
    const zonefileJSON = ZoneFile.parseZoneFile(zonefile);
    if (zonefileJSON.uri && zonefileJSON.hasOwnProperty('$origin')) {
      profileUrl = blockstack.getTokenFileUrl(zonefileJSON);
    }
  } catch (e) {
    profileObj = null;
  }

  const ret = {
    zonefile: zonefile,
    profile: profileObj,
    profileUrl: profileUrl,
  };
  // @ts-ignore
  return ret;
}

/*
 * Get a password.  Don't echo characters to stdout.
 * Password will be passed to the given callback.
 */
export function getpass(promptStr: string, cb: (passwd: string) => void) {
  const silentOutput = new stream.Writable({
    write: (_chunk, _encoding, callback) => {
      callback();
    },
  });

  const rl = readline.createInterface({
    input: process.stdin,
    output: silentOutput,
    terminal: true,
  });

  process.stderr.write(promptStr);
  rl.question('', passwd => {
    rl.close();
    process.stderr.write('\n');
    cb(passwd);
  });

  return;
}

/*
 * Extract a 12-word backup phrase.  If the raw 12-word phrase is given, it will
 * be returned.  If the ciphertext is given, the user will be prompted for a password
 * (if a password is not given as an argument).
 */
export async function getBackupPhrase(
  backupPhraseOrCiphertext: string,
  password?: string
): Promise<string> {
  if (backupPhraseOrCiphertext.split(/ +/g).length > 1) {
    // raw backup phrase
    return backupPhraseOrCiphertext;
  } else {
    // ciphertext
    const pass: string = await new Promise((resolve, reject) => {
      if (!process.stdin.isTTY && !password) {
        // password must be given
        reject(new Error('Password argument required in non-interactive mode'));
      } else if (process.env.password) {
        // Do not prompt password for unit tests
        resolve(process.env.password);
      } else {
        // prompt password
        getpass('Enter password: ', p => {
          resolve(p);
        });
      }
    });
    return await decryptBackupPhrase(Buffer.from(backupPhraseOrCiphertext, 'base64'), pass);
  }
}

/*
 * mkdir -p
 * path must be absolute
 */
export function mkdirs(path: string): void {
  if (path.length === 0 || path[0] !== '/') {
    throw new Error('Path must be absolute');
  }

  const pathParts = path.replace(/^\//, '').split('/');
  let tmpPath = '/';
  for (let i = 0; i <= pathParts.length; i++) {
    try {
      const statInfo = fs.lstatSync(tmpPath);
      if ((statInfo.mode & fs.constants.S_IFDIR) === 0) {
        throw new Error(`Not a directory: ${tmpPath}`);
      }
    } catch (e: any) {
      if (e.code === 'ENOENT') {
        // need to create
        fs.mkdirSync(tmpPath);
      } else {
        throw e;
      }
    }
    if (i === pathParts.length) {
      break;
    }
    tmpPath = `${tmpPath}/${pathParts[i]}`;
  }
}

/*
 * Given a name or ID address, return a promise to the ID Address
 */
export async function getIDAddress(
  network: CLINetworkAdapter,
  nameOrIDAddress: string
): Promise<string> {
  if (nameOrIDAddress.match(ID_ADDRESS_PATTERN)) {
    return nameOrIDAddress;
  } else {
    // need to look it up
    const nameInfo = await network.getNameInfo(nameOrIDAddress);
    return `ID-${nameInfo.address}`;
  }
}

/*
 * Find all identity addresses until we have one that matches the given one.
 * Loops forever if not found
 */
export async function getOwnerKeyFromIDAddress(
  network: CLINetworkAdapter,
  mnemonic: string,
  idAddress: string
): Promise<string> {
  let index = 0;
  while (true) {
    const keyInfo = await getOwnerKeyInfo(network, mnemonic, index);
    if (keyInfo.idAddress === idAddress) {
      return keyInfo.privateKey;
    }
    index++;
  }
}

/*
 * Given a name or an ID address and a possibly-encrypted mnemonic, get the owner and app
 * private keys.
 * May prompt for a password if mnemonic is encrypted.
 */
export interface IDAppKeys {
  ownerPrivateKey: string;
  appPrivateKey: string;
  mnemonic: string;
}

export async function getIDAppKeys(
  network: CLINetworkAdapter,
  nameOrIDAddress: string,
  appOrigin: string,
  mnemonicOrCiphertext: string
): Promise<IDAppKeys> {
  const mnemonic = await getBackupPhrase(mnemonicOrCiphertext);
  const idAddress = await getIDAddress(network, nameOrIDAddress);
  const appKeyInfo = await getApplicationKeyInfo(network, mnemonic, idAddress, appOrigin);
  const appPrivateKey = extractAppKey(network, appKeyInfo);
  const ownerPrivateKey = await getOwnerKeyFromIDAddress(network, mnemonic, idAddress);
  const ret = {
    appPrivateKey,
    ownerPrivateKey,
    mnemonic,
  };
  return ret;
}

interface InquirerPrompt {
  type: string;
  name: string;
  message: string;
  choices?: string[];
}

export function makePromptsFromArgList(expectedArgs: ClarityFunctionArg[]): InquirerPrompt[] {
  const prompts = [];
  for (let i = 0; i < expectedArgs.length; i++) {
    prompts.push(argToPrompt(expectedArgs[i]));
  }
  return prompts;
}

export interface ClarityFunctionArg {
  name: string;
  type: ClarityAbiType;
}

export function argToPrompt(arg: ClarityFunctionArg): InquirerPrompt {
  const name = arg.name;
  const type = arg.type;
  const typeString = getTypeString(type);
  if (isClarityAbiPrimitive(type)) {
    if (type === 'uint128') {
      return {
        type: 'input',
        name,
        message: `Enter value for function argument "${name}" of type ${typeString}`,
      };
    } else if (type === 'int128') {
      return {
        type: 'input',
        name,
        message: `Enter value for function argument "${name}" of type ${typeString}`,
      };
    } else if (type === 'bool') {
      return {
        type: 'list',
        name,
        message: `Enter value for function argument "${name}" of type ${typeString}`,
        choices: ['True', 'False'],
      };
    } else if (type === 'principal') {
      return {
        type: 'input',
        name,
        message: `Enter value for function argument "${name}" of type ${typeString}`,
      };
    } else {
      throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
    }
  } else if (isClarityAbiBuffer(type)) {
    return {
      type: 'input',
      name,
      message: `Enter value for function argument "${name}" of type ${typeString}`,
    };
  } else if (isClarityAbiStringAscii(type)) {
    return {
      type: 'input',
      name,
      message: `Enter value for function argument "${name}" of type ${typeString}`,
    };
  } else if (isClarityAbiStringUtf8(type)) {
    return {
      type: 'input',
      name,
      message: `Enter value for function argument "${name}" of type ${typeString}`,
    };
  } else if (isClarityAbiResponse(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else if (isClarityAbiOptional(type)) {
    return {
      type: 'input',
      name,
      message: `Enter value for function argument "${name}" of type ${typeString}`,
    };
  } else if (isClarityAbiTuple(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else if (isClarityAbiList(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  }
}

export function parseClarityFunctionArgAnswers(
  answers: any,
  expectedArgs: ClarityFunctionArg[]
): ClarityValue[] {
  const functionArgs: ClarityValue[] = [];
  for (let i = 0; i < expectedArgs.length; i++) {
    const expectedArg = expectedArgs[i];
    const answer = answers[expectedArg.name];
    functionArgs.push(answerToClarityValue(answer, expectedArg));
  }
  return functionArgs;
}

export function answerToClarityValue(answer: any, arg: ClarityFunctionArg): ClarityValue {
  const type = arg.type;
  const typeString = getTypeString(type);
  if (isClarityAbiPrimitive(type)) {
    if (type === 'uint128') {
      return uintCV(answer);
    } else if (type === 'int128') {
      return intCV(answer);
    } else if (type === 'bool') {
      return answer == 'True' ? trueCV() : falseCV();
    } else if (type === 'principal') {
      // TODO handle contract principals
      return standardPrincipalCV(answer);
    } else {
      throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
    }
  } else if (isClarityAbiBuffer(type)) {
    return bufferCVFromString(answer);
  } else if (isClarityAbiStringAscii(type)) {
    return stringAsciiCV(answer);
  } else if (isClarityAbiStringUtf8(type)) {
    return stringUtf8CV(answer);
  } else if (isClarityAbiResponse(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else if (isClarityAbiOptional(type)) {
    return someCV(
      answerToClarityValue(answer, { name: arg.name, type: type.optional } as ClarityFunctionArg)
    );
  } else if (isClarityAbiTuple(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else if (isClarityAbiList(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  }
}

export function generateExplorerTxPageUrl(txid: string, network: StacksNetwork): string {
  if (network.version === TransactionVersion.Testnet) {
    return `https://explorer.stacks.co/txid/0x${txid}?chain=testnet`;
  } else {
    return `https://explorer.stacks.co/txid/0x${txid}?chain=mainnet`;
  }
}

export function isTestnetAddress(address: string) {
  const addressInfo = bitcoinjs.address.fromBase58Check(address);
  return addressInfo.version === bitcoinjs.networks.testnet.pubKeyHash;
}

/**
 * Reference: https://github.com/stacks-network/subdomain-registrar/blob/da2d144f4355bb1d67f67d1ae5f329b476d647d6/src/operations.js#L18
 */
export type SubdomainOp = {
  owner: string;
  sequenceNumber: number;
  zonefile: string;
  subdomainName: string;
  signature?: string;
};

/**
 * Reference: https://github.com/stacks-network/subdomain-registrar/blob/da2d144f4355bb1d67f67d1ae5f329b476d647d6/src/operations.js#L55
 */
function destructZonefile(zonefile: string) {
  const encodedZonefile = Buffer.from(zonefile).toString('base64');
  // we pack into 250 byte strings -- the entry "zf99=" eliminates 5 useful bytes,
  // and the max is 255.
  const pieces = 1 + Math.floor(encodedZonefile.length / 250);
  const destructed = [];
  for (let i = 0; i < pieces; i++) {
    const startIndex = i * 250;
    const currentPiece = encodedZonefile.slice(startIndex, startIndex + 250);
    if (currentPiece.length > 0) {
      destructed.push(currentPiece);
    }
  }
  return destructed;
}

/**
 * Reference: https://github.com/stacks-network/subdomain-registrar/blob/da2d144f4355bb1d67f67d1ae5f329b476d647d6/src/operations.js#L71
 */
export function subdomainOpToZFPieces(operation: SubdomainOp) {
  const destructedZonefile = destructZonefile(operation.zonefile);
  const txt = [
    `owner=${operation.owner}`,
    `seqn=${operation.sequenceNumber}`,
    `parts=${destructedZonefile.length}`,
  ];
  destructedZonefile.forEach((zfPart, ix) => txt.push(`zf${ix}=${zfPart}`));

  if (operation.signature) {
    txt.push(`sig=${operation.signature}`);
  }

  return {
    name: operation.subdomainName,
    txt,
  };
}
