/*
 This file is part of GNU Taler
 (C) 2021-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/>
 */

// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import {
  AccessToken,
  assertUnreachable,
  HttpStatusCode,
  KycStatusLongPollingReason,
  MerchantAccountKycRedirect,
  MerchantAccountKycStatus,
  TalerHttpError,
  TalerMerchantManagementResultByMethod,
} from "@gnu-taler/taler-util";
import _useSWR, { SWRHook, mutate } from "swr";
import { useSessionContext } from "../context/session.js";
import { useState, useMemo } from "preact/hooks";
import { useEffect } from "preact/compat";
const useSWR = _useSWR as unknown as SWRHook;

export function revalidateInstanceDetails() {
  return mutate(
    (key) =>
      Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceDetails",
    undefined,
    { revalidate: true },
  );
}
export function useInstanceDetails() {
  const { state, lib } = useSessionContext();

  async function fetcher([token]: [AccessToken]) {
    return await lib.instance.getCurrentInstanceDetails(token);
  }

  const { data, error } = useSWR<
    TalerMerchantManagementResultByMethod<"getCurrentInstanceDetails">,
    TalerHttpError
  >([state.token, "getCurrentInstanceDetails"], fetcher);

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

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

/**
 * @deprecated use useInstanceKYCDetailsLongPolling
 * @returns
 */
export function useInstanceKYCDetails() {
  const { state, lib } = useSessionContext();

  async function fetcher([token]: [AccessToken]) {
    return await lib.instance.getCurrentInstanceKycStatus(token, {});
  }

  const { data, error } = useSWR<
    TalerMerchantManagementResultByMethod<"getCurrentInstanceKycStatus">,
    TalerHttpError
  >([state.token, "getCurrentIntanceKycStatus"], fetcher);

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

// FIXME: move to web-utils and reuse
function useAsyncWithRetry<Res>(
  fetcher: (() => Promise<Res>) | undefined,
  retry?: (res: Res | undefined, err?: TalerHttpError | undefined) => boolean,
): { result: Res | undefined; error: TalerHttpError | undefined } {
  const [result, setResult] = useState<Res>();
  const [error, setError] = useState<TalerHttpError>();
  const [retryCounter, setRetryCounter] = useState(0);

  useEffect(() => {
    let unloaded = false;
    if (fetcher) {
      fetcher()
        .then((resp) => {
          if (unloaded) return;
          setResult(resp);
        })
        .catch((error: TalerHttpError) => {
          if (unloaded) return;
          setError(error);
        });
    }

    return () => {
      unloaded = true;
    };
  }, [fetcher, retryCounter]);

  // retry on result or error
  useEffect(() => {
    if (retry && retry(result, error)) {
      setRetryCounter((c) => c + 1);
    }
  }, [result, error]);

  return { result, error };
}

async function delay(t_ms: number) {
  return new Promise((res) => {
    setTimeout(() => {
      res(0);
    }, t_ms);
  });
}

let latestRequestTime = 0;
/**
 * FIXME: this will not wait for the fist KYC event to be triggered
 * if we wait for kyc transfer event for account 1 then we may miss
 * aml investigation event for account 2
 *
 * @returns
 */
export function useInstanceKYCDetailsLongPolling() {
  const { state, lib } = useSessionContext();

  // we may want to convert this into a set of reason of why to do a long poll
  // but that will be a multiple request
  // first, consider adding support on the server side for multi-reason
  // long polling
  const [latestReason, setLatestReason] =
    useState<KycStatusLongPollingReason>();

  const token = state.token;
  const fetcher = useMemo(() => {
    return async () => {
      const DEFAULT_WAIT = 5000;
      const now = new Date().getTime();
      if (latestReason) {
        const diff = now - latestRequestTime;
        if (diff < DEFAULT_WAIT) {
          console.log("PREVENT REQUEST BURST");
          await delay(DEFAULT_WAIT);
        }
      }
      latestRequestTime = new Date().getTime();
      return lib.instance.getCurrentInstanceKycStatus(token, {
        timeout: DEFAULT_WAIT,
        reason: latestReason,
      });
    };
  }, [token, latestReason]);

  const { result: data, error } = useAsyncWithRetry(fetcher, (r, err) => {
    // halt if error
    if (err !== undefined) return false;
    // loading, just wait
    if (r == undefined) return false;
    // error, we don't know the kyc status
    if (r.type === "fail") {
      return false;
    }
    // kyc ok, no need to retry
    if (!r.body) {
      return false;
    }
    // search for a non-ready kyc
    const acc = getLongPollingReasonSet(r.body.kyc_data);

    // retry if there is an account with long polling reason
    return acc !== undefined;
  });

  const currentStatus =
    data !== undefined && data.type === "ok" && data.body
      ? getLongPollingReasonSet(data.body.kyc_data)
      : undefined;

  useEffect(() => {
    if (currentStatus !== undefined && currentStatus !== latestReason) {
      // withdrawal has a new state, save the current
      // and make the query again
      setLatestReason(currentStatus);
    }
  }, [currentStatus]);

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

/**
 * given a response of the kyc endpoint, why should we keep long polling?
 *
 * @param data
 * @returns
 */
function getLongPollingReasonSet(
  data: MerchantAccountKycRedirect[],
): KycStatusLongPollingReason | undefined {
  const acc = data.find((k) => getLongPollingReason(k));
  if (!acc) return undefined;
  // console.log("found reason", acc);
  return getLongPollingReason(acc);
}

/**
 * do we have a reason to wait for this account?
 *
 * @param acc
 * @returns
 */
function getLongPollingReason(
  acc: MerchantAccountKycRedirect,
): KycStatusLongPollingReason | undefined {
  switch (acc.status) {
    case MerchantAccountKycStatus.READY:
    case MerchantAccountKycStatus.KYC_WIRE_IMPOSSIBLE:
    case MerchantAccountKycStatus.LOGIC_BUG:
    case MerchantAccountKycStatus.NO_EXCHANGE_KEY:
    case MerchantAccountKycStatus.EXCHANGE_INTERNAL_ERROR:
    case MerchantAccountKycStatus.EXCHANGE_GATEWAY_TIMEOUT:
    case MerchantAccountKycStatus.EXCHANGE_UNREACHABLE:
    case MerchantAccountKycStatus.EXCHANGE_STATUS_INVALID:
      return undefined;
    case MerchantAccountKycStatus.KYC_WIRE_REQUIRED:
      return KycStatusLongPollingReason.AUTH_TRANSFER;
    case MerchantAccountKycStatus.KYC_REQUIRED:
      return KycStatusLongPollingReason.TO_BE_OK;
    case MerchantAccountKycStatus.AWAITING_AML_REVIEW:
      return KycStatusLongPollingReason.AML_INVESTIGATION;
    default: {
      assertUnreachable(acc.status);
    }
  }
}

export function revalidateManagedInstanceDetails() {
  return mutate(
    (key) => Array.isArray(key) && key[key.length - 1] === "getInstanceDetails",
    undefined,
    { revalidate: true },
  );
}
export function useManagedInstanceDetails(instanceId: string) {
  const { state, lib } = useSessionContext();

  async function fetcher([token, instanceId]: [AccessToken, string]) {
    return await lib.instance.getInstanceDetails(token, instanceId);
  }

  const { data, error } = useSWR<
    TalerMerchantManagementResultByMethod<"getInstanceDetails">,
    TalerHttpError
  >([state.token, instanceId, "getInstanceDetails"], fetcher);

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

export function revalidateBackendInstances() {
  return mutate(
    (key) => Array.isArray(key) && key[key.length - 1] === "listInstances",
    undefined,
    { revalidate: true },
  );
}
export function useBackendInstances() {
  const { state, lib } = useSessionContext();

  async function fetcher([token]: [AccessToken]) {
    return await lib.instance.listInstances(token);
  }

  const { data, error } = useSWR<
    TalerMerchantManagementResultByMethod<"listInstances">,
    TalerHttpError
  >([state.token, "listInstances"], fetcher);

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