/*
 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,
  TalerError,
  TalerExchangeApi,
  TalerExchangeCacheEviction,
  TalerExchangeHttpClient,
} from "@gnu-taler/taler-util";
import {
  ComponentChildren,
  FunctionComponent,
  VNode,
  createContext,
  h,
} from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
import {
  BrowserFetchHttpLib,
  ErrorLoading,
  useTranslationContext,
} from "../index.browser.js";
import {
  APIClient,
  ActiviyTracker,
  ExchangeLib,
  Subscriber,
} from "./activity.js";

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

export type ExchangeContextType = {
  url: URL;
  config: KeysAndConfigType;
  lib: ExchangeLib;
  /**
   * Do not use. This is here because the AML dashboard does too many
   * request to the event API
   *
   * FIXME: The server should expose a better API
   * https://bugs.gnunet.org/view.php?id=9776
   * @deprecated
   */
  unthrottledApi: ExchangeLib;
  hints: VersionHint[];
  onActivity: Subscriber<ObservabilityEvent>;
  cancelRequest: (eventId: string) => void;
};

// FIXME: below
// @ts-expect-error default value to undefined, should it be another thing?
const ExchangeContext = createContext<ExchangeContextType>(undefined);

export const useExchangeApiContext = (): ExchangeContextType =>
  useContext(ExchangeContext);

enum VersionHint {
  NONE,
}

type Evictors = {
  exchange?: CacheEvictor<TalerExchangeCacheEviction>;
};

type ConfigResult<T> =
  | undefined
  | { type: "ok"; config: T; hints: VersionHint[] }
  | ConfigResultFail<T>;

type ConfigResultFail<T> =
  | { type: "incompatible"; result: T; supported: string }
  | { type: "error"; error: TalerError };

const CONFIG_FAIL_TRY_AGAIN_MS = 5000;

export type KeysAndConfigType = {
  config: TalerExchangeApi.ExchangeVersionResponse;
  keys: TalerExchangeApi.ExchangeKeysResponse;
};

export const ExchangeApiProvider = ({
  baseUrl,
  children,
  evictors = {},
  frameOnError,
  preventCompression,
}: {
  baseUrl: URL;
  evictors?: Evictors;
  children: ComponentChildren;
  frameOnError: FunctionComponent<{ children: ComponentChildren }>;
  preventCompression?: boolean;
}): VNode => {
  const [checked, setChecked] = useState<ConfigResult<KeysAndConfigType>>();
  const { i18n } = useTranslationContext();

  const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
    buildExchangeApiClient(baseUrl, evictors, !!preventCompression);

  useEffect(() => {
    let keepRetrying = true;
    async function testConfig(): Promise<void> {
      try {
        const config = await getRemoteConfig();
        if (LibtoolVersion.compare(VERSION, config.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.config.version}"`,
      ),
    });
  }

  const { lib: unthrottledApi } = buildExchangeApiClient(
    baseUrl,
    evictors,
    !!preventCompression,
    true,
  );

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

function buildExchangeApiClient(
  url: URL,
  evictors: Evictors,
  preventCompression: boolean,
  disableThrottling?: boolean,
): APIClient<ExchangeLib, KeysAndConfigType> {
  const httpFetch = new BrowserFetchHttpLib({
    enableThrottling: !disableThrottling,
    requireTls: false,
  });
  const tracker = new ActiviyTracker<ObservabilityEvent>();

  const httpLib = new ObservableHttpClientLibrary(httpFetch, {
    observe(ev) {
      tracker.notify(ev);
    },
  });

  const ex = new TalerExchangeHttpClient(url.href, {
    httpClient: httpLib,
    cacheEvictor: evictors.exchange,
    preventCompression,
  });

  async function getRemoteConfig(): Promise<KeysAndConfigType> {
    const configResp = await ex.getConfig();
    if (configResp.type === "fail") {
      if (configResp.detail) {
        throw TalerError.fromUncheckedDetail(configResp.detail);
      } else {
        throw TalerError.fromException(
          new Error("failed to get exchange remote config"),
        );
      }
    }
    const keysResp = await ex.getKeys();
    // if (keysResp.type === "fail") {
    //   if (keysResp.detail) {
    //     throw TalerError.fromUncheckedDetail(keysResp.detail);
    //   } else {
    //     throw TalerError.fromException(
    //       new Error("failed to get exchange remote config"),
    //     );
    //   }
    // }
    return {
      config: configResp.body,
      keys: keysResp.body,
    };
  }

  return {
    getRemoteConfig,
    VERSION: TalerExchangeHttpClient.SUPPORTED_EXCHANGE_PROTOCOL_VERSION,
    lib: {
      exchange: ex,
    },
    onActivity: tracker.subscribe,
    cancelRequest: httpLib.cancelRequest,
  };
}

export const ExchangeApiProviderTesting = ({
  children,
  value,
}: {
  value: ExchangeContextType;
  children: ComponentChildren;
}): VNode => {
  return h(ExchangeContext.Provider, {
    value,
    children,
  });
};
