/*
 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 {
  CacheEvictor,
  LibtoolVersion,
  ObservabilityEvent,
  ObservableHttpClientLibrary,
  TalerBankConversionCacheEviction,
  TalerBankConversionHttpClient,
  TalerCoreBankCacheEviction,
  TalerCoreBankHttpClient,
  TalerCorebankApi,
  TalerError,
} from "@gnu-taler/taler-util";
import {
  ComponentChildren,
  FunctionComponent,
  VNode,
  createContext,
  h,
} from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
import { APIClient, ActiviyTracker, BankLib, Subscriber } from "./activity.js";
import { useTranslationContext } from "./translation.js";

/**
 *
 * @author Sebastian Javier Marchano (sebasjm)
 */

export type BankContextType = {
  url: URL;
  config: TalerCorebankApi.TalerCorebankConfigResponse;
  lib: BankLib;
  hints: VersionHint[];
  onActivity: Subscriber<ObservabilityEvent>;
  cancelRequest: (eventId: string) => void;
};

// @ts-expect-error default value to undefined, should it be another thing?
const BankContext = createContext<BankContextType>(undefined);

export const useBankCoreApiContext = (): BankContextType =>
  useContext(BankContext);

enum VersionHint {
  NONE,
}

type Evictors = {
  conversion?: CacheEvictor<TalerBankConversionCacheEviction>;
  bank?: CacheEvictor<TalerCoreBankCacheEviction>;
};

type ConfigResult<T> =
  | undefined
  | { type: "ok"; config: T; hints: VersionHint[] }
  | { type: "incompatible"; result: T; supported: string }
  | { type: "error"; error: TalerError };

const CONFIG_FAIL_TRY_AGAIN_MS = 5000;

export const BankApiProvider = ({
  baseUrl,
  children,
  frameOnError,
  evictors = {},
}: {
  baseUrl: URL;
  children: ComponentChildren;
  evictors?: Evictors;
  frameOnError: FunctionComponent<{ children: ComponentChildren }>;
}): VNode => {
  const [checked, setChecked] =
    useState<ConfigResult<TalerCorebankApi.TalerCorebankConfigResponse>>();
  const { i18n } = useTranslationContext();

  const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
    buildBankApiClient(baseUrl, evictors);

  useEffect(() => {
    let keepRetrying = true;
    async function testConfig(): Promise<void> {
      try {
        const config = await getRemoteConfig();
        if (LibtoolVersion.compare(VERSION, config.version)) {
          setChecked({ type: "ok", config, hints: [] });
        } else {
          setChecked({
            type: "incompatible",
            result: config,
            supported: VERSION,
          });
        }
      } catch (error) {
        if (error instanceof TalerError) {
          if (keepRetrying) {
            setTimeout(() => {
              testConfig();
            }, CONFIG_FAIL_TRY_AGAIN_MS);
          }
          setChecked({ type: "error", error });
        } else {
          setChecked({ type: "error", error: TalerError.fromException(error) });
        }
      }
    }
    testConfig();
    return () => {
      // on unload, stop retry
      keepRetrying = false;
    };
  }, []);

  if (checked === undefined) {
    return h(frameOnError, {
      children: h("div", {}, "checking compatibility with server..."),
    });
  }
  if (checked.type === "error") {
    return h(frameOnError, {
      children: h(ErrorLoading, { error: checked.error }),
    });
  }
  if (checked.type === "incompatible") {
    return h(frameOnError, {
      children: h(
        "div",
        {},
        i18n.str`The server version is not supported. Supported version "${checked.supported}", server version "${checked.result.version}"`,
      ),
    });
  }

  const value: BankContextType = {
    url: baseUrl,
    config: checked.config,
    onActivity: onActivity,
    lib,
    cancelRequest,
    hints: checked.hints,
  };
  return h(BankContext.Provider, {
    value,
    children,
  });
};

function buildBankApiClient(
  url: URL,
  evictors: Evictors,
): APIClient<BankLib, TalerCorebankApi.TalerCorebankConfigResponse> {
  const httpFetch = new BrowserFetchHttpLib({
    enableThrottling: true,
    requireTls: false,
  });
  const tracker = new ActiviyTracker<ObservabilityEvent>();
  const httpLib = new ObservableHttpClientLibrary(httpFetch, {
    observe(ev) {
      tracker.notify(ev);
    },
  });

  const bank = new TalerCoreBankHttpClient(url.href, httpLib, evictors.bank);
  const conversion = new TalerBankConversionHttpClient(
    bank.getConversionInfoAPI().href,
    httpLib,
    evictors.conversion,
  );

  async function getRemoteConfig(): Promise<TalerCorebankApi.TalerCorebankConfigResponse> {
    const resp = await bank.getConfig();
    if (resp.type === "fail") {
      if (resp.detail) {
        throw TalerError.fromUncheckedDetail(resp.detail);
      } else {
        throw TalerError.fromException(
          new Error("failed to get bank remote config"),
        );
      }
    }
    return resp.body;
  }

  return {
    getRemoteConfig,
    VERSION: TalerCoreBankHttpClient.PROTOCOL_VERSION,
    lib: {
      bank,
      conversion,
      conversionForClass(classId) {
        return new TalerBankConversionHttpClient(
          bank.getConversionInfoAPIForClass(classId).href,
          httpLib,
          evictors.conversion,
        );
      },
      conversionForUser(username) {
        return new TalerBankConversionHttpClient(
          bank.getConversionInfoAPIForUser(username).href,
          httpLib,
          evictors.conversion,
        );
      },
    },
    onActivity: tracker.subscribe,
    cancelRequest: httpLib.cancelRequest,
  };
}

export const BankApiProviderTesting = ({
  children,
  value,
}: {
  value: BankContextType;
  children: ComponentChildren;
}): VNode => {
  return h(BankContext.Provider, {
    value,
    children,
  });
};
