/*
 This file is part of GNU Taler
 (C) 2025 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 { opUnknownFailure } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";

/**
 * convert the async function into preact hook
 *
 * @param callback the async function
 *
 * @returns operation status
 */
export function useAsync<Res>(
  callback: (() => Promise<Res>) | undefined,
  deps: Array<any> = [],
) {
  const [data, setData] = useState<Res>();
  const [error, setError] = useState<unknown>();

  useEffect(() => {
    let unloaded = false;
    if (callback) {
      callback()
        .then((resp) => {
          if (unloaded) return;
          setData(resp);
        })
        .catch((error: unknown) => {
          if (unloaded) return;
          setError(error);
        });
    }
    return () => {
      unloaded = true;
    };
  }, deps);

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

/**
 * First start with `first` value
 * Check if the it should do long-polling with `shouldRetryFn`
 * Call `retryFn` if is required, long poll is expected for this function
 *
 * If `retryFn` returns faster than `minTime` (by error or because server
 * returned a response faster) then wait until `minTime`
 *
 * @param fetcher fetcher should be a memoized function
 * @param retry
 * @param deps
 * @returns
 */
export function useLongPolling<Res, Rt>(
  first: Res,
  shouldRetryFn: (res: Res, count: number) => Rt | undefined,
  retryFn: (last: Rt) => Promise<Res>,
  deps: Array<any> = [],
  opts: { minTime?: number } = {},
) {
  const mt = opts?.minTime ?? 1000; // do not try faster than this

  const [retry, setRetry] = useState<{
    count: number;
    fn: (() => Promise<Res>) | undefined;
    startMs: number;
  }>({
    count: 0,
    fn: undefined,
    startMs: new Date().getTime(),
  });

  const result = useAsync(retry.fn, [retry.count, ...deps]);

  const body = result ?? first;

  useEffect(() => {
    if (!body) return;
    const _body = body;

    function doChceck() {
      const rt = shouldRetryFn(_body, retry.count);
      if (!rt) return;
      // call again
      setRetry((lt) => ({
        count: lt.count + 1,
        fn: () => retryFn(rt),
        startMs: new Date().getTime(),
      }));
    }

    const diff = new Date().getTime() - retry.startMs;
    if (diff < mt) {
      // calling too fast, wait
      delayMs(mt - diff).then(doChceck);
    } else {
      doChceck();
    }
  }, [body]);

  return body;
}

/**
 * this should be in taler-utils
 * @param ms
 * @returns
 */
export async function delayMs(ms: number): Promise<void> {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(), ms);
  });
}

/**
 * If the function `fn` is called faster than `minTime` wait
 * before calling again.
 *
 * Helps to prevent burst calling when the server does not implement
 * long polling correctly.
 *
 * @returns a wrapper of `fn` function
 */
export function preventBurst<type, args>(
  fn: (...args: args[]) => Promise<type>,
  opts: {
    minTimeMs?: number;
    errorTimeMs?: number;
  } = {},
): typeof fn {
  const mt = opts.minTimeMs ?? 3000;
  const et = opts.errorTimeMs ?? mt;

  let nextCallShouldWait = 0;
  console.log("nooo");
  return async (...args): Promise<type> => {
    const start = new Date().getTime();
    console.log("que onda", nextCallShouldWait);
    if (nextCallShouldWait > 0) {
      console.log("waiting", nextCallShouldWait);
      await delayMs(nextCallShouldWait);
      nextCallShouldWait = 0;
    }
    const r = fn(...args)
      .then((result) => {
        const diff = new Date().getTime() - start;
        nextCallShouldWait = Math.max(0, mt - diff);
        console.log(
          "next call",
          nextCallShouldWait,
          diff,
          mt,
          Math.max(0, mt - diff),
        );
        return result;
      })
      .catch((error) => {
        const diff = new Date().getTime() - start;
        console.log(
          "error call",
          nextCallShouldWait,
          diff,
          et,
          Math.max(0, et - diff),
        );
        nextCallShouldWait = Math.max(0, et - diff);
        throw error;
      });
    return r;
  };
}
