/*
 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 {
  CacheEvictor,
  TalerMerchantApi,
  TalerMerchantInstanceCacheEviction,
  TalerMerchantManagementCacheEviction,
  assertUnreachable,
  canonicalizeBaseUrl,
} from "@gnu-taler/taler-util";
import {
  BrowserHashNavigationProvider,
  ConfigResultFail,
  MerchantApiProvider,
  TalerWalletIntegrationBrowserProvider,
  TranslationProvider,
  useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { SWRConfig } from "swr";
import { Routing } from "./Routing.js";
import { Loading } from "./components/exception/loading.js";
import { NotificationCard } from "./components/menu/index.js";
import { SessionContextProvider } from "./context/session.js";
import { SettingsProvider } from "./context/settings.js";
import { revalidateInstanceAccessTokens } from "./hooks/access-tokens.js";
import {
  revalidateBankAccountDetails,
  revalidateInstanceBankAccounts,
} from "./hooks/bank.js";
import { revalidateInstanceCategories } from "./hooks/category.js";
import {
  revalidateBackendInstances,
  revalidateInstanceDetails,
  revalidateManagedInstanceDetails,
} from "./hooks/instance.js";
import {
  revalidateInstanceOrders,
  revalidateOrderDetails,
} from "./hooks/order.js";
import {
  revalidateInstanceOtpDevices,
  revalidateOtpDeviceDetails,
} from "./hooks/otp.js";
import {
  revalidateInstanceProducts,
  revalidateProductDetails,
} from "./hooks/product.js";
import {
  revalidateInstanceTemplates,
  revalidateTemplateDetails,
} from "./hooks/templates.js";
import {
  revalidateTokenFamilies,
  revalidateTokenFamilyDetails,
} from "./hooks/tokenfamily.js";
import {
  revalidateInstanceWebhooks,
  revalidateWebhookDetails,
} from "./hooks/webhooks.js";
import { strings } from "./i18n/strings.js";
import {
  MerchantUiSettings,
  buildDefaultBackendBaseURL,
  fetchSettings,
} from "./settings.js";
import {
  revalidateInstanceConfirmedTransfers,
  revalidateInstanceIncomingTransfers,
} from "./hooks/transfer.js";

const TALER_SCREEN_ID = 2;
const WITH_LOCAL_STORAGE_CACHE = false;

export function Application(): VNode {
  const [settings, setSettings] = useState<MerchantUiSettings>();
  useEffect(() => {
    fetchSettings(setSettings);
  }, []);
  if (!settings) return <Loading />;

  const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
  return (
    <SettingsProvider value={settings}>
      <TranslationProvider
        source={strings}
        completeness={{
          uk: strings["uk"].completeness,
          tr: strings["tr"].completeness,
          // ru: strings["ru"].completeness,
          sv: strings["sv"].completeness,
          it: strings["it"].completeness,
          fr: strings["fr"].completeness,
          es: strings["es"].completeness,
          de: strings["de"].completeness,
        }}
      >
        <MerchantApiProvider
          baseUrl={new URL("./", baseUrl)}
          frameOnError={OnConfigError}
          evictors={{
            management: swrCacheEvictor,
          }}
        >
          <SessionContextProvider>
            <SWRConfig
              value={{
                provider: WITH_LOCAL_STORAGE_CACHE
                  ? localStorageProvider
                  : undefined,
                // normally, do not revalidate
                revalidateOnFocus: false,
                revalidateOnReconnect: false,
                revalidateIfStale: false,
                revalidateOnMount: undefined,
                focusThrottleInterval: undefined,

                // normally, do not refresh
                refreshInterval: undefined,
                dedupingInterval: 2000,
                refreshWhenHidden: false,
                refreshWhenOffline: false,

                // ignore errors
                shouldRetryOnError: false,
                errorRetryCount: 0,
                errorRetryInterval: undefined,

                // do not go to loading again if already has data
                keepPreviousData: true,
              }}
            >
              <TalerWalletIntegrationBrowserProvider>
                <BrowserHashNavigationProvider>
                  <Routing />
                </BrowserHashNavigationProvider>
              </TalerWalletIntegrationBrowserProvider>
            </SWRConfig>
          </SessionContextProvider>
        </MerchantApiProvider>
      </TranslationProvider>
    </SettingsProvider>
  );
}

function getInitialBackendBaseURL(
  backendFromSettings: string | undefined,
): string {
  /**
   * For testing purpose
   */
  const overrideUrl =
    typeof localStorage !== "undefined"
      ? localStorage.getItem("merchant-base-url")
      : undefined;
  let result: string;

  if (overrideUrl) {
    // testing/development path
    result = overrideUrl;
  } else {
    // normal path
    if (!backendFromSettings) {
      console.error(
        "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
      );
      result = buildDefaultBackendBaseURL();
    } else {
      result = backendFromSettings;
    }
  }
  try {
    return canonicalizeBaseUrl(result);
  } catch (e) {
    // fall back
    return canonicalizeBaseUrl(window.origin);
  }
}

function localStorageProvider(): Map<unknown, unknown> {
  const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));

  window.addEventListener("beforeunload", () => {
    const appCache = JSON.stringify(Array.from(map.entries()));
    localStorage.setItem("app-cache", appCache);
  });
  return map;
}

function OnConfigError({
  state,
}: {
  state: ConfigResultFail<TalerMerchantApi.MerchantVersionResponse> | undefined;
}): VNode {
  const { i18n } = useTranslationContext();
  if (!state) {
    return (
      <i18n.Translate>checking compatibility with server...</i18n.Translate>
    );
  }
  switch (state.type) {
    case "error": {
      return (
        <NotificationCard
          notification={{
            message: i18n.str`Contacting the server failed`,
            description: state.error.message,
            details: JSON.stringify(state.error.errorDetail, undefined, 2),
            type: "ERROR",
          }}
        />
      );
    }
    case "incompatible": {
      return (
        <NotificationCard
          notification={{
            message: i18n.str`The server version is not supported`,
            description: i18n.str`Supported version "${state.supported}", server version "${state.result.version}".`,
            type: "WARN",
          }}
        />
      );
    }
    default:
      assertUnreachable(state);
  }
}

const swrCacheEvictor = new (class
  implements
    CacheEvictor<
      TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction
    >
{
  async notifySuccess(
    op:
      | TalerMerchantManagementCacheEviction
      | TalerMerchantInstanceCacheEviction,
  ) {
    switch (op) {
      case TalerMerchantManagementCacheEviction.CREATE_INSTANCE: {
        await Promise.all([revalidateBackendInstances()]);
        return;
      }
      case TalerMerchantManagementCacheEviction.UPDATE_INSTANCE: {
        await Promise.all([revalidateManagedInstanceDetails()]);
        return;
      }
      case TalerMerchantManagementCacheEviction.DELETE_INSTANCE: {
        await Promise.all([revalidateBackendInstances()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE: {
        await Promise.all([revalidateInstanceDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE: {
        await Promise.all([revalidateInstanceDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT: {
        await Promise.all([revalidateInstanceBankAccounts()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT: {
        await Promise.all([revalidateBankAccountDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT: {
        await Promise.all([revalidateInstanceBankAccounts()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_CATEGORY: {
        await Promise.all([revalidateInstanceCategories()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_CATEGORY: {
        await Promise.all([revalidateInstanceCategories()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_CATEGORY: {
        await Promise.all([revalidateInstanceCategories()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_PRODUCT: {
        await Promise.all([
          revalidateInstanceProducts(),
          revalidateInstanceCategories(),
        ]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT: {
        await Promise.all([
          revalidateProductDetails(),
          revalidateInstanceProducts(),
          revalidateInstanceCategories(),
        ]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_PRODUCT: {
        await Promise.all([
          revalidateInstanceProducts(),
          revalidateInstanceCategories(),
        ]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_TRANSFER: {
        await Promise.all([
          revalidateInstanceConfirmedTransfers(),
          revalidateInstanceIncomingTransfers(),
        ]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_TRANSFER: {
        await Promise.all([
          revalidateInstanceConfirmedTransfers(),
          revalidateInstanceIncomingTransfers(),
        ]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_DEVICE: {
        await Promise.all([revalidateInstanceOtpDevices()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_DEVICE: {
        await Promise.all([revalidateOtpDeviceDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_DEVICE: {
        await Promise.all([revalidateInstanceOtpDevices()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE: {
        await Promise.all([revalidateInstanceTemplates()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE: {
        await Promise.all([revalidateTemplateDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE: {
        await Promise.all([revalidateInstanceTemplates()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK: {
        await Promise.all([revalidateInstanceWebhooks()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK: {
        await Promise.all([revalidateWebhookDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK: {
        await Promise.all([revalidateInstanceWebhooks()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_ORDER: {
        await Promise.all([revalidateInstanceOrders()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_ORDER: {
        await Promise.all([revalidateOrderDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_ORDER: {
        await Promise.all([revalidateInstanceOrders()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY: {
        await Promise.all([revalidateTokenFamilies()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY: {
        await Promise.all([revalidateTokenFamilyDetails()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY: {
        await Promise.all([revalidateTokenFamilies()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.CREATE_ACCESSTOKEN: {
        await Promise.all([revalidateInstanceAccessTokens()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.DELETE_ACCESSTOKEN: {
        await Promise.all([revalidateInstanceAccessTokens()]);
        return;
      }
      case TalerMerchantInstanceCacheEviction.LAST: {
        return;
      }
      default: {
        assertUnreachable(op);
      }
    }
  }
})();
