/*
 This file is part of GNU Taler
 (C) 2022 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

import {
  AmountJson,
  AmountString,
  Amounts,
  ExchangeFullDetails,
  ExchangeListItem,
  NotificationType,
  ScopeInfo,
  ScopeType,
  TransactionMajorState,
  TransactionMinorState,
  parseWithdrawExchangeUri,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useCallback, useEffect, useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
import { RecursiveState } from "../../utils/index.js";
import { PropsFromParams, PropsFromURI, State } from "./index.js";

export function useComponentStateFromParams({
  talerExchangeWithdrawUri: maybeTalerUri,
  scope,
  amount: paramsAmount,
  cancel,
  onSuccess,
}: PropsFromParams): RecursiveState<State> {
  const api = useBackendContext();
  const { i18n } = useTranslationContext();
  // const paramsAmount = amount ? Amounts.parse(amount) : undefined;
  const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>();
  const uriInfoHook = useAsyncAsHook(async () => {
    const uri = maybeTalerUri
      ? parseWithdrawExchangeUri(maybeTalerUri)
      : undefined;

    const exchangeUrlByScope =
      scope?.type === ScopeType.Exchange ? scope.url : undefined;
    const exchangeByScopeOrTalerUri =
      updatedExchangeByUser ?? exchangeUrlByScope ?? uri?.exchangeBaseUrl;

    let ex: ExchangeFullDetails | undefined;
    if (exchangeByScopeOrTalerUri) {
      await api.wallet.call(WalletApiOperation.AddExchange, {
        exchangeBaseUrl: exchangeByScopeOrTalerUri,
      });
      const info = await api.wallet.call(
        WalletApiOperation.GetExchangeDetailedInfo,
        {
          exchangeBaseUrl: exchangeByScopeOrTalerUri,
        },
      );

      ex = info.exchange;
    }

    const exchanges = await api.wallet.call(WalletApiOperation.ListExchanges, {
      filterByScope: scope,
    });

    const chosenAmount =
      !uri || !uri.amount ? undefined : Amounts.parse(uri.amount);
    return { amount: chosenAmount, exchanges, exchange: ex };
  });

  useEffect(() => {
    uriInfoHook?.retry();
  }, [paramsAmount]);

  if (!uriInfoHook) return { status: "loading", error: undefined };

  if (uriInfoHook.hasError) {
    return {
      status: "error",
      error: alertFromError(
        i18n,
        i18n.str`Could not load the list of exchanges`,
        uriInfoHook,
      ),
    };
  }

  const currency = uriInfoHook.response.exchange?.currency ?? scope?.currency;
  const exchangeByUrl = uriInfoHook.response.exchange?.exchangeBaseUrl;
  const exchangeList = uriInfoHook.response.exchanges.exchanges;

  const maybeAmount = uriInfoHook.response.amount ?? paramsAmount;
  if (!currency) {
    return {
      status: "error",
      error: alertFromError(
        i18n,
        i18n.str`Could not load the list of exchanges`,
        {} as any,
      ),
    };
  }

  const chosenAmount = maybeAmount ?? Amounts.zeroOfCurrency(currency);

  async function doManualWithdraw(
    exchange: string,
    ageRestricted: number | undefined,
    amount: AmountString,
  ): Promise<{
    transactionId: string;
    confirmTransferUrl: string | undefined;
  }> {
    const res = await api.wallet.call(
      WalletApiOperation.AcceptManualWithdrawal,
      {
        exchangeBaseUrl: exchange,
        amount,
        restrictAge: ageRestricted,
      },
    );
    return {
      confirmTransferUrl: undefined,
      transactionId: res.transactionId,
    };
  }

  const exchangeScope = scope ?? {
    type: ScopeType.Exchange,
    currency: currency,
    url: exchangeByUrl!,
  };

  return () =>
    exchangeSelectionState(
      doManualWithdraw,
      cancel,
      onSuccess,
      undefined,
      {
        amount: chosenAmount,
        currency: chosenAmount.currency,
        maxAmount: Amounts.zeroOfCurrency(chosenAmount.currency),
        bankFee: Amounts.zeroOfCurrency(chosenAmount.currency),
        editableAmount: true,
        editableExchange: true,
        exchange: exchangeByUrl,
        exchangeList: exchangeList,
      },
      setUpdatedExchangeByUser,
      exchangeScope,
    );
}

export function useComponentStateFromURI({
  talerWithdrawUri: maybeTalerUri,
  cancel,
  onSuccess,
}: PropsFromURI): RecursiveState<State> {
  const api = useBackendContext();
  const { i18n } = useTranslationContext();

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [updatedExchangeByUser, setUpdatedExchangeByUser] = useState<string>();
  /**
   * Ask the wallet about the withdraw URI
   */
  const uriInfoHook = useAsyncAsHook(async () => {
    if (!maybeTalerUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL");
    const talerWithdrawUri = maybeTalerUri.startsWith("ext+")
      ? maybeTalerUri.substring(4)
      : maybeTalerUri;

    const uriInfo = await api.wallet.call(
      WalletApiOperation.PrepareBankIntegratedWithdrawal,
      { talerWithdrawUri },
    );
    const { status } = uriInfo.info;
    const txInfo = await api.wallet.call(
      WalletApiOperation.GetTransactionById,
      {
        transactionId: uriInfo.transactionId,
      },
    );
    return {
      talerWithdrawUri,
      status,
      transactionId: uriInfo.transactionId,
      bankWithdrawalInfo: uriInfo.info,
      txInfo: txInfo,
    };
  });

  const readyToListen = uriInfoHook && !uriInfoHook.hasError;

  useEffect(() => {
    if (!uriInfoHook || uriInfoHook.hasError) {
      return;
    }
    const txId = uriInfoHook.response.transactionId;

    return api.listener.onUpdateNotification(
      [NotificationType.TransactionStateTransition],
      (notif) => {
        if (
          notif.type === NotificationType.TransactionStateTransition &&
          notif.transactionId === txId
        ) {
          uriInfoHook.retry();
        }
      },
    );
  }, [readyToListen]);

  if (!uriInfoHook) return { status: "loading", error: undefined };

  if (uriInfoHook.hasError) {
    return {
      status: "error",
      error: alertFromError(
        i18n,
        i18n.str`Could not load info from URI`,
        uriInfoHook,
      ),
    };
  }

  const uri = uriInfoHook.response.talerWithdrawUri;
  const txId = uriInfoHook.response.transactionId;
  const bwi = uriInfoHook.response.bankWithdrawalInfo;

  const amount =
    bwi.amount === undefined
      ? Amounts.zeroOfCurrency(bwi.currency)
      : Amounts.parseOrThrow(bwi.amount);

  const maxAmount =
    bwi.maxAmount === undefined
      ? Amounts.zeroOfCurrency(bwi.currency)
      : Amounts.parseOrThrow(bwi.maxAmount);

  const bankFee =
    bwi.wireFee === undefined
      ? Amounts.zeroOfCurrency(bwi.currency)
      : Amounts.parseOrThrow(bwi.wireFee);

  async function doManagedWithdraw(
    exchange: string,
    ageRestricted: number | undefined,
    amount: AmountString,
  ): Promise<{
    transactionId: string;
    confirmTransferUrl: string | undefined;
  }> {
    const res = await api.wallet.call(WalletApiOperation.ConfirmWithdrawal, {
      exchangeBaseUrl: exchange,
      amount,
      restrictAge: ageRestricted,
      transactionId: txId,
    });
    return {
      confirmTransferUrl: res.confirmTransferUrl,
      transactionId: res.transactionId,
    };
  }

  if (uriInfoHook.response.status !== "pending") {
    const info = uriInfoHook.response.txInfo;
    const anotherWallet =
      info.txState.major === TransactionMajorState.Aborted &&
      info.txState.minor === TransactionMinorState.CompletedByOtherWallet;
    return {
      status: "already-completed",
      operationState: uriInfoHook.response.status,
      confirmTransferUrl: bwi.confirmTransferUrl,
      thisWallet: !anotherWallet,
      redirectToTx: () => onSuccess(info.transactionId),
      error: undefined,
    };
  }
  if (
    uriInfoHook.response.txInfo.txState.major === TransactionMajorState.Failed
  ) {
    return {
      status: "already-completed",
      thisWallet: true,
      redirectToTx: () => onSuccess(uriInfoHook.response.txInfo.transactionId),
      error: undefined,
      operationState: "aborted",
    };
  }

  const exchangeScope: ScopeInfo = {
    type: ScopeType.Global,
    currency: bwi.currency,
  };
  return useCallback(() => {
    return exchangeSelectionState(
      doManagedWithdraw,
      cancel,
      onSuccess,
      uri,
      {
        amount,
        bankFee,
        maxAmount,
        currency: bwi.currency,
        editableAmount: bwi.editableAmount,
        editableExchange: bwi.editableExchange,
        exchange: bwi.defaultExchangeBaseUrl,
        exchangeList: bwi.possibleExchanges,
      },
      setUpdatedExchangeByUser,
      exchangeScope,
    );
  }, []);
}

type WithdrawalInfo = {
  currency: string;
  amount: AmountJson;
  bankFee: AmountJson;
  maxAmount: AmountJson;
  editableAmount: boolean;
  exchange: string | undefined;
  editableExchange: boolean;
  exchangeList: ExchangeListItem[];
};

type ManualOrManagedWithdrawFunction = (
  exchange: string,
  ageRestricted: number | undefined,
  amount: AmountString,
) => Promise<{ transactionId: string; confirmTransferUrl: string | undefined }>;

function exchangeSelectionState(
  doWithdraw: ManualOrManagedWithdrawFunction,
  cancel: () => Promise<void>,
  onSuccess: (txid: string) => Promise<void>,
  talerWithdrawUri: string | undefined,
  wInfo: WithdrawalInfo,
  onExchangeUpdated: (ex: string) => void,
  scope: ScopeInfo,
): RecursiveState<State> {
  const api = useBackendContext();
  const selectedExchange = useSelectedExchange({
    scope,
    defaultExchange: wInfo.exchange,
    list: wInfo.exchangeList,
  });

  const current =
    selectedExchange.status !== "ready"
      ? undefined
      : selectedExchange.selected.exchangeBaseUrl;
  useEffect(() => {
    if (current) {
      onExchangeUpdated(current);
    }
  }, [current]);

  const safeAmount = wInfo.amount
    ? wInfo.amount
    : Amounts.zeroOfCurrency(wInfo.currency);

  if (selectedExchange.status !== "ready") {
    return selectedExchange;
  }

  return useCallback(():
    | State.Success
    | State.LoadingUriError
    | State.Loading => {
    const { i18n } = useTranslationContext();
    const { pushAlertOnError } = useAlertContext();

    const [choosenAmount, setChoosenAmount] = useState(safeAmount);
    const [ageRestricted, setAgeRestricted] = useState(0);

    const [selectedCurrency, setSelectedCurrency] = useState<string>(
      wInfo.currency,
    );
    /**
     * With the exchange and amount, ask the wallet the information
     * about the withdrawal
     */
    const amountHook = useAsyncAsHook(async () => {
      const info = await api.wallet.call(
        WalletApiOperation.GetWithdrawalDetailsForAmount,
        {
          exchangeBaseUrl: selectedExchange.selected.exchangeBaseUrl,
          amount: Amounts.stringify(choosenAmount),
          restrictAge: ageRestricted,
        },
      );

      const withdrawAmount = {
        raw: Amounts.parseOrThrow(info.amountRaw),
        effective: Amounts.parseOrThrow(info.amountEffective),
      };

      return {
        amount: withdrawAmount,
        currentExchange: selectedExchange.selected,
        ageRestrictionOptions: info.ageRestrictionOptions,
        accounts: info.withdrawalAccountsList,
      };
    }, [choosenAmount, selectedExchange.selected, ageRestricted]);

    const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);

    if (!amountHook) {
      return { status: "loading", error: undefined };
    }
    if (amountHook.hasError) {
      return {
        status: "error",
        error: alertFromError(
          i18n,
          i18n.str`Could not load the withdrawal details`,
          amountHook,
        ),
      };
    }
    if (!amountHook.response) {
      return { status: "loading", error: undefined };
    }

    const currentExchange = amountHook.response.currentExchange;

    async function doWithdrawAndCheckError(): Promise<void> {
      try {
        setDoingWithdraw(true);
        const res = await doWithdraw(
          currentExchange.exchangeBaseUrl,
          !ageRestricted ? undefined : ageRestricted,
          Amounts.stringify(choosenAmount),
        );
        if (res.confirmTransferUrl) {
          document.location.href = res.confirmTransferUrl;
        } else {
          onSuccess(res.transactionId);
        }
      } catch (e) {
        console.error(e);
        // if (e instanceof TalerError) {
        // }
      }
      setDoingWithdraw(false);
    }

    const toBeSent = amountHook.response.amount.raw;
    const toBeReceived = amountHook.response.amount.effective;

    const bankFee = wInfo.bankFee;

    const ageRestrictionOptions =
      amountHook.response.ageRestrictionOptions?.reduce(
        (p, c) => ({ ...p, [c]: i18n.str`under ${c}` }),
        {} as Record<string, string>,
      );

    const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
    if (ageRestrictionEnabled) {
      ageRestrictionOptions["0"] = "Not restricted";
    }

    //TODO: calculate based on exchange info
    const ageRestriction = ageRestrictionEnabled
      ? {
          list: ageRestrictionOptions,
          value: String(ageRestricted),
          onChange: pushAlertOnError(async (v: string) =>
            setAgeRestricted(parseInt(v, 10)),
          ),
        }
      : undefined;

    const altCurrencies = amountHook.response.accounts
      .filter((a) => !!a.currencySpecification)
      .map((a) => a.currencySpecification!.name);
    const chooseCurrencies = altCurrencies.length <= 1 ? [] : altCurrencies;

    const convAccount = amountHook.response.accounts.find((c) => {
      return (
        c.currencySpecification &&
        c.currencySpecification.name === selectedCurrency
      );
    });
    const conversionInfo = !convAccount
      ? undefined
      : {
          spec: convAccount.currencySpecification!,
          amount: Amounts.parseOrThrow(convAccount.transferAmount!),
        };

    const amountError = Amounts.isZero(choosenAmount)
      ? i18n.str`should be greater than zero`
      : Amounts.isNonZero(wInfo.maxAmount) &&
          Amounts.cmp(choosenAmount, wInfo.maxAmount) === 1
        ? i18n.str`can't be greater than ${Amounts.stringifyValue(
            wInfo.maxAmount,
          )}`
        : undefined;

    return {
      status: "success",
      error: undefined,
      doSelectExchange: {
        onClick: wInfo.editableExchange
          ? selectedExchange.doSelect.onClick
          : undefined,
      },
      editableAmount: wInfo.editableAmount,
      editableExchange: wInfo.editableExchange,
      currentExchange,
      toBeReceived,
      toBeSent,
      chooseCurrencies,
      bankFee,
      selectedCurrency,
      changeCurrency: (s) => {
        setSelectedCurrency(s);
      },
      conversionInfo,
      amount: {
        value: choosenAmount,
        onInput: wInfo.editableAmount
          ? pushAlertOnError(async (v) => {
              setChoosenAmount(v);
            })
          : undefined,
        error: amountError,
      },
      talerWithdrawUri,
      ageRestriction,
      doWithdrawal: {
        onClick:
          doingWithdraw || amountError
            ? undefined
            : pushAlertOnError(doWithdrawAndCheckError),
      },
      cancel,
    };
  }, [selectedExchange.selected]);
}
