/*
 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,
  TalerMerchantApi,
  TalerMerchantInstanceCacheEviction,
  TalerMerchantManagementCacheEviction,
  TalerMerchantManagementHttpClient,
} from "@gnu-taler/taler-util";
import {
  ComponentChildren,
  FunctionComponent,
  VNode,
  createContext,
  h,
} from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
import { BrowserFetchHttpLib } from "../index.browser.js";
import {
  APIClient,
  ActiviyTracker,
  MerchantLib,
  Subscriber,
} from "./activity.js";

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

export type MerchantContextType = {
  url: URL;
  config: TalerMerchantApi.MerchantVersionResponse;
  lib: MerchantLib;
  hints: VersionHint[];
  onActivity: Subscriber<ObservabilityEvent>;
  cancelRequest: (eventId: string) => void;
  changeBackend: (url: URL) => void;
};

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

export const useMerchantApiContext = (): MerchantContextType =>
  useContext(MerchantContext);

enum VersionHint {
  NONE,
}

type Evictors = {
  management?: CacheEvictor<
    TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction
  >;
};

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

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

const CONFIG_FAIL_TRY_AGAIN_MS = 5000;

export const MerchantApiProvider = ({
  baseUrl,
  children,
  evictors = {},
  frameOnError,
}: {
  baseUrl: URL;
  evictors?: Evictors;
  children: ComponentChildren;
  frameOnError: FunctionComponent<{
    state:
      | ConfigResultFail<TalerMerchantApi.MerchantVersionResponse>
      | undefined;
  }>;
}): VNode => {
  const [checked, setChecked] =
    useState<ConfigResult<TalerMerchantApi.MerchantVersionResponse>>();

  const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl);

  const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
    buildMerchantApiClient(merchantEndpoint, 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 || checked.type !== "ok") {
    return h(frameOnError, { state: checked }, []);
  }

  const value: MerchantContextType = {
    url: merchantEndpoint,
    config: checked.config,
    onActivity: onActivity,
    lib,
    cancelRequest,
    changeBackend: changeMerchantEndpoint,
    hints: checked.hints,
  };
  return h(MerchantContext.Provider, {
    value,
    children,
  });
};

function buildMerchantApiClient(
  url: URL,
  evictors: Evictors,
): APIClient<MerchantLib, TalerMerchantApi.MerchantVersionResponse> {
  const httpFetch = new BrowserFetchHttpLib({
    enableThrottling: true,
    requireTls: false,
  });
  const tracker = new ActiviyTracker<ObservabilityEvent>();

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

  const instance = new TalerMerchantManagementHttpClient(
    url.href,
    httpLib,
    evictors.management,
  );

  function getSubInstanceAPI(instanceId: string): MerchantLib {
    const api = buildMerchantApiClient(
      new URL(instance.getSubInstanceAPI(instanceId)),
      evictors,
    );
    return api.lib;
  }

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

  return {
    getRemoteConfig,
    VERSION: TalerMerchantManagementHttpClient.PROTOCOL_VERSION,
    lib: {
      instance,
      subInstanceApi: getSubInstanceAPI,
    },
    onActivity: tracker.subscribe,
    cancelRequest: httpLib.cancelRequest,
  };
}

export const MerchantApiProviderTesting = ({
  children,
  value,
}: {
  value: MerchantContextType;
  children: ComponentChildren;
}): VNode => {
  return h(MerchantContext.Provider, {
    value,
    children,
  });
};
