import {
  TxBuilder,
  Credential,
  stakeCredentialOf,
  toHex,
  UTxO,
  toText,
  fromText,
  Assets,
  addAssets,
  fromHex,
} from '@lucid-evolution/lucid';
import {
  addrDetails,
  burnCdp,
  closeCdp,
  depositCdp,
  freezeCdp,
  fromSystemParamsAsset,
  liquidateCdp,
  mintCdp,
  mkTreasuryAddr,
  openCdp,
  redeemCdp,
  SystemParams,
  withdrawCdp,
} from '../../src';
import {
  findAllActiveCdps,
  findAllNecessaryOrefs,
  findCdp,
  findFrozenCDPs,
  findPrice,
  findPriceOracleFromCollateralAsset,
} from './cdp-queries';
import {
  LucidContext,
  repeat,
  runAndAwaitTx,
  runAndAwaitTxBuilder,
} from '../test-helpers';
import { AssetInfo } from '../endpoints/initialize';
import { assert, expect } from 'vitest';
import { feedPriceOracleTx } from '../../src/contracts/price-oracle/transactions';
import {
  adaAssetClass,
  AssetClass,
  assetClassToUnit,
  assetClassValueOf,
  getInlineDatumOrThrow,
  matchSingle,
  mkAssetsOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import { parseCdpDatumOrThrow } from '../../src/contracts/cdp/types-new';
import { match, P } from 'ts-pattern';
import { findPriceOracle } from '../price-oracle/price-oracle-queries';
import { findCollateralAsset } from '../queries/iasset-queries';
import { runFeedPriceToOracle } from '../price-oracle/actions';
import { rationalFromInt, rationalMul } from '../../src/types/rational';
import { array as A } from 'fp-ts';
import { sendValueTo } from '../utils';
import { findRandomTreasuryUtxoWithAsset } from '../treasury/treasury-queries';

// Selects users wallet and opens a CDP with the given initial collateral and mint amount
export async function runOpenCdp(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  initialCollateral: bigint,
  initialMint: bigint,
  pythMessage?: string,
  directTreasuryPayment: boolean = false,
): Promise<TxBuilder> {
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
    context.lucid,
    orefs.collateralAsset,
  );

  const pythStateOref = pythMessage
    ? await context.lucid.utxoByUnit(
        assetClassToUnit(
          fromSystemParamsAsset(sysParams.pythConfig.pythStateAssetClass),
        ),
      )
    : undefined;

  return openCdp(
    initialCollateral,
    initialMint,
    sysParams,
    orefs.cdpCreatorUtxo,
    orefs.iasset.utxo,
    orefs.collateralAsset.utxo,
    priceOracleUtxo,
    orefs.interestOracleUtxo,
    directTreasuryPayment ? undefined : orefs.treasuryUtxo,
    context.lucid,
    context.emulator.slot,
    pythMessage,
    pythStateOref,
  );
}

export async function runDepositCdp(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  amount: bigint = 1_000_000n,
): Promise<TxBuilder> {
  const [pkh, skh] = await addrDetails(context.lucid);

  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  return await depositCdp(
    amount,
    cdp.utxo,
    orefs.iasset.utxo,
    orefs.collateralAsset.utxo,
    orefs.interestOracleUtxo,
    orefs.treasuryUtxo,
    orefs.interestCollectorUtxo,
    sysParams,
    context.lucid,
    context.emulator.slot,
  );
}

export async function runWithdrawCdp(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  amount: bigint = 1_000_000n,
  pythMessage?: string,
): Promise<TxBuilder> {
  const [pkh, skh] = await addrDetails(context.lucid);

  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo)
    .with({ OracleNft: P.select() }, (oracleNft) =>
      findPriceOracle(context.lucid, oracleNft),
    )
    .otherwise(() => undefined);

  const pythStateOref = pythMessage
    ? await context.lucid.utxoByUnit(
        sysParams.pythConfig.pythStateAssetClass[0].unCurrencySymbol +
          fromText('Pyth State'),
      )
    : undefined;

  return await withdrawCdp(
    amount,
    cdp.utxo,
    orefs.iasset.utxo,
    orefs.collateralAsset.utxo,
    priceOracleUtxo,
    orefs.interestOracleUtxo,
    orefs.treasuryUtxo,
    orefs.interestCollectorUtxo,
    sysParams,
    context.lucid,
    context.emulator.slot,
    pythMessage,
    pythStateOref,
  );
}

export async function runMintCdp(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  amount: bigint = 100_000n,
  pythMessage?: string,
  directTreasuryPayment: boolean = false,
): Promise<TxBuilder> {
  const [pkh, skh] = await addrDetails(context.lucid);

  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo)
    .with({ OracleNft: P.select() }, (oracleNft) =>
      findPriceOracle(context.lucid, oracleNft),
    )
    .otherwise(() => undefined);

  const pythStateOref = pythMessage
    ? await context.lucid.utxoByUnit(
        sysParams.pythConfig.pythStateAssetClass[0].unCurrencySymbol +
          fromText('Pyth State'),
      )
    : undefined;

  return await mintCdp(
    amount,
    cdp.utxo,
    orefs.iasset.utxo,
    orefs.collateralAsset.utxo,
    priceOracleUtxo,
    orefs.interestOracleUtxo,
    directTreasuryPayment ? undefined : orefs.treasuryUtxo,
    orefs.interestCollectorUtxo,
    sysParams,
    context.lucid,
    context.emulator.slot,
    pythMessage,
    pythStateOref,
  );
}

export async function runBurnCdp(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  amount: bigint = 100_000n,
): Promise<TxBuilder> {
  const [pkh, skh] = await addrDetails(context.lucid);

  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  return await burnCdp(
    amount,
    cdp.utxo,
    orefs.iasset.utxo,
    orefs.collateralAsset.utxo,
    orefs.interestOracleUtxo,
    orefs.treasuryUtxo,
    orefs.interestCollectorUtxo,
    sysParams,
    context.lucid,
    context.emulator.slot,
  );
}

export async function runCloseCdp(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
): Promise<TxBuilder> {
  const [pkh, skh] = await addrDetails(context.lucid);

  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  return await closeCdp(
    cdp.utxo,
    orefs.collateralAsset.utxo,
    orefs.interestOracleUtxo,
    orefs.interestCollectorUtxo,
    sysParams,
    context.lucid,
    context.emulator.slot,
  );
}

export async function runRedeemCdp(
  context: LucidContext,
  sysParams: SystemParams,
  iusdAssetInfo: AssetInfo,
  collateralAsset: AssetClass,
  pkh: string,
  skh: Credential | undefined,
  directTreasuryPayment: boolean = false,
): Promise<TxBuilder> {
  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh,
    skh,
  );

  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    iusdAssetInfo.iassetTokenNameAscii,
    collateralAsset,
  );

  const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo)
    .with({ OracleNft: P.select() }, (oracleNft) =>
      findPriceOracle(context.lucid, oracleNft),
    )
    .otherwise(() => undefined);

  return await redeemCdp(
    cdp.datum.mintedAmt,
    cdp.utxo,
    orefs.iasset.utxo,
    orefs.collateralAsset.utxo,
    priceOracleUtxo,
    orefs.interestOracleUtxo,
    orefs.interestCollectorUtxo,
    directTreasuryPayment ? undefined : orefs.treasuryUtxo,
    orefs.govUtxo,
    sysParams,
    context.lucid,
    context.emulator.slot,
  );
}

export async function runFreezeCdp(
  context: LucidContext,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
  pkh: string,
  skh: Credential | undefined,
  pythMessage?: string,
): Promise<TxBuilder> {
  const cdp = await findCdp(
    context.lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh,
    skh,
  );

  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const priceOracleUtxo = await match(orefs.collateralAsset.datum.priceInfo)
    .with({ OracleNft: P.select() }, (oracleNft) =>
      findPriceOracle(context.lucid, oracleNft),
    )
    .otherwise(() => undefined);

  const pythStateOref = pythMessage
    ? await context.lucid.utxoByUnit(
        sysParams.pythConfig.pythStateAssetClass[0].unCurrencySymbol +
          fromText('Pyth State'),
      )
    : undefined;
  return freezeCdp(
    cdp.utxo,
    orefs.iasset.utxo,
    orefs.collateralAsset.utxo,
    priceOracleUtxo,
    orefs.interestOracleUtxo,
    sysParams,
    context.lucid,
    context.emulator.slot,
    pythMessage,
    pythStateOref,
  );
}

export async function runCreateAndFreezeCdps(
  context: LucidContext,
  sysParams: SystemParams,
  assetInfo: AssetInfo,
  numberOfCdps: number,
  iasset: string,
  collateralAsset: AssetClass,
): Promise<void> {
  const collateralAssetOutput = await findCollateralAsset(
    context.lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken),
    iasset,
    collateralAsset,
  );

  let priceOracleUtxo = await findPriceOracleFromCollateralAsset(
    context.lucid,
    collateralAssetOutput,
  );

  await repeat(numberOfCdps, async () => {
    const orefs = await findAllNecessaryOrefs(
      context.lucid,
      sysParams,
      iasset,
      collateralAsset,
    );

    await runAndAwaitTx(
      context.lucid,
      openCdp(
        12_000_000n,
        6_000_000n,
        sysParams,
        orefs.cdpCreatorUtxo,
        orefs.iasset.utxo,
        orefs.collateralAsset.utxo,
        priceOracleUtxo,
        orefs.interestOracleUtxo,
        orefs.treasuryUtxo,
        context.lucid,
        context.emulator.slot,
      ),
    );
  });

  await runAndAwaitTx(
    context.lucid,
    feedPriceOracleTx(
      context.lucid,
      priceOracleUtxo!,
      { numerator: 18n, denominator: 10n }, // 1.8
      assetInfo.collateralAssets[0].oracleParams!,
      context.emulator.slot,
    ),
  );

  priceOracleUtxo = await findPriceOracleFromCollateralAsset(
    context.lucid,
    collateralAssetOutput,
  );

  {
    const activeCdps = await findAllActiveCdps(
      context.lucid,
      sysParams,
      iasset,
      stakeCredentialOf(context.users.admin.address),
    );

    expect(
      activeCdps.length === numberOfCdps,
      `Expected ${numberOfCdps} cdps`,
    ).toBeTruthy();

    for (const cdp of activeCdps) {
      const orefs = await findAllNecessaryOrefs(
        context.lucid,
        sysParams,
        iasset,
        collateralAsset,
      );

      await runAndAwaitTx(
        context.lucid,
        freezeCdp(
          cdp.utxo,
          orefs.iasset.utxo,
          orefs.collateralAsset.utxo,
          priceOracleUtxo,
          orefs.interestOracleUtxo,
          sysParams,
          context.lucid,
          context.emulator.slot,
        ),
      );
    }
  }
}

export async function runLiquidateCdp(
  context: LucidContext,
  sysParams: SystemParams,
  frozenCdpUtxo: UTxO,
  treasuryUtxo?: UTxO,
  directTreasuryPayment: boolean = false,
): Promise<TxBuilder> {
  const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(frozenCdpUtxo));
  const orefs = await findAllNecessaryOrefs(
    context.lucid,
    sysParams,
    toText(toHex(cdpDatum.iasset)),
    cdpDatum.collateralAsset,
  );
  return liquidateCdp(
    frozenCdpUtxo,
    orefs.stabilityPoolUtxo,
    orefs.interestCollectorUtxo,
    directTreasuryPayment
      ? undefined
      : treasuryUtxo
        ? treasuryUtxo
        : orefs.treasuryUtxo,
    sysParams,
    context.lucid,
  );
}

/**
 * Expecting price double will make CDP liquidatable.
 */
export async function executeLiquidation(
  context: LucidContext,
  sysParams: SystemParams,
  liquidatedDebt: bigint,
  liquidatedCollateral: bigint,
  collateralAsset: AssetClass,
  assetInfo: AssetInfo,
  liquidateWrapper: (liquidateTx: TxBuilder) => Promise<void> = async (tx) => {
    await runAndAwaitTxBuilder(context.lucid, tx);
  },
  directTreasuryPayment: boolean = false,
) {
  const price = await findPrice(
    context.lucid,
    sysParams,
    assetInfo.iassetTokenNameAscii,
    collateralAsset,
  );

  const [pkh, skh] = await addrDetails(context.lucid);

  await runAndAwaitTx(
    context.lucid,
    runOpenCdp(
      context,
      sysParams,
      assetInfo.iassetTokenNameAscii,
      collateralAsset,
      liquidatedCollateral,
      liquidatedDebt,
    ),
  );

  const user4Value = A.reduce<UTxO, Assets>({}, (acc, utxo) =>
    addAssets(acc, utxo.assets),
  )(await context.lucid.utxosAt(context.users['user4'].address));

  const iassetAc: AssetClass = {
    currencySymbol: fromHex(
      sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
    ),
    tokenName: fromHex(fromText(assetInfo.iassetTokenNameAscii)),
  };

  // Send iAssets to treasury so liquidation is performed from a clean address.
  await sendValueTo(
    mkTreasuryAddr(context.lucid, sysParams),
    mkAssetsOf(iassetAc, assetClassValueOf(user4Value, iassetAc)),
    context.lucid,
  );

  const user4collateralAssetAmount = assetClassValueOf(
    user4Value,
    collateralAsset,
  );

  // Send collateral asset to treasury so liquidation is performed from a clean address.
  if (collateralAsset !== adaAssetClass && user4collateralAssetAmount > 0n) {
    await sendValueTo(
      mkTreasuryAddr(context.lucid, sysParams),
      mkAssetsOf(collateralAsset, user4collateralAssetAmount),
      context.lucid,
    );
  }

  const user4ValueBeforeLiquidation = A.reduce<UTxO, Assets>({}, (acc, utxo) =>
    addAssets(acc, utxo.assets),
  )(await context.lucid.utxosAt(context.users['user4'].address));

  assert(Object.keys(user4ValueBeforeLiquidation).length === 1);

  context.emulator.awaitSlot(1000);

  await runFeedPriceToOracle(
    context,
    sysParams,
    assetInfo,
    collateralAsset,
    rationalMul(price, rationalFromInt(2n)),
  );

  await runAndAwaitTx(
    context.lucid,
    runFreezeCdp(
      context,
      sysParams,
      assetInfo.iassetTokenNameAscii,
      collateralAsset,
      pkh.hash,
      skh,
    ),
  );

  const frozenCdp = matchSingle(
    await findFrozenCDPs(
      context.lucid,
      sysParams.validatorHashes.cdpHash,
      fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
      assetInfo.iassetTokenNameAscii,
    ),
    (_) => new Error('Expected only single frozen CDP'),
  );

  let treasuryUtxo = undefined;

  if (!directTreasuryPayment) {
    treasuryUtxo = await findRandomTreasuryUtxoWithAsset(
      context.lucid,
      sysParams,
      collateralAsset,
    );

    // This treasury UTxO should contain ADA and collateral asset.
    if (collateralAsset !== adaAssetClass) {
      assert(Object.keys(treasuryUtxo.assets).length === 2);
    }
  }

  await liquidateWrapper(
    await runLiquidateCdp(
      context,
      sysParams,
      frozenCdp.utxo,
      treasuryUtxo,
      directTreasuryPayment,
    ),
  );

  await runFeedPriceToOracle(
    context,
    sysParams,
    assetInfo,
    collateralAsset,
    price,
  );
}
