import {
  addAssets,
  credentialToAddress,
  Data,
  fromHex,
  fromText,
  LucidEvolution,
  mintingPolicyToId,
  PolicyId,
  SpendingValidator,
  validatorToScriptHash,
} from '@lucid-evolution/lucid';
import {
  AssetClass,
  assetClassToUnit,
  assetClassValueOf,
  lovelacesAmt,
  mkAssetsOf,
  mkLovelacesOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
  addrDetails,
  createScriptAddress,
  submitTx,
} from '../../utils/lucid-utils';
import { runOneShotMintTx } from '../one-shot/transactions';
import {
  fromSysParamsCredential,
  getPythFeedConfig,
  Input,
} from '../../types/system-params';
import type {
  AssetInfo,
  CollateralAssetInfo,
  InitialAssetParam,
  InitialCollateralAssetParam,
  InitializeOptions,
} from './types';
import type {
  CdpParamsSP,
  CollectorParamsSP,
  PythConfig,
} from '../../types/system-params';
import type {
  CDPCreatorParamsSP,
  TreasuryParamsSP,
} from '../../types/system-params';
import type {
  IAssetParamsSP,
  StabilityPoolParamsSP,
  StakingParamsSP,
} from '../../types/system-params';
import type { GovParamsSP } from '../../types/system-params';
import type { InterestCollectionDatum } from '../interest-collection/types-new';
import type { PriceOracleParams } from '../price-oracle/types';
import type { InterestOracleParams } from '../interest-oracle/types';
import { mkAuthTokenPolicy } from '../../scripts/auth-token-policy';
import { mkIAssetValidatorFromSP } from '../iasset/scripts';
import { mkCollectorValidatorFromSP } from '../collector/scripts';
import { mkCDPCreatorValidatorFromSP } from '../cdp-creator/scripts';
import { mkTreasuryValidatorFromSP } from '../treasury/scripts';
import { mkStakingValidatorFromSP } from '../staking/scripts';
import { mkGovValidatorFromSP } from '../gov/scripts';
import { mkStabilityPoolValidatorFromSP } from '../stability-pool/scripts';
import { serialiseInterestCollectionDatum } from '../interest-collection/types-new';
import { startPriceOracleTx } from '../price-oracle/transactions';
import { startInterestOracle } from '../interest-oracle/transactions';
import { serialiseStakingDatum } from '../staking/types-new';
import { initSpState } from '../stability-pool/helpers';
import {
  serialiseStabilityPoolDatum,
  StabilityPoolContent,
} from '../stability-pool/types-new';
import {
  CollateralAssetContent,
  IAssetContent,
  IAssetPriceInfo,
  serialiseIAssetDatum,
} from '../iasset/types';
import { GovDatum, ProtocolParams, serialiseGovDatum } from '../gov/types-new';
import { match } from 'ts-pattern';
import { runAndAwaitTxBuilder } from '../../../tests/test-helpers';
import {
  serialiseStableswapPoolDatum,
  StableswapPoolContent,
} from '../cdp/types-new';
import { mkCdpValidatorFromSP } from '../cdp/scripts';

const alwaysFailValidatorHash =
  'ea84d625650d066e1645e3e81d9c70a73f9ed837bd96dc49850ae744';

/** Default init options used by tests; users must pass explicit options to init(). */
export const DEFAULT_INIT_OPTIONS: InitializeOptions = {
  numCdpCreators: 2n,
  numCollectors: 2n,
  numInterestCollectors: 2n,
  numTreasuryUtxos: 5n,
  treasuryIndyAmount: 1_000_000n,
  totalIndySupply: 35000000000000n,

  accountCreateFeeLovelaces: 5_000_000n,
  accountProcessingCooldownMs: 300_000n,
  accountProcessingBiasTime: 120_000n,

  interestSettlementCooldown: 432_000_000n,

  partialRedemptionExtraFeeLovelace: 10_000_000,
  biasTime: 1200_000n,
  gBiasTime: 120_000n,

  interestCollectorUtxoLovelaces: 5_000_000n,
};

export const INIT_TOKEN_NAMES = {
  indy: 'INDY',
  dao: 'DAO',
  multisigUtxo: 'INTEREST_COLLECTION_ADMIN',
  govNft: 'GOV_NFT',
  pollManager: 'POLL_MANAGER',
  upgrade: 'UPGRADE',
  iasset: 'IASSET',
  collateralAsset: 'COLLATERAL_ASSET',
  stabilityPool: 'STABILITY_POOL',
  versionRecord: 'VERSION_RECORD',
  cdpCreator: 'CDP_CREATOR',
  cdp: 'CDP',
  stakingManager: 'STAKING_MANAGER',
  staking: 'STAKING_POSITION',
  snapshotEpochToScaleToSum: 'SNAPSHOT_EPOCH_TO_SCALE_TO_SUM',
  account: 'SP_ACCOUNT',
} as const;

export async function mintOneTimeToken(
  lucid: LucidEvolution,
  tokenName: string,
  amount: bigint,
): Promise<PolicyId> {
  const utxos = await lucid.wallet().getUtxos();
  return await runOneShotMintTx(lucid, {
    referenceOutRef: {
      txHash: utxos[0].txHash,
      outputIdx: BigInt(utxos[0].outputIndex),
    },
    mintAmounts: [{ tokenName: tokenName, amount: amount }],
  });
}

/** Mint a one-shot token and return its AssetClass. */
export async function mintOneTimeAsset(
  lucid: LucidEvolution,
  tokenName: string,
  amount: bigint,
): Promise<AssetClass> {
  const policyId = await mintOneTimeToken(lucid, fromText(tokenName), amount);
  return {
    currencySymbol: fromHex(policyId),
    tokenName: fromHex(fromText(tokenName)),
  };
}

/** Build an auth-token AssetClass derived from a parent token. */
export function deriveAuthToken(
  parent: AssetClass,
  tokenName: string,
): AssetClass {
  const policy = mkAuthTokenPolicy(parent, fromText(tokenName));
  return {
    currencySymbol: fromHex(mintingPolicyToId(policy)),
    tokenName: fromHex(fromText(tokenName)),
  };
}

export async function initScriptRef(
  lucid: LucidEvolution,
  validator: SpendingValidator,
): Promise<Input> {
  const tx = lucid.newTx().pay.ToContract(
    credentialToAddress(lucid.config().network!, {
      hash: alwaysFailValidatorHash,
      type: 'Script',
    }),
    undefined,
    undefined,
    validator,
  );
  const txHash = await tx
    .complete()
    .then((t) => t.sign.withWallet().complete())
    .then((t) => t.submit());
  await lucid.awaitTx(txHash);
  return { transactionId: txHash, index: 0 };
}

export async function initCollector(
  lucid: LucidEvolution,
  collectorParams: CollectorParamsSP,
  numCollectors: bigint,
): Promise<void> {
  const tx = lucid.newTx();

  for (let i = 0; i < Number(numCollectors); i++) {
    tx.pay.ToContract(
      createScriptAddress(
        lucid.config().network!,
        validatorToScriptHash(mkCollectorValidatorFromSP(collectorParams)),
      ),
      {
        kind: 'inline',
        value: Data.void(),
      },
    );
  }

  await submitTx(lucid, tx);
}

export async function initInterestCollector(
  lucid: LucidEvolution,
  interestCollectionValHash: string,
  multisigUtxoNft: AssetClass,
  numInterestCollectors: bigint,
  interestCollectorUtxoLovelaces: bigint,
): Promise<void> {
  const [pkh, _] = await addrDetails(lucid);
  const interestCollectionAdminDatum: InterestCollectionDatum = {
    admin_permissions: {
      Signature: {
        keyHash: fromHex(pkh.hash),
      },
    },
  };

  await submitTx(
    lucid,
    lucid.newTx().pay.ToContract(
      createScriptAddress(lucid.config().network!, interestCollectionValHash),
      {
        kind: 'inline',
        value: serialiseInterestCollectionDatum(interestCollectionAdminDatum),
      },
      mkAssetsOf(multisigUtxoNft, 1n),
    ),
  );

  const interestCollectionUtxoDeploymentTx = lucid.newTx();

  for (let i = 0; i < Number(numInterestCollectors); i++) {
    interestCollectionUtxoDeploymentTx.pay.ToContract(
      createScriptAddress(lucid.config().network!, interestCollectionValHash),
      undefined,
      mkLovelacesOf(interestCollectorUtxoLovelaces),
    );
  }

  await submitTx(lucid, interestCollectionUtxoDeploymentTx);
}

export async function initCDPCreator(
  lucid: LucidEvolution,
  cdpCreatorParams: CDPCreatorParamsSP,
  numCdpCreators: bigint,
): Promise<void> {
  const tx = lucid.newTx();

  for (let i = 0; i < Number(numCdpCreators); i++) {
    tx.pay.ToContract(
      credentialToAddress(lucid.config().network!, {
        hash: validatorToScriptHash(
          mkCDPCreatorValidatorFromSP(cdpCreatorParams),
        ),
        type: 'Script',
      }),
      { kind: 'inline', value: Data.void() },
      {
        [cdpCreatorParams.cdpCreatorNft[0].unCurrencySymbol +
        fromText(cdpCreatorParams.cdpCreatorNft[1].unTokenName)]: 1n,
      },
    );
  }

  await submitTx(lucid, tx);
}

export async function initTreasury(
  lucid: LucidEvolution,
  treasuryParams: TreasuryParamsSP,
  daoAsset: AssetClass,
  indyAsset: AssetClass,
  treasuryIndyAmount: bigint,
  numTreasuryCollectors: bigint,
): Promise<void> {
  const treasuryAddr = createScriptAddress(
    lucid.config().network!,
    validatorToScriptHash(mkTreasuryValidatorFromSP(treasuryParams)),
    treasuryParams.treasuryUtxosStakeCredential != null
      ? fromSysParamsCredential(treasuryParams.treasuryUtxosStakeCredential)
      : undefined,
  );
  const tx = lucid
    .newTx()
    .pay.ToContract(
      treasuryAddr,
      { kind: 'inline', value: Data.void() },
      addAssets(
        mkAssetsOf(daoAsset, 1n),
        mkAssetsOf(indyAsset, treasuryIndyAmount),
      ),
    );

  for (let i = 0; i < Number(numTreasuryCollectors); i++) {
    tx.pay.ToContract(treasuryAddr, {
      kind: 'inline',
      value: Data.void(),
    });
  }

  await submitTx(lucid, tx);
}

export async function initStakingManager(
  lucid: LucidEvolution,
  stakingParams: StakingParamsSP,
): Promise<void> {
  const tx = lucid.newTx().pay.ToContract(
    createScriptAddress(
      lucid.config().network!,
      validatorToScriptHash(mkStakingValidatorFromSP(stakingParams)),
    ),
    {
      kind: 'inline',
      value: serialiseStakingDatum({
        totalStake: 0n,
        managerSnapshot: { snapshotAda: 0n },
      }),
    },
    {
      [stakingParams.stakingManagerNFT[0].unCurrencySymbol +
      fromText(stakingParams.stakingManagerNFT[1].unTokenName)]: 1n,
    },
  );
  await submitTx(lucid, tx);
}

export async function handleOracleForCollateralAsset(
  lucid: LucidEvolution,
  iasset: InitialAssetParam,
  collateralAsset: InitialCollateralAssetParam,
  pythConfig: PythConfig,
  currentSlot: number,
): Promise<{ info: IAssetPriceInfo; params: PriceOracleParams | undefined }> {
  return match(collateralAsset.priceOracle)
    .returnType<
      Promise<{ info: IAssetPriceInfo; params: PriceOracleParams | undefined }>
    >()
    .with({ tag: '_indigo_oracle_nft' }, async (val) => {
      const [pkh, _] = await addrDetails(lucid);

      const priceOracleParams: PriceOracleParams = {
        owner: pkh.hash,
        biasTime: val.params.biasTime,
        expirationPeriod: val.params.expirationPeriod,
      };

      const [priceOracleStartTx, priceOracleNft] = await startPriceOracleTx(
        lucid,
        val.tokenName,
        val.startPrice,
        priceOracleParams,
        currentSlot,
      );
      await runAndAwaitTxBuilder(lucid, priceOracleStartTx);

      return { info: { OracleNft: priceOracleNft }, params: priceOracleParams };
    })
    .with({ tag: '_pyth_oracle_nft' }, () => {
      return Promise.resolve({
        info: {
          DeferredValidation: {
            feedValHash: fromHex(
              getPythFeedConfig(
                pythConfig,
                fromHex(fromText(iasset.name)),
                collateralAsset.collateralAsset,
              ).pythFeedValHash,
            ),
          },
        } satisfies IAssetPriceInfo,
        params: undefined,
      });
    })
    .exhaustive();
}

export async function initializeAsset(
  lucid: LucidEvolution,
  iassetParams: IAssetParamsSP,
  iassetToken: AssetClass,
  collateralAssetAuthToken: AssetClass,
  cdpAuthToken: AssetClass,
  cdpParams: CdpParamsSP,
  stableswapValidatorHash: string,
  stabilityPoolParams: StabilityPoolParamsSP,
  stabilityPoolToken: AssetClass,
  asset: InitialAssetParam,
  pythConfig: PythConfig,
  currentSlot: number,
): Promise<AssetInfo> {
  const iassetName = fromHex(fromText(asset.name));

  const collateralAssetInfos: CollateralAssetInfo[] = [];

  for (const collateralAsset of asset.collateralAssets) {
    const [pkh, _] = await addrDetails(lucid);

    const interestOracleParams: InterestOracleParams = {
      owner: pkh.hash,
      biasTime: collateralAsset.interestOracle.params.biasTime,
    };
    const [startInterestOracleTx, interestOracleNft] =
      await startInterestOracle(
        0n,
        collateralAsset.interestOracle.initialInterestRate,
        0n,
        interestOracleParams,
        lucid,
        collateralAsset.interestOracle.tokenName,
      );
    await submitTx(lucid, startInterestOracleTx);
    const priceInfo = await handleOracleForCollateralAsset(
      lucid,
      asset,
      collateralAsset,
      pythConfig,
      currentSlot,
    );

    const collateralAssetContent: CollateralAssetContent = {
      iasset: iassetName,
      collateralAsset: collateralAsset.collateralAsset,
      extraDecimals: collateralAsset.extraDecimals,
      priceInfo: priceInfo.info,
      interestOracleNft: interestOracleNft,
      redemptionRatio: collateralAsset.redemptionRatio,
      maintenanceRatio: collateralAsset.maintenanceRatio,
      liquidationRatio: collateralAsset.liquidationRatio,
      minCollateralAmt: collateralAsset.minCollateralAmt,
      firstCollateralAsset: collateralAsset.firstCollateralAsset,
      nextCollateralAsset:
        collateralAsset.nextCollateralAsset != null
          ? collateralAsset.nextCollateralAsset
          : null,
    };

    const collateralAssetTx = lucid.newTx().pay.ToContract(
      createScriptAddress(
        lucid.config().network!,
        validatorToScriptHash(mkIAssetValidatorFromSP(iassetParams)),
      ),
      {
        kind: 'inline',
        value: serialiseIAssetDatum({
          CollateralAssetContent: collateralAssetContent,
        }),
      },
      mkAssetsOf(collateralAssetAuthToken, 1n),
    );
    await submitTx(lucid, collateralAssetTx);

    collateralAssetInfos.push({
      collateralAsset: collateralAsset.collateralAsset,
      interestOracleNft: interestOracleNft,
      interestOracleParams: interestOracleParams,
      oracleParams: priceInfo.params,
    });
  }

  for (const stableswapPool of asset.stablepools) {
    const stableswapPoolDatum: StableswapPoolContent = {
      collateralAsset: stableswapPool.collateralAsset,
      collateralToIassetRatio: stableswapPool.collateralToIassetRatio,
      mintingFeeRatio: stableswapPool.mintingFeeRatio,
      redemptionFeeRatio: stableswapPool.redemptionFeeRatio,
      mintingEnabled: stableswapPool.mintingEnabled,
      redemptionEnabled: stableswapPool.redemptionEnabled,
      feeManager: stableswapPool.feeManager
        ? fromHex(stableswapPool.feeManager)
        : null,
      minMintOrderAmount: stableswapPool.minMintOrderAmount,
      minRedemptionOrderAmount: stableswapPool.minRedemptionOrderAmount,
      iasset: iassetName,
      stableswapValHash: fromHex(stableswapValidatorHash),
    };

    await submitTx(
      lucid,
      lucid.newTx().pay.ToContract(
        createScriptAddress(
          lucid.config().network!,
          validatorToScriptHash(mkCdpValidatorFromSP(cdpParams)),
        ),
        {
          kind: 'inline',
          value: serialiseStableswapPoolDatum(stableswapPoolDatum),
        },
        mkAssetsOf(cdpAuthToken, 1n),
      ),
    );
  }

  const iassetDatum: IAssetContent = {
    assetName: iassetName,
    collateralAssetsCount: BigInt(asset.collateralAssets.length),
    debtMintingFeeRatio: asset.debtMintingFeeRatio,
    liquidationProcessingFeeRatio: asset.liquidationProcessingFeeRatio,
    stabilityPoolWithdrawalFeeRatio: asset.stabilityPoolWithdrawalFeeRatio,
    redemptionReimbursementRatio: asset.redemptionReimbursementRatio,
    redemptionProcessingFeeRatio: asset.redemptionProcessingFeeRatio,
    firstIAsset: asset.firstAsset,
    nextIAsset: asset.nextAsset ? fromHex(fromText(asset.nextAsset)) : null,
  };

  const assetTx = lucid.newTx().pay.ToContract(
    createScriptAddress(
      lucid.config().network!,
      validatorToScriptHash(mkIAssetValidatorFromSP(iassetParams)),
    ),
    {
      kind: 'inline',
      value: serialiseIAssetDatum({ IAssetContent: iassetDatum }),
    },
    mkAssetsOf(iassetToken, 1n),
  );
  await submitTx(lucid, assetTx);

  const stabilityPoolDatum: StabilityPoolContent = {
    iasset: fromHex(fromText(asset.name)),
    state: initSpState,
    assetStates: [],
  };

  const spTx = lucid.newTx().pay.ToContract(
    credentialToAddress(lucid.config().network!, {
      hash: validatorToScriptHash(
        mkStabilityPoolValidatorFromSP(stabilityPoolParams),
      ),
      type: 'Script',
    }),
    {
      kind: 'inline',
      value: serialiseStabilityPoolDatum({ StabilityPool: stabilityPoolDatum }),
    },
    mkAssetsOf(stabilityPoolToken, 1n),
  );
  await submitTx(lucid, spTx);

  return {
    iassetTokenNameAscii: asset.name,
    collateralAssets: collateralAssetInfos,
  };
}

export async function initGovernance(
  lucid: LucidEvolution,
  governanceParams: GovParamsSP,
  govToken: AssetClass,
  initialAssets: InitialAssetParam[],
  protocolParams: ProtocolParams,
): Promise<void> {
  const datum: GovDatum = {
    currentProposal: 0n,
    currentVersion: 0n,
    protocolParams: protocolParams,
    activeProposals: 0n,
    iassetsCount: BigInt(initialAssets.length),
  };
  const tx = lucid.newTx().pay.ToContract(
    credentialToAddress(lucid.config().network!, {
      hash: validatorToScriptHash(mkGovValidatorFromSP(governanceParams)),
      type: 'Script',
    }),
    { kind: 'inline', value: serialiseGovDatum(datum) },
    mkAssetsOf(govToken, 1n),
  );

  await submitTx(lucid, tx);
}

export async function mintAuthTokenDirect(
  lucid: LucidEvolution,
  asset: AssetClass,
  tokenName: string,
  amount: bigint,
): Promise<void> {
  const script = mkAuthTokenPolicy(asset, fromText(tokenName));
  const policyId = mintingPolicyToId(script);
  const address = await lucid.wallet().address();

  // Only select utxos that contain exactly 1 asset of the given asset class and sort by lovelaces amount in descending order
  const utxos = (await lucid.utxosAtWithUnit(address, assetClassToUnit(asset)))
    .filter((utxo) => assetClassValueOf(utxo.assets, asset) === 1n)
    .sort((a, b) => Number(lovelacesAmt(b.assets) - lovelacesAmt(a.assets)));
  if (utxos.length === 0) {
    throw new Error('No utxos found');
  }

  const utxo = utxos[0];

  const tx = lucid
    .newTx()
    .attach.MintingPolicy(script)
    .collectFrom([utxo])
    .mintAssets(
      {
        [policyId + fromText(tokenName)]: amount,
      },
      Data.void(),
    );

  await submitTx(lucid, tx);
}
