import { hex } from '@scure/base';
import * as P from 'micro-packed';
import { Address, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.js';
import * as psbt from './psbt.js';
import { CompactSizeLen, RawOutput, RawTx, RawWitness, Script, VarBytes } from './script.js';
import {
  DEFAULT_SEQUENCE,
  SignatureHash,
  Transaction,
  type TxOpts,
  inputBeforeSign,
} from './transaction.js'; // circular
import {
  type Bytes,
  NETWORK,
  PubT,
  TAPROOT_UNSPENDABLE_KEY,
  compareBytes,
  equalBytes,
  isBytes,
  sha256,
  validatePubkey,
} from './utils.js';

export type PSBTInputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTInput>;

// Normalizes input
export function getPrevOut(input: psbt.TransactionInput): P.UnwrapCoder<typeof RawOutput> {
  if (input.nonWitnessUtxo) {
    if (input.index === undefined) throw new Error('Unknown input index');
    return input.nonWitnessUtxo.outputs[input.index];
  } else if (input.witnessUtxo) return input.witnessUtxo;
  else throw new Error('Cannot find previous output info');
}

export function normalizeInput(
  i: psbt.TransactionInputUpdate,
  cur?: psbt.TransactionInput,
  allowedFields?: (keyof psbt.TransactionInput)[],
  disableScriptCheck = false,
  allowUnknown = false
): psbt.TransactionInput {
  let { nonWitnessUtxo, txid } = i;
  // String support for common fields. We usually prefer Uint8Array to avoid errors
  // like hex looking string accidentally passed, however, in case of nonWitnessUtxo
  // it is better to expect string, since constructing this complex object will be
  // difficult for user
  if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = hex.decode(nonWitnessUtxo);
  if (isBytes(nonWitnessUtxo)) nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
  if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
    nonWitnessUtxo = cur?.nonWitnessUtxo;
  if (typeof txid === 'string') txid = hex.decode(txid);
  // TODO: if we have nonWitnessUtxo, we can extract txId from here
  if (txid === undefined) txid = cur?.txid;
  let res: PSBTInputs = { ...cur, ...i, nonWitnessUtxo, txid };
  if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
  if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
  if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
  res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown);
  psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point

  let prevOut;
  if (res.nonWitnessUtxo && res.index !== undefined)
    prevOut = res.nonWitnessUtxo.outputs[res.index];
  else if (res.witnessUtxo) prevOut = res.witnessUtxo;
  if (prevOut && !disableScriptCheck)
    checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
  return res;
}

export function getInputType(input: psbt.TransactionInput, allowLegacyWitnessUtxo = false) {
  let txType = 'legacy';
  let defaultSighash = SignatureHash.ALL;
  const prevOut = getPrevOut(input);
  const first = OutScript.decode(prevOut.script);
  let type = first.type;
  let cur = first;
  const stack = [first];
  if (first.type === 'tr') {
    defaultSighash = SignatureHash.DEFAULT;
    return {
      txType: 'taproot',
      type: 'tr',
      last: first,
      lastScript: prevOut.script,
      defaultSighash,
      sighash: input.sighashType || defaultSighash,
    };
  } else {
    if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
    if (first.type === 'sh') {
      if (!input.redeemScript) throw new Error('inputType: sh without redeemScript');
      let child = OutScript.decode(input.redeemScript);
      if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
      stack.push(child);
      cur = child;
      type += `-${child.type}`;
    }
    // wsh can be inside sh
    if (cur.type === 'wsh') {
      if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript');
      let child = OutScript.decode(input.witnessScript);
      if (child.type === 'wsh') txType = 'segwit';
      stack.push(child);
      cur = child;
      type += `-${child.type}`;
    }
    const last = stack[stack.length - 1];
    if (last.type === 'sh' || last.type === 'wsh')
      throw new Error('inputType: sh/wsh cannot be terminal type');
    const lastScript = OutScript.encode(last);
    const res = {
      type,
      txType,
      last,
      lastScript,
      defaultSighash,
      sighash: input.sighashType || defaultSighash,
    };
    if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
      throw new Error(
        `Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
      );
    }
    return res;
  }
}

export const toVsize = (weight: number): number => Math.ceil(weight / 4);
// UTXO Select
export type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint };
export type Accumulated =
  | {
      indices: number[];
      fee: bigint | undefined;
      weight: number;
      total: bigint;
    }
  | undefined;
type TapLeafScript = psbt.TransactionInput['tapLeafScript'];
type TB = Parameters<typeof psbt.TaprootControlBlock.encode>[0];
const encodeTapBlock = (item: TB) => psbt.TaprootControlBlock.encode(item);

function iterLeafs(tapLeafScript: TapLeafScript, sigSize: number, customScripts?: CustomScript[]) {
  if (!tapLeafScript || !tapLeafScript.length) throw new Error('no leafs');
  const empty = () => new Uint8Array(sigSize);
  // If user want to select specific leaf, which can signed,
  // it is possible to remove all other leafs manually.
  // Sort leafs by control block length.
  const leafs = tapLeafScript.sort(
    (a, b) => encodeTapBlock(a[0]).length - encodeTapBlock(b[0]).length
  );
  for (const [cb, _script] of leafs) {
    // Last byte is version
    const script = _script.slice(0, -1);
    const ver = _script[_script.length - 1];
    const outs = OutScript.decode(script);

    let signatures: Bytes[] = [];
    if (outs.type === 'tr_ms') {
      const m = outs.m;
      const n = outs.pubkeys.length - m;
      for (let i = 0; i < m; i++) signatures.push(empty());
      for (let i = 0; i < n; i++) signatures.push(P.EMPTY);
    } else if (outs.type === 'tr_ns') {
      for (const _pub of outs.pubkeys) signatures.push(empty());
    } else {
      if (!customScripts) throw new Error('Finalize: Unknown tapLeafScript');
      const leafHash = tapLeafHash(script, ver);
      for (const c of customScripts) {
        if (!c.finalizeTaproot) continue;
        const scriptDecoded = Script.decode(script);
        const csEncoded = c.encode(scriptDecoded);
        if (csEncoded === undefined) continue;
        const pubKeys = scriptDecoded.filter((i) => {
          if (!isBytes(i)) return false;
          try {
            validatePubkey(i, PubT.schnorr);
            return true;
          } catch (e) {
            return false;
          }
        }) as Bytes[];
        const finalized = c.finalizeTaproot(
          script,
          csEncoded,
          pubKeys.map((pubKey) => [{ pubKey, leafHash }, empty()])
        );
        if (!finalized) continue;
        return finalized.concat(encodeTapBlock(cb));
      }
    }
    // Witness is stack, so last element will be used first
    return signatures.reverse().concat([script, encodeTapBlock(cb)]);
  }
  throw new Error('there was no witness');
}

function estimateInput(
  inputType: ReturnType<typeof getInputType>,
  input: psbt.TransactionInput,
  opts: TxOpts
) {
  let script: Bytes = P.EMPTY;
  let witness: Bytes[] | undefined;

  // schnorr sig is always 64 bytes. except for cases when sighash is not default!
  if (inputType.txType === 'taproot') {
    const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
    if (input.tapInternalKey && !equalBytes(input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
      witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
    } else if (input.tapLeafScript) {
      witness = iterLeafs(input.tapLeafScript, SCHNORR_SIG_SIZE, opts.customScripts);
    } else throw new Error('estimateInput/taproot: unknown input');
  } else {
    // It is possible to grind signatures until it has minimal size (but changing fee value +N satoshi),
    // which will make estimations exact. But will be very hard for multi sig (need to make sure all signatures has small size).
    const empty = () => new Uint8Array(72); // max size of sigs
    const emptyPub = () => new Uint8Array(33); // size of pubkey
    let inputScript = P.EMPTY;
    let inputWitness: Uint8Array[] = [];
    const ltype = inputType.last.type;
    if (ltype === 'ms') {
      const m = inputType.last.m;
      const sig: (number | Uint8Array)[] = [0];
      for (let i = 0; i < m; i++) sig.push(empty());
      inputScript = Script.encode(sig);
    } else if (ltype === 'pk') {
      // 71 sig + 1 sighash
      inputScript = Script.encode([empty()]);
    } else if (ltype === 'pkh') {
      inputScript = Script.encode([empty(), emptyPub()]);
    } else if (ltype === 'wpkh') {
      inputScript = P.EMPTY;
      inputWitness = [empty(), emptyPub()];
    } else if (ltype === 'unknown' && !opts.allowUnknownInputs)
      throw new Error('Unknown inputs are not allowed');
    if (inputType.type.includes('wsh-')) {
      // P2WSH
      if (inputScript.length && inputType.lastScript.length) {
        inputWitness = Script.decode(inputScript).map((i) => {
          if (i === 0) return P.EMPTY;
          if (isBytes(i)) return i;
          throw new Error(`Wrong witness op=${i}`);
        });
      }
      inputWitness = inputWitness.concat(inputType.lastScript);
    }
    if (inputType.txType === 'segwit') witness = inputWitness;
    if (inputType.type.startsWith('sh-wsh-')) {
      script = Script.encode([Script.encode([0, new Uint8Array(sha256.outputLen)])]);
    } else if (inputType.type.startsWith('sh-')) {
      script = Script.encode([...Script.decode(inputScript), inputType.lastScript]);
    } else if (inputType.type.startsWith('wsh-')) {
    } else if (inputType.txType !== 'segwit') script = inputScript;
  }
  let weight = 160 + 4 * VarBytes.encode(script).length;
  let hasWitnesses = false;
  if (witness) {
    weight += RawWitness.encode(witness).length;
    hasWitnesses = true;
  }
  return { weight, hasWitnesses };
}

// Exported for tests, internal method
export const _cmpBig = (a: bigint, b: bigint): 0 | 1 | -1 => {
  const n = a - b;
  if (n < 0n) return -1;
  else if (n > 0n) return 1;
  return 0;
};

export type EstimatorOpts = TxOpts & {
  // NOTE: fees less than 1 satoshi per vbyte is not supported. Please create issue if you have valid use case for that.
  feePerByte: bigint; // satoshi per vbyte
  changeAddress: string; // address where change will be sent
  // Optional
  alwaysChange?: boolean; // always create change, even if less than dust threshold
  bip69?: boolean; // https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
  network?: typeof NETWORK;
  dust?: number; // how much vbytes considered dust?
  dustRelayFeeRate?: bigint; // fee per dust byte (DUST_RELAY_TX_FEE)
  createTx?: boolean; // Create tx inside selection
  requiredInputs?: psbt.TransactionInputUpdate[]; // these inputs always will be used
  allowSameUtxo?: boolean; // allow using UTXO multiple times (for test purposes)
};

function getScript(o: Output, opts: TxOpts = {}, network = NETWORK) {
  let script;
  if ('script' in o && isBytes(o.script)) {
    script = o.script;
  }
  if ('address' in o) {
    if (typeof o.address !== 'string')
      throw new Error(`Estimator: wrong output address=${o.address}`);
    script = OutScript.encode(Address(network).decode(o.address));
  }
  if (!script) throw new Error('Estimator: wrong output script');
  if (typeof o.amount !== 'bigint')
    throw new Error(
      `Estimator: wrong output amount=${
        o.amount
      }, should be of type bigint but got ${typeof o.amount}.`
    );
  if (script && !opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
    throw new Error(
      'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
    );
  }
  if (!opts.disableScriptCheck) checkScript(script);
  return script;
}

// exact is meaningless without additional accum (will often fail if not possible to find right utxo)
// -> we support only exact+accum or accum
type SortStrategy = 'Newest' | 'Oldest' | 'Smallest' | 'Biggest';
type ExactStrategy = `exact${SortStrategy}`;
type AccumStrategy = `accum${SortStrategy}`;

export type SelectionStrategy =
  | 'all'
  | 'default'
  | AccumStrategy
  | `${ExactStrategy}/${AccumStrategy}`;

// class, because we need to re-use normalized inputs, instead of parsing each time
// internal stuff, exported for tests only
export class _Estimator {
  private baseWeight: number;
  private changeWeight: number;
  private amount: bigint;
  private requiredIndices: number[] = [];
  private normalizedInputs: {
    inputType: ReturnType<typeof getInputType>;
    normalized: ReturnType<typeof normalizeInput>;
    amount: bigint;
    value: bigint;
    estimate: { weight: number; hasWitnesses: boolean };
  }[];
  // Dust used in accumExact + change address algo
  // - change address: can be smaller for segwit
  // - accumExact: ???
  private dust: bigint; // total dust limit (3||opts.dustRelayFeeRate * 182||opts.dust). Default: 546
  private outputs: Output[];
  private opts: EstimatorOpts;
  constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
    this.outputs = outputs;
    this.opts = opts;
    if (typeof opts.feePerByte !== 'bigint')
      throw new Error(
        `Estimator: wrong feePerByte=${
          opts.feePerByte
        }, should be of type bigint but got ${typeof opts.feePerByte}.`
      );
    // Dust stuff
    // TODO: think about this more:
    // - current dust filters tx which cannot be relayed by core
    // - but actual dust meaning is 'can be this amount spent?'
    // - dust contains full tx size. but we can use other inputs to pay for outputDust (and parially inputsDust)?
    // - not sure if we can spent anything with feePerByte: 3. It will be relayed, but will it be mined?
    // - for now it works exactly as bitcoin-core. But will create change/outputs which cannot be spent (reasonable).
    // Number of bytes needed to create and spend a UTXO.
    // https://github.com/bitcoin/bitcoin/blob/27a770b34b8f1dbb84760f442edb3e23a0c2420b/src/policy/policy.cpp#L28-L41
    const inputsDust = 32 + 4 + 1 + 107 + 4; // NOTE: can be smaller for segwit tx?
    const outputDust = 34; // NOTE: 'nSize = GetSerializeSize(txout)'
    const dustBytes = opts.dust === undefined ? BigInt(inputsDust + outputDust) : opts.dust;
    if (typeof dustBytes !== 'bigint') {
      throw new Error(
        `Estimator: wrong dust=${opts.dust}, should be of type bigint but got ${typeof opts.dust}.`
      );
    }
    // 3 sat/vb is the default minimum fee rate used to calculate dust thresholds by bitcoin core.
    // 3000 sat/kvb -> 3 sat/vb.
    // https://github.com/bitcoin/bitcoin/blob/27a770b34b8f1dbb84760f442edb3e23a0c2420b/src/policy/policy.h#L55
    const dustFee = opts.dustRelayFeeRate === undefined ? 3n : opts.dustRelayFeeRate;
    if (typeof dustFee !== 'bigint') {
      throw new Error(
        `Estimator: wrong dustRelayFeeRate=${opts.dustRelayFeeRate}, should be of type bigint but got ${typeof opts.dustRelayFeeRate}.`
      );
    }
    // Dust uses feePerbyte by default, but we allow separate dust fee if needed
    this.dust = dustBytes * dustFee;
    if (opts.requiredInputs !== undefined && !Array.isArray(opts.requiredInputs))
      throw new Error(`Estimator: wrong required inputs=${opts.requiredInputs}`);
    const network = opts.network || NETWORK;
    let amount = 0n;
    // Base weight: tx with outputs, no inputs
    let baseWeight = 32;
    for (const o of outputs) {
      const script = getScript(o, opts, opts.network);
      baseWeight += 32 + 4 * VarBytes.encode(script).length;
      amount += o.amount;
    }
    if (typeof opts.changeAddress !== 'string')
      throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
    let changeWeight =
      baseWeight +
      32 +
      4 * VarBytes.encode(OutScript.encode(Address(network).decode(opts.changeAddress))).length;
    baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
    // If there a lot of outputs change can change fee
    changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
    this.baseWeight = baseWeight;
    this.changeWeight = changeWeight;
    this.amount = amount;
    const allInputs = Array.from(inputs);
    if (opts.requiredInputs) {
      for (let i = 0; i < opts.requiredInputs.length; i++)
        this.requiredIndices.push(allInputs.push(opts.requiredInputs[i]) - 1);
    }
    const inputKeys = new Set();
    this.normalizedInputs = allInputs.map((i) => {
      const normalized = normalizeInput(
        i,
        undefined,
        undefined,
        opts.disableScriptCheck,
        opts.allowUnknown
      );
      inputBeforeSign(normalized); // check fields
      const key = `${hex.encode(normalized.txid!)}:${normalized.index}`;
      if (!opts.allowSameUtxo && inputKeys.has(key))
        throw new Error(`Estimator: same input passed multiple times: ${key}`);
      inputKeys.add(key);
      const inputType = getInputType(normalized, opts.allowLegacyWitnessUtxo);
      const prev = getPrevOut(normalized);
      const estimate = estimateInput(inputType, normalized, this.opts);
      const value = prev.amount - opts.feePerByte * BigInt(toVsize(estimate.weight)); // value = amount-fee
      return { inputType, normalized, amount: prev.amount, value, estimate };
    });
  }
  private checkInputIdx(idx: number) {
    if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.normalizedInputs.length)
      throw new Error(`Wrong input index=${idx}`);
    return idx;
  }
  private sortIndices(indices: number[]) {
    return indices.slice().sort((a, b) => {
      const ai = this.normalizedInputs[this.checkInputIdx(a)];
      const bi = this.normalizedInputs[this.checkInputIdx(b)];
      const out = compareBytes(ai.normalized.txid!, bi.normalized.txid!);
      if (out !== 0) return out;
      return ai.normalized.index! - bi.normalized.index!;
    });
  }
  private sortOutputs(outputs: Output[]) {
    const scripts = outputs.map((o) => getScript(o, this.opts, this.opts.network));
    const indices = outputs.map((_, j) => j);
    return indices.sort((a, b) => {
      const aa = outputs[a].amount;
      const ba = outputs[b].amount;
      const out = _cmpBig(aa, ba);
      if (out !== 0) return out;
      return compareBytes(scripts[a], scripts[b]);
    });
  }
  private getSatoshi(weigth: number) {
    return this.opts.feePerByte * BigInt(toVsize(weigth));
  }

  // Sort by value instead of amount
  get biggest(): number[] {
    return this.normalizedInputs
      .map((_i, j) => j)
      .sort((a, b) => _cmpBig(this.normalizedInputs[b].value, this.normalizedInputs[a].value));
  }
  get smallest(): number[] {
    return this.biggest.reverse();
  }
  // These assume that UTXO array has historical order.
  // Otherwise, we have no way to know which tx is oldest
  // Explorers usually give UTXO in this order.
  get oldest(): number[] {
    return this.normalizedInputs.map((_i, j) => j);
  }
  get newest(): number[] {
    return this.oldest.reverse();
  }
  // exact - like blackjack from coinselect.
  // exact(biggest) will select one big utxo which is closer to targetValue+dust, if possible.
  // If not, it will accumulate largest utxo until value is close to targetValue+dust.
  accumulate(indices: number[], exact = false, skipNegative = true, all = false): Accumulated {
    // TODO: how to handle change addresses?
    // - cost of input
    // - cost of change output (if input requires change)
    // - cost of output spending
    // Dust threshold should be significantly bigger, no point in
    // creating an output, which cannot be spent.
    // coinselect doesn't consider cost of output address for dust.
    // Changing that can actually reduce privacy
    let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
    let hasWitnesses = false;
    let num = 0;
    let inputsAmount = 0n;
    const targetAmount = this.amount;
    const res: Set<number> = new Set();
    let fee;
    for (const idx of this.requiredIndices) {
      this.checkInputIdx(idx);
      if (res.has(idx)) throw new Error('required input encountered multiple times'); // should not happen
      const { estimate, amount } = this.normalizedInputs[idx];
      let newWeight = weight + estimate.weight;
      if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
      const totalWeight = newWeight + 4 * CompactSizeLen.encode(num).length; // number of outputs can change weight
      fee = this.getSatoshi(totalWeight);
      weight = newWeight;
      if (estimate.hasWitnesses) hasWitnesses = true;
      num++;
      inputsAmount += amount;
      res.add(idx);
      // inputsAmount is enough to cover cost of tx
      if (!all && targetAmount + fee <= inputsAmount)
        return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
    }
    for (const idx of indices) {
      this.checkInputIdx(idx);
      if (res.has(idx)) continue; // skip required inputs
      const { estimate, amount, value } = this.normalizedInputs[idx];
      let newWeight = weight + estimate.weight;
      if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
      const totalWeight = newWeight + 4 * CompactSizeLen.encode(num).length; // number of outputs can change weight
      fee = this.getSatoshi(totalWeight);
      // Best case scenario exact(biggest) -> we find biggest output, less than target+threshold
      if (exact && amount + inputsAmount > targetAmount + fee + this.dust) continue; // skip if added value is bigger than dust
      // Negative: cost of using input is more than value provided (negative)
      // By default 'blackjack' mode in coinselect doesn't use that, which means
      // it will use negative output if sorted by 'smallest'
      if (skipNegative && value <= 0n) continue;
      weight = newWeight;
      if (estimate.hasWitnesses) hasWitnesses = true;
      num++;
      inputsAmount += amount;
      res.add(idx);
      // inputsAmount is enough to cover cost of tx
      if (!all && targetAmount + fee <= inputsAmount)
        return { indices: Array.from(res), fee, weight: totalWeight, total: inputsAmount };
    }
    if (all) {
      const newWeight = weight + 4 * CompactSizeLen.encode(num).length;
      return { indices: Array.from(res), fee, weight: newWeight, total: inputsAmount };
    }
    return undefined;
  }

  // Works like coinselect default method
  default(): Accumulated {
    const { biggest } = this;
    const exact = this.accumulate(biggest, true, false);
    if (exact) return exact;
    return this.accumulate(biggest);
  }

  private select(strategy: SelectionStrategy) {
    if (strategy === 'all') {
      return this.accumulate(
        this.normalizedInputs.map((_, j) => j),
        false,
        true,
        true
      );
    }
    if (strategy === 'default') return this.default();
    const data: Record<SortStrategy, () => number[]> = {
      Oldest: () => this.oldest,
      Newest: () => this.newest,
      Smallest: () => this.smallest,
      Biggest: () => this.biggest,
    };
    if (strategy.startsWith('exact')) {
      const [exactData, left] = strategy.slice(5).split('/') as [SortStrategy, SelectionStrategy];
      if (!data[exactData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
      strategy = left;
      const exact = this.accumulate(data[exactData](), true, true);
      if (exact) return exact;
    }
    if (strategy.startsWith('accum')) {
      const accumData = strategy.slice(5) as SortStrategy;
      if (!data[accumData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
      return this.accumulate(data[accumData]());
    }
    throw new Error(`Estimator.select: wrong strategy=${strategy}`);
  }

  result(strategy: SelectionStrategy) {
    const s = this.select(strategy);
    if (!s) return;
    const { indices, weight, total } = s;
    let needChange = this.opts.alwaysChange;
    const changeWeight = this.opts.alwaysChange
      ? weight
      : weight + (this.changeWeight - this.baseWeight);

    const changeFee = this.getSatoshi(changeWeight);
    let fee = s.fee;
    const change = total - this.amount - changeFee;
    if (change > this.dust) needChange = true;
    let inputs = indices;
    let outputs = Array.from(this.outputs);
    if (needChange) {
      fee = changeFee;
      // this shouldn't happen!
      if (change < 0n) throw new Error(`Estimator.result: negative change=${change}`);
      outputs.push({ address: this.opts.changeAddress, amount: change });
    }
    if (this.opts.bip69) {
      inputs = this.sortIndices(inputs);
      outputs = this.sortOutputs(outputs).map((i) => outputs[i]);
    }
    const res = {
      inputs: inputs.map((i) => this.normalizedInputs[i].normalized),
      outputs,
      fee,
      weight: this.opts.alwaysChange ? s.weight : changeWeight,
      change: !!needChange,
    };
    let tx;
    if (this.opts.createTx) {
      const { inputs, outputs } = res;
      tx = new Transaction(this.opts);
      for (const i of inputs) tx.addInput(i);
      for (const o of outputs)
        tx.addOutput({ ...o, script: getScript(o, this.opts, this.opts.network) });
    }
    return Object.assign(res, { tx });
    // return { ...res, tx: tx };
  }
}

export function selectUTXO(
  inputs: psbt.TransactionInputUpdate[],
  outputs: Output[],
  strategy: SelectionStrategy,
  opts: EstimatorOpts
) {
  // Defaults: do we want bip69 by default?
  const _opts = { createTx: true, bip69: true, ...opts };
  const est = new _Estimator(inputs, outputs, _opts);
  return est.result(strategy);
}
