import {
  IPublicClientApplication,
  createNestablePublicClientApplication
} from "@azure/msal-browser";
import { LoadStatus } from "../types/addInTypes";
import { AvailableScope } from "../types/msTypes/msGraphTypes";

/**
 * Handles authentication using MSAL (Microsoft Authentication Library) for acquiring OAuth2 tokens.
 * 
 * This class manages the initialization and state of the MSAL public client application,
 * and provides a method to acquire access tokens either silently or via a popup if necessary.
 * 
 * @remarks
 * - The client is initialized asynchronously in the constructor.
 * - The `getToken` method will throw an error if called before initialization is complete.
 * - Tokens and their expiration times are stored in `localStorage` under the keys `oauth2token` and `tokenExpire`.
 * 
 * @example
 * ```typescript
 * const authHandler = new AuthHandler();
 * const token = await authHandler.getToken(['user.read']);
 * ```
 */
class AuthHandler {
	client: IPublicClientApplication | null = null;
	readyStatus: LoadStatus = LoadStatus.NOT_LOADED

	constructor() {
		if (this.client) return;
		this.readyStatus = LoadStatus.LOADING;

		createNestablePublicClientApplication({
			auth: {
				clientId: "101dc183-f205-4125-b2a6-ce72ccb15669",
				authority: "https://login.microsoftonline.com/common",
			},
		}).then(client => {
			this.client = client;
			this.readyStatus = LoadStatus.LOADED;
		})
	}

	/**
	 * Acquires an OAuth2 access token for the specified scopes.
	 *
	 * Attempts to silently acquire a token using the authentication client. If silent acquisition fails
	 * (e.g., user interaction is required), falls back to an interactive popup. The acquired token and its
	 * expiration time are stored in localStorage.
	 *
	 * @param scopes - An array of scopes for which the access token is requested.
	 * @returns A promise that resolves to the acquired access token as a string.
	 * @throws Error if the authentication handler is not initialized.
	 */
	async getToken(scopes:AvailableScope[]): Promise<string> {
		if (!this.client || this.readyStatus != LoadStatus.LOADED)
			throw new Error('The auth handler is still not initialized');

		try {
			const res = await this.client.acquireTokenSilent({ scopes });
			localStorage.setItem('oauth2token', res.accessToken);
			localStorage.setItem('tokenExpire', res.expiresOn.toISOString())
			return res.accessToken;
		} catch (e:any) {
			// If acquire silent fails because user is not logged in display a modal.
			const res = await this.client.acquireTokenPopup({ scopes });
			localStorage.setItem('oauth2token', res.accessToken);
			localStorage.setItem('tokenExpire', res.expiresOn.toISOString())
	    	return res.accessToken;
		}
	}
}

const authHandler = new AuthHandler();

/**
 * Determines whether a new OAuth2 token is required.
 *
 * Checks if there is a current token stored in localStorage and whether it has expired.
 * Returns `true` if no token exists or if the token's expiration date has passed.
 *
 * @returns {boolean} `true` if a new token is needed, otherwise `false`.
 */
export const requiresNewToken = () => {
	const currentToken = localStorage.getItem('oauth2token');
	const tokenExpire = new Date(localStorage.getItem('tokenExpire'));

	if (!currentToken || (tokenExpire && new Date() > tokenExpire)) {
		return true;
	}
	return false;
}

/**
 * Retrieves a valid OAuth2 token for accessing mail resources.
 *
 * If the current token is invalid or expired (as determined by `requiresNewToken()`),
 * this function requests a new token with the 'Mail.Read' scope using `authHandler.getToken`.
 * Otherwise, it returns the existing token from local storage.
 *
 * @returns {Promise<string>} A promise that resolves to a valid OAuth2 token string.
 */
export const getValidToken = async(): Promise<string> => {
	const currentToken = localStorage.getItem('oauth2token');

	if (requiresNewToken()) {
		return await authHandler.getToken(['Mail.Read']);
	}

	return currentToken;
}