import sha256 from 'crypto-js/sha256';
import ConfigAccumulator from '@/utils/config/ConfigAccumalator';
import ErrorHandler from '@/utils/logging/ErrorHandler';
import { cookieKeys, localStorageKeys, userStateEnum } from '@/utils/config/defaults';
import getApiAuthHeader from '@/utils/auth/getApiAuthHeader';
import { isKeycloakEnabled } from '@/utils/auth/KeycloakAuth';
import { isOidcEnabled } from '@/utils/auth/OidcAuth';

/* Uses config accumulator to get and return app config */
const getAppConfig = () => {
  const Accumulator = new ConfigAccumulator();
  return Accumulator.appConfig() || {};
};

/**
 * Called when the user is still using array for users, prints warning
 * This was a breaking change, implemented in V 1.6.5
 * Support for old user structure will be removed in V 1.7.0
 */
const printWarning = () => {
  ErrorHandler('From V 1.6.5 onwards, the structure of the users object has changed.');
};

/* Returns array of users from appConfig.auth, if available, else an empty array */
const getUsers = () => {
  const appConfig = getAppConfig();
  const auth = appConfig.auth || {};
  // Check if the user is still using previous schema type
  if (Array.isArray(auth)) {
    printWarning(); // Print warning message
    return []; // Support for old data structure now removed
  }
  // Otherwise, return the users array, if available

  const users = auth.users ? [...auth.users] : [];
  if ((isOidcEnabled() || isKeycloakEnabled()) && localStorage[localStorageKeys.USERNAME]) {
    users.push({
      user: localStorage[localStorageKeys.USERNAME],
      type: localStorage[localStorageKeys.ISADMIN] === 'true' ? 'admin' : 'normal',
    });
  }

  return users;
};

/**
 * Generates a 1-way hash, in order to be stored in local storage for authentication
 * @param {String} user The username of user
 * @returns {String} The hashed token
 */
const generateUserToken = (user) => {
  if (!user.user || (!user.hash && !user.password)) {
    ErrorHandler('Invalid user object. Must have `user` and either a `hash` or `password` param');
    return undefined;
  }
  const envKey = (user.password || '').replace(/^VUE_APP_/, 'VITE_APP_');
  const passHash = user.hash || sha256(import.meta.env[envKey]).toString().toUpperCase();
  const strAndUpper = (input) => input.toString().toUpperCase();
  const sha = sha256(strAndUpper(user.user) + strAndUpper(passHash));
  return strAndUpper(sha);
};

export const getCookieToken = () => {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${cookieKeys.AUTH_TOKEN}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
  return null;
};

export const makeBasicAuthHeaders = () => {
  if (getApiAuthHeader()) return {};
  const token = getCookieToken();
  const bearerAuth = (token && token.length > 5) ? `Bearer ${token}` : null;

  const username = import.meta.env.VITE_APP_BASIC_AUTH_USERNAME
    || localStorage[localStorageKeys.USERNAME]
    || 'user';
  const password = import.meta.env.VITE_APP_BASIC_AUTH_PASSWORD || bearerAuth;
  const basicAuth = `Basic ${btoa(`${username}:${password}`)}`;

  return password ? { headers: { Authorization: basicAuth } } : {};
};

/**
 * Checks if the user is currently authenticated
 * @returns {Boolean} Will return true if the user is logged in, else false
 */
export const isLoggedIn = () => {
  const users = getUsers();
  const cookieToken = getCookieToken();

  if (isOidcEnabled() || isKeycloakEnabled()) {
    const username = localStorage[localStorageKeys.USERNAME]; // Get username
    if (!username) return false; // No username
    return users.some((user) => (
      user.user === username || generateUserToken(user) === cookieToken
    ));
  }

  return users.some((user) => {
    if (generateUserToken(user) !== cookieToken) return false;
    localStorage.setItem(localStorageKeys.USERNAME, user.user);
    return true;
  });
};

/* Returns true if authentication is enabled */
export const isAuthEnabled = () => {
  const users = getUsers();
  return (users.length > 0);
};

/* Returns true if guest access is enabled */
export const isGuestAccessEnabled = () => {
  const appConfig = getAppConfig();
  if (appConfig.auth && typeof appConfig.auth === 'object') {
    return appConfig.auth.enableGuestAccess || false;
  }
  return false;
};

/**
 * Checks credentials entered by the user against those in the config
 * Returns an object containing a boolean indicating success/ failure
 * along with a message outlining what's not right
 * @param {String} username The username entered by the user
 * @param {String} pass The password entered by the user
 * @param {String[]} users An array of valid user objects
 * @param {Object} messages A static message template object
 * @returns {Object} An object containing a boolean result and a message
 */
export const checkCredentials = (username, pass, users, messages) => {
  let response; // Will store an object containing boolean and message
  if (!username) {
    response = { correct: false, msg: messages.missingUsername };
  } else if (!pass) {
    response = { correct: false, msg: messages.missingPassword };
  } else {
    users.forEach((user) => {
      if (user.user.toLowerCase() === username.toLowerCase()) { // User found
        if (user.password) {
          const credEnvKey = user.password.replace(/^VUE_APP_/, 'VITE_APP_');
          if (!user.password.startsWith('VUE_APP_') && !user.password.startsWith('VITE_APP_')) {
            ErrorHandler('Invalid password format. Please use VITE_APP_ prefix');
            response = { correct: false, msg: messages.incorrectPassword };
          } else if (!import.meta.env[credEnvKey]) {
            ErrorHandler(`Missing environmental variable for ${credEnvKey}`);
            response = { correct: false, msg: messages.incorrectPassword };
          } else if (import.meta.env[credEnvKey] === pass) {
            response = { correct: true, msg: messages.successMsg };
          } else {
            response = { correct: false, msg: messages.incorrectPassword };
          }
        } else if (user.hash && user.hash.toLowerCase() === sha256(pass).toString().toLowerCase()) {
          response = { correct: true, msg: messages.successMsg }; // Password is correct
        } else { // User found, but password is not a match
          response = { correct: false, msg: messages.incorrectPassword };
        }
      }
    });
  }
  return response || { correct: false, msg: messages.incorrectUsername };
};

/**
 * Sets the cookie value in order to login the user locally
 * @param {String} username - The users username
 * @param {String} pass - Password, not yet hashed
 * @param {Number} timeout - A desired timeout for the session, in ms
 */
export const login = (username, pass, timeout) => {
  const now = new Date();
  const expiry = new Date(now.setTime(now.getTime() + timeout)).toGMTString();
  const userObject = { user: username, hash: sha256(pass).toString().toLowerCase() };
  document.cookie = `${cookieKeys.AUTH_TOKEN}=${generateUserToken(userObject)};`
    + `${timeout > 0 ? `expires=${expiry}` : ''}`;
  localStorage.setItem(localStorageKeys.USERNAME, username);
};

/**
 * Removed the browsers' cookie, causing user to be logged out
 */
export const logout = () => {
  document.cookie = `${cookieKeys.AUTH_TOKEN}=null`;
  localStorage.removeItem(localStorageKeys.USERNAME);
};

/* Returns the URL to redirect to after logout if set */
export const getLogoutRedirectUrl = () => {
  const auth = getAppConfig().auth || {};
  return auth.logoutRedirectUrl || null;
};

/**
 * If correctly logged in as a valid, authenticated user,
 * then returns the user object for the current user
 * If not logged in, will return false
 * */
export const getCurrentUser = () => {
  if (!isLoggedIn()) return false; // User not logged in
  const username = localStorage[localStorageKeys.USERNAME]; // Get username
  if (!username) return false; // No username
  let foundUserObject = false; // Value to return
  getUsers().forEach((user) => {
    // If current logged-in user found, then return that user
    if (user.user.toLowerCase() === username.toLowerCase()) foundUserObject = user;
  });
  return foundUserObject;
};

/**
 * Checks if the user is viewing the dashboard as a guest
 * Returns true if guest mode enabled, and user not logged in
 * */
export const isLoggedInAsGuest = () => {
  const guestEnabled = isGuestAccessEnabled();
  const loggedIn = isLoggedIn();
  return guestEnabled && !loggedIn;
};

/**
 * Checks if the current user has admin privileges.
 * If no users are set up, then function will always return true
 * But if auth is configured, then will verify user is correctly
 * logged in and then check weather they are of type admin, and
 * return false if any conditions fail
 * @returns {Boolean} - True if admin privileges
 */
export const isUserAdmin = () => {
  const users = getUsers();
  if (users.length === 0) return true; // Authentication not setup
  if (!isLoggedIn()) return false; // Auth setup, but not signed in as a valid user
  const currentUser = localStorage[localStorageKeys.USERNAME];
  return users.some((user) => (
    user.user.toLowerCase() === currentUser.toLowerCase() && user.type === 'admin'
  ));
};

/**
  * Determines which button should display, based on the user type
  * 0 = Auth not configured (don't show anything)
  * 1 = Auth configured, and user logged in (show logout button)
  * 2 = Auth configured, guest access enabled, not logged in (show login)
  * Note that if auth is enabled, but not guest access, and user not logged in,
  * then they will never be able to view the homepage, so no button needed
  */
export const getUserState = () => {
  const {
    notConfigured,
    loggedIn,
    guestAccess,
    keycloakEnabled,
    oidcEnabled,
  } = userStateEnum; // Numeric enum options
  if (isKeycloakEnabled()) {
    if (isLoggedIn()) return keycloakEnabled;
    if (isGuestAccessEnabled()) return guestAccess;
    return keycloakEnabled; // not logged in, no guest — preserve login button
  }
  if (isOidcEnabled()) {
    if (isLoggedIn()) return oidcEnabled; // OIDC user logged in (show OIDC logout button)
    if (isGuestAccessEnabled()) return guestAccess; // Guest viewing with OIDC enabled
    return oidcEnabled; // OIDC enabled, not logged in, no guest access
  }
  if (!isAuthEnabled()) return notConfigured; // No auth enabled
  if (isLoggedIn()) return loggedIn; // User is logged in
  if (isGuestAccessEnabled()) return guestAccess; // Guest is viewing
  return notConfigured;
};
