/*
 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 { Duration } from "@gnu-taler/taler-util";
import {
  AbsoluteTime,
  AccessToken,
  Codec,
  buildCodecForObject,
  buildCodecForUnion,
  codecForAbsoluteTime,
  codecForBoolean,
  codecForConstString,
  codecForString,
  codecOptionalDefault,
} from "@gnu-taler/taler-util";
import {
  buildStorageKey,
  useBankCoreApiContext,
  useLocalStorage,
} from "@gnu-taler/web-util/browser";
import { mutate } from "swr";
import { SESSION_DURATION } from "../pages/LoginForm.js";
import { createRFC8959AccessTokenEncoded } from "@gnu-taler/taler-util";
import { useEffect } from "preact/hooks";

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

export interface LoggedIn {
  status: "loggedIn";
  isUserAdministrator: boolean;
  username: string;
  token: AccessToken;
  expiration: AbsoluteTime;
}
interface Expired {
  status: "expired";
  isUserAdministrator: boolean;
  username: string;
  expiration: AbsoluteTime;
}
interface LoggedOut {
  status: "loggedOut";
}

export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> =>
  buildCodecForObject<LoggedIn>()
    .property("status", codecForConstString("loggedIn"))
    .property("username", codecForString())
    .property(
      "expiration",
      codecOptionalDefault(codecForAbsoluteTime, AbsoluteTime.now()),
    )
    .property("token", codecForString() as Codec<AccessToken>)
    .property("isUserAdministrator", codecForBoolean())
    .build("SessionState.LoggedIn");

export const codecForSessionStateExpired = (): Codec<Expired> =>
  buildCodecForObject<Expired>()
    .property("status", codecForConstString("expired"))
    .property("username", codecForString())
    .property(
      "expiration",
      codecOptionalDefault(codecForAbsoluteTime, AbsoluteTime.now()),
    )
    .property("isUserAdministrator", codecForBoolean())
    .build("SessionState.Expired");

export const codecForSessionStateLoggedOut = (): Codec<LoggedOut> =>
  buildCodecForObject<LoggedOut>()
    .property("status", codecForConstString("loggedOut"))
    .build("SessionState.LoggedOut");

export const codecForSessionState = (): Codec<SessionState> =>
  buildCodecForUnion<SessionState>()
    .discriminateOn("status")
    .alternative("loggedIn", codecForSessionStateLoggedIn())
    .alternative("loggedOut", codecForSessionStateLoggedOut())
    .alternative("expired", codecForSessionStateExpired())
    .build("SessionState");

export const defaultState: SessionState = {
  status: "loggedOut",
};

export interface SessionStateHandler {
  state: SessionState;
  logOut(): void;
  expired(): void;
  logIn(info: {
    username: string;
    token: AccessToken;
    expiration: AbsoluteTime;
  }): void;
}

const SESSION_STATE_KEY = buildStorageKey(
  "bank-session",
  codecForSessionState(),
);

/**
 * Return getters and setters for
 * login credentials and backend's
 * base URL.
 */
export function useSessionState(): SessionStateHandler {
  const { value: state, update } = useLocalStorage(
    SESSION_STATE_KEY,
    defaultState,
  );

  useEffect(() => {
    if (
      state.status === "loggedIn" &&
      AbsoluteTime.isExpired(state.expiration)
    ) {
      const nextState: SessionState = {
        status: "expired",
        username: state.username,
        expiration: state.expiration,
        isUserAdministrator: state.username === "admin",
      };
      update(nextState);
    }
  });

  return {
    state,
    logOut() {
      update(defaultState);
    },
    expired() {
      if (state.status === "loggedOut") return;
      const nextState: SessionState = {
        status: "expired",
        username: state.username,
        expiration: state.expiration,
        isUserAdministrator: state.username === "admin",
      };
      update(nextState);
    },
    logIn(info) {
      // admin is defined by the username
      const nextState: SessionState = {
        status: "loggedIn",
        ...info,
        isUserAdministrator: info.username === "admin",
      };
      update(nextState);
      cleanAllCache();
    },
  };
}

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

/**
 * Loads the session from local storage
 * Sets a timeout before the session expires
 * Makes a request to refresh the session
 * Saves new session
 */
export function useRefreshSessionBeforeExpires() {
  const session = useSessionState();

  const {
    lib: { bank },
  } = useBankCoreApiContext();

  const refreshSession =
    session.state.status !== "loggedIn" ||
    session.state.expiration.t_ms === "never"
      ? undefined
      : session.state;

  useEffect(() => {
    if (!refreshSession) return;
    /**
     * we need to wait before refreshing the session. Waiting too much and the token will
     * be expired. So 20% before expiration should be close enough.
     */
    const timeLeftBeforeExpiration = Duration.getRemaining(
      refreshSession.expiration,
    );
    const refreshWindow = Duration.multiply(
      Duration.fromTalerProtocolDuration(SESSION_DURATION),
      0.2,
    );
    if (
      timeLeftBeforeExpiration.d_ms === "forever" ||
      refreshWindow.d_ms === "forever"
    )
      return;
    const remain = Math.max(
      timeLeftBeforeExpiration.d_ms - refreshWindow.d_ms,
      0,
    );

    const timeoutId = setTimeout(async () => {
      const result = await bank.createAccessToken(
        refreshSession.username,
        { type: "bearer", accessToken: refreshSession.token },
        {
          scope: "readwrite",
          duration: SESSION_DURATION,
          refreshable: true,
        },
      );
      if (result.type === "fail") {
        console.log(
          `could not refresh session ${result.case}: ${JSON.stringify(result)}`,
        );
        return;
      }
      session.logIn({
        username: refreshSession.username,
        token: createRFC8959AccessTokenEncoded(result.body.access_token),
        expiration: AbsoluteTime.fromProtocolTimestamp(result.body.expiration),
      });
    }, remain);
    return () => {
      clearTimeout(timeoutId);
    };
  }, [refreshSession]);
}
