import {
  AbsoluteTime,
  assertUnreachable,
  Challenge,
  ChallengeRequestResponse,
  ChallengeResponse,
  HttpStatusCode,
  TalerErrorCode,
  TanChannel,
} from "@gnu-taler/taler-util";
import {
  ButtonBetterBulma,
  LocalNotificationBannerBulma,
  SafeHandlerTemplate,
  undefinedIfEmpty,
  useCommonPreferences,
  useLocalNotificationBetter,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { useSessionContext } from "../context/session.js";
import {
  datetimeFormatForPreferences,
  usePreference,
} from "../hooks/preference.js";
import { FormErrors, FormProvider } from "./form/FormProvider.js";
import { InputCode } from "./form/InputCode.js";

const TALER_SCREEN_ID = 5;

export interface Props {
  onCompleted: SafeHandlerTemplate<[challenges: string[]], any>;
  onCancel(): void;
  currentChallenge: ChallengeResponse;
  focus?: boolean;
  initial?: { request: Challenge; response: ChallengeRequestResponse };
  showFull?: Partial<Record<TanChannel, string>>;
}

interface Form {
  code: string;
}
interface Tries {
  numTries: number;
  solvable: boolean;
}

function RetransmissionCodeLimitExpiration({
  expiration,
}: {
  expiration: AbsoluteTime;
}): VNode {
  const { i18n } = useTranslationContext();
  const remain = AbsoluteTime.remaining(expiration);
  const [id, rerender] = useState(0);
  const [preferences] = usePreference();

  const timeToHalfMinute = remain.d_ms === "forever" ? -1 : remain.d_ms - 30000;
  const timeToMinute = remain.d_ms === "forever" ? -1 : remain.d_ms - 60000;
  const timeToFiveMinutes =
    remain.d_ms === "forever" ? -1 : remain.d_ms - 60000 * 5;
  const reRenderInMs =
    timeToFiveMinutes > 0
      ? timeToFiveMinutes
      : timeToMinute > 0
        ? timeToMinute
        : timeToHalfMinute > 0
          ? timeToHalfMinute
          : 1000;
  useEffect(() => {
    setTimeout(() => {
      rerender(Date.now());
    }, reRenderInMs);
  }, [id]);
  if (
    remain.d_ms === "forever" ||
    expiration.t_ms === "never" ||
    remain.d_ms < 1001
  )
    return <Fragment />;
  if (remain.d_ms < 30000) {
    return (
      <p class="help">
        <i18n.Translate>
          Resend code possible in {Math.floor(remain.d_ms / 1000)} seconds.
        </i18n.Translate>
      </p>
    );
  }
  if (remain.d_ms < 60000) {
    return (
      <p class="help">
        <i18n.Translate>
          Resend code possible in less than one minute.
        </i18n.Translate>
      </p>
    );
  }
  if (remain.d_ms < 1000 * 60 * 5) {
    return (
      <p class="help">
        <i18n.Translate>
          Resend code possible in less than 5 minutes.
        </i18n.Translate>
      </p>
    );
  }
  return (
    <p class="help">
      <i18n.Translate>
        Resend will be possible after{" "}
        <span>
          {format(expiration.t_ms, datetimeFormatForPreferences(preferences))}
        </span>
      </i18n.Translate>
    </p>
  );
}

function SolveChallenge({
  challenge,
  expiration,
  retransmission,
  onCancel,
  onSolved,
  focus,
  showFull,
}: {
  onCancel: () => void;
  challenge: Challenge;
  expiration: AbsoluteTime;
  retransmission: AbsoluteTime;
  onSolved: () => void;
  focus?: boolean;
  showFull: Partial<Record<TanChannel, string>>;
}): VNode {
  const { i18n } = useTranslationContext();
  const { lib } = useSessionContext();

  const [value, setValue] = useState<Partial<Form>>({});
  const [tries, setTries] = useState<Tries>({
    numTries: 0,
    solvable: true,
  });
  const [{ showDebugInfo }] = useCommonPreferences();

  const [currentExpiration, setCurrentExpiration] = useState(expiration);
  const [showExpired, setExpired] = useState(
    AbsoluteTime.isExpired(currentExpiration),
  );
  const remainExpiration = AbsoluteTime.remaining(currentExpiration).d_ms;
  useEffect(() => {
    if (showExpired) return;
    if (remainExpiration === "forever") return;
    const handler = setTimeout(() => {
      setExpired(true);
    }, remainExpiration);
    return () => {
      clearTimeout(handler);
    };
  }, []);

  const [currentRetransmission, setCurrentRetransmission] =
    useState(retransmission);
  const [showResend, setResend] = useState(
    AbsoluteTime.isExpired(currentRetransmission),
  );
  const remainResend = AbsoluteTime.remaining(currentRetransmission).d_ms;
  useEffect(() => {
    if (showResend) return;
    if (remainResend === "forever") return;
    const handler = setTimeout(() => {
      setResend(true);
    }, remainResend);
    return () => {
      clearTimeout(handler);
    };
  }, []);

  const errors = undefinedIfEmpty<FormErrors<Form>>({
    code: showExpired
      ? i18n.str`Expired`
      : !value.code
        ? i18n.str`Required`
        : undefined,
  });
  function valueHandler(s: (d: Partial<Form>) => Partial<Form>): void {
    const next = s(value);
    const v: Form = {
      code: next.code ?? "",
    };
    setValue(v);
  }
  const tan = !value.code || !!errors ? undefined : value.code;
  const [notification, safeFunctionHandler] = useLocalNotificationBetter();

  const verify = safeFunctionHandler(
    i18n.str`verify code`,
    lib.instance.confirmChallenge.bind(lib.instance),
    !tan ? undefined : [challenge.challenge_id, { tan }],
  );
  verify.onSuccess = onSolved;
  verify.onFail = (fail) => {
    setValue({});
    setTries((t) => ({
      numTries: t.numTries + 1,
      solvable: fail.case !== TalerErrorCode.MERCHANT_TAN_TOO_MANY_ATTEMPTS,
    }));
    switch (fail.case) {
      case HttpStatusCode.Unauthorized:
        return i18n.str`Unauthorized`;
      case TalerErrorCode.MERCHANT_TAN_CHALLENGE_FAILED:
        return i18n.str`The code is not correct.`;
      case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN:
        return i18n.str`The challenge is no longer valid.`;
      case TalerErrorCode.MERCHANT_TAN_TOO_MANY_ATTEMPTS:
        return i18n.str`Too many attempts trying to solve the challenge.`;
      default:
        assertUnreachable(fail);
    }
  };

  const resend = safeFunctionHandler(
    i18n.str`send challenge`,
    () => lib.instance.sendChallenge(challenge.challenge_id),
    !showResend ? undefined : [],
  );

  resend.onSuccess = (success) => {
    setCurrentRetransmission(
      !success.earliest_retransmission
        ? AbsoluteTime.never()
        : AbsoluteTime.fromProtocolTimestamp(success.earliest_retransmission),
    );
    setCurrentExpiration(
      !success.solve_expiration
        ? AbsoluteTime.never()
        : AbsoluteTime.fromProtocolTimestamp(success.solve_expiration),
    );
  };

  resend.onFail = (fail) => {
    switch (fail.case) {
      case HttpStatusCode.Unauthorized:
        return i18n.str`Failed to send the verification code.`;
      case HttpStatusCode.Forbidden:
        return i18n.str`The request was valid, but the server is refusing action.`;
      case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN:
        return i18n.str`The backend is not aware of the specified MFA challenge.`;
      case TalerErrorCode.MERCHANT_TAN_MFA_HELPER_EXEC_FAILED:
        return i18n.str`Code transmission failed.`;
      case TalerErrorCode.MERCHANT_TAN_CHALLENGE_SOLVED:
        return i18n.str`Already solved.`;
      case TalerErrorCode.MERCHANT_TAN_TOO_EARLY:
        return i18n.str`It is too early to request another transmission of the challenge.`;
      default:
        assertUnreachable(fail);
    }
  };

  useEffect(() => {
    if (!tan || tries.numTries > 0) {
      return;
    }
    verify.call();
  }, [tan]);

  /**
   * We used this computed key so after the code has been tried
   * the <InputCode /> is rendered from scratch and the
   * uncontrolled input state wipe out
   */
  const codeKey = "key" + tries.numTries;

  return (
    <Fragment>
      <LocalNotificationBannerBulma notification={notification} />

      {showDebugInfo ? (
        <pre>
          {JSON.stringify(
            {
              retransmission:
                currentRetransmission.t_ms === "never"
                  ? "never"
                  : new Date(currentRetransmission.t_ms).toISOString(),
              expiration:
                currentExpiration.t_ms === "never"
                  ? "never"
                  : new Date(currentExpiration.t_ms).toISOString(),
            },
            undefined,
            2,
          )}
        </pre>
      ) : undefined}

      <div class="columns is-centered" style={{ margin: "auto" }}>
        <div class="column is-two-thirds ">
          <div class="modal is-active" style={{ position: "initial" }}>
            <div class="modal-card" style={{ width: "100%", margin: 0 }}>
              <header
                class="modal-card-head"
                style={{ border: "1px solid", borderBottom: 0 }}
              >
                <p class="modal-card-title">
                  <i18n.Translate>Validation code sent.</i18n.Translate>
                </p>
              </header>
              <FormProvider<Form>
                name="settings"
                errors={errors}
                object={value}
                valueHandler={valueHandler}
              >
                <section
                  class="modal-card-body"
                  style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
                >
                  <p>
                    {(function (): VNode {
                      switch (challenge.tan_channel) {
                        case TanChannel.SMS:
                          if (showFull[TanChannel.SMS]) {
                            return (
                              <i18n.Translate>
                                The verification code sent to the phone "
                                <b>{showFull[TanChannel.SMS]}</b>"
                              </i18n.Translate>
                            );
                          }
                          return (
                            <i18n.Translate>
                              The verification code sent to the phone number
                              ending with "<b>{challenge.tan_info}</b>"
                            </i18n.Translate>
                          );
                        case TanChannel.EMAIL:
                          if (showFull[TanChannel.EMAIL]) {
                            return (
                              <i18n.Translate>
                                The verification code sent to the email address
                                "<b>{showFull[TanChannel.EMAIL]}</b>"
                              </i18n.Translate>
                            );
                          }
                          return (
                            <i18n.Translate>
                              The verification code sent to the email address
                              starting with "<b>{challenge.tan_info}</b>"
                            </i18n.Translate>
                          );
                      }
                    })()}
                  </p>

                  <InputCode<Form>
                    name="code"
                    key={codeKey}
                    label={i18n.str`Verification code`}
                    dashesIndex={[3]}
                    size={8}
                    focus={focus}
                    readonly={!tries.solvable || showExpired}
                    filter={(c) => {
                      const v = Number.parseInt(c, 10);
                      if (Number.isNaN(v) || v > 9 || v < 0) return undefined;
                      return String(v);
                    }}
                  />

                  {showExpired ? (
                    <div style={{ display: "flex" }}>
                      <p
                        class="has-text-danger"
                        style={{ alignContent: "center", marginRight: 8 }}
                      >
                        <i18n.Translate>Code expired.</i18n.Translate>
                      </p>
                    </div>
                  ) : undefined}
                  <div>
                    <div style={{ display: "flex" }}>
                      <p style={{ alignContent: "center", marginRight: 8 }}>
                        <i18n.Translate>
                          Didn't received the code?
                        </i18n.Translate>
                      </p>
                      <ButtonBetterBulma
                        class="button"
                        type="button"
                        onClick={resend}
                      >
                        <i18n.Translate>Resend</i18n.Translate>
                      </ButtonBetterBulma>
                    </div>
                  </div>
                  <RetransmissionCodeLimitExpiration
                    expiration={currentRetransmission}
                  />
                </section>
                <footer
                  class="modal-card-foot "
                  style={{
                    justifyContent: "space-between",
                    border: "1px solid",
                    borderTop: 0,
                  }}
                >
                  <button class="button" type="button" onClick={onCancel}>
                    <i18n.Translate>Cancel</i18n.Translate>
                  </button>
                  <ButtonBetterBulma
                    type="submit"
                    disabled={!tries.numTries}
                    onClick={verify}
                  >
                    <i18n.Translate>Verify</i18n.Translate>
                  </ButtonBetterBulma>
                </footer>
              </FormProvider>
            </div>
          </div>
        </div>
      </div>
    </Fragment>
  );
}

export function SolveMFAChallenges({
  currentChallenge,
  onCompleted,
  onCancel,
  initial,
  focus,
  showFull,
}: Props): VNode {
  const { i18n } = useTranslationContext();
  const { lib } = useSessionContext();

  const [solved, setSolved] = useState<string[]>([]);
  // FIXME: we should save here also the expiration of the
  // tan channel to be used when the user press "i have the code"

  type Selected = {
    ch: Challenge;
    expiration: AbsoluteTime;
    retransmission: AbsoluteTime;
  };
  const initialSelected: Selected | undefined = initial
    ? ({
        ch: initial.request,
        expiration: !initial.response.solve_expiration
          ? AbsoluteTime.never()
          : AbsoluteTime.fromProtocolTimestamp(
              initial.response.solve_expiration,
            ),
        retransmission: !initial.response.earliest_retransmission
          ? AbsoluteTime.never()
          : AbsoluteTime.fromProtocolTimestamp(
              initial.response.earliest_retransmission,
            ),
      } as Selected)
    : undefined;

  // const [retransmission, setRetransmission] = useState(initialRetrans);

  const [selected, setSelected] = useState<Selected | undefined>(
    initialSelected,
  );

  const [notification, safeFunctionHandler] = useLocalNotificationBetter();

  const sendMessage = safeFunctionHandler(
    i18n.str`send challenge`,
    (ch: Challenge) => lib.instance.sendChallenge(ch.challenge_id),
  );

  sendMessage.onSuccess = (success, ch) => {
    setSelected({
      ch,
      retransmission: !success.earliest_retransmission
        ? AbsoluteTime.never()
        : AbsoluteTime.fromProtocolTimestamp(success.earliest_retransmission),
      expiration: !success.solve_expiration
        ? AbsoluteTime.never()
        : AbsoluteTime.fromProtocolTimestamp(success.solve_expiration),
    });
  };

  sendMessage.onFail = (fail) => {
    switch (fail.case) {
      case HttpStatusCode.Unauthorized:
        return i18n.str`Failed to send the verification code.`;
      case HttpStatusCode.Forbidden:
        return i18n.str`The request was valid, but the server is refusing action.`;
      case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN:
        return i18n.str`The backend is not aware of the specified MFA challenge.`;
      case TalerErrorCode.MERCHANT_TAN_MFA_HELPER_EXEC_FAILED:
        return i18n.str`Code transmission failed.`;
      case TalerErrorCode.MERCHANT_TAN_CHALLENGE_SOLVED:
        return i18n.str`Already solved.`;
      case TalerErrorCode.MERCHANT_TAN_TOO_EARLY:
        return i18n.str`It is too early to request another transmission of the challenge.`;
      default:
        assertUnreachable(fail);
    }
  };

  if (selected) {
    return (
      <SolveChallenge
        onCancel={onCancel}
        challenge={selected.ch}
        expiration={selected.expiration}
        retransmission={selected.retransmission}
        focus={focus}
        showFull={showFull ?? {}}
        onSolved={async () => {
          setSelected(undefined);
          const total = [...solved, selected.ch.challenge_id];
          const enough = currentChallenge.combi_and
            ? total.length === currentChallenge.challenges.length
            : total.length > 0;
          if (enough) {
            onCompleted.withArgs(total).call();
          } else {
            setSolved(total);
            const nextPending = currentChallenge.challenges.find((c) => {
              const pending = total.indexOf(c.challenge_id) === -1;
              return pending;
            });
            if (nextPending) {
              sendMessage.withArgs(nextPending).call();
            }
          }
        }}
      />
    );
  }

  const hasSolvedEnough = currentChallenge.combi_and
    ? solved.length === currentChallenge.challenges.length
    : solved.length > 0;

  return (
    <Fragment>
      <LocalNotificationBannerBulma notification={notification} />

      <div class="columns is-centered" style={{ margin: "auto" }}>
        <div class="column is-two-thirds ">
          <div class="modal is-active" style={{ position: "initial" }}>
            <div class="modal-card" style={{ width: "100%", margin: 0 }}>
              <header
                class="modal-card-head"
                style={{ border: "1px solid", borderBottom: 0 }}
              >
                <p class="modal-card-title">
                  <i18n.Translate>
                    Multi-factor authentication required.
                  </i18n.Translate>
                </p>
              </header>
              <section
                class="modal-card-body"
                style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
              >
                {currentChallenge.combi_and ? (
                  <i18n.Translate>
                    You must complete all of these requirements.
                  </i18n.Translate>
                ) : (
                  <i18n.Translate>
                    You need to complete at least one of this requirements.
                  </i18n.Translate>
                )}
              </section>
              {currentChallenge.challenges.map((challenge, idx) => {
                const noNeedToComplete =
                  hasSolvedEnough ||
                  solved.indexOf(challenge.challenge_id) !== -1;

                const doSend = noNeedToComplete
                  ? sendMessage
                  : sendMessage.withArgs(challenge);

                return (
                  <section
                    class="modal-card-body"
                    style={{
                      border: "1px solid",
                      borderTop: 0,
                      borderBottom: 0,
                    }}
                  >
                    {(function (ch: TanChannel): VNode {
                      switch (ch) {
                        case TanChannel.SMS:
                          return (
                            <i18n.Translate>
                              An SMS to the phone number ending with{" "}
                              <span>{challenge.tan_info}</span>
                            </i18n.Translate>
                          );
                        case TanChannel.EMAIL:
                          return (
                            <i18n.Translate>
                              An email to the address starting with{" "}
                              <span>{challenge.tan_info}</span>
                            </i18n.Translate>
                          );
                      }
                    })(challenge.tan_channel)}

                    <div
                      style={{
                        justifyContent: "space-between",
                        display: "flex",
                      }}
                    >
                      <div />
                      <ButtonBetterBulma
                        type="button"
                        onClick={doSend}
                        focus={idx === 0 && focus}
                      >
                        <i18n.Translate>Continue</i18n.Translate>
                      </ButtonBetterBulma>
                    </div>
                  </section>
                );
              })}
              <footer
                class="modal-card-foot "
                style={{
                  justifyContent: "space-between",
                  border: "1px solid",
                  borderTop: 0,
                }}
              >
                <button class="button" onClick={onCancel}>
                  <i18n.Translate>Cancel</i18n.Translate>
                </button>
                <div />
              </footer>
            </div>
          </div>
        </div>
      </div>
    </Fragment>
  );
}
