import type {
  ApiInputInvoice, ApiInputInvoicePremiumGiftStars, ApiInputInvoiceStarGift,
  ApiInputInvoiceStarGiftAuctionBid, ApiInputInvoiceStarGiftResale,
  ApiRequestInputInvoice,
} from '../../../api/types';
import type { ApiCredentials } from '../../../components/payment/PaymentModal';
import type { RegularLangFnParameters } from '../../../util/localization';
import type {
  ActionReturnType, GlobalState, TabArgs,
} from '../../types';
import { PaymentStep } from '../../../types';

import { DEBUG_PAYMENT_SMART_GLOCAL, STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import * as langProvider from '../../../util/oldLangProvider';
import { getStripeError } from '../../../util/payments/stripe';
import { getServerTime } from '../../../util/serverTime';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs';
import { isChatChannel, isChatSuperGroup } from '../../helpers';
import {
  getRequestInputInvoice,
  getRequestInputSavedStarGift,
} from '../../helpers/payments';
import {
  addActionHandler, getActions, getGlobal, setGlobal,
} from '../../index';
import {
  closeInvoice,
  openStarsTransactionFromReceipt,
  setPaymentStep,
  setReceipt,
  setRequestInfoId,
  setSmartGlocalCardInfo,
  setStripeCardInfo,
  updateChatFullInfo,
  updatePayment,
  updateShippingOptions,
  updateStarsPayment,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
  selectChat,
  selectChatFullInfo,
  selectIsCurrentUserFrozen,
  selectPaymentInputInvoice,
  selectPaymentRequestId,
  selectProviderPublicToken,
  selectProviderPublishableKey,
  selectSmartGlocalCredentials,
  selectStarsPayment,
  selectStripeCredentials,
  selectTabState,
} from '../../selectors';

const LOCAL_BOOST_COOLDOWN = 86400; // 24 hours
const SMART_GLOCAL_DOMAIN = 'smart-glocal.com';
const SMART_GLOCAL_TOKENIZE_PATH = '/cds/v1/tokenize/card';

function isValidSmartGlocalTokenizeUrl(tokenizeUrl: string) {
  if (tokenizeUrl !== tokenizeUrl.trim()) {
    return false;
  }

  let parsedUrl: URL;
  try {
    parsedUrl = new URL(tokenizeUrl);
  } catch {
    return false;
  }

  const { protocol, hostname, pathname, port, username, password } = parsedUrl;
  const isSmartGlocalHost = hostname === SMART_GLOCAL_DOMAIN || hostname.endsWith(`.${SMART_GLOCAL_DOMAIN}`);

  return protocol === 'https:'
    && isSmartGlocalHost
    && pathname === SMART_GLOCAL_TOKENIZE_PATH
    && !port
    && !username
    && !password;
}

addActionHandler('validateRequestedInfo', (global, actions, payload): ActionReturnType => {
  const { requestInfo, saveInfo, tabId = getCurrentTabId() } = payload;

  const inputInvoice = selectPaymentInputInvoice(global, tabId);
  if (!inputInvoice) {
    return;
  }

  const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
  if (!requestInputInvoice) {
    return;
  }

  validateRequestedInfo(global, requestInputInvoice, requestInfo, saveInfo, tabId);
});

addActionHandler('openInvoice', async (global, actions, payload): Promise<void> => {
  const { tabId = getCurrentTabId(), ...inputInvoice } = payload;

  const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
  if (!requestInputInvoice) {
    return;
  }

  global = updateTabState(global, {
    isPaymentFormLoading: true,
  }, tabId);
  setGlobal(global);

  const theme = extractCurrentThemeParams();
  const form = await callApi('getPaymentForm', requestInputInvoice, theme);

  if (!form) {
    return;
  }

  global = getGlobal();

  global = updateTabState(global, {
    isPaymentFormLoading: false,
  }, tabId);

  if ('error' in form) {
    setGlobal(global);

    handlePaymentFormError(form.error, tabId);
    return;
  }

  if (form.type === 'regular') {
    global = updatePayment(global, {
      inputInvoice: payload,
      form,
      isPaymentModalOpen: true,
      isExtendedMedia: (payload as any).isExtendedMedia,
      confirmPaymentUrl: undefined,
      status: undefined,
    }, tabId);
    global = setPaymentStep(global, PaymentStep.Checkout, tabId);
  }

  if (form.type === 'stars') {
    global = updateTabState(global, {
      starsPayment: {
        inputInvoice,
        form,
        status: 'pending',
      },
    }, tabId);
  }

  setGlobal(global);
});

addActionHandler('sendStarGift', (global, actions, payload): ActionReturnType => {
  const {
    gift, peerId, message, shouldHideName, shouldUpgrade, tabId = getCurrentTabId(),
  } = payload;

  const inputInvoice: ApiInputInvoiceStarGift = {
    type: 'stargift',
    peerId,
    giftId: gift.id,
    message,
    shouldHideName,
    shouldUpgrade: shouldUpgrade || undefined,
  };

  payInputStarInvoice(global, inputInvoice, gift.stars, tabId);
});

addActionHandler('buyStarGift', (global, actions, payload): ActionReturnType => {
  const {
    slug, peerId, price, tabId = getCurrentTabId(),
  } = payload;

  const inputInvoice: ApiInputInvoiceStarGiftResale = {
    type: 'stargiftResale',
    slug,
    peerId,
    currency: price.currency,
  };

  payInputStarInvoice(global, inputInvoice, price.amount, tabId);
});

addActionHandler('sendPremiumGiftByStars', (global, actions, payload): ActionReturnType => {
  const {
    userId, months, amount, message, tabId = getCurrentTabId(),
  } = payload;

  const inputInvoice: ApiInputInvoicePremiumGiftStars = {
    type: 'premiumGiftStars',
    userId,
    months,
    message,
  };

  payInputStarInvoice(global, inputInvoice, amount, tabId);
});

addActionHandler('getReceipt', async (global, actions, payload): Promise<void> => {
  const {
    chatId, messageId, tabId = getCurrentTabId(),
  } = payload;
  const chat = chatId && selectChat(global, chatId);
  if (!messageId || !chat) {
    return;
  }

  const result = await callApi('getReceipt', chat, messageId);
  if (!result) {
    return;
  }

  global = getGlobal();
  if (result.receipt.type === 'stars') {
    global = openStarsTransactionFromReceipt(global, result.receipt, tabId);
  } else {
    global = setReceipt(global, result.receipt, tabId);
  }
  setGlobal(global);
});

addActionHandler('clearPaymentError', (global, actions, payload): ActionReturnType => {
  const { tabId = getCurrentTabId() } = payload || {};
  global = updateTabState(global, {
    payment: {
      ...selectTabState(global, tabId).payment,
      error: undefined,
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('clearReceipt', (global, actions, payload): ActionReturnType => {
  const { tabId = getCurrentTabId() } = payload || {};
  return updateTabState(global, {
    payment: {
      ...selectTabState(global, tabId).payment,
      receipt: undefined,
    },
  }, tabId);
});

addActionHandler('sendCredentialsInfo', (global, actions, payload): ActionReturnType => {
  const { credentials, tabId = getCurrentTabId() } = payload;

  const { nativeProvider } = selectTabState(global, tabId).payment.form!;
  const { data } = credentials;

  if (nativeProvider === 'stripe') {
    const publishableKey = selectProviderPublishableKey(global, tabId);
    if (!publishableKey) {
      return;
    }
    void sendStripeCredentials(global, data, publishableKey, tabId);
  } else if (nativeProvider === 'smartglocal') {
    const publicToken = selectProviderPublicToken(global, tabId);
    if (!publicToken) {
      return;
    }
    void sendSmartGlocalCredentials(global, data, publicToken, tabId);
  }
});

addActionHandler('sendPaymentForm', async (global, actions, payload): Promise<void> => {
  const {
    shippingOptionId, saveCredentials, savedCredentialId, tipAmount,
    tabId = getCurrentTabId(),
  } = payload;
  const requestInfoId = selectPaymentRequestId(global, tabId);
  const paymentState = selectTabState(global, tabId).payment;
  const { form, temporaryPassword, inputInvoice } = paymentState;

  if (!inputInvoice || !form) {
    return;
  }

  const { nativeProvider, formId } = form;

  const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
  if (!requestInputInvoice) {
    return;
  }

  global = updatePayment(global, { status: 'pending' }, tabId);
  setGlobal(global);

  const credentials = {
    save: saveCredentials,
    data: nativeProvider === 'stripe'
      ? selectStripeCredentials(global, tabId) : selectSmartGlocalCredentials(global, tabId),
  };
  const result = await callApi('sendPaymentForm', {
    inputInvoice: requestInputInvoice,
    formId,
    credentials,
    requestedInfoId: requestInfoId,
    shippingOptionId,
    savedCredentialId,
    temporaryPassword: temporaryPassword?.value,
    tipAmount,
  });

  global = getGlobal();
  if (selectTabState(global, tabId).payment.form?.formId !== formId) {
    return;
  }

  if (!result) {
    return;
  }

  if ('verificationUrl' in result) {
    global = updatePayment(global, {
      confirmPaymentUrl: result.verificationUrl,
      step: PaymentStep.ConfirmPayment,
    }, tabId);
    setGlobal(global);
    return;
  }

  global = updatePayment(global, { status: 'paid' }, tabId);
  global = closeInvoice(global, tabId);
  setGlobal(global);

  actions.apiUpdate({
    '@type': 'updatePaymentStateCompleted',
    paymentState,
    tabId,
  });
});

addActionHandler('sendStarPaymentForm', async (global, actions, payload): Promise<void> => {
  const { directInfo, tabId = getCurrentTabId() } = payload;
  const starPayment = selectStarsPayment(global, tabId);
  const inputInvoice = starPayment?.inputInvoice || directInfo?.inputInvoice;
  if (!inputInvoice) return;

  const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
  if (!requestInputInvoice) {
    return;
  }

  const formId = (starPayment.form?.formId || starPayment.subscriptionInfo?.subscriptionFormId || directInfo?.formId)!;

  global = updateStarsPayment(global, { status: 'pending' }, tabId);
  setGlobal(global);

  const result = await callApi('sendStarPaymentForm', {
    inputInvoice: requestInputInvoice,
    formId,
  });

  if (!result) {
    global = getGlobal();
    global = updateStarsPayment(global, { status: 'failed' }, tabId);
    setGlobal(global);
    actions.closeStarsPaymentModal({ tabId });
    actions.closeGiftModal({ tabId });
    return;
  }

  global = getGlobal();
  global = updateStarsPayment(global, { status: 'paid' }, tabId);
  setGlobal(global);
  actions.closeStarsPaymentModal({ tabId });

  if ('channelId' in result) {
    actions.openChat({ id: result.channelId, tabId });
  }

  actions.apiUpdate({
    '@type': 'updateStarPaymentStateCompleted',
    paymentState: directInfo ? { inputInvoice } : starPayment,
    tabId,
  });
  actions.loadStarStatus();
});

async function sendStripeCredentials<T extends GlobalState>(
  global: T,
  data: ApiCredentials['data'],
  publishableKey: string,
  ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
  const body = new URLSearchParams({
    'card[number]': data.cardNumber,
    'card[exp_month]': data.expiryMonth,
    'card[exp_year]': data.expiryYear,
    'card[cvc]': data.cvv,
    'card[name]': data.cardholder,
    'card[address_zip]': data.zip,
    'card[address_country]': data.country,
  });

  const response = await fetch('https://api.stripe.com/v1/tokens', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Bearer ${publishableKey}`,
    },
    body,
  });
  const result = await response.json();
  if (result.error) {
    const error = getStripeError(result.error);
    global = getGlobal();
    global = updateTabState(global, {
      payment: {
        ...selectTabState(global, tabId).payment,
        status: 'failed',
        error: {
          ...error,
        },
      },
    }, tabId);
    setGlobal(global);
    return;
  }
  global = getGlobal();
  global = setStripeCardInfo(global, {
    type: result.type,
    id: result.id,
  }, tabId);
  global = setPaymentStep(global, PaymentStep.Checkout, tabId);
  setGlobal(global);
}

async function sendSmartGlocalCredentials<T extends GlobalState>(
  global: T,
  data: ApiCredentials['data'],
  publicToken: string,
  ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
  const params = {
    card: {
      number: data.cardNumber.replace(/\D+/g, ''),
      expiration_month: data.expiryMonth,
      expiration_year: data.expiryYear,
      security_code: data.cvv.replace(/\D+/g, ''),
    },
  };

  const tokenizeUrl = selectTabState(global, tabId).payment.form?.nativeParams.tokenizeUrl;

  let url;
  if (DEBUG_PAYMENT_SMART_GLOCAL) {
    url = 'https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card';
  } else {
    url = 'https://tgb.smart-glocal.com/cds/v1/tokenize/card';
  }

  if (tokenizeUrl && isValidSmartGlocalTokenizeUrl(tokenizeUrl)) {
    url = tokenizeUrl;
  }

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      'X-PUBLIC-TOKEN': publicToken,
    },
    body: JSON.stringify(params),
  });
  const result = await response.json();

  if (result.status !== 'ok') {
    // TODO после получения документации сделать аналог getStripeError(result.error);
    const error = { descriptionKey: { key: 'ErrorUnexpected' } satisfies RegularLangFnParameters };
    global = getGlobal();
    global = updateTabState(global, {
      payment: {
        ...selectTabState(global, tabId).payment,
        status: 'failed',
        error: {
          ...error,
        },
      },
    }, tabId);
    setGlobal(global);
    return;
  }

  global = getGlobal();
  global = setSmartGlocalCardInfo(global, {
    type: 'card',
    token: result.data.token,
  }, tabId);
  global = setPaymentStep(global, PaymentStep.Checkout, tabId);
  setGlobal(global);
}

addActionHandler('setSmartGlocalCardInfo', (global, actions, payload): ActionReturnType => {
  const { tabId = getCurrentTabId(), type, token } = payload;
  return setSmartGlocalCardInfo(global, {
    type,
    token,
  }, tabId);
});

addActionHandler('setPaymentStep', (global, actions, payload): ActionReturnType => {
  const { step, tabId = getCurrentTabId() } = payload;
  return setPaymentStep(global, step ?? PaymentStep.Checkout, tabId);
});

addActionHandler('closePremiumModal', (global, actions, payload): ActionReturnType => {
  const { tabId = getCurrentTabId() } = payload || {};

  const tabState = selectTabState(global, tabId);
  if (!tabState.premiumModal) return undefined;
  return updateTabState(global, {
    premiumModal: {
      promo: tabState.premiumModal.promo, // Cache promo
      isOpen: false,
    },
  }, tabId);
});

addActionHandler('openPremiumModal', async (global, actions, payload): Promise<void> => {
  const {
    initialSection, fromUserId, isSuccess, isGift, daysAmount, toUserId, gift,
    tabId = getCurrentTabId(),
  } = payload || {};

  actions.loadPremiumStickers();

  const result = await callApi('fetchPremiumPromo');
  if (!result) return;

  global = getGlobal();

  global = updateTabState(global, {
    premiumModal: {
      promo: result.promo,
      initialSection,
      isOpen: true,
      fromUserId,
      toUserId,
      isGift,
      daysAmount,
      isSuccess,
      gift,
    },
  }, tabId);
  setGlobal(global);

  actions.closeReactionPicker({ tabId });
});

addActionHandler('openGiveawayModal', async (global, actions, payload): Promise<void> => {
  const {
    chatId, prepaidGiveaway,
    tabId = getCurrentTabId(),
  } = payload;

  const chat = selectChat(global, chatId);
  if (!chat) return;

  const result = await callApi('getPremiumGiftCodeOptions', {
    chat,
  });

  const starOptions = await callApi('fetchStarsGiveawayOptions');

  if (!result || !starOptions) {
    return;
  }

  global = getGlobal();

  global = updateTabState(global, {
    giveawayModal: {
      chatId,
      gifts: result,
      isOpen: true,
      prepaidGiveaway,
      starOptions,
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('checkCanSendGift', async (global, actions, payload): Promise<void> => {
  const {
    gift, onSuccess, tabId = getCurrentTabId(),
  } = payload;

  if (gift.type !== 'starGift' || !gift.lockedUntilDate) {
    onSuccess();
    return;
  }

  const result = await callApi('fetchCheckCanSendGift', {
    giftId: gift.id,
  });

  if (!result) return;

  if (result?.canSend) {
    onSuccess();
  } else {
    actions.openLockedGiftModalInfo({
      untilDate: gift.type === 'starGift' ? gift.lockedUntilDate : undefined,
      reason: result.reason,
      tabId,
    });
  }
});

addActionHandler('openGiftModal', async (global, actions, payload): Promise<void> => {
  const {
    forUserId, selectedGift, selectedResaleGift, tabId = getCurrentTabId(),
  } = payload;

  if (selectIsCurrentUserFrozen(global)) {
    actions.openFrozenAccountModal({ tabId });
    return;
  }

  const gifts = await callApi('getPremiumGiftCodeOptions', {});
  if (!gifts) return;

  global = getGlobal();
  global = updateTabState(global, {
    giftModal: {
      forPeerId: forUserId,
      gifts,
      selectedResaleGift,
      selectedGift,
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('openStarsGiftModal', async (global, actions, payload): Promise<void> => {
  const {
    forUserId,
    tabId = getCurrentTabId(),
  } = payload || {};

  if (selectIsCurrentUserFrozen(global)) {
    actions.openFrozenAccountModal({ tabId });
    return;
  }

  const chat = forUserId ? selectChat(global, forUserId) : undefined;
  if (forUserId && !chat) return;

  const starsGiftOptions = await callApi('fetchStarsGiftOptions', {
    chat,
  });

  global = getGlobal();
  global = updateTabState(global, {
    starsGiftModal: {
      isOpen: true,
      forUserId,
      starsGiftOptions,
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('validatePaymentPassword', async (global, actions, payload): Promise<void> => {
  const { password, tabId = getCurrentTabId() } = payload;
  const result = await callApi('fetchTemporaryPaymentPassword', password);

  global = getGlobal();

  if (!result) {
    global = updatePayment(global, { error: { messageKey: { key: 'ErrorUnexpected' }, field: 'password' } }, tabId);
  } else if ('error' in result) {
    global = updatePayment(global, { error: { messageKey: result.messageKey, field: 'password' } }, tabId);
  } else {
    global = updatePayment(global, { temporaryPassword: result, step: PaymentStep.Checkout }, tabId);
  }

  setGlobal(global);
});

async function validateRequestedInfo<T extends GlobalState>(
  global: T, inputInvoice: ApiRequestInputInvoice, requestInfo: any, shouldSave?: boolean,
  ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
  const result = await callApi('validateRequestedInfo', {
    inputInvoice, requestInfo, shouldSave,
  });
  if (!result) {
    return;
  }

  const { id, shippingOptions } = result;
  global = getGlobal();

  global = setRequestInfoId(global, id, tabId);
  if (shippingOptions?.length) {
    global = updateShippingOptions(global, shippingOptions, tabId);
    global = setPaymentStep(global, PaymentStep.Shipping, tabId);
  } else {
    global = setPaymentStep(global, PaymentStep.Checkout, tabId);
  }
  setGlobal(global);
}

addActionHandler('openBoostModal', async (global, actions, payload): Promise<void> => {
  const { chatId, tabId = getCurrentTabId() } = payload;
  const chat = selectChat(global, chatId);
  if (!chat || !(isChatChannel(chat) || isChatSuperGroup(chat))) return;

  global = updateTabState(global, {
    boostModal: {
      chatId,
    },
  }, tabId);
  setGlobal(global);

  const result = await callApi('fetchBoostStatus', {
    chat,
  });

  if (!result) {
    actions.closeBoostModal({ tabId });
    return;
  }

  global = getGlobal();
  global = updateTabState(global, {
    boostModal: {
      chatId,
      boostStatus: result,
    },
  }, tabId);
  setGlobal(global);

  const myBoosts = await callApi('fetchMyBoosts');

  if (!myBoosts) return;

  global = getGlobal();
  const tabState = selectTabState(global, tabId);
  if (!tabState.boostModal) return;

  global = updateTabState(global, {
    boostModal: {
      ...tabState.boostModal,
      myBoosts: myBoosts.boosts,
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('openBoostStatistics', async (global, actions, payload): Promise<void> => {
  const { chatId, tabId = getCurrentTabId() } = payload;

  const chat = selectChat(global, chatId);
  if (!chat) return;

  global = updateTabState(global, {
    boostStatistics: {
      chatId,
    },
  }, tabId);
  setGlobal(global);

  const [boostListResult, boostListGiftResult,
    boostStatusResult] = await Promise.all([
    callApi('fetchBoostList', { chat }),
    callApi('fetchBoostList', { chat, isGifts: true }),
    callApi('fetchBoostStatus', { chat }),
  ]);

  global = getGlobal();
  if (!boostListResult || !boostListGiftResult || !boostStatusResult) {
    global = updateTabState(global, {
      boostStatistics: undefined,
    }, tabId);
    setGlobal(global);
    return;
  }

  global = updateTabState(global, {
    boostStatistics: {
      chatId,
      boostStatus: boostStatusResult,
      nextOffset: boostListResult.nextOffset,
      boosts: {
        count: boostListResult.count,
        list: boostListResult.boostList,
      },
      giftedBoosts: {
        count: boostListGiftResult?.count,
        list: boostListGiftResult?.boostList,
      },
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('openMonetizationStatistics', (global, actions, payload): ActionReturnType => {
  const { chatId, tabId = getCurrentTabId() } = payload;

  const chat = selectChat(global, chatId);
  if (!chat) return;

  global = updateTabState(global, {
    monetizationStatistics: {
      chatId,
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<void> => {
  const { isGifts, tabId = getCurrentTabId() } = payload || {};
  let tabState = selectTabState(global, tabId);
  if (!tabState.boostStatistics) return;

  const chat = selectChat(global, tabState.boostStatistics.chatId);
  if (!chat) return;

  global = updateTabState(global, {
    boostStatistics: {
      ...tabState.boostStatistics,
      isLoadingBoosters: true,
    },
  }, tabId);
  setGlobal(global);

  const result = await callApi('fetchBoostList', {
    chat,
    offset: tabState.boostStatistics.nextOffset,
    isGifts,
  });
  if (!result) return;

  global = getGlobal();

  tabState = selectTabState(global, tabId);
  if (!tabState.boostStatistics) return;

  const updatedBoostList = (isGifts
    ? tabState.boostStatistics.giftedBoosts?.list || []
    : tabState.boostStatistics.boosts?.list || []).concat(result.boostList);

  global = updateTabState(global, {
    boostStatistics: {
      ...tabState.boostStatistics,
      nextOffset: result.nextOffset,
      isLoadingBoosters: false,
      [isGifts ? 'giftedBoosts' : 'boosts']: {
        count: result.count,
        list: updatedBoostList,
      },
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('applyBoost', async (global, actions, payload): Promise<void> => {
  const { chatId, slots, tabId = getCurrentTabId() } = payload;

  const chat = selectChat(global, chatId);
  if (!chat) return;

  const oldChatFullInfo = selectChatFullInfo(global, chatId);
  const oldBoostsApplied = oldChatFullInfo?.boostsApplied || 0;

  const appliedBoostsCount = slots.length;

  let tabState = selectTabState(global, tabId);
  const oldStatus = tabState.boostModal?.boostStatus;

  if (oldStatus) {
    const boostsPerLevel = oldStatus.nextLevelBoosts ? oldStatus.nextLevelBoosts - oldStatus.currentLevelBoosts : 1;
    const newBoosts = oldStatus.boosts + appliedBoostsCount;
    const isLevelUp = oldStatus.nextLevelBoosts && newBoosts >= oldStatus.nextLevelBoosts;
    const newCurrentLevelBoosts = isLevelUp ? oldStatus.nextLevelBoosts! : oldStatus.currentLevelBoosts;
    const newNextLevelBoosts = isLevelUp ? oldStatus.nextLevelBoosts! + boostsPerLevel : oldStatus.nextLevelBoosts;

    global = updateTabState(global, {
      boostModal: {
        ...tabState.boostModal!,
        boostStatus: {
          ...oldStatus,
          level: isLevelUp ? oldStatus.level + 1 : oldStatus.level,
          currentLevelBoosts: newCurrentLevelBoosts,
          nextLevelBoosts: newNextLevelBoosts,
          hasMyBoost: true,
          boosts: newBoosts,
        },
      },
    }, tabId);
    setGlobal(global);
  }

  global = getGlobal();
  tabState = selectTabState(global, tabId);
  const oldMyBoosts = tabState.boostModal?.myBoosts;

  if (oldMyBoosts) {
    const unixNow = getServerTime();
    const newMyBoosts = oldMyBoosts.map((boost) => {
      if (slots.includes(boost.slot)) {
        return {
          ...boost,
          chatId,
          date: unixNow,
          cooldownUntil: unixNow + LOCAL_BOOST_COOLDOWN, // Will be refetched below
        };
      }
      return boost;
    });

    global = updateTabState(global, {
      boostModal: {
        ...tabState.boostModal!,
        myBoosts: newMyBoosts,
      },
    }, tabId);
    setGlobal(global);
  }

  const result = await callApi('applyBoost', {
    slots,
    chat,
  });

  global = getGlobal();

  if (!result) {
    // Rollback local changes
    const boostModal = selectTabState(global, tabId).boostModal;
    if (boostModal) {
      global = updateTabState(global, {
        boostModal: {
          ...boostModal,
          boostStatus: oldStatus,
          myBoosts: oldMyBoosts,
        },
      }, tabId);
      setGlobal(global);
    }
    return;
  }

  tabState = selectTabState(global, tabId);
  if (oldChatFullInfo) {
    global = updateChatFullInfo(global, chatId, {
      boostsApplied: oldBoostsApplied + slots.length,
    });
  }

  if (tabState.boostModal) {
    global = updateTabState(global, {
      boostModal: {
        ...tabState.boostModal,
        myBoosts: result.boosts,
      },
    }, tabId);
  }
  setGlobal(global);
});

addActionHandler('checkGiftCode', async (global, actions, payload): Promise<void> => {
  const { slug, message, tabId = getCurrentTabId() } = payload;

  const result = await callApi('checkGiftCode', {
    slug,
  });

  if (!result) {
    actions.showNotification({
      message: langProvider.oldTranslate('lng_gift_link_expired'),
      tabId,
    });
    return;
  }

  global = getGlobal();
  global = updateTabState(global, {
    giftCodeModal: {
      slug,
      info: result.code,
      message,
    },
  }, tabId);
  setGlobal(global);
});

addActionHandler('applyGiftCode', async (global, actions, payload): Promise<void> => {
  const { slug, tabId = getCurrentTabId() } = payload;

  const result = await callApi('applyGiftCode', {
    slug,
  });

  if (!result) {
    return;
  }
  actions.requestConfetti({ withStars: true, tabId });
  actions.closeGiftCodeModal({ tabId });
});

addActionHandler('launchPrepaidGiveaway', async (global, actions, payload): Promise<void> => {
  const {
    chatId, giveawayId, paymentPurpose, tabId = getCurrentTabId(),
  } = payload;

  const chat = selectChat(global, chatId);
  if (!chat) return;

  const additionalChannels = paymentPurpose?.additionalChannelIds?.map((id) => selectChat(global, id)).filter(Boolean);

  const result = await callApi('launchPrepaidGiveaway', {
    chat,
    giveawayId,
    paymentPurpose: {
      type: 'giveaway',
      chat,
      areWinnersVisible: paymentPurpose?.areWinnersVisible,
      additionalChannels,
      countries: paymentPurpose?.countries,
      prizeDescription: paymentPurpose.prizeDescription,
      untilDate: paymentPurpose.untilDate,
      currency: paymentPurpose.currency,
      amount: paymentPurpose.amount,
    },
  });

  if (!result) {
    return;
  }

  actions.openBoostStatistics({ chatId, tabId });
});

addActionHandler('launchPrepaidStarsGiveaway', async (global, actions, payload): Promise<void> => {
  const {
    chatId, giveawayId, paymentPurpose, tabId = getCurrentTabId(),
  } = payload;

  const chat = selectChat(global, chatId);
  if (!chat) return;

  const additionalChannels = paymentPurpose?.additionalChannelIds?.map((id) => selectChat(global, id)).filter(Boolean);

  const result = await callApi('launchPrepaidGiveaway', {
    chat,
    giveawayId,
    paymentPurpose: {
      type: 'starsgiveaway',
      chat,
      areWinnersVisible: paymentPurpose?.areWinnersVisible,
      additionalChannels,
      countries: paymentPurpose?.countries,
      prizeDescription: paymentPurpose.prizeDescription,
      untilDate: paymentPurpose.untilDate,
      currency: paymentPurpose.currency,
      amount: paymentPurpose.amount,
      stars: paymentPurpose.stars,
      users: paymentPurpose.users,
    },
  });

  if (!result) {
    return;
  }

  actions.openBoostStatistics({ chatId, tabId });
});

addActionHandler('upgradeGift', async (global, actions, payload): Promise<void> => {
  const {
    gift, shouldKeepOriginalDetails, upgradeStars, tabId = getCurrentTabId(),
  } = payload;

  const requestSavedGift = getRequestInputSavedStarGift(global, gift);
  if (!requestSavedGift) {
    return;
  }

  global = updateTabState(global, {
    isWaitingForStarGiftUpgrade: true,
  }, tabId);

  setGlobal(global);
  global = getGlobal();

  actions.closeGiftUpgradeModal({ tabId });
  actions.closeGiftInfoModal({ tabId });

  if (!upgradeStars) {
    const result = await callApi('upgradeStarGift', {
      inputSavedGift: requestSavedGift,
      shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
    });

    global = getGlobal();
    if (result && global.currentUserId) {
      actions.reloadPeerSavedGifts({ peerId: global.currentUserId });
    }

    return;
  }

  const invoice: ApiInputInvoice = {
    type: 'stargiftUpgrade',
    inputSavedGift: gift,
    shouldKeepOriginalDetails: shouldKeepOriginalDetails || undefined,
  };

  payInputStarInvoice(global, invoice, upgradeStars, tabId);
});

addActionHandler('transferGift', (global, actions, payload): ActionReturnType => {
  const {
    gift, recipientId, transferStars, tabId = getCurrentTabId(),
  } = payload;

  const peer = selectChat(global, recipientId);

  const requestSavedGift = getRequestInputSavedStarGift(global, gift);
  if (!peer || !requestSavedGift) {
    return;
  }

  global = updateTabState(global, {
    isWaitingForStarGiftTransfer: true,
  }, tabId);

  setGlobal(global);
  global = getGlobal();

  actions.closeGiftTransferModal({ tabId });
  actions.closeGiftInfoModal({ tabId });

  if (!transferStars) {
    callApi('transferStarGift', {
      inputSavedGift: requestSavedGift,
      toPeer: peer,
    });

    return;
  }

  const invoice: ApiInputInvoice = {
    type: 'stargiftTransfer',
    inputSavedGift: gift,
    recipientId,
  };

  payInputStarInvoice(global, invoice, transferStars, tabId);
});

addActionHandler('removeGiftDescription', (global, actions, payload): ActionReturnType => {
  const { gift, price, tabId = getCurrentTabId() } = payload;

  const invoice: ApiInputInvoice = {
    type: 'stargiftDropOriginalDetails',
    inputSavedGift: gift,
  };

  payInputStarInvoice(global, invoice, price, tabId);
});

addActionHandler('upgradePrepaidGift', (global, actions, payload): ActionReturnType => {
  const { peerId, hash, stars, tabId = getCurrentTabId() } = payload;

  const invoice: ApiInputInvoice = {
    type: 'stargiftPrepaidUpgrade',
    peerId,
    hash,
  };

  payInputStarInvoice(global, invoice, stars, tabId);
});

addActionHandler('sendStarGiftAuctionBid', (global, actions, payload): ActionReturnType => {
  const {
    giftId, bidAmount, peerId, message, shouldHideName, isUpdateBid, tabId = getCurrentTabId(),
  } = payload;

  const invoice: ApiInputInvoiceStarGiftAuctionBid = {
    type: 'stargiftAuctionBid',
    giftId,
    bidAmount,
    peerId,
    message,
    shouldHideName,
    isUpdateBid,
  };

  payInputStarInvoice(global, invoice, bidAmount, tabId);
});

async function payInputStarInvoice<T extends GlobalState>(
  global: T, inputInvoice: ApiInputInvoice, price: number,
  ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
  const actions = getActions();
  const isTon = inputInvoice.type === 'stargiftResale' && inputInvoice.currency === TON_CURRENCY_CODE;
  const balance = isTon ? global.ton?.balance : global.stars?.balance;
  const currency = isTon ? TON_CURRENCY_CODE : STARS_CURRENCY_CODE;

  if (balance === undefined) return;

  if (balance.amount < price) {
    actions.openStarsBalanceModal({ currency, tabId });
    return;
  }

  const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
  if (!requestInputInvoice) {
    return;
  }

  global = updateTabState(global, {
    isPaymentFormLoading: true,
  }, tabId);
  setGlobal(global);

  const theme = extractCurrentThemeParams();
  const form = await callApi('getPaymentForm', requestInputInvoice, theme);

  if (!form) {
    return;
  }

  global = getGlobal();

  global = updateTabState(global, {
    isPaymentFormLoading: false,
  }, tabId);
  setGlobal(global);

  if ('error' in form) {
    handlePaymentFormError(form.error, tabId);
    return;
  }

  const formPrice = form.invoice.totalAmount;
  if (formPrice !== price) {
    actions.openPriceConfirmModal({
      originalAmount: price,
      newAmount: formPrice,
      currency,
      directInfo: {
        inputInvoice,
        formId: form.formId,
      },
      tabId,
    });
    return;
  }

  actions.sendStarPaymentForm({
    directInfo: {
      inputInvoice,
      formId: form.formId,
    },
    tabId,
  });
}

addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promise<void> => {
  const {
    slug, tabId = getCurrentTabId(),
  } = payload;

  const result = await callApi('fetchUniqueStarGift', { slug });

  if (!result || 'error' in result) {
    const isBurned = result && 'error' in result && result.errorMessage === 'STARGIFT_ALREADY_BURNED';
    actions.showNotification({
      message: {
        key: isBurned ? 'ActionStarGiftUniqueBurnedError' : 'GiftWasNotFound',
      },
      tabId,
    });
    return;
  }

  actions.openGiftInfoModal({ gift: result, tabId });
});

addActionHandler('openGiftAuctionBySlug', async (global, actions, payload): Promise<void> => {
  const {
    slug, tabId = getCurrentTabId(),
  } = payload;

  const auctionState = await callApi('fetchStarGiftAuctionState', { slug });

  if (!auctionState) {
    actions.showNotification({
      message: {
        key: 'GiftWasNotFound',
      },
      tabId,
    });
    return;
  }

  actions.openGiftAuctionModal({ gift: auctionState.gift, tabId });
});

addActionHandler('processStarGiftWithdrawal', async (global, actions, payload): Promise<void> => {
  const {
    gift, password, tabId = getCurrentTabId(),
  } = payload;

  let giftWithdrawModal = selectTabState(global, tabId).giftWithdrawModal;
  if (!giftWithdrawModal) return;

  global = updateTabState(global, {
    giftWithdrawModal: {
      ...giftWithdrawModal,
      isLoading: true,
      errorKey: undefined,
    },
  }, tabId);
  setGlobal(global);

  const inputGift = getRequestInputSavedStarGift(global, gift);
  if (!inputGift) {
    return;
  }

  const result = await callApi('fetchStarGiftWithdrawalUrl', { inputGift, password });

  if (!result) {
    return;
  }

  global = getGlobal();
  giftWithdrawModal = selectTabState(global, tabId).giftWithdrawModal;
  if (!giftWithdrawModal) return;

  if ('error' in result) {
    global = updateTabState(global, {
      giftWithdrawModal: {
        ...giftWithdrawModal,
        isLoading: false,
        errorKey: result.messageKey,
      },
    }, tabId);
    setGlobal(global);
    return;
  }

  actions.openUrl({ url: result.url, tabId });
  actions.closeGiftWithdrawModal({ tabId });
});

function handlePaymentFormError(error: string, tabId: number) {
  if (error === 'SLUG_INVALID') {
    getActions().showNotification({
      message: {
        key: 'PaymentInvoiceNotFound',
      },
      tabId,
    });
    return;
  }

  getActions().showDialog({ data: { type: 'error', message: error, hasErrorKey: true }, tabId });
}
