import {
  credentialToRewardAddress,
  Data,
  fromHex,
  LucidEvolution,
  OutRef,
  scriptHashToCredential,
  slotToUnixTime,
  toHex,
  TxBuilder,
  UTxO,
} from '@lucid-evolution/lucid';
import { IAssetPriceInfo } from './types';
import { matchSingle } from '../../utils/utils';
import { parsePriceOracleDatum } from '../price-oracle/types-new';
import { getInlineDatumOrThrow } from '../../utils/lucid-utils';
import { oracleExpirationAwareValidity } from '../price-oracle/helpers';
import {
  fromSysParamsDerivedPythPrice,
  fromSystemParamsScriptRef,
  getPythFeedConfig,
  PythConfig,
} from '../../types/system-params';
import {
  parsePythStateDatum,
  serialisePythFeedRedeemer,
  serialisePythUpdatesRedeemer,
} from '../pyth-feed/types';
import { match, P } from 'ts-pattern';
import { derivePythPrice } from '../pyth-feed/helpers';
import { AssetClass } from '@3rd-eye-labs/cardano-offchain-common';
import * as Core from '@evolution-sdk/evolution';
import { decodePriceUpdate, decodePythMessage } from '../../utils/pyth';

export const MAX_COLLATERAL_ASSETS_COUNT_PER_IASSET = 9;

type Interval = {
  validFrom: number;
  validTo: number;
};

export function attachOracle(
  iasset: Uint8Array<ArrayBufferLike>,
  collateralAsset: AssetClass,
  priceInfo: IAssetPriceInfo,
  priceOracleOref: OutRef | undefined,
  pythStateOref: OutRef | undefined,
  pythMessage: string | undefined,
  pythConfig: PythConfig,
  biasTime: bigint,
  currentSlot: number,
  lucid: LucidEvolution,
  tx: TxBuilder,
): Promise<{ interval: Interval; referenceInputs: UTxO[] }> {
  return match(priceInfo)
    .returnType<Promise<{ interval: Interval; referenceInputs: UTxO[] }>>()
    .with({ Delisted: P.any }, () => {
      const currentTime = BigInt(
        slotToUnixTime(lucid.config().network!, currentSlot),
      );

      // We want to make sure the transaction is valid within the bias time for interest tracking AND Pyth
      // For validFrom, we take the higher of the two bias times
      const validFrom = Number(currentTime - BigInt(biasTime / 2n));
      // For validTo, we take the lower of the two bias times
      const validTo = Number(currentTime + BigInt(biasTime / 2n));

      return Promise.resolve({
        interval: {
          validFrom,
          validTo,
        },
        referenceInputs: [],
      });
    })
    .with({ OracleNft: P.any }, async () => {
      if (!priceOracleOref) throw new Error('Missing price oracle');

      const priceOracleUtxo = matchSingle<UTxO>(
        await lucid.utxosByOutRef([priceOracleOref]),
        (_) => new Error('Expected a single price oracle UTXO'),
      );
      const priceOracleDatum = parsePriceOracleDatum(
        getInlineDatumOrThrow(priceOracleUtxo),
      );

      return {
        interval: oracleExpirationAwareValidity(
          currentSlot,
          Number(biasTime),
          Number(priceOracleDatum.expirationTime),
          lucid.config().network!,
        ),
        referenceInputs: [priceOracleUtxo],
      };
    })
    .with(
      { DeferredValidation: { feedValHash: P.select() } },
      async (feedValHash) => {
        if (priceOracleOref) throw new Error('Cannot pass price oracle oref');
        if (!pythMessage) throw new Error('Missing Pyth message');
        if (!pythStateOref) throw new Error('Missing pyth state out ref');

        const pythStateUtxo = matchSingle(
          await lucid.utxosByOutRef([pythStateOref]),
          (_) => new Error('Expected a single pyth state UTXO'),
        );

        const pythStateDatum = parsePythStateDatum(
          getInlineDatumOrThrow(pythStateUtxo),
        );

        const pythFeedCfg = getPythFeedConfig(
          pythConfig,
          iasset,
          collateralAsset,
        );

        // Lookup PFV Validator
        const pfvScriptRefUtxo = matchSingle(
          await lucid.utxosByOutRef([
            fromSystemParamsScriptRef(pythFeedCfg.pythFeedValScriptRef),
          ]),
          (_) =>
            new Error('Expected a single pyth feed validator Ref Script UTXO'),
        );

        const derivedPythPrice = fromSysParamsDerivedPythPrice(
          pythFeedCfg.params.config,
        );

        const price = derivePythPrice(derivedPythPrice, pythMessage);

        const decodedMessage = decodePythMessage(fromHex(pythMessage));
        const pricePayload = decodePriceUpdate(decodedMessage.payload);
        const pythTimestamp = Number(pricePayload.timestampUs) / 1_000;

        tx.withdraw(
          credentialToRewardAddress(
            lucid.config().network!,
            scriptHashToCredential(toHex(pythStateDatum.withdraw_script)),
          ),
          0n,
          serialisePythUpdatesRedeemer([fromHex(pythMessage)]),
        );

        tx.withdraw(
          credentialToRewardAddress(
            lucid.config().network!,
            scriptHashToCredential(toHex(feedValHash)),
          ),
          0n,
          serialisePythFeedRedeemer({
            price: price,
            auxiliaryData: Core.Data.fromCBORHex(Data.void()),
          }),
        );

        // 5 minutes - 20 seconds
        const pythMaxDelay = 280 * 1000;

        const validFrom = Number(pythTimestamp);
        const validTo = Number(pythTimestamp + pythMaxDelay);

        return {
          interval: {
            validFrom: validFrom,
            validTo: validTo,
          },
          referenceInputs: [pythStateUtxo, pfvScriptRefUtxo],
        };
      },
    )
    .exhaustive();
}
