import { pipe } from 'fp-ts/lib/function';
import {
  array as A,
  option as O,
  string as S,
  ord as Ord,
  function as F,
} from 'fp-ts';
import {
  addAssets,
  Assets,
  LucidEvolution,
  OutRef,
  toHex,
  toText,
  UTxO,
} from '@lucid-evolution/lucid';
import { matchSingle } from '../../utils/utils';
import { getInlineDatumOrThrow } from '../../utils/lucid-utils';
import {
  AddCollateralAsssetContent,
  ProposeAssetContent,
  TreasuryWithdrawal,
  TreasuryWithdrawalItem,
} from './types-new';
import { AssetClass, mkAssetsOf } from '@3rd-eye-labs/cardano-offchain-common';
import {
  CollateralAssetContent,
  CollateralAssetOutput,
  IAssetContent,
  IAssetOutput,
  parseCollateralAssetDatumOrThrow,
  parseIAssetDatumOrThrow,
} from '../iasset/types';
import { getAssetClassComparisonStr, ParsedOutput } from '../../types/generic';

export function proposalDeposit(
  baseDeposit: bigint,
  activeProposals: bigint,
): bigint {
  return baseDeposit * 2n ** activeProposals;
}

export function createValueFromWithdrawal(w: TreasuryWithdrawal): Assets {
  return A.reduce<TreasuryWithdrawalItem, Assets>({}, (acc, item) =>
    addAssets(
      acc,
      mkAssetsOf(
        {
          currencySymbol: item.currencySymbol,
          tokenName: item.tokenName,
        },
        item.amount,
      ),
    ),
  )([...w.value]);
}

/**
 * A generic solution for iassets and collateral assets chain.
 */
async function findRelativeAssetForInsertion<
  T extends Uint8Array<ArrayBufferLike> | AssetClass,
  K,
>(
  newAsset: T,
  allIAssetOrefs: OutRef[],
  lucid: LucidEvolution,
  assetOrd: Ord.Ord<T>,
  getComparisonAsset: (datum: K) => T,
  parseDatum: (utxo: UTxO) => K,
): Promise<O.Option<ParsedOutput<K>>> {
  const parsedUtxos = await Promise.all(
    allIAssetOrefs.map(async (oref) => {
      const utxo = matchSingle(
        await lucid.utxosByOutRef([oref]),
        (_) => new Error('Expected a single UTXO'),
      );

      const datum = parseDatum(utxo);

      return { datum: datum, utxo: utxo };
    }),
  );

  // The iasset just before the new token name based on assets ordering
  return pipe(
    parsedUtxos,
    // Sort by the asset names
    A.sort(
      Ord.contramap<T, ParsedOutput<K>>((a) => getComparisonAsset(a.datum))(
        assetOrd,
      ),
    ),
    // split head and tail
    A.foldLeft(
      () => O.none,
      (head, rest) =>
        O.some<[ParsedOutput<K>, ParsedOutput<K>[]]>([head, rest]),
    ),
    // find the preceding iasset for the new token name
    O.flatMap(([firstIAsset, rest]) =>
      O.some(
        A.reduce<ParsedOutput<K>, ParsedOutput<K>>(
          firstIAsset,
          (acc, iasset) => {
            const isGreater =
              assetOrd.compare(getComparisonAsset(iasset.datum), newAsset) ===
              -1;

            // When the new asset is greater, stop searching for the reference.
            return isGreater ? iasset : acc;
          },
        )(rest),
      ),
    ),
  );
}

/**
 * Find the IAsset that should be a preceding one for the new IAsset token name.
 * In case there are no iassets, none should be returned.
 */
export async function findRelativeIAssetForInsertion(
  newIAssetTokenName: Uint8Array<ArrayBufferLike>,
  allIAssetOrefs: OutRef[],
  lucid: LucidEvolution,
): Promise<O.Option<IAssetOutput>> {
  return findRelativeAssetForInsertion(
    newIAssetTokenName,
    allIAssetOrefs,
    lucid,
    Ord.contramap<string, Uint8Array<ArrayBufferLike>>((x) => toText(toHex(x)))(
      S.Ord,
    ),
    (d) => d.assetName,
    (utxo) => parseIAssetDatumOrThrow(getInlineDatumOrThrow(utxo)),
  );
}

/**
 * Find collateral asset that should be a preceding one for the new Collateral asset.
 * In case there are no collateral assets, none should be returned.
 */
export async function findRelativeCollateralAssetForInsertion(
  newCollateralAsset: AssetClass,
  allCollateralAssetOrefs: OutRef[],
  lucid: LucidEvolution,
): Promise<O.Option<CollateralAssetOutput>> {
  return findRelativeAssetForInsertion(
    newCollateralAsset,
    allCollateralAssetOrefs,
    lucid,
    Ord.contramap<string, AssetClass>((x) => getAssetClassComparisonStr(x))(
      S.Ord,
    ),
    (d) => d.collateralAsset,
    (utxo) => parseCollateralAssetDatumOrThrow(getInlineDatumOrThrow(utxo)),
  );
}

/**
 * A generic solution for iassets and collateral assets chain.
 */
async function findFirstAsset<
  T extends Uint8Array<ArrayBufferLike> | AssetClass,
  K,
>(
  allIAssetOrefs: OutRef[],
  lucid: LucidEvolution,
  assetOrd: Ord.Ord<T>,
  getComparisonAsset: (datum: K) => T,
  parseDatum: (utxo: UTxO) => K,
): Promise<O.Option<ParsedOutput<K>>> {
  const parsedUtxos = await Promise.all(
    allIAssetOrefs.map(async (oref) => {
      const utxo = matchSingle(
        await lucid.utxosByOutRef([oref]),
        (_) => new Error('Expected a single UTXO'),
      );

      const datum = parseDatum(utxo);

      return { datum: datum, utxo: utxo };
    }),
  );

  // The iasset just before the new token name based on assets ordering
  return pipe(
    parsedUtxos,
    // Sort by the asset names
    A.sort(
      Ord.contramap<T, ParsedOutput<K>>((a) => getComparisonAsset(a.datum))(
        assetOrd,
      ),
    ),
    A.head,
  );
}

/**
 * Find the collateral asset that is first in the distributed list.
 * In case there are no collateral assets, none should be returned.
 */
export async function findFirstCollateralAsset(
  allCollateralAssetOrefs: OutRef[],
  lucid: LucidEvolution,
): Promise<O.Option<CollateralAssetOutput>> {
  return findFirstAsset(
    allCollateralAssetOrefs,
    lucid,
    Ord.contramap<string, AssetClass>((x) => getAssetClassComparisonStr(x))(
      S.Ord,
    ),
    (d) => d.collateralAsset,
    (utxo) => parseCollateralAssetDatumOrThrow(getInlineDatumOrThrow(utxo)),
  );
}

type IAssetCreationDatumHelperReturnType = {
  newIAsset: IAssetContent;
  newReferencedIAsset: O.Option<IAssetContent>;
};

export function iassetCreationDatumHelper(
  proposeAssetContent: ProposeAssetContent,
  referencedIAsset: O.Option<IAssetContent>,
): IAssetCreationDatumHelperReturnType {
  const newContent: IAssetContent = {
    assetName: proposeAssetContent.asset,
    collateralAssetsCount: 0n,
    debtMintingFeeRatio: proposeAssetContent.debtMintingFeeRatio,
    liquidationProcessingFeeRatio:
      proposeAssetContent.liquidationProcessingFeeRatio,
    stabilityPoolWithdrawalFeeRatio:
      proposeAssetContent.stabilityPoolWithdrawalFeeRatio,
    redemptionReimbursementRatio:
      proposeAssetContent.redemptionReimbursementRatio,
    redemptionProcessingFeeRatio:
      proposeAssetContent.redemptionProcessingFeeRatio,
    firstIAsset: true,
    nextIAsset: null,
  };

  return F.pipe(
    referencedIAsset,
    O.match<IAssetContent, IAssetCreationDatumHelperReturnType>(
      () => ({
        newIAsset: newContent,
        newReferencedIAsset: O.none,
      }),
      (referencedIA) => {
        if (
          toText(toHex(proposeAssetContent.asset)) <
          toText(toHex(referencedIA.assetName))
        ) {
          return {
            newIAsset: {
              ...newContent,
              firstIAsset: true,
              nextIAsset: referencedIA.assetName,
            },
            newReferencedIAsset: O.some({
              ...referencedIA,
              firstIAsset: false,
            }),
          };
        } else {
          return {
            newIAsset: {
              ...newContent,
              firstIAsset: false,
              nextIAsset: referencedIA.nextIAsset,
            },
            newReferencedIAsset: O.some({
              ...referencedIA,
              nextIAsset: proposeAssetContent.asset,
            }),
          };
        }
      },
    ),
  );
}

type CollateralAssetCreationDatumHelperReturnType = {
  newCollateralAsset: CollateralAssetContent;
  newReferencedCollateralAsset: O.Option<CollateralAssetContent>;
};

export function collateralAssetCreationDatumHelper(
  addAssetContent: AddCollateralAsssetContent,
  referencedCollateralAsset: O.Option<CollateralAssetContent>,
): CollateralAssetCreationDatumHelperReturnType {
  const newContent: CollateralAssetContent = {
    iasset: addAssetContent.correspondingIAsset,
    collateralAsset: addAssetContent.collateralAsset,
    extraDecimals: addAssetContent.assetExtraDecimals,
    priceInfo: addAssetContent.assetPriceInfo,
    interestOracleNft: addAssetContent.interestOracleNft,
    redemptionRatio: addAssetContent.redemptionRatio,
    maintenanceRatio: addAssetContent.maintenanceRatio,
    liquidationRatio: addAssetContent.liquidationRatio,
    minCollateralAmt: addAssetContent.minCollateralAmt,
    firstCollateralAsset: true,
    nextCollateralAsset: null,
  };

  return F.pipe(
    referencedCollateralAsset,
    O.match<
      CollateralAssetContent,
      CollateralAssetCreationDatumHelperReturnType
    >(
      () => ({
        newCollateralAsset: newContent,
        newReferencedCollateralAsset: O.none,
      }),
      (referencedCA) => {
        if (
          getAssetClassComparisonStr(addAssetContent.collateralAsset) <
          getAssetClassComparisonStr(referencedCA.collateralAsset)
        ) {
          return {
            newCollateralAsset: {
              ...newContent,
              firstCollateralAsset: true,
              nextCollateralAsset: referencedCA.collateralAsset,
            },
            newReferencedCollateralAsset: O.some({
              ...referencedCA,
              firstCollateralAsset: false,
            }),
          };
        } else {
          return {
            newCollateralAsset: {
              ...newContent,
              firstCollateralAsset: false,
              nextCollateralAsset: referencedCA.nextCollateralAsset,
            },
            newReferencedCollateralAsset: O.some({
              ...referencedCA,
              nextCollateralAsset: addAssetContent.collateralAsset,
            }),
          };
        }
      },
    ),
  );
}
