import pAll from 'p-all';
import { broadcastEvmTransaction, executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
import type { CallbackArgs } from '../../libs/auth';
import dayjs from '../../libs/dayjs';
import logger from '../../libs/logger';
import { isDelegationSufficientForPayment, checkTokenBalance } from '../../libs/payment';
import { getSubscriptionTrialSetup } from '../../libs/subscription';
import { getFastCheckoutAmount, getSubscriptionCreateSetup, type SlippageOptions } from '../../libs/session';
import { getTxMetadata } from '../../libs/util';
import { normalizeSlippageConfigFromMetadata } from '../../libs/slippage';
import { invoiceQueue } from '../../queues/invoice';
import { addSubscriptionJob } from '../../queues/subscription';
import type { Invoice, Subscription, TLineItemExpanded } from '../../store/models';
import { Price } from '../../store/models';
import {
  ensureInvoicesForSubscriptions,
  ensurePaymentIntent,
  executeOcapTransactions,
  getAuthPrincipalClaim,
  getDelegationTxClaim,
  getStakeTxClaim,
} from './shared';
import { ensureStakeInvoice } from '../../libs/invoice';
import { EVM_CHAIN_TYPES } from '../../libs/constants';
import { updateDataConcurrency } from '../../libs/env';

const updateInvoices = async (invoices: Invoice[], update: Partial<Invoice>) => {
  await pAll(
    invoices.map((invoice) => async () => {
      await invoice.update(update);
    }),
    { concurrency: updateDataConcurrency }
  );
};

const updateSubscriptions = async (subscriptions: Subscription[], update: Partial<Subscription>) => {
  await pAll(
    subscriptions.map((subscription) => async () => {
      await subscription.update(update);
    }),
    { concurrency: updateDataConcurrency }
  );
};

export default {
  action: 'subscription',
  authPrincipal: false,
  persistentDynamicClaims: true,
  claims: {
    authPrincipal: async ({ extraParams }: CallbackArgs) => {
      const { paymentMethod } = await ensurePaymentIntent(extraParams.checkoutSessionId);
      return getAuthPrincipalClaim(paymentMethod, 'pay');
    },
  },
  onConnect: async (args: CallbackArgs) => {
    const { userDid, userPk, extraParams } = args;
    const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
    const {
      checkoutSession,
      paymentMethod,
      paymentCurrency,
      subscriptions,
      subscription: primarySubscription,
    } = await ensurePaymentIntent(checkoutSessionId, connectedDid || sessionUserDid || userDid);
    if (!subscriptions || subscriptions.length === 0) {
      throw new Error('No subscriptions found for checkoutSession');
    }

    const now = dayjs().unix();
    // Expand line_items to include full price and upsell_price objects
    // This is critical for getSubscriptionCreateSetup to correctly use upsell_price when calculating authorization amount
    const items = await Price.expand(checkoutSession.line_items);
    const { trialEnd, trialInDays } = getSubscriptionTrialSetup(
      checkoutSession.subscription_data as any,
      paymentCurrency.id
    );
    const trialing = trialInDays > 0 || trialEnd > now;
    const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
    const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);

    // Calculate required amount considering slippage_config for dynamic pricing
    // Priority: primarySubscription.slippage_config > checkoutSession.metadata.slippage
    const slippageConfig =
      primarySubscription?.slippage_config || normalizeSlippageConfigFromMetadata(checkoutSession.metadata);
    let requiredAmount: string;
    if (slippageConfig?.min_acceptable_rate) {
      // Use slippage_config for precise calculation
      const slippageOptions: SlippageOptions = {
        percent: slippageConfig.percent ?? 0.5,
        minAcceptableRate: slippageConfig.min_acceptable_rate,
        currencyDecimal: paymentCurrency.decimal,
      };
      const setup = getSubscriptionCreateSetup(items, paymentCurrency.id, trialInDays, trialEnd, slippageOptions);
      requiredAmount = setup.amount.setup;
    } else {
      requiredAmount = await getFastCheckoutAmount({
        items,
        mode: checkoutSession.mode,
        currencyId: paymentCurrency.id,
        trialing,
      });
    }
    const claimsList: any[] = [];

    const allSubscriptionIds = subscriptions.map((sub) => sub.id);
    const requiredStake = !checkoutSession.subscription_data?.no_stake;
    if (paymentMethod.type === 'arcblock') {
      const delegation = await isDelegationSufficientForPayment({
        paymentMethod,
        paymentCurrency,
        userDid,
        amount: requiredAmount,
      });

      // if we can complete purchase without any wallet interaction
      // we forced to delegate if we can skip stake
      if (delegation.sufficient === false || !requiredStake) {
        claimsList.push({
          signature: await getDelegationTxClaim({
            mode: checkoutSession.mode,
            userDid,
            userPk,
            nonce: checkoutSession.id,
            data: getTxMetadata({
              subscriptionId: primarySubscription?.id,
              subscriptionIds: allSubscriptionIds,
              checkoutSessionId,
            }),
            paymentCurrency,
            paymentMethod,
            trialing,
            billingThreshold: Math.max(minStakeAmount, billingThreshold),
            items,
            requiredStake,
            slippageConfig: primarySubscription?.slippage_config || undefined,
            // Reuse DelegateState fetched above — avoids a duplicate chain RPC on CF cold isolates
            delegateState: delegation.state,
          }),
        });
      }

      if (requiredStake) {
        claimsList.push({
          prepareTx: await getStakeTxClaim({
            userDid,
            userPk,
            paymentCurrency,
            paymentMethod,
            items,
            subscription: primarySubscription as Subscription,
            subscriptions,
          }),
        });
      }

      if (claimsList.length === 0) {
        throw new Error('No available claims for your subscription at this time.');
      }

      return claimsList;
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      if (!paymentCurrency.contract) {
        throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
      }

      claimsList.push({
        signature: await getDelegationTxClaim({
          mode: checkoutSession.mode,
          userDid,
          userPk,
          nonce: checkoutSession.id,
          data: getTxMetadata({
            subscriptionId: primarySubscription?.id,
            subscriptionIds: allSubscriptionIds,
            checkoutSessionId,
          }),
          paymentCurrency,
          paymentMethod,
          trialing,
          billingThreshold,
          items,
          slippageConfig: primarySubscription?.slippage_config || undefined,
        }),
      });

      return claimsList;
    }

    throw new Error(`subscription: Payment method ${paymentMethod.type} not supported`);
  },
  onAuth: async (args: CallbackArgs) => {
    const { request, userDid, userPk, claims, extraParams, updateSession, step } = args;
    const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
    const { checkoutSession, customer, paymentMethod, subscriptions, paymentCurrency } = await ensurePaymentIntent(
      checkoutSessionId,
      connectedDid || sessionUserDid || userDid
    );
    const result = request?.context?.store?.result || [];
    result.push({
      step,
      claim: claims?.[0],
      stepRequest: {
        headers: request?.headers,
      },
    });
    const requiredStake = !checkoutSession.subscription_data?.no_stake;
    const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
    const isFinalStep =
      (paymentMethod.type === 'arcblock' && (staking || !requiredStake)) || paymentMethod.type !== 'arcblock';
    if (!isFinalStep) {
      await updateSession({
        result,
      });
      return { result };
    }
    try {
      await updateSession({
        result: [],
      });
    } catch (error) {
      logger.error('updateSession', {
        error,
      });
    }
    const claimsList = result.map((x: any) => x.claim);
    if (!subscriptions || subscriptions.length === 0) {
      throw new Error('No subscriptions found for checkoutSession');
    }
    const primarySubscription = subscriptions[0] as Subscription;

    const paymentSettings = {
      payment_method_types: [paymentMethod.type],
      payment_method_options: {
        [paymentMethod.type]: { payer: userDid },
      },
    };
    const prepareTxExecution = async () => {
      await updateSubscriptions(subscriptions, { payment_settings: paymentSettings });
    };

    const afterTxExecution = async (invoices: Invoice[], paymentDetails: Record<string, any>) => {
      await updateSubscriptions(subscriptions, { payment_details: { [paymentMethod.type]: paymentDetails } });

      for (const invoice of invoices) {
        if (invoice) {
          // eslint-disable-next-line no-await-in-loop
          await invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
        }
      }

      await pAll(
        subscriptions.map((subscription) => async () => {
          await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
        }),
        { concurrency: updateDataConcurrency }
      );

      logger.info('CheckoutSession updated with multiple subscriptions', {
        checkoutSession: checkoutSession.id,
        subscriptionIds: subscriptions.map((s) => s.id),
        paymentDetails,
      });
    };

    const now = dayjs().unix();
    const items = checkoutSession.line_items as TLineItemExpanded[];
    const { trialEnd, trialInDays } = getSubscriptionTrialSetup(
      checkoutSession.subscription_data as any,
      paymentCurrency.id
    );
    const trialing = trialInDays > 0 || trialEnd > now;
    const fastCheckoutAmount = await getFastCheckoutAmount({
      items,
      mode: checkoutSession.mode,
      currencyId: paymentCurrency.id,
      trialing,
    });
    // check token balance
    const tokenBalanceCheck = await checkTokenBalance({
      paymentMethod,
      paymentCurrency,
      userDid,
      amount: fastCheckoutAmount,
      skipUserCheck: true,
    });

    if (tokenBalanceCheck.sufficient === false) {
      logger.error('Payment aborted on final token balance check', {
        checkoutSessionId,
        userDid,
        reason: tokenBalanceCheck.reason,
        requestedAmount: tokenBalanceCheck.requestedAmount,
      });
      throw new Error(`Insufficient balance for payment: ${tokenBalanceCheck.reason}`);
    }

    if (paymentMethod.type === 'arcblock') {
      await prepareTxExecution();

      const { invoices } = await ensureInvoicesForSubscriptions({
        checkoutSession,
        customer,
        subscriptions,
        invoiceProps: {
          status: 'draft',
        },
      });

      if (invoices.length === 0) {
        throw new Error('No invoices found for subscriptions');
      }

      const requestArray = result
        .map((item: { stepRequest?: Request }) => item.stepRequest)
        .filter(Boolean) as Request[];

      const requestSource = requestArray.length > 0 ? requestArray : request;

      const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
        userDid,
        userPk,
        claimsList,
        paymentMethod,
        requestSource,
        primarySubscription?.id, // use the primary subscription id
        paymentCurrency?.contract,
        subscriptions.map((s) => s.id).join('-') // use all subscription ids as nonce
      );

      await updateInvoices(invoices, { payment_settings: paymentSettings, status: 'open' });

      // create a stake invoice
      if (stakingAmount && stakingAmount !== '0') {
        await ensureStakeInvoice(
          {
            total: stakingAmount,
            description: 'Stake for subscription',
            checkout_session_id: checkoutSessionId,
            currency_id: paymentCurrency.id,
            metadata: {
              payment_details: {
                arcblock: {
                  tx_hash: paymentDetails?.staking?.tx_hash,
                  payer: paymentDetails?.payer,
                  address: paymentDetails?.staking?.address,
                },
              },
              subscription_ids: subscriptions.map((s) => s.id),
              is_group_stake: subscriptions.length > 1,
            },
          },
          primarySubscription,
          paymentMethod,
          customer,
          subscriptions
        );
      }

      await afterTxExecution(invoices, paymentDetails);

      return { hash: paymentDetails.tx_hash };
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      await prepareTxExecution();

      const { invoices } = await ensureInvoicesForSubscriptions({
        checkoutSession,
        customer,
        subscriptions,
        invoiceProps: {
          status: 'draft',
        },
      });

      if (invoices.length === 0) {
        throw new Error('No invoices found for subscriptions');
      }

      broadcastEvmTransaction(checkoutSessionId, 'pending', claimsList);

      const paymentDetails = await executeEvmTransaction('approve', userDid, claimsList, paymentMethod);
      waitForEvmTxConfirm(
        paymentMethod.getEvmClient(),
        Number(paymentDetails.block_height),
        paymentMethod.confirmation.block
      )
        .then(async () => {
          await updateInvoices(invoices, { payment_settings: paymentSettings, status: 'open' });
          await afterTxExecution(invoices, paymentDetails);
          broadcastEvmTransaction(checkoutSessionId, 'confirmed', claimsList);
        })
        .catch(console.error);

      return { hash: paymentDetails.tx_hash };
    }

    throw new Error(`Payment method ${paymentMethod.type} not supported`);
  },
};
