/**
 * @file authService.ts - Authentication service with wrapper functions for handling IWS5 API authentication.
 */

import {
  SESSION_STORAGE_USER,
  SESSION_STORAGE_TOKEN,
  SESSION_STORAGE_REFRESH,
  IWS5_JWT_ROLE_CLAIMS_PROPERTY_KEY,
  PD_USER_GROUP_NAME,
} from '../constants.ts';
import { jwtDecode } from 'jwt-decode';
import i18next from 'i18next';
import { toast } from 'react-toastify';

/** Base URL for IWS5 authentication API endpoint; derived from global config object */
const authenticationEndpointUrl = `${window.__APP_CONFIG__?.IWS5_API_URL}/api/Authentication`;

/**
 * Payload sent to the IWS5 login endpoint.
 */
export type LogInRequest = {
  userName: string;
  password: string;

  /** Active Directory domain; null when domain authentication is not used */
  domain: string | null;
};

/**
 * Response returned by the IWS5 login and refresh endpoints.
 */
export type LogInResponse = {
  userName: string;

  /** Short-lived JWT access token used for API authorization */
  token: string;

  /** Long-lived JWT token used to obtain a new access token without re-authenticating */
  refreshToken: string;
};

/**
 * Payload sent to the IWS5 token refresh endpoint.
 */
export type RefreshTokenRequest = {
  userName: string;
  refreshToken: string;
};

/**
 * Payload sent to the IWS5 logout endpoint.
 */
export type LogOutRequest = {
  userName: string;
};

/**
 * Response returned by the IWS5 logout endpoint.
 */
export type LogOutResponse = {
  logOutSuccessful: boolean;
};

/**
 * Discriminated union of messages exchanged between browser tabs via the BroadcastChannel API to keep authentication sessions in sync.
 *
 * - `REQUEST_SESSION` -- A newly opened tab requests the current session from sibling tabs
 * - `SESSION_RECEIVED` -- A sibling tab responds with its active session credentials
 * - `SESSION_UPDATED` -- Broadcast after a fresh login so all tabs adopt the new session
 * - `SESSION_REFRESHED` -- Broadcast after a token refresh so all tabs receive the new tokens
 * - `OWNERSHIP_CLAIMED` -- A tab announces it has taken ownership of token refresh duties
 * - `IDLE_LOGOUT_NEEDED` -- Signals that the user has been idle and all tabs should log out
 * - `LOGOUT` -- Signals that the user has explicitly logged out
 */
export type SessionSyncMessage =
  | { type: 'REQUEST_SESSION' }
  | {
      type: 'SESSION_RECEIVED';
      payload: LogInResponse;
    }
  | { type: 'SESSION_UPDATED'; payload: LogInResponse }
  | { type: 'SESSION_REFRESHED'; payload: LogInResponse }
  | { type: 'OWNERSHIP_CLAIMED' }
  | { type: 'IDLE_LOGOUT_NEEDED' }
  | { type: 'LOGOUT' };

/**
 * Returns the API username stored in the browser's session storage.
 *
 * @returns The API username
 */
export function getUserName(): string | null {
  return sessionStorage.getItem(SESSION_STORAGE_USER);
}

/**
 * Returns the API access token stored in the browser's session storage.
 *
 * @returns The API access token
 */
export function getAccessToken(): string | null {
  return sessionStorage.getItem(SESSION_STORAGE_TOKEN);
}

/**
 * Returns the API refresh token stored in the browser's session storage.
 *
 * @returns The API refresh token
 */
export function getRefreshToken(): string | null {
  return sessionStorage.getItem(SESSION_STORAGE_REFRESH);
}

/**
 * Updates the API username and tokens stored in the browser's session storage.
 *
 * @param userName The API username
 * @param accessToken The API access token
 * @param refreshToken The API refresh token
 */
export function setAuthKeys({
  userName,
  token,
  refreshToken,
}: LogInResponse): void {
  sessionStorage.setItem(SESSION_STORAGE_USER, userName);
  sessionStorage.setItem(SESSION_STORAGE_TOKEN, token);
  sessionStorage.setItem(SESSION_STORAGE_REFRESH, refreshToken);
}

/**
 * Clears the API username and tokens stored in the browser's session storage.
 */
export function clearAuthKeys(preserveUsername: boolean = false): void {
  if (!preserveUsername) {
    sessionStorage.removeItem(SESSION_STORAGE_USER);
  }
  sessionStorage.removeItem(SESSION_STORAGE_TOKEN);
  sessionStorage.removeItem(SESSION_STORAGE_REFRESH);
}

/**
 * Performs an HTTP request to the API login endpoint and updates the stored API tokens in the browser's session storage.
 *
 * @param userName The username of the user logging in
 * @param password The password of the user logging in
 * @param domain The domain of the user logging in
 */
export async function logIn({
  userName,
  password,
  domain,
}: LogInRequest): Promise<void> {
  clearAuthKeys();

  let response: Response;
  try {
    response = await fetch(`${authenticationEndpointUrl}/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userName, password, domain }),
    });
  } catch (error) {
    const errorMessage = (error as Error).message;
    toast.error(errorMessage);
    throw new Error(errorMessage);
  }

  if (!response.ok) {
    const errorMessage =
      `${i18next.t('services.auth_service.errors.login.failed')}: ` +
      (await response.json()).error;
    toast.error(errorMessage);
    throw new Error(errorMessage);
  }

  const data = (await response.json()) as LogInResponse;
  setAuthKeys(data);

  // Check user rights
  const decodedToken = jwtDecode(data.token);
  if (Object.hasOwn(decodedToken, IWS5_JWT_ROLE_CLAIMS_PROPERTY_KEY)) {
    const roleClaims: string[] =
      // @ts-expect-error JWT role claims key
      decodedToken[IWS5_JWT_ROLE_CLAIMS_PROPERTY_KEY];
    if (!roleClaims.includes(PD_USER_GROUP_NAME)) {
      logOut({ userName: data.userName });

      const errorMessage = i18next.t(
        'services.auth_service.errors.login.no_pd_user_group',
        {
          group_name: PD_USER_GROUP_NAME,
        }
      );
      toast.error(errorMessage);
      throw new Error(errorMessage);
    }
  } else {
    logOut({ userName: data.userName });

    const errorMessage = i18next.t(
      'services.auth_service.errors.login.no_pd_groups'
    );
    toast.error(errorMessage);
    throw new Error(errorMessage);
  }
}

/**
 * Performs an HTTP request to the API token refresh endpoint and updates the stored API tokens in the browser's session storage.
 *
 * @param userName The username of the user whose access token is being refreshed
 * @param refreshToken The refresh token of the user whose access token is being refreshed
 * @returns The new API access token
 */
export async function refreshAccessToken({
  userName,
  refreshToken,
}: RefreshTokenRequest): Promise<string> {
  let response: Response;
  try {
    response = await fetch(`${authenticationEndpointUrl}/refresh`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({ userName, refreshToken }),
    });
  } catch (error) {
    const errorMessage = (error as Error).message;
    toast.error(errorMessage);
    throw new Error(errorMessage);
  }

  if (!response.ok) {
    const errorMessage =
      `${i18next.t('services.auth_service.errors.refresh_access_token.failed')}: ` +
      (await response.json()).error;
    toast.error(errorMessage);
    throw new Error(errorMessage);
  }

  const data = (await response.json()) as LogInResponse;
  setAuthKeys(data);

  return data.token;
}

/**
 * Performs an HTTP request to the API logout endpoint and clears the stored API tokens in the browser's session storage.
 *
 * @param userName The username of the user logging out
 * @returns Success of logout operation
 */
export async function logOut({ userName }: LogOutRequest): Promise<boolean> {
  let response: Response;
  try {
    response = await fetch(`${authenticationEndpointUrl}/logout`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${getAccessToken()}`,
      },
      body: JSON.stringify({ userName }),
    });
  } catch (error) {
    const errorMessage = (error as Error).message;
    toast.error(errorMessage);
    throw new Error(errorMessage);
  }

  if (!response.ok) {
    const errorMessage =
      `${i18next.t('services.auth_service.errors.logout.failed')}: ` +
      (await response.json()).error;
    toast.error(errorMessage);
    throw new Error(errorMessage);
  }

  const data = (await response.json()) as LogOutResponse;
  clearAuthKeys();

  return data.logOutSuccessful;
}
