/*
 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,
  ChallengerApi,
  ChallengerCacheEviction,
  ChallengerHttpClient,
  LibtoolVersion,
  ObservabilityEvent,
  ObservableHttpClientLibrary,
  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,
  ChallengerLib,
  Subscriber
} from "./activity.js";
import { useTranslationContext } from "./translation.js";

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

export type ChallengerContextType = {
  url: URL;
  config: ChallengerApi.ChallengerTermsOfServiceResponse;
  lib: ChallengerLib;
  hints: VersionHint[];
  onActivity: Subscriber<ObservabilityEvent>;
  cancelRequest: (eventId: string) => void;
};

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

export const useChallengerApiContext = (): ChallengerContextType =>
  useContext(ChallengerContext);

enum VersionHint {
  NONE,
}

type Evictors = {
  challenger?: CacheEvictor<ChallengerCacheEviction>;
}

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 ChallengerApiProvider = ({
  baseUrl,
  children,
  frameOnError,
  evictors = {},
}: {
  baseUrl: URL;
  children: ComponentChildren;
  evictors?: Evictors;
  frameOnError: FunctionComponent<{ children: ComponentChildren }>;
}): VNode => {
  const [checked, setChecked] =
    useState<ConfigResult<ChallengerApi.ChallengerTermsOfServiceResponse>>();
  const { i18n } = useTranslationContext();

  const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
    buildChallengerApiClient(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, showDetail: true }),
    });
  }
  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: ChallengerContextType = {
    url: baseUrl,
    config: checked.config,
    onActivity: onActivity,
    lib,
    cancelRequest,
    hints: checked.hints,
  };
  return h(ChallengerContext.Provider, {
    value,
    children,
  });
};

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

  const challenger = new ChallengerHttpClient(url.href, httpLib, evictors.challenger);

  async function getRemoteConfig(): Promise<ChallengerApi.ChallengerTermsOfServiceResponse> {
    const resp = await challenger.getConfig();
    if (resp.type === "fail") {
      throw TalerError.fromUncheckedDetail(resp.detail);
    }
    return resp.body;
  }

  return {
    getRemoteConfig,
    VERSION: challenger.PROTOCOL_VERSION,
    lib: {
      challenger,
    },
    onActivity: tracker.subscribe,
    cancelRequest: httpLib.cancelRequest,
  };
}

export const ChallengerApiProviderTesting = ({
  children,
  value,
}: {
  value: ChallengerContextType;
  children: ComponentChildren;
}): VNode => {
  return h(ChallengerContext.Provider, {
    value,
    children,
  });
};
