/*
 This file is part of GNU Taler
 (C) 2022-2024 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 { useSessionState } from "./session.js";

import {
  AbsoluteTime,
  AccessToken,
  AmountJson,
  Amounts,
  HttpStatusCode,
  OperationOk,
  TalerBankConversionResultByMethod,
  TalerCoreBankErrorsByMethod,
  TalerCoreBankResultByMethod,
  TalerCorebankApi,
  TalerError,
  TalerHttpError,
  opFixedSuccess,
} from "@gnu-taler/taler-util";
import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
import _useSWR, { SWRHook, mutate } from "swr";
import { PAGINATED_LIST_REQUEST } from "../utils.js";
import { buildPaginatedResult } from "./account.js";
import { TalerBankConversionHttpClient } from "@gnu-taler/taler-util";
import { assertUnreachable } from "@gnu-taler/taler-util";
import { TalerBankConversionErrorsByMethod } from "@gnu-taler/taler-util";

// FIX default import https://github.com/microsoft/TypeScript/issues/49189
const useSWR = _useSWR as unknown as SWRHook;

export type TransCalc = {
  debit: AmountJson;
  credit: AmountJson;
  beforeFee: AmountJson;
};
export type TransferCalculation = TransCalc | undefined;
type EstimatorFunction = (
  amount: AmountJson,
  fee: AmountJson,
) => Promise<
  | OperationOk<TransCalc>
  | TalerBankConversionErrorsByMethod<"getCashinRate">
  | TalerBankConversionErrorsByMethod<"getCashoutRate">
>;

type ConversionEstimators = {
  estimateByCredit: EstimatorFunction;
  estimateByDebit: EstimatorFunction;
};

export function revalidateConversionInfo() {
  return mutate(
    (key) =>
      Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI",
  );
}
export function useConversionInfo() {
  const {
    lib: { conversion },
    config,
  } = useBankCoreApiContext();

  async function fetcher() {
    return await conversion.getConfig();
  }
  const { data, error } = useSWR<
    TalerBankConversionResultByMethod<"getConfig">,
    TalerHttpError
  >(!config.allow_conversion ? undefined : ["getConversionInfoAPI"], fetcher, {
    refreshInterval: 0,
    refreshWhenHidden: false,
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
    refreshWhenOffline: false,
    errorRetryCount: 0,
    errorRetryInterval: 1,
    shouldRetryOnError: false,
    keepPreviousData: true,
  });

  if (data) return data;
  if (error) return error;
  return undefined;
}

export function useConversionRateForUser(
  username: string,
  token: AccessToken | undefined,
) {
  const {
    lib: { conversionForUser },
    config,
  } = useBankCoreApiContext();

  async function fetcher() {
    return await conversionForUser(username).getRate(token);
  }
  const { data, error } = useSWR<
    TalerBankConversionResultByMethod<"getRate">,
    TalerHttpError
  >(
    !config.allow_conversion ? undefined : ["useConversionInfoForUser"],
    fetcher,
    {
      refreshInterval: 0,
      refreshWhenHidden: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      refreshWhenOffline: false,
      errorRetryCount: 0,
      errorRetryInterval: 1,
      shouldRetryOnError: false,
      keepPreviousData: true,
    },
  );

  if (data) return data;
  if (error) return error;
  return undefined;
}

function buildEstimatorWithTheBackend(
  conversion: TalerBankConversionHttpClient,
  token: AccessToken | undefined,
  estimation:
    | "cashin-rate-from-credit"
    | "cashin-rate-from-debit"
    | "cashout-rate-from-credit"
    | "cashout-rate-from-debit",
): EstimatorFunction {
  return async (amount, fee) => {
    let resp;
    switch (estimation) {
      case "cashin-rate-from-credit": {
        resp = await conversion.getCashinRate(token, {
          credit: amount,
        });
        break;
      }
      case "cashin-rate-from-debit": {
        resp = await conversion.getCashinRate(token, {
          debit: amount,
        });
        break;
      }
      case "cashout-rate-from-credit": {
        resp = await conversion.getCashoutRate(token, {
          credit: amount,
        });
        break;
      }
      case "cashout-rate-from-debit": {
        resp = await conversion.getCashoutRate(token, {
          debit: amount,
        });
        break;
      }
      default: {
        assertUnreachable(estimation);
      }
    }
    if (resp.type === "fail") {
      return resp;
    }
    const credit = Amounts.parseOrThrow(resp.body.amount_credit);
    const debit = Amounts.parseOrThrow(resp.body.amount_debit);
    const beforeFee = Amounts.add(credit, fee).amount;

    return opFixedSuccess({
      debit,
      beforeFee,
      credit,
    });
  };
}

function buildConversionEstimatorsWithTheBackend(
  conversion: TalerBankConversionHttpClient,
  direction: "cashin" | "cashout",
): ConversionEstimators {
  const { state } = useSessionState();
  const token = state.status === "loggedIn" ? state.token : undefined;
  return {
    estimateByCredit: buildEstimatorWithTheBackend(
      conversion,
      token,
      direction == "cashin"
        ? "cashin-rate-from-credit"
        : "cashout-rate-from-credit",
    ),
    estimateByDebit: buildEstimatorWithTheBackend(
      conversion,
      token,
      direction == "cashin"
        ? "cashin-rate-from-debit"
        : "cashout-rate-from-debit",
    ),
  };
}

export function useCashinEstimator(): ConversionEstimators {
  const {
    lib: { conversion },
  } = useBankCoreApiContext();

  return buildConversionEstimatorsWithTheBackend(conversion, "cashin");
}

export function useCashoutEstimator(): ConversionEstimators {
  const {
    lib: { conversion },
  } = useBankCoreApiContext();
  return buildConversionEstimatorsWithTheBackend(conversion, "cashout");
}

export function useCashinEstimatorForClass(
  classId: number,
): ConversionEstimators {
  const {
    lib: { conversionForClass },
  } = useBankCoreApiContext();
  return buildConversionEstimatorsWithTheBackend(
    conversionForClass(classId),
    "cashin",
  );
}

export function useCashoutEstimatorForClass(
  classId: number,
): ConversionEstimators {
  const {
    lib: { conversionForClass },
  } = useBankCoreApiContext();
  return buildConversionEstimatorsWithTheBackend(
    conversionForClass(classId),
    "cashout",
  );
}

export function useCashinEstimatorByUser(
  username: string,
): ConversionEstimators {
  const {
    lib: { conversionForUser },
  } = useBankCoreApiContext();
  return buildConversionEstimatorsWithTheBackend(
    conversionForUser(username),
    "cashin",
  );
}

export function useCashoutEstimatorByUser(
  username: string,
): ConversionEstimators {
  const {
    lib: { conversionForUser },
  } = useBankCoreApiContext();
  return buildConversionEstimatorsWithTheBackend(
    conversionForUser(username),
    "cashout",
  );
}

export async function revalidateBusinessAccounts() {
  return mutate(
    (key) => Array.isArray(key) && key[key.length - 1] === "listAccounts",
    undefined,
    { revalidate: true },
  );
}
export function useBusinessAccounts() {
  const { state: credentials } = useSessionState();
  const token =
    credentials.status !== "loggedIn" ? undefined : credentials.token;
  const {
    lib: { bank: api },
  } = useBankCoreApiContext();

  const [offset, setOffset] = useState<number | undefined>();

  function fetcher([token, aid]: [AccessToken, number]) {
    return api.listAccounts(token, {
      limit: PAGINATED_LIST_REQUEST,
      offset: aid ? String(aid) : undefined,
      order: "asc",
    });
  }

  const { data, error } = useSWR<
    TalerCoreBankResultByMethod<"listAccounts">,
    TalerHttpError
  >([token, offset ?? 0, "listAccounts"], fetcher, {
    refreshInterval: 0,
    refreshWhenHidden: false,
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
    refreshWhenOffline: false,
    errorRetryCount: 0,
    errorRetryInterval: 1,
    shouldRetryOnError: false,
    keepPreviousData: true,
  });

  if (error) return error;
  if (data === undefined) return undefined;
  if (data.type !== "ok") return data;

  //TODO: row_id should not be optional
  return buildPaginatedResult(
    data.body.accounts,
    offset,
    setOffset,
    (d) => d.row_id ?? 0,
  );
}

type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number };
function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
  return c !== undefined;
}
export function revalidateOnePendingCashouts() {
  return mutate(
    (key) =>
      Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts",
    undefined,
    { revalidate: true },
  );
}
export function useOnePendingCashouts(account: string) {
  const { state: credentials } = useSessionState();
  const {
    lib: { bank: api },
    config,
  } = useBankCoreApiContext();
  const token =
    credentials.status !== "loggedIn" ? undefined : credentials.token;

  async function fetcher([username, token]: [string, AccessToken]) {
    const list = await api.getAccountCashouts({ username, token });
    if (list.type !== "ok") {
      return list;
    }
    const pendingCashout =
      list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
    if (!pendingCashout) return opFixedSuccess(undefined);
    const cashoutInfo = await api.getCashoutById(
      { username, token },
      pendingCashout.cashout_id,
    );
    if (cashoutInfo.type !== "ok") {
      return cashoutInfo;
    }
    return opFixedSuccess({
      ...cashoutInfo.body,
      id: pendingCashout.cashout_id,
    });
  }

  const { data, error } = useSWR<
    | OperationOk<CashoutWithId | undefined>
    | TalerCoreBankErrorsByMethod<"getAccountCashouts">
    | TalerCoreBankErrorsByMethod<"getCashoutById">,
    TalerHttpError
  >(
    !config.allow_conversion
      ? undefined
      : [account, token, "useOnePendingCashouts"],
    fetcher,
    {
      refreshInterval: 0,
      refreshWhenHidden: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      refreshWhenOffline: false,
      errorRetryCount: 0,
      errorRetryInterval: 1,
      shouldRetryOnError: false,
      keepPreviousData: true,
    },
  );

  if (data) return data;
  if (error) return error;
  return undefined;
}

export function revalidateCashouts() {
  return mutate(
    (key) => Array.isArray(key) && key[key.length - 1] === "useCashouts",
  );
}
export function useCashouts(account: string) {
  const { state: credentials } = useSessionState();
  const {
    lib: { bank: api },
    config,
  } = useBankCoreApiContext();
  const token =
    credentials.status !== "loggedIn" ? undefined : credentials.token;

  async function fetcher([username, token]: [string, AccessToken]) {
    const list = await api.getAccountCashouts({ username, token });
    if (list.type !== "ok") {
      return list;
    }
    const all: Array<CashoutWithId | undefined> = await Promise.all(
      list.body.cashouts.map(async (c) => {
        const r = await api.getCashoutById({ username, token }, c.cashout_id);
        if (r.type === "fail") {
          return undefined;
        }
        return { ...r.body, id: c.cashout_id };
      }),
    );
    const cashouts = all.filter(notUndefined);
    return opFixedSuccess({ cashouts });
  }
  const { data, error } = useSWR<
    | OperationOk<{ cashouts: CashoutWithId[] }>
    | TalerCoreBankErrorsByMethod<"getAccountCashouts">,
    TalerHttpError
  >(
    !config.allow_conversion ? undefined : [account, token, "useCashouts"],
    fetcher,
    {
      refreshInterval: 0,
      refreshWhenHidden: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      refreshWhenOffline: false,
      errorRetryCount: 0,
      errorRetryInterval: 1,
      shouldRetryOnError: false,
      keepPreviousData: true,
    },
  );

  if (data) return data;
  if (error) return error;
  return undefined;
}

export function revalidateCashoutDetails() {
  return mutate(
    (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById",
    undefined,
    { revalidate: true },
  );
}
export function useCashoutDetails(cashoutId: number | undefined) {
  const { state: credentials } = useSessionState();
  const creds = credentials.status !== "loggedIn" ? undefined : credentials;
  const {
    lib: { bank: api },
  } = useBankCoreApiContext();

  async function fetcher([username, token, id]: [string, AccessToken, number]) {
    return api.getCashoutById({ username, token }, id);
  }

  const { data, error } = useSWR<
    TalerCoreBankResultByMethod<"getCashoutById">,
    TalerHttpError
  >(
    cashoutId === undefined
      ? undefined
      : [creds?.username, creds?.token, cashoutId, "getCashoutById"],
    fetcher,
    {
      refreshInterval: 0,
      refreshWhenHidden: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      refreshWhenOffline: false,
      errorRetryCount: 0,
      errorRetryInterval: 1,
      shouldRetryOnError: false,
      keepPreviousData: true,
    },
  );

  if (data) return data;
  if (error) return error;
  return undefined;
}
export type MonitorMetrics = {
  lastHour: TalerCoreBankResultByMethod<"getMonitor">;
  lastDay: TalerCoreBankResultByMethod<"getMonitor">;
  lastMonth: TalerCoreBankResultByMethod<"getMonitor">;
};

export type LastMonitor = {
  current: TalerCoreBankResultByMethod<"getMonitor">;
  previous: TalerCoreBankResultByMethod<"getMonitor">;
};
export function revalidateLastMonitorInfo() {
  return mutate(
    (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo",
    undefined,
    { revalidate: true },
  );
}
export function useLastMonitorInfo(
  currentMoment: AbsoluteTime,
  previousMoment: AbsoluteTime,
  timeframe: TalerCorebankApi.MonitorTimeframeParam,
) {
  const {
    lib: { bank: api },
  } = useBankCoreApiContext();
  const { state: credentials } = useSessionState();

  const token =
    credentials.status !== "loggedIn" ? undefined : credentials.token;

  async function fetcher([token, timeframe]: [
    AccessToken,
    TalerCorebankApi.MonitorTimeframeParam,
  ]) {
    const [current, previous] = await Promise.all([
      api.getMonitor(token, { timeframe, date: currentMoment }),
      api.getMonitor(token, { timeframe, date: previousMoment }),
    ]);
    return {
      current,
      previous,
    };
  }

  const { data, error } = useSWR<LastMonitor, TalerHttpError>(
    !token ? undefined : [token, timeframe, "useLastMonitorInfo"],
    fetcher,
    {
      refreshInterval: 0,
      refreshWhenHidden: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      refreshWhenOffline: false,
      errorRetryCount: 0,
      errorRetryInterval: 1,
      shouldRetryOnError: false,
      keepPreviousData: true,
    },
  );

  if (data) return data;
  if (error) return error;
  return undefined;
}

export function revalidateConversionRateClasses() {
  return mutate(
    (key) =>
      Array.isArray(key) && key[key.length - 1] === "useConversionRateClasses",
    undefined,
    { revalidate: true },
  );
}
export function useConversionRateClasses() {
  const { state: credentials } = useSessionState();
  const token =
    credentials.status !== "loggedIn" ? undefined : credentials.token;
  const {
    lib: { bank: api },
  } = useBankCoreApiContext();

  const [offset, setOffset] = useState<number | undefined>();

  function fetcher([token, aid]: [AccessToken, number]) {
    return api.listConversionRateClasses(token, {
      limit: PAGINATED_LIST_REQUEST,
      offset: aid ? String(aid) : undefined,
      order: "asc",
    });
  }

  const { data, error } = useSWR<
    TalerCoreBankResultByMethod<"listConversionRateClasses">,
    TalerHttpError
  >([token, offset ?? 0, "useConversionRateClasses"], fetcher, {
    refreshInterval: 0,
    refreshWhenHidden: false,
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
    refreshWhenOffline: false,
    errorRetryCount: 0,
    errorRetryInterval: 1,
    shouldRetryOnError: false,
    keepPreviousData: true,
  });

  if (error) return error;
  if (data === undefined) return undefined;
  if (data.type !== "ok") return data;

  return buildPaginatedResult(
    data.body.classes,
    offset,
    setOffset,
    (d) => d.conversion_rate_class_id,
  );
}

export function revalidateConversionRateClassDetails() {
  return mutate(
    (key) =>
      Array.isArray(key) &&
      key[key.length - 1] === "useConversionRateClassDetails",
    undefined,
    { revalidate: true },
  );
}

export function useConversionRateClassDetails(classId: number) {
  const { state: credentials } = useSessionState();
  const token =
    credentials.status !== "loggedIn" ? undefined : credentials.token;
  const {
    lib: { bank: api },
  } = useBankCoreApiContext();

  async function fetcher([username, token]: [number, AccessToken]) {
    return await api.getConversionRateClass(token, username);
  }

  const { data, error } = useSWR<
    TalerCoreBankResultByMethod<"getConversionRateClass">,
    TalerHttpError
  >([classId, token, "useConversionRateClassDetails"], fetcher, {});

  if (data) return data;
  if (error) return error;
  return undefined;
}

export function revalidateConversionRateClassUsers() {
  return mutate(
    (key) =>
      Array.isArray(key) &&
      key[key.length - 1] === "useConversionRateClassUsers",
    undefined,
    { revalidate: true },
  );
}

export function useConversionRateClassUsers(
  classId: number | undefined,
  username?: string,
) {
  const { state: credentials } = useSessionState();
  const token =
    credentials.status !== "loggedIn" ? undefined : credentials.token;
  const {
    lib: { bank: api },
  } = useBankCoreApiContext();

  const [offset, setOffset] = useState<number | undefined>();

  function fetcher([token, aid, username, classId]: [
    AccessToken,
    number,
    string,
    number,
  ]) {
    return api.listAccounts(token, {
      limit: PAGINATED_LIST_REQUEST,
      offset: aid ? String(aid) : undefined,
      order: "asc",
      account: username,
      conversionRateId: classId,
    });
  }

  const { data, error } = useSWR<
    TalerCoreBankResultByMethod<"listAccounts">,
    TalerHttpError
  >(
    [token, offset ?? 0, username, classId, "useConversionRateClassUsers"],
    fetcher,
    {
      refreshInterval: 0,
      refreshWhenHidden: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      refreshWhenOffline: false,
      errorRetryCount: 0,
      errorRetryInterval: 1,
      shouldRetryOnError: false,
      keepPreviousData: true,
    },
  );

  if (error) return error;
  if (data === undefined) return undefined;
  if (data.type !== "ok") return data;

  return buildPaginatedResult(
    data.body.accounts,
    offset,
    setOffset,
    (d) => d.row_id!,
  );
}
