import { isEthereumDid, isValid } from '@arcblock/did';
import { formatFactoryState, preMintFromFactory } from '@ocap/asset/mint/client';
import merge from 'lodash/merge';

import { wallet } from '../../libs/auth';
import logger from '../../libs/logger';
import { CheckoutSession, PaymentIntent, PaymentMethod, Subscription } from '../../store/models';
import { sendNftNotification } from '../blocklet/notification';

export async function mintNftForCheckoutSession(id: string) {
  const checkoutSession = await CheckoutSession.findByPk(id);
  if (!checkoutSession) {
    logger.warn('checkoutSession not found when attempt to mint nft', { id });
    return;
  }

  if (checkoutSession.status !== 'complete') {
    logger.warn('checkoutSession not completed when attempt to mint nft', { id });
    return;
  }

  if (checkoutSession.nft_mint_status === 'disabled') {
    logger.warn('nft mint not enabled for checkoutSession', { id, settings: checkoutSession.nft_mint_settings });
    return;
  }

  if (checkoutSession.nft_mint_status === 'minted') {
    logger.warn('checkoutSession nft already minted', { id, details: checkoutSession.nft_mint_details });
    return;
  }

  if (checkoutSession.nft_mint_status === 'error') {
    logger.warn('checkoutSession nft already error', { id, details: checkoutSession.nft_mint_details });
    return;
  }

  const { factory, inputs } = checkoutSession.nft_mint_settings as any;
  if (isValid(factory) === false) {
    logger.warn('checkoutSession nft mint settings invalid', { id, factory });
    return;
  }

  // TODO: we only support minting from arcblock chain now
  if (isEthereumDid(factory)) {
    logger.warn('nft mint not supported for factory', { id, factory });
    return;
  }

  // TODO: we may need retry here when the chain is temporarily inaccessible
  const method = await PaymentMethod.findOne({ where: { livemode: checkoutSession.livemode, type: 'arcblock' } });
  if (method) {
    const client = method.getOcapClient();
    const nftOwner = checkoutSession.customer_did;

    const [{ state: factoryState }, { state: appState }] = await Promise.all([
      client.getFactoryState({ address: factory }),
      client.getAccountState({ address: wallet.address }),
    ]);

    const preMint = await preMintFromFactory({
      factory: formatFactoryState(factoryState as any),
      inputs: inputs || {},
      owner: nftOwner,
      issuer: { wallet, name: appState.moniker },
      encoding: 'protobuf',
    });
    logger.info('nft preMint for checkoutSession', { id, inputs, nftOwner, factory, preMint });

    const { state: nftState } = await client.getAssetState({ address: preMint.address });
    if (nftState) {
      await checkoutSession.update({
        nft_mint_status: 'error',
        nft_mint_details: {
          type: 'arcblock',
          arcblock: {
            address: preMint.address,
            owner: nftOwner as string,
            error: 'NFT with same address already exist',
          },
        },
      });
      logger.warn('nft mint duplicate for checkoutSession', { id, details: checkoutSession.nft_mint_details });
      return;
    }

    const hash = await client.sendMintAssetTx({
      tx: {
        itx: {
          factory,
          address: preMint.address,
          assets: [],
          // preMint.variables is Record<string,unknown>; VariableInput expects
          // typed value which we can't narrow here. Cast to any — chain side
          // validates the actual schema.
          variables: Object.entries(preMint.variables).map(([key, value]) => ({ name: key, value })) as any,
          owner: nftOwner,
        },
      },
      wallet,
    });

    logger.info('nft minted for checkoutSession', { id, inputs, nftOwner, factory, hash });
    await checkoutSession.update({
      nft_mint_status: 'minted',
      nft_mint_details: {
        type: 'arcblock',
        arcblock: {
          tx_hash: hash,
          address: preMint.address,
          owner: nftOwner as string,
        },
      },
    });

    // FIXME: checkoutSession has multiple subscriptions?
    if (checkoutSession.subscription_id) {
      const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
      if (subscription && subscription.metadata?.nft_address) {
        await subscription.update({ metadata: merge(subscription.metadata || {}, { nft_address: preMint.address }) });
      }
    }
    if (checkoutSession.payment_intent_id) {
      const paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
      if (paymentIntent && paymentIntent.metadata?.nft_address) {
        await paymentIntent.update({ metadata: merge(paymentIntent.metadata || {}, { nft_address: preMint.address }) });
      }
    }

    await sendNftNotification(
      preMint.address,
      nftOwner as string,
      method.settings.arcblock?.api_host as string,
      factoryState.name
    );
    logger.info('nft sent for checkoutSession', { id, nftOwner });
  }
}
