/*
 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 {
  AbsoluteTime,
  EmptyObject,
  HttpStatusCode,
  TalerError,
  ChallengerApi,
  assertUnreachable,
} from "@gnu-taler/taler-util";
import {
  Attention,
  Button,
  LocalNotificationBanner,
  RouteDefinition,
  ShowInputErrorLabel,
  Time,
  useChallengerApiContext,
  useLocalNotificationHandler,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import {
  revalidateChallengeSession,
  useChallengeSession,
} from "../hooks/challenge.js";
import { SessionId, useSessionState } from "../hooks/session.js";
import { TalerFormAttributes } from "@gnu-taler/taler-util";
import { useMemo } from "preact/compat";

type Props = {
  focus?: boolean;
  session: SessionId;
  onComplete: () => void;
  routeAsk: RouteDefinition<EmptyObject>;
};

function useReloadOnDeadline(deadline: AbsoluteTime): void {
  const [, set] = useState(false);
  function toggle(): void {
    set((s) => !s);
  }
  useEffect(() => {
    if (AbsoluteTime.isExpired(deadline)) {
      return;
    }
    const diff = AbsoluteTime.difference(AbsoluteTime.now(), deadline);
    if (diff.d_ms === "forever") return;
    const timer = setTimeout(toggle, diff.d_ms);
    return () => {
      clearTimeout(timer);
    };
  }, [deadline]);
}

export function getAddressDescriptionFromAddrType(
  type: ChallengerApi.ChallengerTermsOfServiceResponse["address_type"],
  addr: Record<string, string>,
): string {
  switch (type) {
    case "email": {
      return addr[TalerFormAttributes.CONTACT_EMAIL];
    }
    case "phone": {
      return addr[TalerFormAttributes.CONTACT_PHONE];
    }
    case "postal": {
      return addr[TalerFormAttributes.CONTACT_NAME];
    }
    case "postal-ch": {
      return addr[TalerFormAttributes.CONTACT_NAME];
    }
  }
}

export function AnswerChallenge({
  session,
  focus,
  onComplete,
  routeAsk,
}: Props): VNode {
  const { config, lib } = useChallengerApiContext();
  const { i18n } = useTranslationContext();
  const { sent, failed, completed } = useSessionState();
  const [notification, withErrorHandler] = useLocalNotificationHandler();
  const [pin, setPin] = useState<string | undefined>();
  const errors = undefinedIfEmpty({
    pin: !pin ? i18n.str`Can't be empty` : undefined,
  });

  const result = useChallengeSession(session);

  const lastStatus =
    result && !(result instanceof TalerError) && result.type !== "fail"
      ? result.body
      : undefined;

  const deadlineTS =
    lastStatus == undefined ? undefined : lastStatus.retransmission_time;

  const deadline = useMemo(() => {
    return !deadlineTS
      ? AbsoluteTime.never()
      : AbsoluteTime.fromProtocolTimestamp(deadlineTS);
  }, [deadlineTS?.t_s]);
  
  useReloadOnDeadline(deadline);

  const lastAddr = !lastStatus?.last_address
    ? undefined
    : getAddressDescriptionFromAddrType(
        config.address_type,
        lastStatus.last_address,
      );

  const unableToChangeAddr = !lastStatus || lastStatus.changes_left < 1;
  const contact = lastStatus?.last_address;

  const onSendAgain =
    contact === undefined ||
    lastStatus == undefined ||
    lastStatus.pin_transmissions_left === 0 ||
    !AbsoluteTime.isExpired(deadline)
      ? undefined
      : withErrorHandler(
          async () => {
            return await lib.challenger.challenge(session.nonce, contact);
          },
          (ok) => {
            if (ok.body.type === "completed") {
              completed(ok.body);
            } else {
              sent(ok.body);
            }
          },
          (fail) => {
            switch (fail.case) {
              case HttpStatusCode.BadRequest:
                return i18n.str`The request was not accepted, try reloading the app.`;
              case HttpStatusCode.NotFound:
                return i18n.str`Challenge not found.`;
              case HttpStatusCode.NotAcceptable:
                return i18n.str`Server templates are missing due to misconfiguration.`;
              case HttpStatusCode.TooManyRequests:
                return i18n.str`There have been too many attempts to request challenge transmissions.`;
              case HttpStatusCode.InternalServerError:
                return i18n.str`Server is unable to respond due to internal problems.`;
            }
          },
        );

  const onCheck =
    errors !== undefined ||
    lastStatus == undefined ||
    lastStatus.auth_attempts_left === 0
      ? undefined
      : withErrorHandler(
          async () => {
            return lib.challenger.solve(session.nonce, { pin: pin! });
          },
          (ok) => {
            if (ok.body.type === "completed") {
              completed(ok.body);
            } else {
              failed(ok.body);
            }
            onComplete();
          },
          (fail) => {
            switch (fail.case) {
              case HttpStatusCode.BadRequest:
                return i18n.str`The request was not accepted, try reloading the app.`;
              case HttpStatusCode.Forbidden: {
                revalidateChallengeSession();
                return i18n.str`Invalid pin.`;
              }
              case HttpStatusCode.NotFound:
                return i18n.str`Challenge not found.`;
              case HttpStatusCode.NotAcceptable:
                return i18n.str`Server templates are missing due to misconfiguration.`;
              case HttpStatusCode.TooManyRequests: {
                revalidateChallengeSession();
                return i18n.str`There have been too many attempts to request challenge transmissions.`;
              }
              case HttpStatusCode.InternalServerError:
                return i18n.str`Server is unable to respond due to internal problems.`;
              default:
                assertUnreachable(fail);
            }
          },
        );
  const cantTryAnymore = lastStatus?.auth_attempts_left === 0;

  function LastContactSent(): VNode {
    return (
      <p class="mt-2 text-lg leading-8 text-gray-600">
        {!lastStatus ||
        AbsoluteTime.isExpired(deadline) ||
        AbsoluteTime.isNever(deadline) ? (
          <i18n.Translate>
            Last TAN code was sent to &quot;{lastAddr}
            &quot; is not valid anymore.
          </i18n.Translate>
        ) : (
          <Attention title={i18n.str`A TAN code was sent to "${lastAddr}"`}>
            <i18n.Translate>
              You should wait until &quot;
              <Time format="dd/MM/yyyy HH:mm:ss" timestamp={deadline} />
              &quot; to send a new one.
            </i18n.Translate>
          </Attention>
        )}
      </p>
    );
  }

  function TryAnotherCode(): VNode {
    return (
      <div class="mx-auto mt-4 max-w-xl flex justify-between">
        <div>
          <a
            data-disabled={unableToChangeAddr}
            href={unableToChangeAddr ? undefined : routeAsk.url({})}
            class="relative data-[disabled=true]:bg-gray-300 data-[disabled=true]:text-white data-[disabled=true]:cursor-default inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
          >
            <i18n.Translate>Try with another address</i18n.Translate>
          </a>
          {lastStatus === undefined ? undefined : (
            <p class="mt-2 text-sm leading-6 text-gray-400">
              {lastStatus.changes_left < 1 ? (
                <i18n.Translate>
                  You can&#39;t change the contact address anymore.
                </i18n.Translate>
              ) : lastStatus.changes_left === 1 ? (
                <i18n.Translate>
                  You can change the contact address one last time.
                </i18n.Translate>
              ) : (
                <i18n.Translate>
                  You can change the contact address {lastStatus.changes_left}{" "}
                  more times.
                </i18n.Translate>
              )}
            </p>
          )}
        </div>
        <div>
          <Button
            type="submit"
            disabled={!onSendAgain}
            class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
            handler={onSendAgain}
          >
            <i18n.Translate>Send new code</i18n.Translate>
          </Button>
          {lastStatus === undefined ? undefined : (
            <p class="mt-2 text-sm leading-6 text-gray-400">
              {lastStatus.pin_transmissions_left < 1 ? (
                <i18n.Translate>
                  We can&#39;t send you the code anymore.
                </i18n.Translate>
              ) : lastStatus.pin_transmissions_left === 1 ? (
                <i18n.Translate>
                  We can send the code one last time.
                </i18n.Translate>
              ) : (
                <i18n.Translate>
                  We can send the code {lastStatus.pin_transmissions_left} more
                  times.
                </i18n.Translate>
              )}
            </p>
          )}
        </div>
      </div>
    );
  }

  if (cantTryAnymore) {
    return (
      <Fragment>
        <LocalNotificationBanner notification={notification} />
        <div class="isolate bg-white px-6 py-12">
          <div class="mx-auto max-w-2xl text-center">
            <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
              <i18n.Translate>Last TAN code can not be used.</i18n.Translate>
            </h2>

            <LastContactSent />
          </div>

          <TryAnotherCode />
        </div>
      </Fragment>
    );
  }

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

      <div class="isolate bg-white px-6 py-12">
        <div class="mx-auto max-w-2xl text-center">
          <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
            <i18n.Translate>
              Enter the TAN you received to authenticate.
            </i18n.Translate>
          </h2>
          <LastContactSent />

          {lastStatus === undefined ? undefined : (
            <p class="mt-2 text-lg leading-8 text-gray-600">
              {lastStatus.auth_attempts_left < 1 ? (
                <i18n.Translate>
                  You can&#39;t check the PIN anymore.
                </i18n.Translate>
              ) : lastStatus.auth_attempts_left === 1 ? (
                <i18n.Translate>
                  You can check the PIN one last time.
                </i18n.Translate>
              ) : (
                <i18n.Translate>
                  You can check the PIN {lastStatus.auth_attempts_left} more
                  times.
                </i18n.Translate>
              )}
            </p>
          )}
        </div>

        <form
          method="POST"
          class="mx-auto mt-4 max-w-xl"
          onSubmit={(e) => {
            e.preventDefault();
          }}
        >
          <div class="grid grid-cols-1 gap-x-8 gap-y-6">
            <div class="sm:col-span-2">
              <label
                for="pin"
                class="block text-sm font-semibold leading-6 text-gray-900"
              >
                <i18n.Translate>TAN code</i18n.Translate>
              </label>
              <div class="mt-2.5">
                <input
                  autoFocus
                  ref={focus ? doAutoFocus : undefined}
                  type="number"
                  name="pin"
                  id="pin"
                  maxLength={64}
                  value={pin}
                  onChange={(e) => {
                    setPin(e.currentTarget.value);
                  }}
                  placeholder="12345678"
                  class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
                <ShowInputErrorLabel
                  message={errors?.pin}
                  isDirty={pin !== undefined}
                />
              </div>
            </div>
          </div>

          <div class="mt-10">
            <Button
              type="submit"
              class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
              disabled={!onCheck}
              handler={onCheck}
            >
              <i18n.Translate>Check</i18n.Translate>
            </Button>
          </div>
        </form>

        <TryAnotherCode />
      </div>
    </Fragment>
  );
}

/**
 * Show the element when the load ended
 * @param element
 */
export function doAutoFocus(element: HTMLElement | null): void {
  if (element) {
    setTimeout(() => {
      element.focus({ preventScroll: true });
      element.scrollIntoView({
        behavior: "smooth",
        block: "center",
        inline: "center",
      });
    }, 100);
  }
}

export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
  return Object.keys(obj).some(
    (k) => (obj as Record<string, T>)[k] !== undefined,
  )
    ? obj
    : undefined;
}
