/*
 This file is part of GNU Taler
 (C) 2021-2023 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 {
  useTranslationContext,
  HttpError,
  ErrorType,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, FunctionComponent, h, VNode } from "preact";
import { Route, route, Router } from "preact-router";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { Loading } from "./components/exception/loading.js";
import { Menu, NotificationCard } from "./components/menu/index.js";
import { useBackendContext } from "./context/backend.js";
import { InstanceContextProvider } from "./context/instance.js";
import {
  useBackendDefaultToken,
  useBackendInstanceToken,
  useLocalStorage,
} from "./hooks/index.js";
import { useInstanceKYCDetails } from "./hooks/instance.js";
import InstanceCreatePage from "./paths/admin/create/index.js";
import InstanceListPage from "./paths/admin/list/index.js";
import ListKYCPage from "./paths/instance/kyc/list/index.js";
import OrderCreatePage from "./paths/instance/orders/create/index.js";
import OrderDetailsPage from "./paths/instance/orders/details/index.js";
import OrderListPage from "./paths/instance/orders/list/index.js";
import ProductCreatePage from "./paths/instance/products/create/index.js";
import ProductListPage from "./paths/instance/products/list/index.js";
import ProductUpdatePage from "./paths/instance/products/update/index.js";
import ReservesCreatePage from "./paths/instance/reserves/create/index.js";
import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
import ReservesListPage from "./paths/instance/reserves/list/index.js";
import TemplateCreatePage from "./paths/instance/templates/create/index.js";
import TemplateUsePage from "./paths/instance/templates/use/index.js";
import TemplateQrPage from "./paths/instance/templates/qr/index.js";
import TemplateListPage from "./paths/instance/templates/list/index.js";
import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
import WebhookListPage from "./paths/instance/webhooks/list/index.js";
import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
import TransferListPage from "./paths/instance/transfers/list/index.js";
import InstanceUpdatePage, {
  AdminUpdate as InstanceAdminUpdatePage,
  Props as InstanceUpdatePageProps,
} from "./paths/instance/update/index.js";
import LoginPage from "./paths/login/index.js";
import NotFoundPage from "./paths/notfound/index.js";
import { Notification } from "./utils/types.js";
import { MerchantBackend } from "./declaration.js";

export enum InstancePaths {
  // details = '/',
  error = "/error",
  update = "/update",

  product_list = "/products",
  product_update = "/product/:pid/update",
  product_new = "/product/new",

  order_list = "/orders",
  order_new = "/order/new",
  order_details = "/order/:oid/details",

  reserves_list = "/reserves",
  reserves_details = "/reserves/:rid/details",
  reserves_new = "/reserves/new",

  kyc = "/kyc",

  transfers_list = "/transfers",
  transfers_new = "/transfer/new",

  templates_list = "/templates",
  templates_update = "/templates/:tid/update",
  templates_new = "/templates/new",
  templates_use = "/templates/:tid/use",
  templates_qr = "/templates/:tid/qr",

  webhooks_list = "/webhooks",
  webhooks_update = "/webhooks/:tid/update",
  webhooks_new = "/webhooks/new",
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};

export enum AdminPaths {
  list_instances = "/instances",
  new_instance = "/instance/new",
  update_instance = "/instance/:id/update",
}

export interface Props {
  id: string;
  admin?: boolean;
  path: string;
  setInstanceName: (s: string) => void;
}

export function InstanceRoutes({
  id,
  admin,
  path,
  setInstanceName,
}: Props): VNode {
  const [_, updateDefaultToken] = useBackendDefaultToken();
  const [token, updateToken] = useBackendInstanceToken(id);
  const {
    updateLoginStatus: changeBackend,
    addTokenCleaner,
    clearAllTokens,
  } = useBackendContext();
  const cleaner = useCallback(() => {
    updateToken(undefined);
  }, [id]);
  const { i18n } = useTranslationContext();

  type GlobalNotifState = (Notification & { to: string }) | undefined;
  const [globalNotification, setGlobalNotification] =
    useState<GlobalNotifState>(undefined);

  useEffect(() => {
    addTokenCleaner(cleaner);
  }, [addTokenCleaner, cleaner]);

  const changeToken = (token?: string) => {
    if (admin) {
      updateToken(token);
    } else {
      updateDefaultToken(token);
    }
  };
  const updateLoginStatus = (url: string, token?: string) => {
    changeBackend(url);
    if (!token) return;
    changeToken(token);
  };

  const value = useMemo(
    () => ({ id, token, admin, changeToken }),
    [id, token, admin],
  );

  function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
    return function ServerErrorRedirectToImpl(
      error: HttpError<MerchantBackend.ErrorDetail>,
    ) {
      if (error.type === ErrorType.TIMEOUT) {
        setGlobalNotification({
          message: i18n.str`The request to the backend take too long and was cancelled`,
          description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`,
          type: "ERROR",
          to,
        });
      } else {
        setGlobalNotification({
          message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
          description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
          details:
            error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER
              ? error.payload.detail
              : undefined,
          type: "ERROR",
          to,
        });
      }
      return <Redirect to={to} />;
    };
  }

  const LoginPageAccessDenied = () => (
    <Fragment>
      <NotificationCard
        notification={{
          message: i18n.str`Access denied`,
          description: i18n.str`The access token provided is invalid.`,
          type: "ERROR",
        }}
      />
      <LoginPage onConfirm={updateLoginStatus} />
    </Fragment>
  );

  function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
    return function IfAdminCreateDefaultOrImpl(props?: T) {
      if (admin && id === "default") {
        return (
          <Fragment>
            <NotificationCard
              notification={{
                message: i18n.str`No 'default' instance configured yet.`,
                description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`,
                type: "INFO",
              }}
            />
            <InstanceCreatePage
              forceId="default"
              onConfirm={() => {
                route(AdminPaths.list_instances);
              }}
            />
          </Fragment>
        );
      }
      if (props) {
        return <Next {...props} />;
      }
      return <Next />;
    };
  }

  const clearTokenAndGoToRoot = () => {
    clearAllTokens();
    route("/");
  };

  return (
    <InstanceContextProvider value={value}>
      <Menu
        instance={id}
        admin={admin}
        path={path}
        onLogout={clearTokenAndGoToRoot}
        setInstanceName={setInstanceName}
      />
      <KycBanner />
      <NotificationCard notification={globalNotification} />

      <Router
        onChange={(e) => {
          const movingOutFromNotification =
            globalNotification && e.url !== globalNotification.to;
          if (movingOutFromNotification) {
            setGlobalNotification(undefined);
          }
        }}
      >
        <Route path="/" component={Redirect} to={InstancePaths.order_list} />
        {/**
         * Admin pages
         */}
        {admin && (
          <Route
            path={AdminPaths.list_instances}
            component={InstanceListPage}
            onCreate={() => {
              route(AdminPaths.new_instance);
            }}
            onUpdate={(id: string): void => {
              route(`/instance/${id}/update`);
            }}
            setInstanceName={setInstanceName}
            onUnauthorized={LoginPageAccessDenied}
            onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
          />
        )}
        {admin && (
          <Route
            path={AdminPaths.new_instance}
            component={InstanceCreatePage}
            onBack={() => route(AdminPaths.list_instances)}
            onConfirm={() => {
              route(AdminPaths.list_instances);
            }}
          />
        )}
        {admin && (
          <Route
            path={AdminPaths.update_instance}
            component={AdminInstanceUpdatePage}
            onBack={() => route(AdminPaths.list_instances)}
            onConfirm={() => {
              route(AdminPaths.list_instances);
            }}
            onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)}
            onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)}
            onNotFound={NotFoundPage}
          />
        )}
        {/**
         * Update instance page
         */}
        <Route
          path={InstancePaths.update}
          component={InstanceUpdatePage}
          onBack={() => {
            route(`/`);
          }}
          onConfirm={() => {
            route(`/`);
          }}
          onUpdateError={noop}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
        />
        {/**
         * Product pages
         */}
        <Route
          path={InstancePaths.product_list}
          component={ProductListPage}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
          onCreate={() => {
            route(InstancePaths.product_new);
          }}
          onSelect={(id: string) => {
            route(InstancePaths.product_update.replace(":pid", id));
          }}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
        />
        <Route
          path={InstancePaths.product_update}
          component={ProductUpdatePage}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.product_list)}
          onConfirm={() => {
            route(InstancePaths.product_list);
          }}
          onBack={() => {
            route(InstancePaths.product_list);
          }}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
        />
        <Route
          path={InstancePaths.product_new}
          component={ProductCreatePage}
          onConfirm={() => {
            route(InstancePaths.product_list);
          }}
          onBack={() => {
            route(InstancePaths.product_list);
          }}
        />
        {/**
         * Order pages
         */}
        <Route
          path={InstancePaths.order_list}
          component={OrderListPage}
          onCreate={() => {
            route(InstancePaths.order_new);
          }}
          onSelect={(id: string) => {
            route(InstancePaths.order_details.replace(":oid", id));
          }}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
        />
        <Route
          path={InstancePaths.order_details}
          component={OrderDetailsPage}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onBack={() => {
            route(InstancePaths.order_list);
          }}
        />
        <Route
          path={InstancePaths.order_new}
          component={OrderCreatePage}
          onConfirm={() => {
            route(InstancePaths.order_list);
          }}
          onBack={() => {
            route(InstancePaths.order_list);
          }}
        />
        {/**
         * Transfer pages
         */}
        <Route
          path={InstancePaths.transfers_list}
          component={TransferListPage}
          onUnauthorized={LoginPageAccessDenied}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
          onCreate={() => {
            route(InstancePaths.transfers_new);
          }}
        />
        <Route
          path={InstancePaths.transfers_new}
          component={TransferCreatePage}
          onConfirm={() => {
            route(InstancePaths.transfers_list);
          }}
          onBack={() => {
            route(InstancePaths.transfers_list);
          }}
        />
        {/**
         * Webhooks pages
         */}
        <Route
          path={InstancePaths.webhooks_list}
          component={WebhookListPage}
          onUnauthorized={LoginPageAccessDenied}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
          onCreate={() => {
            route(InstancePaths.webhooks_new);
          }}
          onSelect={(id: string) => {
            route(InstancePaths.webhooks_update.replace(":tid", id));
          }}
        />
        <Route
          path={InstancePaths.webhooks_update}
          component={WebhookUpdatePage}
          onConfirm={() => {
            route(InstancePaths.webhooks_list);
          }}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onBack={() => {
            route(InstancePaths.webhooks_list);
          }}
        />
        <Route
          path={InstancePaths.webhooks_new}
          component={WebhookCreatePage}
          onConfirm={() => {
            route(InstancePaths.webhooks_list);
          }}
          onBack={() => {
            route(InstancePaths.webhooks_list);
          }}
        />
        {/**
         * Templates pages
         */}
        <Route
          path={InstancePaths.templates_list}
          component={TemplateListPage}
          onUnauthorized={LoginPageAccessDenied}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
          onCreate={() => {
            route(InstancePaths.templates_new);
          }}
          onNewOrder={(id: string) => {
            route(InstancePaths.templates_use.replace(":tid", id));
          }}
          onQR={(id: string) => {
            route(InstancePaths.templates_qr.replace(":tid", id));
          }}
          onSelect={(id: string) => {
            route(InstancePaths.templates_update.replace(":tid", id));
          }}
        />
        <Route
          path={InstancePaths.templates_update}
          component={TemplateUpdatePage}
          onConfirm={() => {
            route(InstancePaths.templates_list);
          }}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onBack={() => {
            route(InstancePaths.templates_list);
          }}
        />
        <Route
          path={InstancePaths.templates_new}
          component={TemplateCreatePage}
          onConfirm={() => {
            route(InstancePaths.templates_list);
          }}
          onBack={() => {
            route(InstancePaths.templates_list);
          }}
        />
        <Route
          path={InstancePaths.templates_use}
          component={TemplateUsePage}
          onOrderCreated={(id: string) => {
            route(InstancePaths.order_details.replace(":oid", id));
          }}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onBack={() => {
            route(InstancePaths.templates_list);
          }}
        />
        <Route
          path={InstancePaths.templates_qr}
          component={TemplateQrPage}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onBack={() => {
            route(InstancePaths.templates_list);
          }}
        />

        {/**
         * reserves pages
         */}
        <Route
          path={InstancePaths.reserves_list}
          component={ReservesListPage}
          onUnauthorized={LoginPageAccessDenied}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
          onSelect={(id: string) => {
            route(InstancePaths.reserves_details.replace(":rid", id));
          }}
          onCreate={() => {
            route(InstancePaths.reserves_new);
          }}
        />
        <Route
          path={InstancePaths.reserves_details}
          component={ReservesDetailsPage}
          onUnauthorized={LoginPageAccessDenied}
          onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)}
          onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
          onBack={() => {
            route(InstancePaths.reserves_list);
          }}
        />
        <Route
          path={InstancePaths.reserves_new}
          component={ReservesCreatePage}
          onConfirm={() => {
            route(InstancePaths.reserves_list);
          }}
          onBack={() => {
            route(InstancePaths.reserves_list);
          }}
        />
        <Route path={InstancePaths.kyc} component={ListKYCPage} />
        {/**
         * Example pages
         */}
        <Route path="/loading" component={Loading} />
        <Route default component={NotFoundPage} />
      </Router>
    </InstanceContextProvider>
  );
}

export function Redirect({ to }: { to: string }): null {
  useEffect(() => {
    route(to, true);
  });
  return null;
}

function AdminInstanceUpdatePage({
  id,
  ...rest
}: { id: string } & InstanceUpdatePageProps): VNode {
  const [token, changeToken] = useBackendInstanceToken(id);
  const { updateLoginStatus: changeBackend } = useBackendContext();
  const updateLoginStatus = (url: string, token?: string): void => {
    changeBackend(url);
    if (token) changeToken(token);
  };
  const value = useMemo(
    () => ({ id, token, admin: true, changeToken }),
    [id, token],
  );
  const { i18n } = useTranslationContext();

  return (
    <InstanceContextProvider value={value}>
      <InstanceAdminUpdatePage
        {...rest}
        instanceId={id}
        onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => {
          const notif =
            error.type === ErrorType.TIMEOUT
              ? {
                  message: i18n.str`The request to the backend take too long and was cancelled`,
                  description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
                  type: "ERROR" as const,
                }
              : {
                  message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
                  description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
                  details:
                    error.type === ErrorType.CLIENT ||
                    error.type === ErrorType.SERVER
                      ? error.payload.detail
                      : undefined,
                  type: "ERROR" as const,
                };
          return (
            <Fragment>
              <NotificationCard notification={notif} />
              <LoginPage onConfirm={updateLoginStatus} />
            </Fragment>
          );
        }}
        onUnauthorized={() => {
          return (
            <Fragment>
              <NotificationCard
                notification={{
                  message: i18n.str`Access denied`,
                  description: i18n.str`The access token provided is invalid`,
                  type: "ERROR",
                }}
              />
              <LoginPage onConfirm={updateLoginStatus} />
            </Fragment>
          );
        }}
      />
    </InstanceContextProvider>
  );
}

function KycBanner(): VNode {
  const kycStatus = useInstanceKYCDetails();
  const { i18n } = useTranslationContext();
  const today = format(new Date(), "yyyy-MM-dd");
  const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
  const hasBeenHidden = today === lastHide;
  const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
  if (hasBeenHidden || !needsToBeShown) return <Fragment />;
  return (
    <NotificationCard
      notification={{
        type: "WARN",
        message: "KYC verification needed",
        description: (
          <div>
            <p>
              Some transfer are on hold until a KYC process is completed. Go to
              the KYC section in the left panel for more information
            </p>
            <div class="buttons is-right">
              <button class="button" onClick={() => setLastHide(today)}>
                <i18n.Translate>Hide for today</i18n.Translate>
              </button>
            </div>
          </div>
        ),
      }}
    />
  );
}