import {
  fromHex,
  fromText,
  OutRef,
  paymentCredentialOf,
  unixTimeToSlot,
} from '@lucid-evolution/lucid';
import {
  addrDetails,
  createProposal,
  createShardsChunks,
  endProposal,
  executeProposal,
  fromSystemParamsAsset,
  InterestOracleParams,
  mergeShards,
  openStakingPosition,
  PriceOracleParams,
  startInterestOracle,
  SystemParams,
  vote,
} from '../../src';
import { VoteOption } from '../../src/contracts/poll/types-poll-new';
import {
  findAllPollShards,
  findPollManager,
  findRandomPollShard,
} from '../queries/poll-queries';
import { findStakingPosition } from '../queries/staking-queries';
import {
  LucidContext,
  runAndAwaitTx,
  runAndAwaitTxBuilder,
  selectWalletByAddress,
} from '../test-helpers';
import { findGov } from './governance-queries';
import { array as A } from 'fp-ts';
import { expect } from 'vitest';
import { findExecute } from '../queries/execute-queries';
import { CollateralAssetInfo } from '../endpoints/initialize';
import { startPriceOracleTx } from '../../src/contracts/price-oracle/transactions';
import { AssetClass } from '@3rd-eye-labs/cardano-offchain-common';
import {
  findAllCollateralAssetsOfIAsset,
  findIAsset,
} from '../queries/iasset-queries';
import { Rational, rationalFromInt } from '../../src/types/rational';

export async function runVote(
  pollId: bigint,
  option: VoteOption,
  sysParams: SystemParams,
  context: LucidContext,
): Promise<void> {
  const [pkh, _] = await addrDetails(context.lucid);

  const stakingPosOref = await findStakingPosition(
    context.lucid,
    sysParams.validatorHashes.stakingHash,
    fromSystemParamsAsset(sysParams.stakingParams.stakingToken),
    pkh.hash,
  );

  const pollShard = await findRandomPollShard(
    context.lucid,
    sysParams.validatorHashes.pollShardHash,
    fromSystemParamsAsset(sysParams.pollShardParams.pollToken),
    pollId,
  );

  await runAndAwaitTx(
    context.lucid,
    vote(
      option,
      pollShard.utxo,
      stakingPosOref.utxo,
      sysParams,
      context.lucid,
      context.emulator.slot,
    ),
  );
}

export async function runCreateAllShards(
  pollId: bigint,
  sysParams: SystemParams,
  context: LucidContext,
): Promise<void> {
  const govUtxo = await findGov(
    context.lucid,
    sysParams.validatorHashes.govHash,
    fromSystemParamsAsset(sysParams.govParams.govNFT),
  );

  for (
    let i = 0;
    i < Math.ceil(Number(govUtxo.datum.protocolParams.totalShards) / 2);
    i++
  ) {
    const pollUtxo = await findPollManager(
      context.lucid,
      sysParams.validatorHashes.pollManagerHash,
      fromSystemParamsAsset(sysParams.pollManagerParams.pollToken),
      pollId,
    );

    await runAndAwaitTx(
      context.lucid,
      createShardsChunks(
        2n,
        pollUtxo.utxo,
        sysParams,
        context.emulator.slot,
        context.lucid,
      ),
    );
  }
}

export async function runMergeAllShards(
  pollId: bigint,
  sysParams: SystemParams,
  context: LucidContext,
): Promise<void> {
  const govUtxo = await findGov(
    context.lucid,
    sysParams.validatorHashes.govHash,
    fromSystemParamsAsset(sysParams.govParams.govNFT),
  );

  for (
    let i = 0;
    i < Math.ceil(Number(govUtxo.datum.protocolParams.totalShards) / 2);
    i++
  ) {
    const pollUtxo = await findPollManager(
      context.lucid,
      sysParams.validatorHashes.pollManagerHash,
      fromSystemParamsAsset(sysParams.pollManagerParams.pollToken),
      pollId,
    );

    const allPollShards = await findAllPollShards(
      context.lucid,
      sysParams.validatorHashes.pollShardHash,
      fromSystemParamsAsset(sysParams.pollShardParams.pollToken),
      pollUtxo.datum.pollId,
    );

    await runAndAwaitTx(
      context.lucid,
      mergeShards(
        pollUtxo.utxo,
        A.takeLeft(2)(allPollShards).map((u) => u.utxo),
        sysParams,
        context.lucid,
        context.emulator.slot,
      ),
    );
  }
}

export async function runEndProposal(
  pollId: bigint,
  sysParams: SystemParams,
  context: LucidContext,
): Promise<void> {
  const pollUtxo = await findPollManager(
    context.lucid,
    sysParams.validatorHashes.pollManagerHash,
    fromSystemParamsAsset(sysParams.pollManagerParams.pollToken),
    pollId,
  );

  const govUtxo = await findGov(
    context.lucid,
    sysParams.validatorHashes.govHash,
    fromSystemParamsAsset(sysParams.govParams.govNFT),
  );

  await runAndAwaitTx(
    context.lucid,
    endProposal(
      pollUtxo.utxo,
      govUtxo.utxo,
      sysParams,
      context.lucid,
      context.emulator.slot,
    ),
  );
}

export async function waitForVotingEnd(
  pollId: bigint,
  sysParams: SystemParams,
  context: LucidContext,
): Promise<void> {
  const pollUtxo = await findPollManager(
    context.lucid,
    sysParams.validatorHashes.pollManagerHash,
    fromSystemParamsAsset(sysParams.pollManagerParams.pollToken),
    pollId,
  );

  const targetSlot = unixTimeToSlot(
    context.lucid.config().network!,
    Number(pollUtxo.datum.votingEndTime),
  );

  if (targetSlot > context.emulator.slot) {
    expect(targetSlot).toBeGreaterThan(context.emulator.slot);

    context.emulator.awaitSlot(targetSlot - context.emulator.slot + 1);
  }
}

export async function runExecuteProposal(
  pollId: bigint,
  treasuryWithdrawalOref: OutRef | null,
  allIAssetOrefs: OutRef[] | null,
  iAssetOref: OutRef | null,
  allCollateralAssetsOrefsOfIAsset: OutRef[] | null,
  collateralAssetOref: OutRef | null,
  stableswapPoolOrCdpCreatorOref: OutRef | null,
  sysParams: SystemParams,
  context: LucidContext,
): Promise<void> {
  const executeUtxo = await findExecute(
    context.lucid,
    sysParams.validatorHashes.executeHash,
    fromSystemParamsAsset(sysParams.executeParams.upgradeToken),
    pollId,
  );

  await runAndAwaitTx(
    context.lucid,
    executeProposal(
      executeUtxo.utxo,
      (
        await findGov(
          context.lucid,
          sysParams.validatorHashes.govHash,
          fromSystemParamsAsset(sysParams.govParams.govNFT),
        )
      ).utxo,
      treasuryWithdrawalOref,
      allIAssetOrefs,
      iAssetOref,
      allCollateralAssetsOrefsOfIAsset,
      collateralAssetOref,
      stableswapPoolOrCdpCreatorOref,
      sysParams,
      context.lucid,
      context.emulator.slot,
    ),
  );
}

export async function processSuccessfulProposal(
  pollId: bigint,
  treasuryWithdrawalOref: OutRef | null,
  allIAssetOrefs: OutRef[] | null,
  iAssetOref: OutRef | null,
  allCollateralAssetsOrefsOfIAsset: OutRef[] | null,
  collateralAssetOref: OutRef | null,
  stableswapPoolOrCdpCreatorOref: OutRef | null,
  sysParams: SystemParams,
  context: LucidContext,
  createStakingPos: boolean = true,
): Promise<void> {
  await runCreateAllShards(pollId, sysParams, context);

  if (createStakingPos) {
    await runAndAwaitTx(
      context.lucid,
      openStakingPosition(100_000_000_000n, sysParams, context.lucid),
    );
  }

  await runVote(pollId, 'Yes', sysParams, context);

  await waitForVotingEnd(pollId, sysParams, context);

  await runMergeAllShards(pollId, sysParams, context);

  await runEndProposal(pollId, sysParams, context);

  await runExecuteProposal(
    pollId,
    treasuryWithdrawalOref,
    allIAssetOrefs,
    iAssetOref,
    allCollateralAssetsOrefsOfIAsset,
    collateralAssetOref,
    stableswapPoolOrCdpCreatorOref,
    sysParams,
    context,
  );
}

export async function whitelistCollateralAsset(
  context: LucidContext,
  sysParams: SystemParams,
  iassetAscii: string,
  newCollateralAsset: AssetClass,
  initialPrice: Rational,
  initialInterest: bigint,
  createStakingPos: boolean,
): Promise<CollateralAssetInfo> {
  const prevAddr = await context.lucid.wallet().address();

  context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);

  const interestOracleParams: InterestOracleParams = {
    biasTime: 120_000n,
    owner: paymentCredentialOf(context.users.admin.address).hash,
  };
  const [startInterestTx, interestOracleNft] = await startInterestOracle(
    0n,
    initialInterest,
    0n,
    interestOracleParams,
    context.lucid,
  );
  await runAndAwaitTxBuilder(context.lucid, startInterestTx);

  const oracleParams: PriceOracleParams = {
    biasTime: 120_000n,
    expirationPeriod: 1_800_000n,
    owner: paymentCredentialOf(context.users.admin.address).hash,
  };
  const [priceOracleTx, priceOranceNft] = await startPriceOracleTx(
    context.lucid,
    'COLLATERAL_ORACLE',
    initialPrice,
    oracleParams,
    context.emulator.slot,
  );
  await runAndAwaitTxBuilder(context.lucid, priceOracleTx);

  const [tx, pollId] = await createProposal(
    {
      AddCollateralAsset: {
        correspondingIAsset: fromHex(fromText(iassetAscii)),
        collateralAsset: newCollateralAsset,
        assetExtraDecimals: 0n,
        assetPriceInfo: { OracleNft: priceOranceNft },
        interestOracleNft: interestOracleNft,
        redemptionRatio: rationalFromInt(2n),
        maintenanceRatio: { numerator: 15n, denominator: 10n },
        liquidationRatio: { numerator: 12n, denominator: 10n },
        minCollateralAmt: 1_000_000n,
      },
    },
    null,
    sysParams,
    context.lucid,
    context.emulator.slot,
    (
      await findGov(
        context.lucid,
        sysParams.validatorHashes.govHash,
        fromSystemParamsAsset(sysParams.govParams.govNFT),
      )
    ).utxo,
    [],
  );

  await runAndAwaitTxBuilder(context.lucid, tx);

  const iassetOutput = await findIAsset(
    context.lucid,
    sysParams.validatorHashes.iassetHash,
    fromSystemParamsAsset(sysParams.executeParams.iAssetToken),
    iassetAscii,
  );

  await processSuccessfulProposal(
    pollId,
    null,
    null,
    iassetOutput.utxo,
    (
      await findAllCollateralAssetsOfIAsset(
        context.lucid,
        sysParams.validatorHashes.iassetHash,
        fromSystemParamsAsset(sysParams.executeParams.collateralAssetToken),
        fromHex(fromText(iassetAscii)),
      )
    ).map((o) => o.utxo),
    null,
    null,
    sysParams,
    context,
    createStakingPos,
  );

  selectWalletByAddress(context, prevAddr);

  return {
    collateralAsset: newCollateralAsset,
    interestOracleNft: interestOracleNft,
    oracleParams: oracleParams,
    interestOracleParams,
  };
}
