import {
  Data,
  LucidEvolution,
  OutRef,
  toHex,
  TxBuilder,
  UTxO,
  addAssets,
  Assets,
  fromHex,
  slotToUnixTime,
  sortUTxOs,
  getInputIndices,
} from '@lucid-evolution/lucid';
import {
  fromSystemParamsAsset,
  fromSystemParamsScriptRef,
  SystemParams,
} from '../../types/system-params';
import { matchSingle } from '../../utils/utils';
import {
  parseInterestCollectionDatum,
  serialiseInterestCollectionDatum,
  serialiseInterestCollectionRedeemer,
} from './types-new';
import {
  assetClassValueOf,
  getInlineDatumOrThrow,
  mkAssetsOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import { createScriptAddress } from '../../utils/lucid-utils';
import {
  CDPContent,
  parseCdpDatumOrThrow,
  serialiseCdpDatum,
  serialiseCdpRedeemer,
} from '../cdp/types-new';
import { parseInterestOracleDatum } from '../interest-oracle/types-new';
import {
  calculateAccruedInterest,
  calculateUnitaryInterestSinceOracleLastUpdated,
} from '../interest-oracle/helpers';
import { match, P } from 'ts-pattern';
import { array as A, function as F } from 'fp-ts';
import { parseCollateralAssetDatumOrThrow } from '../iasset/types';
import { Multisig } from '../../types/multisig';

type CDPInfo = {
  utxo: UTxO;
  datum: CDPContent;
  accruedInterest: bigint;
};

export async function batchCollectInterest(
  collateralAssetOref: OutRef,
  interestCollectorOutRef: OutRef,
  interestOracleOutRef: OutRef,
  cdpOutRefs: OutRef[],
  params: SystemParams,
  lucid: LucidEvolution,
  currentSlot: number,
): Promise<TxBuilder> {
  const network = lucid.config().network!;
  const currentTime = BigInt(slotToUnixTime(network, currentSlot));

  const interestCollectionRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        params.scriptReferences.interestCollectionValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single interest collection Ref Script UTXO'),
  );

  const cdpRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(params.scriptReferences.cdpValidatorRef),
    ]),
    (_) => new Error('Expected a single cdp Ref Script UTXO'),
  );

  const iAssetTokenPolicyRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(params.scriptReferences.iAssetTokenPolicyRef),
    ]),
    (_) => new Error('Expected a single iasset token policy Ref Script UTXO'),
  );

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

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

  const interestCollectorUtxo: UTxO = matchSingle(
    await lucid.utxosByOutRef([interestCollectorOutRef]),
    (_) => new Error('Expected a single interest collector UTXO'),
  );

  // Confirm that the interest collector UTXO is NOT of InterestCollectorDatum type
  if (
    assetClassValueOf(
      interestCollectorUtxo.assets,
      fromSystemParamsAsset(params.interestCollectionParams.multisigUtxoNft),
    )
  ) {
    throw new Error(
      'Interest collector UTXO is of InterestCollectorDatum type',
    );
  }

  const interestOracleUtxo: UTxO = matchSingle(
    await lucid.utxosByOutRef([interestOracleOutRef]),
    (_) => new Error('Expected a single interest oracle UTXO'),
  );

  const interestOracleDatum = parseInterestOracleDatum(
    getInlineDatumOrThrow(interestOracleUtxo),
  );

  const cdpUtxos = await lucid.utxosByOutRef(cdpOutRefs);

  const sortedCdpUtxos = sortUTxOs(cdpUtxos, 'Canonical');

  if (sortedCdpUtxos.length !== cdpOutRefs.length) {
    throw new Error('Expected certain number of CDPs');
  }

  function getAccruedInterest(cdpDatum: CDPContent): bigint {
    return match(cdpDatum.cdpFees)
      .with({ FrozenCDPAccumulatedFees: P.any }, () => {
        throw new Error('CDP fees wrong');
      })
      .with({ ActiveCDPInterestTracking: P.select() }, (interest) => {
        return calculateAccruedInterest(
          currentTime,
          interest.unitaryInterestSnapshot,
          cdpDatum.mintedAmt,
          interest.lastSettled,
          interestOracleDatum,
        );
      })
      .exhaustive();
  }

  const cdpsInfo: CDPInfo[] = sortedCdpUtxos.map((cdpUtxo) => {
    const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo));
    return {
      utxo: cdpUtxo,
      datum: cdpDatum,
      accruedInterest: getAccruedInterest(cdpDatum),
    };
  });

  const accumulatedInterest = F.pipe(
    cdpsInfo,
    A.reduce<CDPInfo, bigint>(
      0n,
      (acc, cdpInfo) => acc + cdpInfo.accruedInterest,
    ),
  );

  const updatedUnitarySnapshot =
    calculateUnitaryInterestSinceOracleLastUpdated(
      currentTime,
      interestOracleDatum,
    ) + interestOracleDatum.unitaryInterest;

  const referenceInputs = [
    collateralAssetUtxo,
    interestOracleUtxo,
    interestCollectionRefScriptUtxo,
    cdpRefScriptUtxo,
    iAssetTokenPolicyRefScriptUtxo,
  ];

  const referenceInputIndices = getInputIndices(
    [collateralAssetUtxo, interestOracleUtxo],
    referenceInputs,
    false,
  );

  const validateFrom = slotToUnixTime(network, currentSlot - 1);
  const validateTo = validateFrom + Number(params.cdpParams.biasTime);

  const tx = lucid
    .newTx()
    .validFrom(validateFrom)
    .validTo(validateTo)
    .readFrom(referenceInputs)
    .collectFrom([interestCollectorUtxo], {
      kind: 'selected',
      makeRedeemer: (inputIndices: bigint[]) => {
        return serialiseInterestCollectionRedeemer({
          BatchCollectInterest: {
            currentTime: currentTime,
            ownInputIndex: inputIndices[0],
            collateralAssetRefInputIndex: referenceInputIndices[0],
            interestOracleRefInputIndex: referenceInputIndices[1],
          },
        });
      },
      inputs: [interestCollectorUtxo],
    })
    .mintAssets(
      mkAssetsOf(
        {
          currencySymbol: fromHex(
            params.cdpParams.cdpAssetSymbol.unCurrencySymbol,
          ),
          tokenName: collateralAssetDatum.iasset,
        },
        accumulatedInterest,
      ),
      Data.void(),
    )
    .pay.ToContract(
      createScriptAddress(
        lucid.config().network!,
        params.validatorHashes.interestCollectionHash,
      ),
      { kind: 'inline', value: Data.void() },
      addAssets(
        interestCollectorUtxo.assets,
        mkAssetsOf(
          {
            currencySymbol: fromHex(
              params.cdpParams.cdpAssetSymbol.unCurrencySymbol,
            ),
            tokenName: collateralAssetDatum.iasset,
          },
          accumulatedInterest,
        ),
      ),
    );

  F.pipe(
    cdpsInfo,
    A.reduce<CDPInfo, TxBuilder>(tx, (acc, cdpInfo) =>
      acc
        .collectFrom([cdpInfo.utxo], {
          kind: 'selected',
          makeRedeemer: (inputIndices: bigint[]) => {
            return serialiseCdpRedeemer({
              SettleInterest: { interestCollectorInputIndex: inputIndices[0] },
            });
          },
          inputs: [interestCollectorUtxo],
        })
        .pay.ToContract(
          cdpInfo.utxo.address,
          {
            kind: 'inline',
            value: serialiseCdpDatum({
              ...cdpInfo.datum,
              mintedAmt: cdpInfo.datum.mintedAmt + cdpInfo.accruedInterest,
              cdpFees: {
                ActiveCDPInterestTracking: {
                  lastSettled: currentTime,
                  unitaryInterestSnapshot: updatedUnitarySnapshot,
                },
              },
            }),
          },
          cdpInfo.utxo.assets,
        ),
    ),
  );

  return tx;
}

export async function collectInterestTx(
  value: Assets,
  lucid: LucidEvolution,
  sysParams: SystemParams,
  tx: TxBuilder,
  interestCollectorOref: OutRef,
): Promise<UTxO> {
  const interestCollectorUtxo = matchSingle(
    await lucid.utxosByOutRef([interestCollectorOref]),
    (_) => new Error('Expected a single interest collector UTXO'),
  );
  const interestCollectorRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        sysParams.scriptReferences.interestCollectionValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single interest collector Ref Script UTXO'),
  );

  tx.readFrom([interestCollectorRefScriptUtxo])
    .collectFrom(
      [interestCollectorUtxo],
      serialiseInterestCollectionRedeemer('CollectInterest'),
    )
    .pay.ToContract(
      interestCollectorUtxo.address,
      { kind: 'inline', value: Data.void() },
      addAssets(interestCollectorUtxo.assets, value),
    );

  return interestCollectorRefScriptUtxo;
}

export async function distributeInterest(
  interestCollectorOutRefs: OutRef[],
  interestAdminOutRef: OutRef,
  params: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const interestCollectorUtxos = [];

  for (const outRef of interestCollectorOutRefs) {
    interestCollectorUtxos.push(
      matchSingle(
        await lucid.utxosByOutRef([outRef]),
        (_) => new Error('Expected a single UTXO with that reference'),
      ),
    );
  }

  const interestAdminUtxo: UTxO = matchSingle(
    (await lucid.utxosByOutRef([interestAdminOutRef])).filter((utxo) =>
      assetClassValueOf(
        utxo.assets,
        fromSystemParamsAsset(params.interestCollectionParams.multisigUtxoNft),
      ),
    ),
    (_) => new Error('Expected a single interest admin UTXO'),
  );

  const interestAdminDatum = parseInterestCollectionDatum(
    getInlineDatumOrThrow(interestAdminUtxo),
  );

  // Find the script reference UTXO for the interest collection validator
  const interestCollectionRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        params.scriptReferences.interestCollectionValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single interest collection Ref Script UTXO'),
  );

  const tx = lucid
    .newTx()
    .readFrom([interestCollectionRefScriptUtxo, interestAdminUtxo])
    .collectFrom(
      interestCollectorUtxos,
      serialiseInterestCollectionRedeemer('Distribute'),
    );

  // TODO: Handle tx contruction for Multisig
  if ('Signature' in interestAdminDatum.admin_permissions) {
    tx.addSignerKey(
      toHex(interestAdminDatum.admin_permissions.Signature.keyHash),
    );
  } else {
    // TODO: Handle other admin permissions types
    throw new Error('Unsupported admin permissions type');
  }

  return tx;
}

export async function updatePermissions(
  interestAdminOutRef: OutRef,
  govOref: OutRef,
  newAdminPermissions: Multisig,
  expectedSigners: string[],
  params: SystemParams,
  lucid: LucidEvolution,
): Promise<TxBuilder> {
  const interestAdminUtxo: UTxO = matchSingle(
    (await lucid.utxosByOutRef([interestAdminOutRef])).filter((utxo) =>
      assetClassValueOf(
        utxo.assets,
        fromSystemParamsAsset(params.interestCollectionParams.multisigUtxoNft),
      ),
    ),
    (_) => new Error('Expected a single interest admin UTXO'),
  );

  const govUtxo: UTxO = matchSingle(
    (await lucid.utxosByOutRef([govOref])).filter((utxo) =>
      assetClassValueOf(
        utxo.assets,
        fromSystemParamsAsset(params.interestCollectionParams.govAuthTk),
      ),
    ),
    (_) => new Error('Expected a single interest admin UTXO'),
  );

  // Find the script reference UTXO for the interest collection validator
  const interestCollectionRefScriptUtxo = matchSingle(
    await lucid.utxosByOutRef([
      fromSystemParamsScriptRef(
        params.scriptReferences.interestCollectionValidatorRef,
      ),
    ]),
    (_) => new Error('Expected a single interest collection Ref Script UTXO'),
  );

  const tx = lucid
    .newTx()
    .readFrom([interestCollectionRefScriptUtxo, govUtxo])
    .collectFrom(
      [interestAdminUtxo],
      serialiseInterestCollectionRedeemer('UpdatePermissions'),
    )
    .pay.ToContract(
      createScriptAddress(
        lucid.config().network!,
        params.validatorHashes.interestCollectionHash,
      ),
      {
        kind: 'inline',
        value: serialiseInterestCollectionDatum({
          admin_permissions: newAdminPermissions,
        }),
      },
      interestAdminUtxo.assets,
    );

  for (const signer of expectedSigners) {
    tx.addSignerKey(signer);
  }

  return tx;
}
