const express = require('express');
const passport = require('passport');
const crypto = require('node:crypto');
const openIdClient = require('openid-client');
const { CacheKeys } = require('librechat-data-provider');
const {
  logger,
  DEFAULT_SESSION_EXPIRY,
  SystemCapabilities,
  getTenantId,
} = require('@librechat/data-schemas');
const {
  isEnabled,
  getAdminPanelUrl,
  exchangeAdminCode,
  createSetBalanceConfig,
  storeAndStripChallenge,
  tenantContextMiddleware,
  preAuthTenantMiddleware,
  applyAdminRefresh,
  AdminRefreshError,
  buildOpenIDRefreshParams,
} = require('@librechat/api');
const { loginController } = require('~/server/controllers/auth/LoginController');
const { hasCapability, requireCapability } = require('~/server/middleware/roles/capabilities');
const { createOAuthHandler } = require('~/server/controllers/auth/oauth');
const {
  findBalanceByUser,
  findUsers,
  generateToken,
  getUserById,
  upsertBalanceFields,
} = require('~/models');
const { getAppConfig } = require('~/server/services/Config');
const getLogStores = require('~/cache/getLogStores');
const { getOpenIdConfig } = require('~/strategies');
const middleware = require('~/server/middleware');

const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);

const setBalanceConfig = createSetBalanceConfig({
  getAppConfig,
  findBalanceByUser,
  upsertBalanceFields,
});

const router = express.Router();

function resolveRequestOrigin(req) {
  const originHeader = req.get('origin');
  if (originHeader) {
    try {
      return new URL(originHeader).origin;
    } catch {
      return undefined;
    }
  }

  const refererHeader = req.get('referer');
  if (!refererHeader) {
    return undefined;
  }

  try {
    return new URL(refererHeader).origin;
  } catch {
    return undefined;
  }
}

router.post(
  '/login/local',
  middleware.logHeaders,
  middleware.loginLimiter,
  middleware.checkBan,
  middleware.requireLocalAuth,
  tenantContextMiddleware,
  requireAdminAccess,
  setBalanceConfig,
  loginController,
);

router.get('/verify', middleware.requireJwtAuth, requireAdminAccess, (req, res) => {
  const { password: _p, totpSecret: _t, __v, ...user } = req.user;
  user.id = user._id.toString();
  res.status(200).json({ user });
});

router.get('/oauth/openid/check', (req, res) => {
  const openidConfig = getOpenIdConfig();
  if (!openidConfig) {
    return res.status(404).json({
      error: 'OpenID configuration not found',
      error_code: 'OPENID_NOT_CONFIGURED',
    });
  }
  res.status(200).json({ message: 'OpenID check successful' });
});

/**
 * Generates a random hex state string for OAuth flows.
 * @returns {string} A 32-byte random hex string.
 */
function generateState() {
  return crypto.randomBytes(32).toString('hex');
}

/**
 * Middleware to retrieve PKCE challenge from cache using the OAuth state.
 * Reads state from req.oauthState (set by a preceding middleware).
 * @param {string} provider - Provider name for logging.
 * @returns {Function} Express middleware.
 */
function retrievePkceChallenge(provider) {
  return async (req, res, next) => {
    if (!req.oauthState) {
      return next();
    }
    try {
      const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
      const challenge = await cache.get(`pkce:${req.oauthState}`);
      if (challenge) {
        req.pkceChallenge = challenge;
        await cache.delete(`pkce:${req.oauthState}`);
      } else {
        logger.warn(
          `[admin/oauth/${provider}/callback] State present but no PKCE challenge found; PKCE will not be enforced for this request`,
        );
      }
    } catch (err) {
      logger.error(
        `[admin/oauth/${provider}/callback] Failed to retrieve PKCE challenge, aborting:`,
        err,
      );
      return res.redirect(
        `${getAdminPanelUrl()}/auth/${provider}/callback?error=pkce_retrieval_failed&error_description=Failed+to+retrieve+PKCE+challenge`,
      );
    }
    next();
  };
}

/* ──────────────────────────────────────────────
 * OpenID Admin Routes
 * ────────────────────────────────────────────── */

router.get('/oauth/openid', async (req, res, next) => {
  const state = generateState();
  const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
  const stored = await storeAndStripChallenge(cache, req, state, 'openid');
  if (!stored) {
    return res.redirect(
      `${getAdminPanelUrl()}/auth/openid/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
    );
  }

  return passport.authenticate('openidAdmin', {
    session: false,
    state,
  })(req, res, next);
});

router.get(
  '/oauth/openid/callback',
  (req, res, next) => {
    req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
    next();
  },
  passport.authenticate('openidAdmin', {
    failureRedirect: `${getAdminPanelUrl()}/auth/openid/callback?error=auth_failed&error_description=Authentication+failed`,
    failureMessage: true,
    session: false,
  }),
  tenantContextMiddleware,
  retrievePkceChallenge('openid'),
  requireAdminAccess,
  setBalanceConfig,
  middleware.checkDomainAllowed,
  createOAuthHandler(`${getAdminPanelUrl()}/auth/openid/callback`),
);

/* ──────────────────────────────────────────────
 * SAML Admin Routes
 * ────────────────────────────────────────────── */

router.get('/oauth/saml', async (req, res, next) => {
  const state = generateState();
  const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
  const stored = await storeAndStripChallenge(cache, req, state, 'saml');
  if (!stored) {
    return res.redirect(
      `${getAdminPanelUrl()}/auth/saml/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
    );
  }

  return passport.authenticate('samlAdmin', {
    session: false,
    additionalParams: { RelayState: state },
  })(req, res, next);
});

router.post(
  '/oauth/saml/callback',
  (req, res, next) => {
    req.oauthState = typeof req.body.RelayState === 'string' ? req.body.RelayState : undefined;
    next();
  },
  passport.authenticate('samlAdmin', {
    failureRedirect: `${getAdminPanelUrl()}/auth/saml/callback?error=auth_failed&error_description=Authentication+failed`,
    failureMessage: true,
    session: false,
  }),
  tenantContextMiddleware,
  retrievePkceChallenge('saml'),
  requireAdminAccess,
  setBalanceConfig,
  middleware.checkDomainAllowed,
  createOAuthHandler(`${getAdminPanelUrl()}/auth/saml/callback`),
);

/* ──────────────────────────────────────────────
 * Google Admin Routes
 * ────────────────────────────────────────────── */

router.get('/oauth/google', async (req, res, next) => {
  const state = generateState();
  const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
  const stored = await storeAndStripChallenge(cache, req, state, 'google');
  if (!stored) {
    return res.redirect(
      `${getAdminPanelUrl()}/auth/google/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
    );
  }

  return passport.authenticate('googleAdmin', {
    scope: ['openid', 'profile', 'email'],
    session: false,
    state,
  })(req, res, next);
});

router.get(
  '/oauth/google/callback',
  (req, res, next) => {
    req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
    next();
  },
  passport.authenticate('googleAdmin', {
    failureRedirect: `${getAdminPanelUrl()}/auth/google/callback?error=auth_failed&error_description=Authentication+failed`,
    failureMessage: true,
    session: false,
  }),
  tenantContextMiddleware,
  retrievePkceChallenge('google'),
  requireAdminAccess,
  setBalanceConfig,
  middleware.checkDomainAllowed,
  createOAuthHandler(`${getAdminPanelUrl()}/auth/google/callback`),
);

/* ──────────────────────────────────────────────
 * GitHub Admin Routes
 * ────────────────────────────────────────────── */

router.get('/oauth/github', async (req, res, next) => {
  const state = generateState();
  const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
  const stored = await storeAndStripChallenge(cache, req, state, 'github');
  if (!stored) {
    return res.redirect(
      `${getAdminPanelUrl()}/auth/github/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
    );
  }

  return passport.authenticate('githubAdmin', {
    scope: ['user:email', 'read:user'],
    session: false,
    state,
  })(req, res, next);
});

router.get(
  '/oauth/github/callback',
  (req, res, next) => {
    req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
    next();
  },
  passport.authenticate('githubAdmin', {
    failureRedirect: `${getAdminPanelUrl()}/auth/github/callback?error=auth_failed&error_description=Authentication+failed`,
    failureMessage: true,
    session: false,
  }),
  tenantContextMiddleware,
  retrievePkceChallenge('github'),
  requireAdminAccess,
  setBalanceConfig,
  middleware.checkDomainAllowed,
  createOAuthHandler(`${getAdminPanelUrl()}/auth/github/callback`),
);

/* ──────────────────────────────────────────────
 * Discord Admin Routes
 * ────────────────────────────────────────────── */

router.get('/oauth/discord', async (req, res, next) => {
  const state = generateState();
  const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
  const stored = await storeAndStripChallenge(cache, req, state, 'discord');
  if (!stored) {
    return res.redirect(
      `${getAdminPanelUrl()}/auth/discord/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
    );
  }

  return passport.authenticate('discordAdmin', {
    scope: ['identify', 'email'],
    session: false,
    state,
  })(req, res, next);
});

router.get(
  '/oauth/discord/callback',
  (req, res, next) => {
    req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
    next();
  },
  passport.authenticate('discordAdmin', {
    failureRedirect: `${getAdminPanelUrl()}/auth/discord/callback?error=auth_failed&error_description=Authentication+failed`,
    failureMessage: true,
    session: false,
  }),
  tenantContextMiddleware,
  retrievePkceChallenge('discord'),
  requireAdminAccess,
  setBalanceConfig,
  middleware.checkDomainAllowed,
  createOAuthHandler(`${getAdminPanelUrl()}/auth/discord/callback`),
);

/* ──────────────────────────────────────────────
 * Facebook Admin Routes
 * ────────────────────────────────────────────── */

router.get('/oauth/facebook', async (req, res, next) => {
  const state = generateState();
  const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
  const stored = await storeAndStripChallenge(cache, req, state, 'facebook');
  if (!stored) {
    return res.redirect(
      `${getAdminPanelUrl()}/auth/facebook/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
    );
  }

  return passport.authenticate('facebookAdmin', {
    scope: ['public_profile'],
    session: false,
    state,
  })(req, res, next);
});

router.get(
  '/oauth/facebook/callback',
  (req, res, next) => {
    req.oauthState = typeof req.query.state === 'string' ? req.query.state : undefined;
    next();
  },
  passport.authenticate('facebookAdmin', {
    failureRedirect: `${getAdminPanelUrl()}/auth/facebook/callback?error=auth_failed&error_description=Authentication+failed`,
    failureMessage: true,
    session: false,
  }),
  tenantContextMiddleware,
  retrievePkceChallenge('facebook'),
  requireAdminAccess,
  setBalanceConfig,
  middleware.checkDomainAllowed,
  createOAuthHandler(`${getAdminPanelUrl()}/auth/facebook/callback`),
);

/* ──────────────────────────────────────────────
 * Apple Admin Routes (POST callback)
 * ────────────────────────────────────────────── */

router.get('/oauth/apple', async (req, res, next) => {
  const state = generateState();
  const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
  const stored = await storeAndStripChallenge(cache, req, state, 'apple');
  if (!stored) {
    return res.redirect(
      `${getAdminPanelUrl()}/auth/apple/callback?error=pkce_store_failed&error_description=Failed+to+store+PKCE+challenge`,
    );
  }

  return passport.authenticate('appleAdmin', {
    session: false,
    state,
  })(req, res, next);
});

router.post(
  '/oauth/apple/callback',
  (req, res, next) => {
    req.oauthState = typeof req.body.state === 'string' ? req.body.state : undefined;
    next();
  },
  passport.authenticate('appleAdmin', {
    failureRedirect: `${getAdminPanelUrl()}/auth/apple/callback?error=auth_failed&error_description=Authentication+failed`,
    failureMessage: true,
    session: false,
  }),
  tenantContextMiddleware,
  retrievePkceChallenge('apple'),
  requireAdminAccess,
  setBalanceConfig,
  middleware.checkDomainAllowed,
  createOAuthHandler(`${getAdminPanelUrl()}/auth/apple/callback`),
);

/** Regex pattern for valid exchange codes: 64 hex characters */
const EXCHANGE_CODE_PATTERN = /^[a-f0-9]{64}$/;

/**
 * Exchange OAuth authorization code for tokens.
 * This endpoint is called server-to-server by the admin panel.
 * The code is one-time-use and expires in 30 seconds.
 *
 * POST /api/admin/oauth/exchange
 * Body: { code: string, code_verifier?: string }
 * Response: { token: string, refreshToken: string, user: object }
 */
router.post('/oauth/exchange', middleware.loginLimiter, async (req, res) => {
  try {
    const { code, code_verifier: codeVerifier } = req.body;

    if (!code) {
      logger.warn('[admin/oauth/exchange] Missing authorization code');
      return res.status(400).json({
        error: 'Missing authorization code',
        error_code: 'MISSING_CODE',
      });
    }

    if (typeof code !== 'string' || !EXCHANGE_CODE_PATTERN.test(code)) {
      logger.warn('[admin/oauth/exchange] Invalid authorization code format');
      return res.status(400).json({
        error: 'Invalid authorization code format',
        error_code: 'INVALID_CODE_FORMAT',
      });
    }

    if (
      codeVerifier !== undefined &&
      (typeof codeVerifier !== 'string' || codeVerifier.length < 1 || codeVerifier.length > 512)
    ) {
      logger.warn('[admin/oauth/exchange] Invalid code_verifier format');
      return res.status(400).json({
        error: 'Invalid code_verifier',
        error_code: 'INVALID_VERIFIER',
      });
    }

    const cache = getLogStores(CacheKeys.ADMIN_OAUTH_EXCHANGE);
    const requestOrigin = resolveRequestOrigin(req);
    const result = await exchangeAdminCode(cache, code, requestOrigin, codeVerifier);

    if (!result) {
      return res.status(401).json({
        error: 'Invalid or expired authorization code',
        error_code: 'INVALID_OR_EXPIRED_CODE',
      });
    }

    res.json(result);
  } catch (error) {
    logger.error('[admin/oauth/exchange] Error:', error);
    res.status(500).json({
      error: 'Internal server error',
      error_code: 'INTERNAL_ERROR',
    });
  }
});

/**
 * Admin-panel-shaped token refresh.
 *
 * The standard `/api/auth/refresh` controller reads the refresh token from
 * cookies, which a cross-origin admin panel can't set. This endpoint accepts
 * the refresh token in the request body, exchanges it at the IdP, mints a
 * fresh LibreChat JWT, and returns the same response shape as
 * `/api/admin/oauth/exchange`.
 *
 * POST /api/admin/oauth/refresh
 * Body:     { refresh_token: string, user_id?: string }
 * Response: { token: string, refreshToken?: string, user: object, expiresAt: number }
 *
 * Errors (all responses are `{ error: string, error_code: string }`):
 *   400 MISSING_REFRESH_TOKEN  — refresh_token absent or empty
 *   401 REFRESH_FAILED         — IdP rejected the refresh grant
 *   401 USER_NOT_FOUND         — no LibreChat user matches the refreshed sub
 *   401 USER_ID_MISMATCH       — supplied user_id resolves to a user with a different openidId
 *   401 ISSUER_MISMATCH        — refreshed tokenset was issued by an unexpected issuer
 *   401 TENANT_MISMATCH        — resolved user belongs to a different tenant than the request
 *   403 FORBIDDEN              — resolved user no longer holds ACCESS_ADMIN
 *   403 TOKEN_REUSE_DISABLED   — OPENID_REUSE_TOKENS is not enabled on the server
 *   502 IDP_INCOMPLETE         — IdP returned a tokenset missing access_token
 *   502 CLAIMS_INCOMPLETE      — IdP tokenset has no readable claims or no sub
 *   503 OPENID_NOT_CONFIGURED  — OpenID is not configured on this server
 *   500 INTERNAL_ERROR         — anything else (logged server-side)
 */
router.post(
  '/oauth/refresh',
  middleware.loginLimiter,
  preAuthTenantMiddleware,
  async (req, res) => {
    try {
      const { refresh_token: refreshToken, user_id: userId } = req.body ?? {};
      if (typeof refreshToken !== 'string' || refreshToken.length === 0) {
        return res.status(400).json({
          error: 'Missing refresh_token',
          error_code: 'MISSING_REFRESH_TOKEN',
        });
      }

      if (!isEnabled(process.env.OPENID_REUSE_TOKENS)) {
        return res.status(403).json({
          error: 'OpenID token reuse is not enabled',
          error_code: 'TOKEN_REUSE_DISABLED',
        });
      }

      let openIdConfig;
      try {
        openIdConfig = getOpenIdConfig();
      } catch {
        return res.status(503).json({
          error: 'OpenID is not configured',
          error_code: 'OPENID_NOT_CONFIGURED',
        });
      }

      const refreshParams = buildOpenIDRefreshParams();
      logger.debug('[admin/oauth/refresh] OpenID refresh params', {
        has_scope: Boolean(process.env.OPENID_SCOPE),
        has_refresh_audience: Boolean(process.env.OPENID_REFRESH_AUDIENCE),
      });
      let tokenset;
      try {
        tokenset = await openIdClient.refreshTokenGrant(openIdConfig, refreshToken, refreshParams);
        logger.debug('[admin/oauth/refresh] OpenID refresh succeeded', {
          has_access_token: Boolean(tokenset.access_token),
          has_id_token: Boolean(tokenset.id_token),
          has_refresh_token: Boolean(tokenset.refresh_token),
          expires_in: tokenset.expires_in,
        });
      } catch (err) {
        logger.warn('[admin/oauth/refresh] IdP refresh grant failed', {
          code: err?.code,
          name: err?.name,
        });
        return res.status(401).json({
          error: 'Refresh failed',
          error_code: 'REFRESH_FAILED',
        });
      }

      const sessionExpiry = Number(process.env.SESSION_EXPIRY) || DEFAULT_SESSION_EXPIRY;
      const expectedIssuer = openIdConfig.serverMetadata?.()?.issuer;

      try {
        const result = await applyAdminRefresh(
          tokenset,
          {
            findUsers,
            getUserById,
            canAccessAdmin: async (user) => {
              try {
                return await hasCapability(
                  {
                    id: user.id ?? user._id?.toString(),
                    role: user.role ?? '',
                    tenantId: user.tenantId,
                  },
                  SystemCapabilities.ACCESS_ADMIN,
                );
              } catch (err) {
                logger.warn(
                  `[admin/oauth/refresh] capability check failed, denying: ${err?.message}`,
                );
                return false;
              }
            },
            mintToken: async (user) => ({
              token: await generateToken(user, sessionExpiry),
              expiresAt: Date.now() + sessionExpiry,
            }),
          },
          {
            userId: typeof userId === 'string' && userId.length > 0 ? userId : undefined,
            previousRefreshToken: refreshToken,
            expectedIssuer,
            tenantId: getTenantId(),
          },
        );
        return res.json(result);
      } catch (err) {
        if (err instanceof AdminRefreshError) {
          return res.status(err.status).json({ error: err.message, error_code: err.code });
        }
        throw err;
      }
    } catch (error) {
      logger.error('[admin/oauth/refresh] Error:', error);
      res.status(500).json({
        error: 'Internal server error',
        error_code: 'INTERNAL_ERROR',
      });
    }
  },
);

module.exports = router;
