import {getLogger} from "../../utils/Logger";

const logger = getLogger("CoinSelect: ");

// baseline estimates, used to improve performance
const TX_EMPTY_SIZE = 4 + 1 + 1 + 4;
const TX_INPUT_BASE = 32 + 4 + 1 + 4;

const WITNESS_OVERHEAD = 2/4;

const P2WPKH_WITNESS = (1+1+72+1+33)/4;
const P2TR_WITNESS = (1+1+65)/4;

const TX_INPUT_PUBKEYHASH = 107;
const TX_INPUT_P2SH_P2WPKH = 23 + P2WPKH_WITNESS + 1;
const TX_INPUT_P2WPKH = 0 + P2WPKH_WITNESS;
const TX_INPUT_P2WSH = 0 + (1+1+64)/4;
const TX_INPUT_P2TR = 0 + P2TR_WITNESS;

const TX_OUTPUT_BASE = 8 + 1;

const TX_OUTPUT_PUBKEYHASH = 25;
const TX_OUTPUT_P2SH_P2WPKH = 23;
const TX_OUTPUT_P2WPKH = 22;
const TX_OUTPUT_P2WSH = 34;
const TX_OUTPUT_P2TR = 34;

/**
 * Defines a type of the address used by the wallet, for proper coinselection (as coinselection
 *  depends on estimating the cost for spending the input + the cost of adding a change output)
 *
 * @category Bitcoin
 */
export type CoinselectAddressTypes = "p2sh-p2wpkh" | "p2wpkh" | "p2wsh" | "p2tr" | "p2pkh";

export type CoinselectTxInput = {
    script?: Buffer,
    txId: string,
    vout: number,
    type?: CoinselectAddressTypes,
    value: number,
    outputScript?: Buffer,
    address?: string,
    cpfp?: {
        txVsize: number,
        txEffectiveFeeRate: number
    }
};

export type CoinselectTxOutput = {
    script?: Buffer,
    address?: string,
    type?: CoinselectAddressTypes,
    value: number
};


const INPUT_BYTES = {
    "p2sh-p2wpkh": TX_INPUT_P2SH_P2WPKH,
    "p2wpkh": TX_INPUT_P2WPKH,
    "p2tr": TX_INPUT_P2TR,
    "p2pkh": TX_INPUT_PUBKEYHASH,
    "p2wsh": TX_INPUT_P2WSH
};

function inputBytes (input: {
    script?: Buffer,
    type?: CoinselectAddressTypes
}) {
  if(input.script==null && input.type==null) throw new Error("Needs either script or type defined!");
  return TX_INPUT_BASE + (input.script ? input.script.length : INPUT_BYTES[input.type!]);
}

const OUTPUT_BYTES = {
    "p2sh-p2wpkh": TX_OUTPUT_P2SH_P2WPKH,
    "p2wpkh": TX_OUTPUT_P2WPKH,
    "p2tr": TX_OUTPUT_P2TR,
    "p2pkh": TX_OUTPUT_PUBKEYHASH,
    "p2wsh": TX_OUTPUT_P2WSH
};

function outputBytes (output: {
    script?: Buffer,
    type?: CoinselectAddressTypes
}): number {
  if(output.script==null && output.type==null) throw new Error("Needs either script or type defined!");
  return TX_OUTPUT_BASE + (output.script ? output.script.length : OUTPUT_BYTES[output.type!]);
}

export const DUST_THRESHOLDS = {
    "p2sh-p2wpkh": 540,
    "p2wpkh": 294,
    "p2tr": 330,
    "p2pkh": 546,
    "p2wsh": 330
};

function dustThreshold (output: {
    script?: Buffer,
    type: CoinselectAddressTypes
}): number {
  return DUST_THRESHOLDS[output.type];
}

function transactionBytes (
    inputs: {
        script?: Buffer,
        type?: CoinselectAddressTypes
    }[],
    outputs: {
        script?: Buffer,
        type?: CoinselectAddressTypes
    }[],
    changeType?: CoinselectAddressTypes
): number {
    let size = TX_EMPTY_SIZE;
    let isSegwit = false;
    if(changeType!=null && changeType!=="p2pkh") {
        size += WITNESS_OVERHEAD;
        isSegwit = true;
    }
    for(let input of inputs) {
        if(!isSegwit && (input.type!=="p2pkh")) {
          isSegwit = true;
          size += WITNESS_OVERHEAD;
        }
        size += inputBytes(input);
    }
    for(let output of outputs) {
        size += outputBytes(output);
    }
    return Math.ceil(size);
}

function numberOrNaN(v: number): number {
    if (typeof v !== 'number') return NaN;
    if (!isFinite(v)) return NaN;
    if (v < 0) return NaN;
    return v;
}

function uintOrNaN(v: number): number {
  if (typeof v !== 'number') return NaN;
  if (!isFinite(v)) return NaN;
  if (Math.floor(v) !== v) return NaN;
  if (v < 0) return NaN;
  return v;
}

function sumForgiving(range: {value: number}[]): number {
  return range.reduce((a, x) => a + (isFinite(x.value) ? x.value : 0), 0);
}

function sumOrNaN(range: {value: number}[]): number {
  return range.reduce((a, x)  => a + uintOrNaN(x.value), 0);
}

function finalize<T extends Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">>(
    inputs: T[],
    outputs: CoinselectTxOutput[],
    feeRate: number,
    changeType: CoinselectAddressTypes | null,
    cpfpAddFee: number = 0
): {
    inputs?: T[],
    outputs?: CoinselectTxOutput[],
    effectiveFeeRate?: number,
    fee: number
} {
  const bytesAccum = transactionBytes(inputs, outputs, changeType ?? undefined);
  logger.debug("finalize(): Transaction bytes: ", bytesAccum);

  if(changeType!=null) {
      const feeAfterExtraOutput = (feeRate * (bytesAccum + outputBytes({type: changeType}))) + cpfpAddFee;
      logger.debug("finalize(): TX fee after adding change output: ", feeAfterExtraOutput);
      const remainderAfterExtraOutput = Math.floor(sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput));
      logger.debug("finalize(): Leaves change (changeType="+changeType+") value: ", remainderAfterExtraOutput);

      // is it worth a change output?
      if (remainderAfterExtraOutput >= dustThreshold({type: changeType})) {
          outputs = outputs.concat({ value: remainderAfterExtraOutput, type: changeType })
      }
  }

  const fee = sumOrNaN(inputs) - sumOrNaN(outputs);
  logger.debug("finalize(): Re-calculated total fee: ", fee);
  if (!isFinite(fee) || fee<0) return { fee: (feeRate * bytesAccum) + cpfpAddFee }

  let txVSize = utils.transactionBytes(inputs, outputs);
  let txFee = fee;
  const cpfpSortedInputs = [...inputs].sort(
      (a, b) => (b.cpfp?.txEffectiveFeeRate ?? 0) - (a.cpfp?.txEffectiveFeeRate ?? 0)
  );
  cpfpSortedInputs.forEach(input => {
    if(input.cpfp==null) return;
      const currentEffectiveFeeRate = txFee / txVSize;
      if(currentEffectiveFeeRate > input.cpfp.txEffectiveFeeRate) {
        txVSize += input.cpfp.txVsize;
        txFee += input.cpfp.txVsize * input.cpfp.txEffectiveFeeRate;
      }
    }
  );

  return {
    inputs: inputs,
    outputs: outputs,
    effectiveFeeRate: txFee / txVSize,
    fee: fee
  }
}

function isDetrimentalInput(
    feeRate: number,
    utxo: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">
) {
    const utxoBytes = utils.inputBytes(utxo);
    const utxoFee = feeRate * utxoBytes;
    let cpfpFee = 0;
    if(utxo.cpfp!=null && utxo.cpfp.txEffectiveFeeRate<feeRate) cpfpFee = Math.ceil(utxo.cpfp.txVsize*(feeRate - utxo.cpfp.txEffectiveFeeRate));

    // skip detrimental input
    return utxoFee + cpfpFee > utxo.value;
}

export const utils = {
  dustThreshold: dustThreshold,
  finalize: finalize,
  inputBytes: inputBytes,
  outputBytes: outputBytes,
  sumOrNaN: sumOrNaN,
  sumForgiving: sumForgiving,
  transactionBytes: transactionBytes,
  uintOrNaN: uintOrNaN,
  numberOrNaN: numberOrNaN,
  isDetrimentalInput
};
