import React, { useContext, useEffect, useState } from "react";
import auth from "../../services/auth";
import getTimerWorker from "../../services/getTimerWorker";
import { parsePayload } from "../../utils/jwt";
import { getGroups } from "../../utils/roles";

const UserContext = React.createContext();

export const UserProvider = ({ children }) => {
  const [fetching, setFetching] = useState(false);
  const [token, setToken] = useState(null);
  const [groups, setGroups] = useState(null);
  const [error, setError] = useState(null);
  let timer;

  useEffect(() => {
    try {
      if (!localStorage) return;
      const t = JSON.parse(localStorage.getItem("token"));
      if (!t) return;
      if (!t.idToken || !t.idToken.jwtToken) throw new Error("Invalid token");
      setToken(t);
    } catch (e) {
      document.location = signOut();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (!token) return;
    verifyToken();
    return () => {
      if (timer) timer.terminate();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [token]);

  const refreshTokens = async () => {
    if (!token || !token.refreshToken || !token.refreshToken.token) return null;
    return refresh(token.refreshToken.token);
  };

  const startSessionTimer = () => {
    if (!token || !token.idToken || !token.idToken.jwtToken) {
      throw new Error("No id token");
    }
    const payload = parsePayload(token.idToken.jwtToken);
    if (!payload || !payload.exp) throw new Error("Bad id token payload");
    const duration = 1000 * payload.exp - new Date().getTime();
    if (!timer) {
      timer = getTimerWorker();
      timer.onMessage(async (e) => {
        if (e.data === "timeout") {
          const t = await refreshTokens();
          if (t && !t.error) return;
          document.location = signOut();
        }
      });
    }
    timer.start(duration);
  };

  const verifyToken = async () => {
    try {
      const {
        idToken: { jwtToken: idToken },
      } = token;
      const result = await auth.verify(idToken);

      if (!result || !result.valid) {
        const t = await refreshTokens();
        if (!t || t.error) throw new Error("Unable to refresh token");
      }

      setGroups(getGroups(idToken));
      startSessionTimer();
    } catch (e) {
      setError(`Verify error. ${e.message}`);
      document.location = signOut();
    }
  };

  const signIn = async (code) => {
    let result = null;

    try {
      if (!code) return;

      setFetching(true);
      setError(null);

      result = await auth.signIn(code);
      if (result.message) {
        throw new Error(result.message);
      }

      signedIn(result);
    } catch (err) {
      setError(`Sign in error. ${err.message}`);
    } finally {
      setFetching(false);
    }

    return result;
  };

  const signOut = () => {
    setGroups(null);
    setToken(null);
    if (localStorage) {
      localStorage.removeItem("token");
    }
    return getLogoutURL();
  };

  const signedIn = (value) => {
    setToken(value);
    if (!localStorage || !value || !value.idToken) return;
    localStorage.setItem("token", JSON.stringify(value));
  };

  const refresh = async (value) => {
    let result = null;

    try {
      if (!value) {
        throw new Error("Token required");
      }
      setFetching(true);
      setError(null);

      result = await auth.refresh(value);

      if (result.message) {
        throw new Error(result.message);
      }
      signedIn(result);
    } catch (err) {
      setError(`Refresh error. ${err.message}`);
    } finally {
      setFetching(false);
    }

    return result;
  };

  const getAuthorizeURL = () => auth.ssoAuthorize();
  const getLogoutURL = () => auth.ssoLogout();

  return (
    <UserContext.Provider
      value={{
        error,
        fetching,
        groups,
        token,
        getAuthorizeURL,
        getLogoutURL,
        setError,
        signIn,
        signOut,
        refresh,
      }}
    >
      {children}
    </UserContext.Provider>
  );
};

export const useUserContext = () => useContext(UserContext);
