import { expect } from 'chai';
import { Address, PrivateKey, Script, Networks, PublicKey, Transaction } from '../../src/btc';
import { createLiquidationCets, createDlcInitTx, createMaturityCets, createRepaymentCet, applySignaturesCet, fundCetFees } from '../../src/cet/transactions';
import { OracleEvent, LoanConfig, OracleCET } from '../../src/cet/types';
import { Point, utils } from '../../src/crypto/secp256k1';
import { signCetWithAdaptorSig, verifyCetAdaptorSig } from '../../src/cet/signature';
import { AdaptorSignature, EventOutcomeHash, PrivKey } from '../../src/crypto/types';
import { adaptSig, attestEventOutcome, bytesToHex, commitToEvent, sha256, sighashForAdaptorSig, tapleafHash, verifySigStrict } from '../../src';
import { generateEvenYPrivateKey } from '../../src/crypto/counterparty';

const signCetAdaptorSignatures = async (
  privateKey: PrivKey,
  outcomeSignaturePoints: Point[],
  cetTxs: Transaction[],
  leafHash: Buffer,
  inputIndex = 0
): Promise<AdaptorSignature[]> => {

  const adaptorSigs = new Array<AdaptorSignature>(cetTxs.length);
  for (let i = 0; i < cetTxs.length; i++) {
    adaptorSigs[i] = await signCetWithAdaptorSig(
      privateKey,
      outcomeSignaturePoints[i],
      cetTxs[i],
      inputIndex,
      leafHash
    );
  }
  return adaptorSigs;
};

const validateAdaptorSignatures = async (
  adaptorSigs: AdaptorSignature[],
  pubKey: Point,
  outcomeSignaturePoints: Point[],
  cetTxs: Transaction[],
  leafHash: Buffer,
  inputIndex = 0
): Promise<void> => {

  for (let i = 0; i < adaptorSigs.length; i++) {
    const isAdaptorSigValid = await verifyCetAdaptorSig(
      adaptorSigs[i],
      pubKey,
      outcomeSignaturePoints[i],
      cetTxs[i],
      inputIndex,
      leafHash
    );
    expect(isAdaptorSigValid).to.be.true;
  }
};

const createLiquidationEvents = async (
  count: number,
  oraclePubKey: Point,
  startTime: number = (Date.now() / 1000) | 0,
): Promise<OracleEvent[]> => {
    const eventTimeDelta = 60 * 60 * 12;

    const events = new Array<OracleEvent>(count);
    for (let i = 0; i < count; i++) {
      const nOutcomes = 100 * 10;
      const eventOutcomeHashes = new Array<EventOutcomeHash>(nOutcomes);
      const outcomePrices = new Array<number>(nOutcomes);
      for (let j = 0; j < nOutcomes; j++) {
          const eventOutcomeHash = await sha256(new Uint8Array([i, j])); // TODO: Turn into random hash
          eventOutcomeHashes[j] = eventOutcomeHash;
          outcomePrices[j] = 50000 + j * 100;
      }
      const { signaturePoints, nonce } = await commitToEvent(eventOutcomeHashes, oraclePubKey);
      events[i] = {
        id: `event${i}`,
        timestamp: startTime + i * eventTimeDelta,
        outcomeSignaturePoints: signaturePoints,
        outcomeHashes: eventOutcomeHashes,
        outcomePrices,
        nonce,
      };
    }
    return events;
};

describe('End-to-End Tx Flow Test', function() {
  this.timeout(160000);

  before(function() {
    if (process.env.TEST_HEAVY !== 'true') {
      this.skip();
    }
  });
  
  let borrowerKey: PrivateKey;
  let lenderKey: PrivateKey;
  let borrowerPubKey: PublicKey;
  let borrowerAddress: Address;
  let lenderAddress: Address;
  
  let lenderRepaymentSecret: PrivKey;
  let lenderRepaymentCommitment: Point;

  let oraclePrivKey: PrivKey;
  let oraclePubKey: Point;

  let dlcUtxo: any;
  let liquidationEvents: OracleEvent[];
  let maturityEvent: OracleEvent;
  
  before(async function() {
    borrowerKey = new PrivateKey();
    lenderKey = new PrivateKey();
    borrowerPubKey = borrowerKey.toPublicKey();
    borrowerAddress = new Address(borrowerPubKey, Networks.testnet, 'witnesspubkeyhash');
    lenderAddress = new Address(lenderKey.toPublicKey(), Networks.testnet, 'witnesspubkeyhash');
    
    lenderRepaymentSecret = utils.randomPrivateKey();
    lenderRepaymentCommitment = Point.fromPrivateKey(lenderRepaymentSecret);
    
    oraclePrivKey = utils.randomPrivateKey();
    oraclePubKey = Point.fromPrivateKey(oraclePrivKey);
    
    const nEvents = 10;

    liquidationEvents = await createLiquidationEvents(nEvents, oraclePubKey);

    // TODO: Move this to separate function createMaturityEvent
    const maturityEventOutcomeHashes = await Promise.all(
      Array.from({ length: 100 * 10 }, async (_, i) => 
        await sha256(new Uint8Array([i])) // TODO: Turn into random hash
      )
    );
    maturityEvent = {
      id: 'maturity-event',
      outcomeSignaturePoints: Array.from({ length: 100 * 10 }, () => 
        Point.fromPrivateKey(utils.randomPrivateKey())
      ),
      outcomeHashes: maturityEventOutcomeHashes,
      outcomePrices: Array.from({ length: 100 * 10 }, (_, i) => 50000 + i * 100),
      timestamp: Date.now(),
    };
  });

  it('should run successfull end-to-end flow', async () => {
    // The process starts with the borrower picking their loan terms.
    const borrowedAmountUsd = 15000; // $15,000 USD
    const liquidationThreshold = 0.8;
    const annualInterestRate = 0.1;
    const initalBtcPrice = 100000; // 100,000 USD/BTC

    // Gives starting LR = 0.5 < 0.8, decent buffer
    const collateralAmount = (borrowedAmountUsd / initalBtcPrice) * 2;
    
    const config: LoanConfig = {
      collateralAmount,
      annualInterestRate,
      liquidationThreshold,
      borrowedAmount: borrowedAmountUsd,
      penaltyPercentage: 0.1
    };
    
    const inputAmount = collateralAmount * 2;
    const collateralUtxos = [{
      txId: 'a'.repeat(64),
      outputIndex: 0,
      satoshis: inputAmount * 100000000,
      script: (Script as any).buildWitnessV0Out(borrowerAddress)
    }];
    
    const borrowerDlcPrivateKey = generateEvenYPrivateKey();
    const borrowerDlcPubKey = Point.fromPrivateKey(borrowerDlcPrivateKey);
    
    // The borrower can now send the data created above to our service.
    
    // At this point, our service will check that the loan terms are valid.
    // If they are not valid, the borrower will be notified and the flow will end.
    // If they are valid, our service will ask the lender to generate a DLC keypair for this contract.
    // We'll also send over the loan terms for the lender to review and confirm.
    
    const lenderDlcPrivateKey = generateEvenYPrivateKey();
    const lenderDlcPubKey = Point.fromPrivateKey(lenderDlcPrivateKey);
    
    // Our service then forwards the lenders DLC pubkey to the borrower.
    // Now both parties have all the information they need to construct the DLC init tx and all of the CETs.
    
    // Party creates the DLC init tx.
    let feeRate = 5;
    const dlcInitTx = createDlcInitTx(
      collateralUtxos,
      collateralAmount * 100000000,
      borrowerDlcPubKey,
      lenderDlcPubKey,
      borrowerAddress,
      feeRate
    );
    dlcUtxo = dlcInitTx.dlcUtxo;

    // Party creates the liquidation CETs.
    const liquidationCets = await createLiquidationCets(
      liquidationEvents,
      config,
      dlcUtxo,
      borrowerAddress,
      lenderAddress
    );

    // Party creates the maturity CETs.
    const maturityCets = createMaturityCets(
      maturityEvent,
      liquidationEvents[0].timestamp,
      config,
      dlcUtxo,
      borrowerAddress,
      lenderAddress
    );
    
    // Party creates the repayment CET.
    const repaymentCet = createRepaymentCet(
        dlcUtxo,
        collateralAmount * 100000000,
        borrowerAddress
    );

    // After constructing all of the CETs, the borrower and lender can sign them with adaptor sigs.

    const leafHash = tapleafHash(dlcInitTx.witnessScript);

    // Sign borrower's CETs
    const borrowerLiquidationCetAdaptorSigs = await signCetAdaptorSignatures(
      borrowerDlcPrivateKey,
      liquidationCets.map(cet => cet.outcomeSignaturePoint),
      liquidationCets.map(cet => cet.cetTx),
      leafHash
    );
    
    const borrowerMaturityCetAdaptorSigs = await signCetAdaptorSignatures(
      borrowerDlcPrivateKey,
      maturityCets.map(cet => cet.outcomeSignaturePoint),
      maturityCets.map(cet => cet.cetTx),
      leafHash
    );

    const borrowerRepaymentCetAdaptorSig = await signCetWithAdaptorSig(
      borrowerDlcPrivateKey,
      lenderRepaymentCommitment,
      repaymentCet,
      0,
      leafHash
    );
    
    // Sign lender's CETs
    const lenderLiquidationCetAdaptorSigs = await signCetAdaptorSignatures(
      lenderDlcPrivateKey,
      liquidationCets.map(cet => cet.outcomeSignaturePoint),
      liquidationCets.map(cet => cet.cetTx),
      leafHash
    );
    
    const lenderMaturityCetAdaptorSigs = await signCetAdaptorSignatures(
      lenderDlcPrivateKey,
      maturityCets.map(cet => cet.outcomeSignaturePoint),
      maturityCets.map(cet => cet.cetTx),
      leafHash
    );
    
    const lenderRepaymentCetAdaptorSig = await signCetWithAdaptorSig(
      lenderDlcPrivateKey,
      lenderRepaymentCommitment,
      repaymentCet,
      0,
      leafHash
    );
    
    // The borrower and lender can now share eachothers sigs and validate them.
    
    // Validate borrower's adaptor signatures
    await validateAdaptorSignatures(
      borrowerLiquidationCetAdaptorSigs,
      borrowerDlcPubKey,
      liquidationCets.map(cet => cet.outcomeSignaturePoint),
      liquidationCets.map(cet => cet.cetTx),
      leafHash
    );

    await validateAdaptorSignatures(
      borrowerMaturityCetAdaptorSigs,
      borrowerDlcPubKey,
      maturityCets.map(cet => cet.outcomeSignaturePoint),
      maturityCets.map(cet => cet.cetTx),
      leafHash
    );

    const isBorrowerRepaymentCetAdaptorSigValid = await verifyCetAdaptorSig(
      borrowerRepaymentCetAdaptorSig,
      borrowerDlcPubKey,
      lenderRepaymentCommitment,
      repaymentCet,
      0,
      leafHash
    );
    expect(isBorrowerRepaymentCetAdaptorSigValid).to.be.true;

    // Validate lender's adaptor signatures
    await validateAdaptorSignatures(
      lenderLiquidationCetAdaptorSigs,
      lenderDlcPubKey,
      liquidationCets.map(cet => cet.outcomeSignaturePoint),
      liquidationCets.map(cet => cet.cetTx),
      leafHash
    );

    await validateAdaptorSignatures(
      lenderMaturityCetAdaptorSigs,
      lenderDlcPubKey,
      maturityCets.map(cet => cet.outcomeSignaturePoint),
      maturityCets.map(cet => cet.cetTx),
      leafHash
    );
    
    const isLenderRepaymentCetAdaptorSigValid = await verifyCetAdaptorSig(
      lenderRepaymentCetAdaptorSig,
      lenderDlcPubKey,
      lenderRepaymentCommitment,
      repaymentCet,
      0,
      leafHash
    );
    expect(isLenderRepaymentCetAdaptorSigValid).to.be.true;
    
    // Now that everything is validated the lender can proceed with deploying the EVM repayment contract.
    // Once confirmed, the borrower can sign and broadcast the DLC init tx.

    // Signing of the DLC init txs inputs will be done by the users wallet.
    // For example with Unisat wallet, we'll be using the signPsbt function (I think).
    
    dlcInitTx.tx.sign(borrowerKey);
    expect((dlcInitTx.tx as any).isFullySigned()).to.be.true;
    
    // The borrower can now broadcast the DLC init tx.
    
    // ...
    
    ///////////////////////
    // CASE 1: REPAYMENT //
    ///////////////////////
    
    // Once the lenderRepaymentSecret is revealed in the EVM contract, the borrower (or any of the parties)
    // can finalize the adaptor signatures of the repayment CET.
    
    const cetSighash = sighashForAdaptorSig(repaymentCet, 0, leafHash);
    
    const finalizedBorrowerRepaymentCetAdaptorSig = await adaptSig(
      borrowerRepaymentCetAdaptorSig,
      BigInt('0x' + bytesToHex(lenderRepaymentSecret))
    );
    const finalizedLenderRepaymentCetAdaptorSig = await adaptSig(
      lenderRepaymentCetAdaptorSig,
      BigInt('0x' + bytesToHex(lenderRepaymentSecret))
    );
    
    const isBorrowerSigValid = await verifySigStrict(finalizedBorrowerRepaymentCetAdaptorSig, borrowerDlcPubKey, cetSighash);
    const isLenderSigValid = await verifySigStrict(finalizedLenderRepaymentCetAdaptorSig, lenderDlcPubKey, cetSighash);
    expect(isBorrowerSigValid).to.be.true;
    expect(isLenderSigValid).to.be.true;
    
    // Apply the finalized adaptor sigs to the CET tx.
    applySignaturesCet(
      repaymentCet,
      dlcInitTx.witnessScript,
      finalizedBorrowerRepaymentCetAdaptorSig,
      finalizedLenderRepaymentCetAdaptorSig, 
      dlcInitTx.cBlock
    );

    // Create funding tx for the CET, update the CET with an input to unlock it and sign that input.
    // In an actual app the funding UTXOs and signing would be facilitated by the users wallet.
    const fundingUtxos = [{
      txId: 'b'.repeat(64),
      outputIndex: 0,
      satoshis: 1000000,
      script: (Script as any).buildWitnessV0Out(borrowerAddress)
    }];
    feeRate = 6;
    
    const { cet } = fundCetFees(repaymentCet, fundingUtxos, borrowerAddress, borrowerAddress, feeRate);
    
    cet.sign(borrowerKey);
    
    // Now the funding and the CET tx are ready to be broadcast.
    
    // Optionaly locally interpret script execution for the CET tx to see if it's valid.
    const interpreter = new (Script as any).Interpreter();
    const flags = (Script as any).Interpreter.SCRIPT_VERIFY_WITNESS | (Script as any).Interpreter.SCRIPT_VERIFY_TAPROOT;
    const witnesses = (cet.inputs[0] as any).witnesses;
    const res = interpreter.verify(
      new Script(''),
      dlcInitTx.tx.outputs[0].script,
      cet,
      0,
      flags,
      witnesses,
      dlcInitTx.tx.outputs[0].satoshis
    );
    expect(res).to.be.true;
    
    //////////////////////////
    // CASE 2: ORACLE EVENT //
    //////////////////////////
    // If repayment doesn't happend before, then eventually the oracle will attest to an event outcome.
    // This is either one of the liquidation events outcomes or the maturity event outcome.
    const liquidationEvent = liquidationEvents[3];
    
    const oracleSig = await attestEventOutcome(
      oraclePrivKey,
      liquidationEvent.nonce as bigint,
      liquidationEvent.outcomeHashes[125]
    );
    
    // TODO: In the actual app here we would need to check if we created a liquidation cet for the exact outcome hash that the oracle attested to.
    
    // The lender (or any of the parties) can now verify the oracle sig and find and finalize the CET.
    // The related CET may be retrieved via the event ID reference.
    let liquidationCet: OracleCET;
    for (let i = 0; i < liquidationCets.length; i++) {
      if (liquidationCets[i].eventId === liquidationEvent.id) {
        liquidationCet = liquidationCets[i];
        const cetSighash = sighashForAdaptorSig(liquidationCet.cetTx, 0, leafHash);
        
        const borrowerLiquidationCetAdaptorSig = borrowerLiquidationCetAdaptorSigs[i];
        const lenderLiquidationCetAdaptorSig = lenderLiquidationCetAdaptorSigs[i];

        const finalizedBorrowerLiquidationCetAdaptorSig = await adaptSig(
          borrowerLiquidationCetAdaptorSig,
          oracleSig.s
        );
        const finalizedLenderLiquidationCetAdaptorSig = await adaptSig(
          lenderLiquidationCetAdaptorSig,
          oracleSig.s
        );
        
        const isBorrowerSigValid = await verifySigStrict(finalizedBorrowerLiquidationCetAdaptorSig, borrowerDlcPubKey, cetSighash);
        const isLenderSigValid = await verifySigStrict(finalizedLenderLiquidationCetAdaptorSig, lenderDlcPubKey, cetSighash);
        expect(isBorrowerSigValid).to.be.true;
        expect(isLenderSigValid).to.be.true;
        
        // Apply the finalized adaptor sigs to the CET tx.
        applySignaturesCet(
          liquidationCet.cetTx,
          dlcInitTx.witnessScript,
          finalizedBorrowerLiquidationCetAdaptorSig,
          finalizedLenderLiquidationCetAdaptorSig, 
          dlcInitTx.cBlock
        );

        // Create funding tx for the CET, update the CET with an input to unlock it and sign that input.
        const fundingUtxos = [{
          txId: 'b'.repeat(64),
          outputIndex: 0,
          satoshis: 1000000,
          script: (Script as any).buildWitnessV0Out(borrowerAddress)
        }];
        feeRate = 6;
        
        const { cet } = fundCetFees(liquidationCet.cetTx, fundingUtxos, borrowerAddress, borrowerAddress, feeRate);
        
        cet.sign(borrowerKey);
        
        // Now the funding and the CET tx are ready to be broadcast.
        
        // Optionaly locally interpret script execution for the CET tx to see if it's valid.
        const interpreter = new (Script as any).Interpreter();
        const flags = (Script as any).Interpreter.SCRIPT_VERIFY_WITNESS | (Script as any).Interpreter.SCRIPT_VERIFY_TAPROOT;
        const witnesses = (cet.inputs[0] as any).witnesses;
        const res = interpreter.verify(
          new Script(''),
          dlcInitTx.tx.outputs[0].script,
          cet,
          0,
          flags,
          witnesses,
          dlcInitTx.tx.outputs[0].satoshis
        );
        expect(res).to.be.true;

        break;
      }
    }

  });

}); 
