/*
 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 {
  Amounts,
  PaytoString,
  TransactionAmountMode,
  WalletBankAccountInfo,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { RecursiveState } from "../../utils/index.js";
import { Props, State } from "./index.js";

export function useComponentState({
  scope,
  onCancel,
  onSuccess,
}: Props): RecursiveState<State> {
  const api = useBackendContext();
  const { i18n } = useTranslationContext();
  const { pushAlertOnError } = useAlertContext();

  const zero = Amounts.zeroOfCurrency(scope.currency);

  const hook = useAsyncAsHook(async () => {
    const { balances } = await api.wallet.call(
      WalletApiOperation.GetBalances,
      {},
    );

    const { accounts } = await api.wallet.call(
      WalletApiOperation.ListBankAccounts,
      { currency: scope.currency },
    );

    return { accounts, balances };
  });

  const [selectedAccount, setSelectedAccount] = useState<PaytoString>();

  const [addingAccount, setAddingAccount] = useState(false);

  if (!hook) {
    return {
      status: "loading",
      error: undefined,
    };
  }
  if (hook.hasError) {
    return {
      status: "error",
      error: alertFromError(
        i18n,
        i18n.str`Could not load balance information`,
        hook,
      ),
    };
  }
  const { accounts, balances } = hook.response;

  async function updateAccountFromList(accountStr: PaytoString): Promise<void> {
    const uri = accountStr ?? undefined;
    if (uri) {
      setSelectedAccount(uri);
    }
  }

  if (addingAccount) {
    return {
      status: "manage-account",
      error: undefined,
      scope,
      onAccountSelected: (p: PaytoString) => {
        updateAccountFromList(p);
        setAddingAccount(false);
        hook.retry();
      },
      onCancel: () => {
        setAddingAccount(false);
        hook.retry();
      },
    };
  }

  const bs = balances.filter((b) => b.scopeInfo === scope);
  const balance =
    bs.length > 0
      ? Amounts.parseOrThrow(bs[0].available)
      : Amounts.zeroOfCurrency(scope.currency);

  if (Amounts.isZero(balance)) {
    return {
      status: "no-enough-balance",
      error: undefined,
      currency: scope.currency,
    };
  }

  if (accounts.length === 0) {
    return {
      status: "no-accounts",
      error: undefined,
      currency: scope.currency,
      onAddAccount: {
        onClick: pushAlertOnError(async () => {
          setAddingAccount(true);
        }),
      },
    };
  }
  const firstAccount = accounts[0].paytoUri as PaytoString;
  const currentAccount = !selectedAccount ? firstAccount : selectedAccount;
  return (): State => {
    const [instructed, setInstructed] = useState({
      amount: zero,
      type: TransactionAmountMode.Raw,
    });
    const amountStr = Amounts.stringify(instructed.amount);
    const depositPaytoUri = currentAccount;

    const hook = useAsyncAsHook(async () => {
      const fee = await api.wallet.call(
        WalletApiOperation.ConvertDepositAmount,
        {
          amount: amountStr,
          type: instructed.type,
          depositPaytoUri,
        },
      );

      return { fee };
    }, [amountStr, depositPaytoUri]);

    if (!hook) {
      return {
        status: "loading",
        error: undefined,
      };
    }
    if (hook.hasError) {
      return {
        status: "error",
        error: alertFromError(
          i18n,
          i18n.str`Could not load fee for amount ${amountStr}`,
          hook,
        ),
      };
    }

    const { fee } = hook.response;

    const accountMap = createLabelsForBankAccount(accounts);

    const totalFee =
      fee !== undefined
        ? Amounts.sub(fee.effectiveAmount, fee.rawAmount).amount
        : zero;

    const totalToDeposit = Amounts.parseOrThrow(fee.rawAmount);
    const totalEffective = Amounts.parseOrThrow(fee.effectiveAmount);

    const isDirty = instructed.amount !== zero;
    const amountError = !isDirty
      ? undefined
      : Amounts.cmp(balance, totalEffective) === -1
        ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
        : undefined;

    const unableToDeposit =
      Amounts.isZero(totalToDeposit) || //deposit may be zero because of fee
      fee === undefined || //no fee calculated yet
      amountError !== undefined; //amount field may be invalid

    async function doSend(): Promise<void> {
      // if (!currency) return;

      const depositPaytoUri = currentAccount;
      const amountStr = Amounts.stringify(totalEffective);
      await api.wallet.call(WalletApiOperation.CreateDepositGroup, {
        amount: amountStr,
        depositPaytoUri,
      });
      onSuccess(scope!);
    }

    return {
      status: "ready",
      error: undefined,
      currency: scope.currency,
      amount: {
        value: totalEffective,
        onInput: pushAlertOnError(async (a) =>
          setInstructed({
            amount: a,
            type: TransactionAmountMode.Effective,
          }),
        ),
        error: amountError,
      },
      totalToDeposit: {
        value: totalToDeposit,
        onInput: pushAlertOnError(async (a) =>
          setInstructed({
            amount: a,
            type: TransactionAmountMode.Raw,
          }),
        ),
        error: amountError,
      },
      onAddAccount: {
        onClick: pushAlertOnError(async () => {
          setAddingAccount(true);
        }),
      },
      account: {
        list: accountMap,
        value: currentAccount,
        onChange: pushAlertOnError(updateAccountFromList),
      },
      currentAccount,
      cancelHandler: {
        onClick: pushAlertOnError(async () => {
          onCancel(scope!);
        }),
      },
      depositHandler: {
        onClick: unableToDeposit ? undefined : pushAlertOnError(doSend),
      },
      totalFee,
    };
  };
}

export function labelForAccountType(id: string): string {
  switch (id) {
    case "":
      return "Choose one";
    case "x-taler-bank":
      return "Taler Bank";
    case "bitcoin":
      return "Bitcoin";
    case "iban":
      return "IBAN";
    default:
      return id;
  }
}

export function createLabelsForBankAccount(
  bankAccounts: Array<WalletBankAccountInfo>,
): { [value: string]: string } {
  const initialList: Record<string, string> = {};
  if (!bankAccounts.length) return initialList;
  return bankAccounts.reduce((prev, cur) => {
    prev[cur.paytoUri] = cur.label ?? "??";
    return prev;
  }, initialList);
}
