import {
  Credential,
  fromText,
  LucidEvolution,
  ScriptHash,
  toHex,
  toText,
  UTxO,
} from '@lucid-evolution/lucid';
import {
  addrDetails,
  adjustPriceToDecimals,
  cdpCollateralRatioPercentage,
  createScriptAddress,
  fromSystemParamsAsset,
  matchSingle,
  SystemParams,
} from '../../src';
import { option as O, array as A, function as F } from 'fp-ts';
import {
  CDPContent,
  parseCdpDatum,
  parseStableswapPoolDatum,
  StableswapPoolContent,
} from '../../src/contracts/cdp/types-new';
import { match, P } from 'ts-pattern';
import {
  AssetClass,
  assetClassToUnit,
  getInlineDatumOrThrow,
  getRandomElement,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
  InterestOracleDatum,
  parseInterestOracleDatum,
} from '../../src/contracts/interest-oracle/types-new';
import { findCollateralAsset, findIAsset } from '../queries/iasset-queries';
import { findStabilityPool } from '../queries/stability-pool-queries';
import { findPriceOracle } from '../price-oracle/price-oracle-queries';
import { findInterestOracle } from '../queries/interest-oracle-queries';
import { findRandomCollector } from '../queries/collector-queries';
import { findGov } from '../gov/governance-queries';
import { findRandomTreasuryUtxoWithOnlyAda } from '../treasury/treasury-queries';
import { parsePriceOracleDatum } from '../../src/contracts/price-oracle/types-new';
import {
  CollateralAssetOutput,
  IAssetOutput,
} from '../../src/contracts/iasset/types';
import { findRandomNonAdminInterestCollector } from '../interest-collection/interest-collector-queries';
import { LucidContext } from '../test-helpers';
import { Rational } from '../../src/types/rational';

export async function findAllActiveCdps(
  lucid: LucidEvolution,
  sysParams: SystemParams,
  assetAscii: string,
  stakeCred?: Credential,
): Promise<{ utxo: UTxO; datum: CDPContent }[]> {
  const cdpUtxos = await lucid.utxosAtWithUnit(
    createScriptAddress(
      lucid.config().network!,
      sysParams.validatorHashes.cdpHash,
      stakeCred,
    ),
    assetClassToUnit(fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken)),
  );

  return F.pipe(
    cdpUtxos.map((utxo) =>
      F.pipe(
        O.fromNullable(utxo.datum),
        O.flatMap(parseCdpDatum),
        O.flatMap((datum) => {
          if (toHex(datum.iasset) === fromText(assetAscii) && datum.cdpOwner) {
            return O.some({ utxo, datum: datum });
          } else {
            return O.none;
          }
        }),
      ),
    ),
    A.compact,
  );
}

export async function findCdp(
  lucid: LucidEvolution,
  cdpScriptHash: ScriptHash,
  cdpNft: AssetClass,
  ownerPkh: string,
  stakeCred?: Credential,
): Promise<{ utxo: UTxO; datum: CDPContent }> {
  const cdpUtxos = await lucid.utxosAtWithUnit(
    createScriptAddress(lucid.config().network!, cdpScriptHash, stakeCred),
    assetClassToUnit(cdpNft),
  );

  return matchSingle(
    F.pipe(
      cdpUtxos.map((utxo) =>
        F.pipe(
          O.fromNullable(utxo.datum),
          O.flatMap(parseCdpDatum),
          O.flatMap((datum) => {
            if (datum.cdpOwner && toHex(datum.cdpOwner) === ownerPkh) {
              return O.some({ utxo, datum: datum });
            } else {
              return O.none;
            }
          }),
        ),
      ),
      A.compact,
    ),
    (res) => new Error('Expected a single CDP UTXO.: ' + JSON.stringify(res)),
  );
}

// TODO: use the new variant defined below.
export async function findOwnCdp(
  lucid: LucidEvolution,
  cdpScriptHash: ScriptHash,
  cdpNft: AssetClass,
): Promise<{ utxo: UTxO; datum: CDPContent }> {
  const [pkh, skh] = await addrDetails(lucid);
  return findCdp(lucid, cdpScriptHash, cdpNft, pkh.hash, skh);
}

export async function findOwnCdpNew(
  lucid: LucidEvolution,
  sysParams: SystemParams,
): Promise<{ utxo: UTxO; datum: CDPContent }> {
  const [pkh, skh] = await addrDetails(lucid);
  return findCdp(
    lucid,
    sysParams.validatorHashes.cdpHash,
    fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
    pkh.hash,
    skh,
  );
}

export async function findFrozenCDPs(
  lucid: LucidEvolution,
  cdpScriptHash: ScriptHash,
  cdpNft: AssetClass,
  assetAscii: string,
): Promise<{ utxo: UTxO; datum: CDPContent }[]> {
  const cdpUtxos = await lucid.utxosAtWithUnit(
    createScriptAddress(lucid.config().network!, cdpScriptHash),
    assetClassToUnit(cdpNft),
  );

  return F.pipe(
    cdpUtxos.map((utxo) =>
      F.pipe(
        O.fromNullable(utxo.datum),
        O.flatMap(parseCdpDatum),
        O.flatMap((datum) => {
          if (
            datum.cdpOwner == null &&
            toHex(datum.iasset) === fromText(assetAscii)
          ) {
            return O.some({ utxo, datum: datum });
          } else {
            return O.none;
          }
        }),
      ),
    ),
    A.compact,
  );
}

export async function findStableswapPool(
  lucid: LucidEvolution,
  cdpScriptHash: ScriptHash,
  cdpNft: AssetClass,
  iassetName: string,
  collateralAsset: AssetClass,
): Promise<{ utxo: UTxO; datum: StableswapPoolContent }> {
  const cdpUtxos = await lucid.utxosAtWithUnit(
    createScriptAddress(lucid.config().network!, cdpScriptHash),
    assetClassToUnit(cdpNft),
  );

  return matchSingle(
    F.pipe(
      cdpUtxos.map((utxo) =>
        F.pipe(
          O.fromNullable(utxo.datum),
          O.flatMap(parseStableswapPoolDatum),
          O.flatMap((datum) => {
            if (
              toHex(datum.iasset) === iassetName &&
              toHex(datum.collateralAsset.currencySymbol) ===
                toHex(collateralAsset.currencySymbol) &&
              toHex(datum.collateralAsset.tokenName) ===
                toHex(collateralAsset.tokenName)
            ) {
              return O.some({ utxo, datum: datum });
            } else {
              return O.none;
            }
          }),
        ),
      ),
      A.compact,
    ),
    (res) =>
      new Error(
        'Expected a single stableswap pool UTXO.: ' + JSON.stringify(res),
      ),
  );
}

export async function findAllCdpCreators(
  lucid: LucidEvolution,
  cdpCreatorScriptHash: string,
  cdpCreatorNft: AssetClass,
): Promise<UTxO[]> {
  return lucid.utxosAtWithUnit(
    createScriptAddress(lucid.config().network!, cdpCreatorScriptHash),
    assetClassToUnit(cdpCreatorNft),
  );
}

// TODO: replace by the new variant defined below
export async function findRandomCdpCreator(
  lucid: LucidEvolution,
  cdpCreatorScriptHash: string,
  cdpCreatorNft: AssetClass,
): Promise<UTxO> {
  const cdpCreatorUtxos = await findAllCdpCreators(
    lucid,
    cdpCreatorScriptHash,
    cdpCreatorNft,
  );

  return F.pipe(
    O.fromNullable(getRandomElement(cdpCreatorUtxos)),
    O.match(() => {
      throw new Error('Expected some cdp creator UTXOs.');
    }, F.identity),
  );
}

export async function findRandomCdpCreatorNew(
  context: LucidContext,
  sysParams: SystemParams,
): Promise<UTxO> {
  const cdpCreatorUtxos = await findAllCdpCreators(
    context.lucid,
    sysParams.validatorHashes.cdpCreatorHash,
    fromSystemParamsAsset(sysParams.cdpCreatorParams.cdpCreatorNft),
  );

  return F.pipe(
    O.fromNullable(getRandomElement(cdpCreatorUtxos)),
    O.match(() => {
      throw new Error('Expected some cdp creator UTXOs.');
    }, F.identity),
  );
}

export function findPriceOracleFromCollateralAsset(
  lucid: LucidEvolution,
  collateralAsset: CollateralAssetOutput,
): Promise<UTxO | undefined> {
  return match(collateralAsset.datum.priceInfo)
    .with({ OracleNft: P.select() }, (oracleNft) =>
      findPriceOracle(lucid, oracleNft),
    )
    .with({ Delisted: P.any }, () => {
      throw new Error('Cannot find price oracle as iAsset is delisted');
    })
    .otherwise(() => Promise.resolve(undefined));
}

export async function findAllNecessaryOrefs(
  lucid: LucidEvolution,
  sysParams: SystemParams,
  iasset: string,
  collateralAsset: AssetClass,
): Promise<{
  stabilityPoolUtxo: UTxO;
  iasset: IAssetOutput;
  collateralAsset: CollateralAssetOutput;
  cdpCreatorUtxo: UTxO;
  interestOracleUtxo: UTxO;
  collectorUtxo: UTxO;
  interestCollectorUtxo: UTxO;
  govUtxo: UTxO;
  treasuryUtxo: UTxO;
}> {
  const iassetOut = await findIAsset(
    lucid,
    sysParams.validatorHashes.iassetHash,
    fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken),
    iasset,
  );

  const collateralAssetOut = await findCollateralAsset(
    lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpCreatorParams.collateralAssetAuthTk),
    iasset,
    collateralAsset,
  );

  const stabilityPool = await findStabilityPool(lucid, sysParams, iasset);

  return {
    stabilityPoolUtxo: stabilityPool.utxo,
    iasset: iassetOut,
    collateralAsset: collateralAssetOut,
    cdpCreatorUtxo: await findRandomCdpCreator(
      lucid,
      sysParams.validatorHashes.cdpCreatorHash,
      fromSystemParamsAsset(sysParams.cdpCreatorParams.cdpCreatorNft),
    ),
    interestOracleUtxo: await findInterestOracle(
      lucid,
      collateralAssetOut.datum.interestOracleNft,
    ),
    collectorUtxo: await findRandomCollector(
      lucid,
      sysParams.validatorHashes.collectorHash,
    ),
    interestCollectorUtxo: await findRandomNonAdminInterestCollector(
      lucid,
      sysParams.validatorHashes.interestCollectionHash,
      fromSystemParamsAsset(sysParams.interestCollectionParams.multisigUtxoNft),
    ),
    govUtxo: (
      await findGov(
        lucid,
        sysParams.validatorHashes.govHash,
        fromSystemParamsAsset(sysParams.govParams.govNFT),
      )
    ).utxo,
    treasuryUtxo: await findRandomTreasuryUtxoWithOnlyAda(lucid, sysParams),
  };
}

export async function findPrice(
  lucid: LucidEvolution,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
): Promise<Rational> {
  const orefs = await findAllNecessaryOrefs(
    lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const priceIasset = await match(orefs.collateralAsset.datum.priceInfo)
    .with({ OracleNft: P.select() }, async (oracleNft) => {
      const priceOracleUtxo = matchSingle(
        await lucid.utxosByOutRef([await findPriceOracle(lucid, oracleNft)]),
        (_) => new Error('Expected a single price oracle UTXO'),
      );
      return parsePriceOracleDatum(getInlineDatumOrThrow(priceOracleUtxo))
        .price;
    })
    .with({ Delisted: P.select() }, (price) => {
      return price.price;
    })
    .with({ DeferredValidation: P.select() }, (_) => {
      throw new Error('Pyth price not implemented');
    })
    .exhaustive();

  return priceIasset;
}

export async function findInterestDatum(
  lucid: LucidEvolution,
  sysParams: SystemParams,
  asset: string,
  collateralAsset: AssetClass,
): Promise<InterestOracleDatum> {
  const orefs = await findAllNecessaryOrefs(
    lucid,
    sysParams,
    asset,
    collateralAsset,
  );

  const interestOracleUtxo = matchSingle(
    await lucid.utxosByOutRef([orefs.interestOracleUtxo]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );
  return parseInterestOracleDatum(getInlineDatumOrThrow(interestOracleUtxo));
}

export async function findCdpCR(
  lucid: LucidEvolution,
  sysParams: SystemParams,
  cdp: { utxo: UTxO; datum: CDPContent },
  slot: number,
): Promise<number> {
  const iassetAscii = toText(toHex(cdp.datum.iasset));

  const collateralAsset = await findCollateralAsset(
    lucid,
    sysParams,
    fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken),
    iassetAscii,
    cdp.datum.collateralAsset,
  );

  const price = await findPrice(
    lucid,
    sysParams,
    iassetAscii,
    cdp.datum.collateralAsset,
  );

  const adjustedPrice = adjustPriceToDecimals(
    collateralAsset.datum.extraDecimals,
    price,
  );

  return cdpCollateralRatioPercentage(
    slot,
    adjustedPrice,
    cdp.utxo,
    cdp.datum,
    await findInterestDatum(
      lucid,
      sysParams,
      iassetAscii,
      cdp.datum.collateralAsset,
    ),
    lucid.config().network!,
  );
}
