import {
  LucidEvolution,
  TxBuilder,
  Credential,
  OutRef,
  addAssets,
  fromHex,
  toHex,
  fromText,
  Assets,
  getInputIndices,
} from '@lucid-evolution/lucid';
import {
  addrDetails,
  createScriptAddress,
  getInlineDatumOrThrow,
} from '../../utils/lucid-utils';
import {
  readonlyArray as RA,
  array as A,
  function as F,
  option as O,
} from 'fp-ts';
import { unzip, zip } from 'fp-ts/lib/Array';
import {
  adaAssetClass,
  AssetClass,
  assetClassValueOf,
  isSameAssetClass,
  mkAssetsOf,
  mkLovelacesOf,
  negateAssets,
} from '@3rd-eye-labs/cardano-offchain-common';
import { matchSingle } from '../../utils/utils';
import {
  RobDatum,
  RobOrderType,
  parseRobDatumOrThrow,
  serialiseRobDatum,
  serialiseRobRedeemer,
} from './types-new';
import {
  parseCollateralAssetDatumOrThrow,
  parseIAssetDatumOrThrow,
} from '../iasset/types';
import {
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../types/system-params';
import {
  buildRedemptionsTx,
  isFullyRedeemed,
  MIN_ROB_COLLATERAL_AMT,
  robAmtToSpend,
} from './helpers';
import { match, P } from 'ts-pattern';
import { Rational } from '../../types/rational';
import { attachOracle } from '../iasset/helpers';
import { retrieveAdjustedPrice } from '../../utils/oracle-helpers';

export async function openRob(
  assetTokenNameAscii: string,
  depositAmt: bigint,
  orderType: RobOrderType,
  lucid: LucidEvolution,
  sysParams: SystemParams,
  robStakeCredential?: Credential,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const [ownPkh, _] = await addrDetails(lucid);

  const newDatum: RobDatum = {
    owner: fromHex(ownPkh.hash),
    iasset: fromHex(fromText(assetTokenNameAscii)),
    orderType: orderType,
    robRefInput: {
      txHash: fromHex(
        '0000000000000000000000000000000000000000000000000000000000000000',
      ),
      outputIndex: 0n,
    },
  };

  const depositVal = match(orderType)
    .with({ BuyIAssetOrder: P.select() }, (buyContent) =>
      mkAssetsOf(buyContent.collateralAsset, depositAmt),
    )
    .with({ SellIAssetOrder: P.any }, (_) =>
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: newDatum.iasset,
        },
        depositAmt,
      ),
    )
    .exhaustive();

  return lucid.newTx().pay.ToContract(
    createScriptAddress(
      network,
      sysParams.validatorHashes.robHash,
      robStakeCredential,
    ),
    {
      kind: 'inline',
      value: serialiseRobDatum(newDatum),
    },
    addAssets(depositVal, mkLovelacesOf(MIN_ROB_COLLATERAL_AMT)),
  );
}

export async function cancelRob(
  robOutRef: OutRef,
  sysParams: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const robScriptRefUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef),
    ]),
    (_) => new Error('Expected a single ROB Ref Script UTXO'),
  );

  const robUtxo = matchSingle(
    await lucid.utxosByOutRef([robOutRef]),
    (_) => new Error('Expected a single ROB UTXO.'),
  );

  const robDatum = parseRobDatumOrThrow(getInlineDatumOrThrow(robUtxo));

  return lucid
    .newTx()
    .readFrom([robScriptRefUtxo])
    .collectFrom([robUtxo], serialiseRobRedeemer('Cancel'))
    .addSignerKey(toHex(robDatum.owner));
}

export async function redeemRob(
  /**
   * The tuple represents the ROB UTXO and the amount to payout for a redemption. In case of buy order,
   * it's denominated in iAssets, in case of sell order, it's denominated in collateral asset.
   */
  redemptionRobsData: [OutRef, bigint][],
  priceOracleOutRef: OutRef | undefined,
  iassetOutRef: OutRef,
  collateralAssetOutRef: OutRef,
  lucid: LucidEvolution,
  sysParams: SystemParams,
  currentSlot: number,
  pythMessage: string | undefined = undefined,
  pythStateOutRef: OutRef | undefined = undefined,
): Promise<TxBuilder> {
  const robScriptRefUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef),
    ]),
    (_) => new Error('Expected a single ROB Ref Script UTXO'),
  );

  const iassetUtxo = matchSingle(
    await lucid.utxosByOutRef([iassetOutRef]),
    (_) => new Error('Expected a single IAsset UTXO'),
  );

  const iassetDatum = parseIAssetDatumOrThrow(
    getInlineDatumOrThrow(iassetUtxo),
  );

  const collateralAssetUtxo = matchSingle(
    await lucid.utxosByOutRef([collateralAssetOutRef]),
    (_) => new Error('Expected a single collateral asset UTXO'),
  );

  const collateralAssetDatum = parseCollateralAssetDatumOrThrow(
    getInlineDatumOrThrow(collateralAssetUtxo),
  );

  const [robsToRedeemOutRefs, robRedemptionIAssetAmt] =
    unzip(redemptionRobsData);

  const [adjustedPrice, _] = await retrieveAdjustedPrice(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    collateralAssetDatum.extraDecimals,
    priceOracleOutRef,
    pythMessage,
    sysParams.pythConfig,
    lucid,
  );

  const redemptionRobs = await lucid
    .utxosByOutRef(robsToRedeemOutRefs)
    .then((val) => zip(val, robRedemptionIAssetAmt));

  const allRefInputs = [robScriptRefUtxo, iassetUtxo, collateralAssetUtxo];

  const tx = lucid.newTx();
  const { interval, referenceInputs } = await attachOracle(
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    collateralAssetDatum.priceInfo,
    priceOracleOutRef,
    pythStateOutRef,
    pythMessage,
    sysParams.pythConfig,
    sysParams.cdpParams.biasTime,
    currentSlot,
    lucid,
    tx,
  );

  // Set the validity interval for the transaction
  tx.validFrom(interval.validFrom).validTo(interval.validTo);

  // Read from the reference inputs
  allRefInputs.push(...referenceInputs);

  const refInputIdxs = getInputIndices(allRefInputs, allRefInputs);

  buildRedemptionsTx(
    redemptionRobs,
    iassetDatum.assetName,
    collateralAssetDatum.collateralAsset,
    adjustedPrice,
    iassetDatum.redemptionReimbursementRatio,
    sysParams,
    tx,
    0n,
    refInputIdxs[2],
    refInputIdxs[1],
    priceOracleOutRef !== undefined
      ? { OracleRefInputIdx: refInputIdxs[3] }
      : 'OracleVoid',
  );

  tx.readFrom(allRefInputs);

  return tx;
}

/**
 * Create Tx adjusting the ROB and claiming the received iAssets
 */
export async function adjustRob(
  lucid: LucidEvolution,
  robOutRef: OutRef,
  /**
   * A positive amount increases the deposit in the ROB,
   * and a negative amount takes deposit from the ROB.
   */
  adjustmentAmt: bigint,
  newLimitPrice:
    | { BuyOrder: Rational }
    | { SellOrder: { newLimitPrices: [AssetClass, Rational][] } }
    | undefined,
  sysParams: SystemParams,
): Promise<TxBuilder> {
  const robScriptRefUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(sysParams.scriptReferences.robValidatorRef),
    ]),
    (_) => new Error('Expected a single ROB Ref Script UTXO'),
  );

  const robUtxo = matchSingle(
    await lucid.utxosByOutRef([robOutRef]),
    (_) => new Error('Expected a single ROB UTXO.'),
  );

  const robDatum = parseRobDatumOrThrow(getInlineDatumOrThrow(robUtxo));

  // The claim case
  if (
    adjustmentAmt === 0n &&
    isFullyRedeemed(robUtxo.assets, robDatum.orderType, {
      currencySymbol: fromHex(
        sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
      ),
      tokenName: robDatum.iasset,
    })
  ) {
    throw new Error(
      "When there's no more lovelaces to spend, use close instead of claim.",
    );
  }

  // Negative adjust case
  if (
    adjustmentAmt < 0 &&
    robAmtToSpend(robUtxo.assets, robDatum.orderType, {
      currencySymbol: fromHex(
        sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
      ),
      tokenName: robDatum.iasset,
    }) <= -adjustmentAmt
  ) {
    throw new Error(
      "Can't adjust negatively by more than available. Also, for adjusting by exactly the amount deposited, a close action should be used instead.",
    );
  }

  const iassetAc: AssetClass = {
    currencySymbol: fromHex(
      sysParams.robParams.iassetPolicyId.unCurrencySymbol,
    ),
    tokenName: robDatum.iasset,
  };

  const [depositAsset, rewardVal] = match(robDatum.orderType)
    .returnType<[AssetClass, Assets]>()
    .with({ BuyIAssetOrder: P.select() }, (buyContent) => {
      const reward = assetClassValueOf(robUtxo.assets, iassetAc);

      return [buyContent.collateralAsset, mkAssetsOf(iassetAc, reward)];
    })
    .with({ SellIAssetOrder: P.select() }, (content) => {
      const reward = F.pipe(
        content.allowedCollateralAssets,
        RA.reduce<readonly [AssetClass, Rational], Assets>(
          {},
          (acc, [asset, _]) => {
            const amt =
              assetClassValueOf(robUtxo.assets, asset) -
              // in case of ADA, the min has to stay.
              (isSameAssetClass(asset, adaAssetClass)
                ? MIN_ROB_COLLATERAL_AMT
                : 0n);

            return addAssets(acc, mkAssetsOf(asset, amt));
          },
        ),
      );

      return [iassetAc, reward];
    })
    .exhaustive();

  return lucid
    .newTx()
    .readFrom([robScriptRefUtxo])
    .collectFrom([robUtxo], serialiseRobRedeemer('Cancel'))
    .pay.ToContract(
      robUtxo.address,
      {
        kind: 'inline',
        value: serialiseRobDatum({
          ...robDatum,
          orderType:
            newLimitPrice == null
              ? robDatum.orderType
              : match(robDatum.orderType)
                  .returnType<RobOrderType>()
                  .with({ BuyIAssetOrder: P.select() }, (content) => {
                    const newPrice = match(newLimitPrice)
                      .with({ BuyOrder: P.select() }, (price) => price)
                      .otherwise(() => {
                        throw new Error(
                          'Must use buy order price change on buy order.',
                        );
                      });

                    return {
                      BuyIAssetOrder: { ...content, maxPrice: newPrice },
                    };
                  })
                  .with({ SellIAssetOrder: P.select() }, (content) => {
                    const newPrices = match(newLimitPrice)
                      .with(
                        { SellOrder: { newLimitPrices: P.select() } },
                        (prices) => prices,
                      )
                      .otherwise(() => {
                        throw new Error(
                          'Must use sell order price change on sell order.',
                        );
                      });

                    return {
                      SellIAssetOrder: {
                        allowedCollateralAssets:
                          content.allowedCollateralAssets.map((entry) =>
                            F.pipe(
                              newPrices,
                              A.findFirst((newPrice) =>
                                isSameAssetClass(newPrice[0], entry[0]),
                              ),
                              O.match(
                                () => entry,
                                (newPrice) =>
                                  [
                                    entry[0],
                                    newPrice[1],
                                  ] satisfies typeof entry,
                              ),
                            ),
                          ),
                      },
                    };
                  })
                  .exhaustive(),
        }),
      },
      addAssets(
        robUtxo.assets,
        mkAssetsOf(depositAsset, adjustmentAmt),
        negateAssets(rewardVal),
      ),
    )
    .addSignerKey(toHex(robDatum.owner));
}

/**
 * Create Tx claiming the received iAssets.
 */
export async function claimRob(
  lucid: LucidEvolution,
  robOutRef: OutRef,
  sysParams: SystemParams,
): Promise<TxBuilder> {
  return adjustRob(lucid, robOutRef, 0n, undefined, sysParams);
}
