/*
 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/>
 */

import {
  AccessToken,
  Codec,
  MerchantVersionResponse,
  TalerMerchantManagementHttpClient,
  buildCodecForObject,
  codecForString,
  codecForURL,
  codecOptional,
} from "@gnu-taler/taler-util";
import {
  buildStorageKey,
  useLocalStorage,
  useMerchantApiContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, VNode, createContext, h } from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
import { mutate } from "swr";
import { MerchantLib } from "../../../web-util/lib/context/activity.js";

/**
 * Has the information to reach and
 * authenticate at the bank's backend.
 */
export type SessionState = LoggedIn | LoggedOut;

interface LoggedIn {
  status: "loggedIn";

  // is this instance admin? usually "admin" name
  isAdmin: boolean;

  // url where all the request will be made
  // usually this is from where the SPA was loaded
  // unless it's using impersonate feature
  backendUrl: URL;

  // instance name
  instance: string;

  // session is not the same from where it was loaded
  impersonated: boolean;

  //instance access token
  token: AccessToken | undefined;
}

interface LoggedOut {
  status: "loggedOut";
  backendUrl: URL;
  instance: string;
  isAdmin: boolean;
  token: AccessToken | undefined;
}

interface SavedSession {
  backendUrl: URL;
  token: AccessToken | undefined;
  prevToken: AccessToken | undefined;
}

export const codecForSessionState = (): Codec<SavedSession> =>
  buildCodecForObject<SavedSession>()
    .property("backendUrl", codecForURL())
    .property("token", codecOptional(codecForString() as Codec<AccessToken>))
    .property(
      "prevToken",
      codecOptional(codecForString() as Codec<AccessToken>),
    )
    .build("SavedSession");

function inferInstanceName(url: URL) {
  const match = INSTANCE_ID_LOOKUP.exec(url.href);
  return !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1];
}

function recalculateUrlForAnotherUser(original: URL, user:string) {
  const match = INSTANCE_ID_LOOKUP.exec(original.href);
  return !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1];
}

export const defaultState = (url: URL): SavedSession => {
  return {
    backendUrl: url,
    token: undefined,
    prevToken: undefined,
  };
};

export interface SessionStateHandler {
  lib: MerchantLib;
  config: MerchantVersionResponse;

  state: SessionState;
  /**
   * from every state to logout state
   */
  logOut(): void;
  /**
   * from impersonate to loggedIn
   */
  deImpersonate(): void;
  /**
   * from any to loggedIn
   * @param info
   */
  logIn(username: string, token: AccessToken | undefined): void;
  /**
   * from loggedIn to impersonate
   * @param info
   */
  impersonate(baseUrl: URL): void;

  getInstanceForUsername(username: string): TalerMerchantManagementHttpClient;
}

export const DEFAULT_ADMIN_USERNAME = "admin";

export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;

export function cleanAllCache(): void {
  mutate(() => true, undefined, { revalidate: false });
}

const Context = createContext<SessionStateHandler>(undefined!);

export const useSessionContext = (): SessionStateHandler => useContext(Context);

/**
 * Creates the session in loggedIn state.
 * Infer the instance name based on the URL.
 * Create the instance of the merchant api http rest.
 * Returns API that handle impersonation.
 *
 * @param param0
 * @returns
 */
export const SessionContextProvider = ({
  children,
  // value,
}: {
  // value: MerchantUiSettings;
  children: ComponentChildren;
}): VNode => {
  const {
    lib: rootLib,
    config: rootConfig,
    url: merchantUrl,
  } = useMerchantApiContext();
  const [status, setStatus] = useState<"loggedIn" | "loggedOut">("loggedIn");
  const [currentConfig, setCurrentConfig] =
    useState<MerchantVersionResponse>();
  const SESSION_STATE_KEY = buildStorageKey(
    `merchant-session-${merchantUrl.pathname}`,
    codecForSessionState(),
  );

  const { value: state, update } = useLocalStorage(
    SESSION_STATE_KEY,
    defaultState(merchantUrl),
  );

  const currentInstance = inferInstanceName(state.backendUrl);

  let lib: MerchantLib;
  let config: MerchantVersionResponse;
  const doingImpersonation = state.backendUrl.href !== merchantUrl.href;
  if (doingImpersonation) {
    /**
     * FIXME: can't impersonate other than local instances
     */
    lib = rootLib.subInstanceApi(inferInstanceName(state.backendUrl));

    config = currentConfig ?? rootConfig;
  } else {
    lib = rootLib;
    config = rootConfig;
  }

  useEffect(() => {
    // FIXME: handle what happen if the subinstance /config
    // fails
    if (!doingImpersonation) return;
    lib.instance.getConfig().then((resp) => {
      if (resp.type === "ok") {
        setCurrentConfig(resp.body);
      }
    });
  }, [state.backendUrl.href]);

  const value: SessionStateHandler = {
    state: {
      backendUrl: state.backendUrl,
      token: state.token,
      impersonated: false, // doingImpersonation, FIXME: removing impersonation feature for 1.2
      instance: currentInstance,
      isAdmin: currentInstance === DEFAULT_ADMIN_USERNAME,
      status: status,
    },
    lib,
    config,
    logOut() {
      setStatus("loggedOut");
      update({
        backendUrl: merchantUrl,
        token: undefined,
        prevToken: undefined,
      });
      cleanAllCache();
    },
    deImpersonate() {
      cleanAllCache();
      update({
        backendUrl: merchantUrl,
        token: state.prevToken,
        prevToken: undefined,
      });
      setStatus("loggedIn");
    },
    impersonate(baseUrl) {
      /**
       * FIXME: can't impersonate other than local instances
       */
      update({
        backendUrl: baseUrl,
        token: undefined,
        prevToken: state.token,
      });
      setStatus("loggedIn");
      cleanAllCache();
    },
    logIn(username, token) {
      cleanAllCache();
      setStatus("loggedIn");
      let backendUrl: URL;
      if (currentInstance !== username) {
        backendUrl = new URL(rootLib.subInstanceApi(username).instance.baseUrl)
      } else {
        backendUrl = state.backendUrl
      }
      update({
        backendUrl,
        token,
        prevToken: state.prevToken,
      });
    },
    getInstanceForUsername(username:string) {
      return username !== currentInstance ? rootLib.subInstanceApi(username).instance : lib.instance
    }
  };

  return h(Context.Provider, {
    value,
    children,
  });
};
