import dotenv from 'dotenv';
import { keycloak } from '../../keycloak.js';
import log4js from 'log4js';
import axios from 'axios';
import { userModel } from '../../database/schemas/management/user.schema.js';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import NodeCache from 'node-cache';
import jwt from 'jsonwebtoken';
dotenv.config();

const logger = log4js.getLogger('Auth');
logger.level = process.env.LOG_LEVEL;

// Initialize NodeCache with 5-minute TTL for token-based user lookup
const tokenUserCache = new NodeCache({ stdTTL: 300 }); // 300 seconds = 5 minutes

// Cache event listeners for monitoring
tokenUserCache.on('expired', (key, value) => {
  logger.debug(`Token user cache entry expired: ${key.substring(0, 20)}...`);
});

tokenUserCache.on('flush', () => {
  logger.info('Token user cache flushed');
});

const loginTokenRequests = new Map();

// Token-based user lookup function with caching
const lookupUserByToken = async (token) => {
  try {
    // Check cache first
    const cachedUser = tokenUserCache.get(token);
    if (cachedUser) {
      logger.trace(`User found in token cache for token: ${token.substring(0, 20)}...`);
      return cachedUser;
    }

    // If not in cache, decode token and lookup user
    logger.trace(`User not in token cache, decoding token: ${token.substring(0, 20)}...`);
    const decodedToken = jwt.decode(token);

    if (!decodedToken || !decodedToken.preferred_username) {
      logger.trace('Invalid token or missing preferred_username');
      return null;
    }

    // Query database for user
    const user = await userModel.findOne({ username: decodedToken.preferred_username });

    if (user) {
      // Store in cache using token as key
      tokenUserCache.set(token, user);
      logger.trace(`User stored in token cache for token: ${token.substring(0, 20)}...`);
      return user;
    }

    logger.warn(`User not found in database for username: ${decodedToken.preferred_username}`);
    return null;
  } catch (error) {
    logger.error(`Error looking up user by token:`, error.message);
    return null;
  }
};

// Cache management utility functions
const clearTokenUserCache = () => {
  tokenUserCache.flushAll();
  logger.info('Token user cache cleared');
};

const getTokenUserCacheStats = () => {
  return tokenUserCache.getStats();
};

const removeUserFromTokenCache = (token) => {
  tokenUserCache.del(token);
  logger.debug(`User removed from token cache for token: ${token.substring(0, 20)}...`);
};

// Login handler
export const loginRouteHandler = (req, res, redirectType = 'web') => {
  // Get the redirect URL from form data or default to production overview
  const redirectUrl = req.query.redirect_uri || '/production/overview';

  // Store the original URL to redirect after login
  const authUrl = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`;
  const callBackState = `/auth/${redirectType}/callback`;
  const callbackUrl = `${process.env.APP_URL_API}${callBackState}`;
  const state = encodeURIComponent(redirectUrl);

  logger.warn(req.query.redirect_uri);

  res.redirect(
    `${authUrl}?client_id=${process.env.KEYCLOAK_CLIENT_ID}&redirect_uri=${callbackUrl}&response_type=code&scope=openid&state=${state}`
  );
};

// Function to fetch user from Keycloak and store in database and session
const fetchAndStoreUser = async (req, token) => {
  const userInfoUrl = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/userinfo`;

  try {
    const response = await axios.post(
      userInfoUrl,
      new URLSearchParams({
        client_id: process.env.KEYCLOAK_CLIENT_ID,
        client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
      }),
      {
        headers: {
          Authorization: `Bearer ${token.access_token}`,
        },
      }
    );

    const userInfo = {
      roles: token.realm_access?.roles || [],
      username: response.data.preferred_username,
      email: response.data.email,
      name: response.data.name,
      firstName: response.data.given_name,
      lastName: response.data.family_name,
    };

    // Create or update user in database
    const user = await createOrUpdateUser(userInfo);
    const fullUserInfo = { ...userInfo, _id: user._id };

    // Store user info in session
    req.session.user = fullUserInfo;

    return fullUserInfo;
  } catch (error) {
    logger.error('Error fetching and storing user:', error);
    throw error;
  }
};

// Function to exchange authorization code for tokens, fetch user, and set session
export const loginTokenRouteHandler = async (req, res, redirectType = 'web') => {
  const code = req.query.code;
  if (!code) {
    return res.status(400).json({ error: 'Authorization code missing' });
  }

  try {
    // If a request for this code is already in progress, wait for it
    if (loginTokenRequests.has(code)) {
      const tokenData = await loginTokenRequests.get(code);
      return res.status(200).json(tokenData);
    }

    // Otherwise, start the request and store the promise
    const tokenPromise = (async () => {
      const callBackState = `/auth/${redirectType}/callback`;
      const callbackUrl = `${process.env.APP_URL_API}${callBackState}`;
      const tokenUrl = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`;

      const response = await axios.post(
        tokenUrl,
        new URLSearchParams({
          grant_type: 'authorization_code',
          client_id: process.env.KEYCLOAK_CLIENT_ID,
          client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
          code: code,
          redirect_uri: callbackUrl,
        }).toString(),
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
        }
      );
      const tokenData = {
        access_token: response.data.access_token,
        refresh_token: response.data.refresh_token,
        id_token: response.data.id_token,
        expires_at: new Date().getTime() + response.data.expires_in * 1000,
      };

      req.session['keycloak-token'] = tokenData;
      // Fetch and store user data, set session
      const userData = await fetchAndStoreUser(req, tokenData);
      const userAndTokenData = { ...tokenData, ...userData };

      return userAndTokenData;
    })();

    loginTokenRequests.set(code, tokenPromise);
    const userAndTokenData = await tokenPromise;
    res.status(200).json(userAndTokenData);
  } catch (err) {
    var error = err?.response?.data?.error_description || err.message;
    res.status(err?.status || 500).json({ error: error });
  }
};

// Login callback handler
export const loginCallbackRouteHandler = async (req, res, redirectType = 'web') => {
  // Don't use keycloak.protect() here as it expects an already authenticated session

  // Extract the code and state from the query parameters
  const code = req.query.code;
  const state = req.query.state || '/production/overview';

  if (!code) {
    return res.status(400).send('Authorization code missing');
  }

  var appUrl;
  switch (redirectType) {
    case 'web':
      appUrl = process.env.APP_URL_CLIENT || 'http://localhost:3000';
      break;
    case 'app-scheme':
      appUrl = 'farmcontrol://app';
      break;
    case 'app-localhost':
      appUrl = process.env.APP_DEV_AUTH_CLIENT || 'http://localhost:3500';
      break;
    default:
      appUrl = process.env.APP_URL_CLIENT || 'http://localhost:3000';
      break;
  }
  const redirectUriRaw = `${appUrl}${state}`;
  let redirectUri;
  try {
    // Try to parse as a URL (works for http/https)
    const url = new URL(redirectUriRaw);
    url.searchParams.set('authCode', code);
    redirectUri = url.toString();
  } catch (e) {
    // Fallback for custom schemes (e.g., farmcontrol://app)
    if (redirectUriRaw.includes('?')) {
      redirectUri = `${redirectUriRaw}&authCode=${encodeURIComponent(code)}`;
    } else {
      redirectUri = `${redirectUriRaw}?authCode=${encodeURIComponent(code)}`;
    }
  }
  // Save session and redirect to the original URL
  req.session.save(async () => {
    if (redirectType == 'app-scheme') {
      // Read HTML template and inject redirectUri
      const templatePath = resolve(process.cwd(), 'src/services/misc/applaunch.html');
      let html = readFileSync(templatePath, 'utf8');
      html = html.replace('__REDIRECT_URI__', redirectUri);
      res.send(html);
    } else {
      res.redirect(redirectUri);
    }
  });
};

// Function to create or update user
const createOrUpdateUser = async (userInfo) => {
  try {
    const { username, email, name, firstName, lastName } = userInfo;

    // Find existing user by username
    const existingUser = await userModel.findOne({ username });

    if (existingUser) {
      // Check if any values have changed
      const hasChanges =
        existingUser.email !== email ||
        existingUser.name !== name ||
        existingUser.firstName !== firstName ||
        existingUser.lastName !== lastName;

      if (hasChanges) {
        // Update existing user only if there are changes
        const updateData = {
          email,
          name,
          firstName,
          lastName,
          updatedAt: new Date(),
        };

        await userModel.updateOne({ username }, { $set: updateData });

        // Fetch the updated user to return
        return await userModel.findOne({ username });
      }

      return existingUser;
    } else {
      // Create new user
      const newUser = new userModel({
        username,
        email,
        name,
        firstName,
        lastName,
      });

      await newUser.save();
      return newUser;
    }
  } catch (error) {
    logger.error('Error creating/updating user:', error);
    throw error;
  }
};

export const userRouteHandler = (req, res) => {
  if (req.session && req.session.user) {
    res.json(req.session.user);
  } else {
    res.status(401).json({ error: 'Not authenticated' });
  }
};

// Logout handler
export const logoutRouteHandler = (req, res) => {
  // Get the redirect URL from query or default to login page
  const redirectUrl = req.query.redirect_uri || '/login';

  // Destroy the session
  req.session.destroy((err) => {
    if (err) {
      logger.error('Error destroying session:', err);
      return res.status(500).json({ error: 'Failed to logout' });
    }

    // Construct the Keycloak logout URL with the redirect URI
    const logoutUrl = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/logout`;
    const encodedRedirectUri = encodeURIComponent(`${process.env.APP_URL_CLIENT}${redirectUrl}`);

    // Redirect to Keycloak logout with the redirect URI
    res.redirect(
      `${logoutUrl}?client_id=${process.env.KEYCLOAK_CLIENT_ID}&post_logout_redirect_uri=${encodedRedirectUri}`
    );
  });
};

// Token validation - protected route middleware
export const validateTokenMiddleware = keycloak.protect();

// Check if user has a specific role
export const hasRole = (role) => {
  return keycloak.protect((token) => {
    return token && token.hasRole(role);
  });
};

// Get user info from the token
export const getUserInfoHandler = (req, res) => {
  if (req.kauth && req.kauth.grant) {
    const token = req.kauth.grant.access_token;
    const userInfo = {
      id: token.content.sub,
      email: token.content.email,
      name:
        token.content.name ||
        `${token.content.given_name || ''} ${token.content.family_name || ''}`.trim(),
      roles: token.content.realm_access?.roles || [],
    };
    return res.json(userInfo);
  }
  return res.status(401).json({ error: 'Not authenticated' });
};

// Register route - Since we're using Keycloak, registration should be handled there
// This endpoint will redirect to Keycloak's registration page
export const registerRouteHandler = (req, res) => {
  const registrationUrl = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/registrations`;
  const redirectUri = encodeURIComponent(process.env.APP_URL_CLIENT + '/auth/login');

  res.redirect(
    `${registrationUrl}?client_id=${process.env.KEYCLOAK_CLIENT_ID}&redirect_uri=${redirectUri}`
  );
};

// Forgot password handler - redirect to Keycloak's reset password page
export const forgotPasswordRouteHandler = (req, res) => {
  const resetUrl = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/login-actions/reset-credentials`;
  const redirectUri = encodeURIComponent(process.env.APP_URL_CLIENT + '/auth/login');

  res.redirect(
    `${resetUrl}?client_id=${process.env.KEYCLOAK_CLIENT_ID}&redirect_uri=${redirectUri}`
  );
};

// Refresh token handler
export const refreshTokenRouteHandler = (req, res) => {
  if (
    !req.session ||
    !req.session['keycloak-token'] ||
    !req.session['keycloak-token'].refresh_token
  ) {
    return res.status(401).json({ error: 'No refresh token available' });
  }

  const refreshToken = req.session['keycloak-token'].refresh_token;
  const tokenUrl = `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`;

  axios
    .post(
      tokenUrl,
      new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: process.env.KEYCLOAK_CLIENT_ID,
        client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
        refresh_token: refreshToken,
      }).toString(),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      }
    )
    .then((response) => {
      // Update session with new tokens
      req.session['keycloak-token'] = {
        ...req.session['keycloak-token'],
        access_token: response.data.access_token,
        refresh_token: response.data.refresh_token,
        expires_at: new Date().getTime() + response.data.expires_in * 1000,
      };

      // Save session and return new token info
      req.session.save(() => {
        res.json({
          access_token: response.data.access_token,
          expires_at: req.session['keycloak-token'].expires_at,
        });
      });
    })
    .catch((error) => {
      logger.error('Token refresh error:', error.response?.data || error.message);

      // If refresh token is invalid, clear the session
      if (error.response?.status === 400) {
        req.session.destroy();
      }

      res.status(500).json({ error: 'Failed to refresh token' });
    });
};

// Middleware to populate req.user from session or token
export const populateUserMiddleware = async (req, res, next) => {
  const authHeader = req.headers.authorization || req.headers.Authorization;
  if (authHeader && authHeader.startsWith('Bearer ')) {
    const token = authHeader.substring(7);

    try {
      // Use token-based cache to lookup user
      const user = await lookupUserByToken(token);
      if (user) {
        req.user = user;
        // Also set session user for compatibility
        req.session.user = user;
        return next();
      }
    } catch (error) {
      logger.error('Error in token-based user lookup:', error.message);
    }
  }

  // Fallback to session-based authentication
  if (req.session && req.session.user) {
    req.user = req.session.user;
  } else {
    req.user = null;
  }
  next();
};

// Export cache management functions
export { lookupUserByToken, clearTokenUserCache, getTokenUserCacheStats, removeUserFromTokenCache };

// Example of how to set up your routes in Express
/*
import express from "express";
const app = express();

// Apply session middleware
app.use(sessionMiddleware);

// Initialize Keycloak middleware
app.use(keycloak.middleware());

// Set up routes
app.get('/auth/login', loginRouteHandler);
app.get('/auth/logout', logoutRouteHandler);
app.get('/auth/register', registerRouteHandler);
app.get('/auth/forgot-password', forgotPasswordRouteHandler);

// Protected route example
app.get('/api/profile', validateTokenMiddleware, getUserInfoHandler);

// Admin-only route example
app.get('/api/admin', hasRole('admin'), (req, res) => {
  res.json({ message: 'Admin access granted' });
});
*/
