/*
 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/>
 */
/**
 *
 * @author Sebastian Javier Marchano (sebasjm)
 */

import { ChallengeResponse } from "@gnu-taler/taler-util";
import { useCallback, useRef, useState } from "preact/hooks";

/**
 * State of the current MFA operation and handler to manage
 * the state and retry.
 *
 */
export interface MfaState {
  /**
   * If a mfa has been started this will contain
   * the challenge response.
   */
  pendingChallenge: ChallengeResponse | undefined;

  /**
   * Cancel the current pending challenge.
   *
   * @returns
   */
  doCancelChallenge: () => void;

  /**
   * Similar to withMfaHandler.
   * Take a function that expects an MfaHanlder on the first argument.
   * Returns the original function for the first call, another to repeat with the new credentials
   * and the list of pending challenges.
   * @param d
   * @returns
   */
  addMfaHandler: <Type extends Array<any>, R>(
    fn: (mfa: MfaHandler, ...args: Type) => Promise<R>,
  ) => [
    (...args: Type) => Promise<R>, // function for the first call
    (newChallenges: string[]) => Promise<R>, // function to repeat with new chIds
    Type,
  ];

  /**
   * Similar to addMfaHandler.
   * Takes a function that receive an MFA handler all execute business logic that
   * may require a MFA.
   * Returns the original function for the first call, another to repeat with the new credentials
   * and the list of pending challenges.
   * @param builder
   * @returns
   */
  withMfaHandler: <Type extends Array<any>, R>(
    builder: CallbackFactory<Type, R>,
  ) => [
    (...args: Type) => Promise<R>, // function for the first call
    (newChallenges: string[]) => Promise<R>, // function to repeat with new chIds
    Type,
  ];
}

/**
 * Handler to be used by the function performing the MFA
 * guarded operation
 */
export interface MfaHandler {
  /**
   * Callback handler to use when the operation fails with MFA required
   * @param challenge
   * @param params
   * @returns
   */
  onChallengeRequired: (challenge: ChallengeResponse, ...params: any[]) => void;
  /**
   * Challenges that are already solved and can be used for the operation.
   * If this is undefined it may mean that it is the first call.
   */
  challengeIds: string[] | undefined;
}

/**
 * asd
 */
type CallbackFactory<T extends any[], R> = (
  h: MfaHandler,
) => (...args: T) => Promise<R>;

/**
 * Take a function that may require MFA and return and MfaState
 * to solve the MFA challenges.
 *
 *
 * @param cf A function that receives MfaHandler with callback and solved challenges and returns a the function to be guarded.
 * @returns
 */
export function useChallengeHandler(): MfaState {
  const [current, onChallengeRequired] = useState<ChallengeResponse>();
  const ref = useRef<any[]>([]);
  let exeOrder = 0;

  /**
   * This have the same machanism that useEffect, needs to be called always on order
   * @param builder
   * @returns
   */
  function withMfaHandler<T extends any[], R>(
    builder: CallbackFactory<T, R>,
  ): [
    ReturnType<CallbackFactory<T, R>>,
    (newChallenges: string[]) => Promise<R>,
    T,
  ] {
    const thisIdx = exeOrder;
    exeOrder = exeOrder + 1;

    async function saveArgsAndProceed(...currentArgs: T): Promise<R> {
      ref.current[thisIdx] = currentArgs;
      return builder({
        challengeIds: undefined,
        onChallengeRequired,
      })(...currentArgs);
    }

    async function repeatCall(challengeIds: string[]): Promise<R> {
      if (!ref.current[thisIdx])
        throw Error("calling repeat function without doing the first call");

      return builder({
        challengeIds,
        onChallengeRequired,
      })(...ref.current[thisIdx]);
    }

    return [saveArgsAndProceed, repeatCall, ref.current[thisIdx]];
  }

  function addMfaHandler<T extends any[], R>(
    fn: (mfa: MfaHandler, ...args: T) => Promise<R>,
  ): [
    ReturnType<CallbackFactory<T, R>>,
    (newChallenges: string[]) => Promise<R>,
    T,
  ] {
    const thisIdx = exeOrder;
    exeOrder = exeOrder + 1;

    async function saveArgsAndProceed(...currentArgs: T): Promise<R> {
      ref.current[thisIdx] = currentArgs;
      const mfa = {
        challengeIds: undefined,
        onChallengeRequired,
      };
      return fn(mfa, ...currentArgs);
    }

    async function repeatCall(challengeIds: string[]): Promise<R> {
      if (!ref.current[thisIdx])
        throw Error("calling repeat function without doing the first call");
      const mfa = {
        challengeIds,
        onChallengeRequired,
      };
      return fn(mfa, ...ref.current[thisIdx]);
    }

    return [saveArgsAndProceed, repeatCall, ref.current[thisIdx]];
  }

  function reset() {
    onChallengeRequired(undefined);
  }

  return {
    withMfaHandler,
    addMfaHandler,
    doCancelChallenge: reset,
    pendingChallenge: current,
  };
}
