import { Router, Request, Response, NextFunction } from 'express';
import { AuthService } from '@/services/auth/auth.service.js';
import { OAuthConfigService } from '@/services/auth/oauth-config.service.js';
import { AuthConfigService } from '@/services/auth/auth-config.service.js';
import { OAuthPKCEService } from '@/services/auth/oauth-pkce.service.js';
import { AuditService } from '@/services/logs/audit.service.js';
import { TokenManager } from '@/infra/security/token.manager.js';
import { AppError } from '@/utils/errors.js';
import { successResponse } from '@/utils/response.js';
import { AuthRequest, verifyAdmin } from '@/api/middlewares/auth.js';
import { setRefreshTokenCookie } from '@/utils/cookies.js';
import { parseClientType } from '@/utils/utils.js';
import { SocketManager } from '@/infra/socket/socket.manager.js';
import { DataUpdateResourceType, ServerEvents } from '@/types/socket.js';
import logger from '@/utils/logger.js';
import jwt from 'jsonwebtoken';

import {
  ERROR_CODES,
  createOAuthConfigRequestSchema,
  updateOAuthConfigRequestSchema,
  oAuthInitRequestSchema,
  oAuthCodeExchangeRequestSchema,
  type ListOAuthConfigsResponse,
  oAuthProvidersSchema,
} from '@insforge/shared-schemas';
import { isOAuthSharedKeysAvailable } from '@/utils/environment.js';

const router = Router();
const authService = AuthService.getInstance();
const authConfigService = AuthConfigService.getInstance();
const oAuthConfigService = OAuthConfigService.getInstance();
const oAuthPKCEService = OAuthPKCEService.getInstance();
const auditService = AuditService.getInstance();

// Helper function to validate JWT_SECRET
const validateJwtSecret = (): string => {
  const jwtSecret = process.env.JWT_SECRET;
  if (!jwtSecret || jwtSecret.trim() === '') {
    throw new AppError(
      'JWT_SECRET environment variable is not configured.',
      500,
      ERROR_CODES.INTERNAL_ERROR
    );
  }
  return jwtSecret;
};

// OAuth Configuration Management Routes (must come before wildcard routes)
// GET /api/auth/oauth/configs - List all OAuth configurations (admin only)
router.get('/configs', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => {
  try {
    const configs = await oAuthConfigService.getAllConfigs();
    const response: ListOAuthConfigsResponse = {
      data: configs,
      count: configs.length,
    };
    successResponse(res, response);
  } catch (error) {
    logger.error('Failed to list OAuth configurations', { error });
    next(error);
  }
});

// GET /api/auth/oauth/:provider/config - Get specific OAuth configuration (admin only)
router.get(
  '/:provider/config',
  verifyAdmin,
  async (req: AuthRequest, res: Response, next: NextFunction) => {
    try {
      const { provider } = req.params;
      const config = await oAuthConfigService.getConfigByProvider(provider);
      const clientSecret = await oAuthConfigService.getClientSecretByProvider(provider);

      if (!config) {
        throw new AppError(
          `OAuth configuration for ${provider} not found`,
          404,
          ERROR_CODES.AUTH_OAUTH_CONFIG_NOT_FOUND
        );
      }

      successResponse(res, {
        ...config,
        clientSecret: clientSecret || undefined,
      });
    } catch (error) {
      logger.error('Failed to get OAuth config by provider', {
        provider: req.params.provider,
        error,
      });
      next(error);
    }
  }
);

// POST /api/auth/oauth/configs - Create new OAuth configuration (admin only)
router.post(
  '/configs',
  verifyAdmin,
  async (req: AuthRequest, res: Response, next: NextFunction) => {
    try {
      const validationResult = createOAuthConfigRequestSchema.safeParse(req.body);
      if (!validationResult.success) {
        throw new AppError(
          validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
          400,
          ERROR_CODES.INVALID_INPUT
        );
      }

      const input = validationResult.data;

      // Check if using shared keys when not allowed
      if (input.useSharedKey && !isOAuthSharedKeysAvailable()) {
        throw new AppError(
          'Shared OAuth keys are not enabled in this environment',
          400,
          ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
        );
      }

      const config = await oAuthConfigService.createConfig(input);

      await auditService.log({
        actor: req.hasApiKey ? 'api-key' : req.user?.id,
        action: 'CREATE_OAUTH_CONFIG',
        module: 'AUTH',
        details: {
          provider: input.provider,
          useSharedKey: input.useSharedKey || false,
        },
        ip_address: req.ip,
      });

      successResponse(res, config);
    } catch (error) {
      logger.error('Failed to create OAuth configuration', { error });
      next(error);
    }
  }
);

// PUT /api/auth/oauth/:provider/config - Update OAuth configuration (admin only)
router.put(
  '/:provider/config',
  verifyAdmin,
  async (req: AuthRequest, res: Response, next: NextFunction) => {
    try {
      const provider = req.params.provider;
      if (!provider || provider.length === 0 || provider.length > 50) {
        throw new AppError('Invalid provider name', 400, ERROR_CODES.INVALID_INPUT);
      }

      const validationResult = updateOAuthConfigRequestSchema.safeParse(req.body);
      if (!validationResult.success) {
        throw new AppError(
          validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
          400,
          ERROR_CODES.INVALID_INPUT
        );
      }

      const input = validationResult.data;

      // Check if using shared keys when not allowed
      if (input.useSharedKey && !isOAuthSharedKeysAvailable()) {
        throw new AppError(
          'Shared OAuth keys are not enabled in this environment',
          400,
          ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
        );
      }

      const config = await oAuthConfigService.updateConfig(provider, input);

      await auditService.log({
        actor: req.hasApiKey ? 'api-key' : req.user?.id,
        action: 'UPDATE_OAUTH_CONFIG',
        module: 'AUTH',
        details: {
          provider,
          updatedFields: Object.keys(input),
        },
        ip_address: req.ip,
      });

      successResponse(res, config);
    } catch (error) {
      logger.error('Failed to update OAuth configuration', {
        error,
        provider: req.params.provider,
      });
      next(error);
    }
  }
);

// DELETE /api/auth/oauth/:provider/config - Delete OAuth configuration (admin only)
router.delete(
  '/:provider/config',
  verifyAdmin,
  async (req: AuthRequest, res: Response, next: NextFunction) => {
    try {
      const provider = req.params.provider;
      if (!provider || provider.length === 0 || provider.length > 50) {
        throw new AppError('Invalid provider name', 400, ERROR_CODES.INVALID_INPUT);
      }
      const deleted = await oAuthConfigService.deleteConfig(provider);

      if (!deleted) {
        throw new AppError(
          `OAuth configuration for ${provider} not found`,
          404,
          ERROR_CODES.AUTH_OAUTH_CONFIG_NOT_FOUND
        );
      }

      await auditService.log({
        actor: req.hasApiKey ? 'api-key' : req.user?.id,
        action: 'DELETE_OAUTH_CONFIG',
        module: 'AUTH',
        details: { provider },
        ip_address: req.ip,
      });

      successResponse(res, {
        success: true,
        message: `OAuth configuration for ${provider} deleted successfully`,
      });
    } catch (error) {
      logger.error('Failed to delete OAuth configuration', {
        error,
        provider: req.params.provider,
      });
      next(error);
    }
  }
);

// OAuth Flow Routes
// GET /api/auth/oauth/:provider - Initialize OAuth flow for any supported provider
router.get('/:provider', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { provider } = req.params;

    // Validate provider using OAuthProvidersSchema
    const providerValidation = oAuthProvidersSchema.safeParse(provider);
    if (!providerValidation.success) {
      throw new AppError(
        `Unsupported OAuth provider: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
        400,
        ERROR_CODES.INVALID_INPUT
      );
    }

    // Validate query params (PKCE code_challenge per RFC 7636)
    const queryValidation = oAuthInitRequestSchema.safeParse(req.query);
    if (!queryValidation.success) {
      throw new AppError(
        queryValidation.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
        400,
        ERROR_CODES.INVALID_INPUT
      );
    }

    const { redirect_uri, code_challenge, ...additionalParams } = queryValidation.data;
    const validatedProvider = providerValidation.data;
    const redirectUri = redirect_uri;

    if (!(await authConfigService.validateRedirectUrl(redirectUri))) {
      throw new AppError(
        `${redirectUri} is not in the allowed redirect URLs`,
        400,
        ERROR_CODES.INVALID_INPUT,
        'Please add this URL to the allowed redirect URLs in the authentication configuration.'
      );
    }

    const jwtPayload = {
      provider: validatedProvider,
      redirectUri,
      codeChallenge: code_challenge,
      createdAt: Date.now(),
    };
    const jwtSecret = validateJwtSecret();
    const state = jwt.sign(jwtPayload, jwtSecret, {
      algorithm: 'HS256',
      expiresIn: '1h', // Set expiration time for the state token
    });

    const authUrl = await authService.generateOAuthUrl(validatedProvider, state, additionalParams);
    successResponse(res, { authUrl });
  } catch (error) {
    logger.error(`${req.params.provider} OAuth error`, { error });

    // If it's already an AppError, pass it through
    if (error instanceof AppError) {
      next(error);
      return;
    }

    // For other errors, return the generic OAuth configuration error
    next(
      new AppError(
        `${req.params.provider} OAuth is not properly configured. Please check your oauth configurations.`,
        500,
        ERROR_CODES.AUTH_OAUTH_CONFIG_ERROR
      )
    );
  }
});

// GET /api/auth/oauth/shared/callback/:state - Shared callback for OAuth providers
router.get('/shared/callback/:state', async (req: Request, res: Response, next: NextFunction) => {
  let redirectUri: string | undefined;

  try {
    const { state } = req.params;
    const { success, error, payload } = req.query;

    if (!state) {
      logger.warn('Shared OAuth callback called without state parameter');
      throw new AppError('State parameter is required', 400, ERROR_CODES.INVALID_INPUT);
    }

    let provider: string;
    let codeChallenge: string;
    try {
      const jwtSecret = validateJwtSecret();
      const decodedState = jwt.verify(state, jwtSecret) as {
        provider: string;
        redirectUri: string;
        codeChallenge: string;
      };
      redirectUri = decodedState.redirectUri || '';
      provider = decodedState.provider || '';
      codeChallenge = decodedState.codeChallenge || '';
    } catch {
      logger.warn('Invalid state parameter', { state });
      throw new AppError('Invalid state parameter', 400, ERROR_CODES.INVALID_INPUT);
    }

    // Validate provider using OAuthProvidersSchema
    const providerValidation = oAuthProvidersSchema.safeParse(provider);
    if (!providerValidation.success) {
      logger.warn('Invalid provider in state', { provider });
      throw new AppError(
        `Invalid provider in state: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
        400,
        ERROR_CODES.INVALID_INPUT
      );
    }
    const validatedProvider = providerValidation.data;
    if (!redirectUri) {
      throw new AppError('redirectUri is required', 400, ERROR_CODES.INVALID_INPUT);
    }

    if (redirectUri && !(await authConfigService.validateRedirectUrl(redirectUri))) {
      logger.warn('Redirect URI is not in allowed redirect URLs in shared callback', {
        redirectUri,
      });
      throw new AppError(
        `${redirectUri} is not in the allowed redirect URLs`,
        400,
        ERROR_CODES.INVALID_INPUT,
        'Please add this URL to the allowed redirect URLs in the authentication configuration.'
      );
    }

    if (!codeChallenge) {
      throw new AppError('code_challenge is required in state', 400, ERROR_CODES.INVALID_INPUT);
    }

    if (success !== 'true') {
      const errorMessage = error || 'OAuth Authentication Failed';
      logger.warn('Shared OAuth callback failed', { error: errorMessage, provider });
      const errorUrl = new URL(redirectUri);
      errorUrl.searchParams.set('error', String(errorMessage));
      return res.redirect(errorUrl.toString());
    }

    try {
      if (!payload) {
        throw new AppError('No payload provided in callback', 400, ERROR_CODES.INVALID_INPUT);
      }

      const payloadData = JSON.parse(
        Buffer.from(payload as string, 'base64').toString('utf8')
      ) as Record<string, unknown>;

      // Handle shared callback - transforms payload and creates/finds user
      const result = await authService.handleSharedCallback(validatedProvider, payloadData);

      // Create exchange code for PKCE flow (instead of exposing tokens in URL)
      // Only store minimal data - user and token are fetched fresh on exchange
      const exchangeCode = oAuthPKCEService.createCode({
        userId: result.user.id,
        codeChallenge,
        provider: validatedProvider,
      });

      // Redirect with only the exchange code (no sensitive tokens in URL)
      const successUrl = new URL(redirectUri);
      successUrl.searchParams.set('insforge_code', exchangeCode);
      return res.redirect(successUrl.toString());
    } catch (error) {
      logger.error('Shared OAuth callback completion error', {
        error: error instanceof Error ? error.message : error,
        stack: error instanceof Error ? error.stack : undefined,
        provider: validatedProvider,
      });

      const errorMessage = error instanceof Error ? error.message : 'OAuth Authentication Failed';
      const errorUrl = new URL(redirectUri);
      errorUrl.searchParams.set('error', errorMessage);
      return res.redirect(errorUrl.toString());
    }
  } catch (error) {
    logger.error('Shared OAuth callback error', { error });
    next(error);
  }
});

/**
 * Handle OAuth provider callback (shared logic for GET and POST)
 * Most providers use GET, but Apple uses POST with form data
 */
const handleOAuthCallback = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { provider } = req.params;
    // Support both query params (GET) and body params (POST for Apple)
    // Use method-based source selection to prevent parameter pollution attacks
    const isPostRequest = req.method === 'POST';
    const code = isPostRequest ? (req.body.code as string) : (req.query.code as string);
    const state = isPostRequest ? (req.body.state as string) : (req.query.state as string);
    const token = isPostRequest ? (req.body.id_token as string) : (req.query.token as string);

    if (!state) {
      logger.warn('OAuth callback called without state parameter');
      throw new AppError('State parameter is required', 400, ERROR_CODES.INVALID_INPUT);
    }

    // Decode state data (needed for both success and error paths)
    let redirectUri: string;
    let codeChallenge: string;

    try {
      const jwtSecret = validateJwtSecret();
      const stateData = jwt.verify(state, jwtSecret) as {
        provider: string;
        redirectUri: string;
        codeChallenge: string;
      };
      redirectUri = stateData.redirectUri || '';
      codeChallenge = stateData.codeChallenge || '';
    } catch {
      // Invalid state
      logger.warn('Invalid state in provider callback', { state });
      throw new AppError('Invalid state parameter', 400, ERROR_CODES.INVALID_INPUT);
    }

    if (!redirectUri) {
      throw new AppError('redirectUri is required', 400, ERROR_CODES.INVALID_INPUT);
    }

    if (!(await authConfigService.validateRedirectUrl(redirectUri))) {
      logger.warn('Redirect URI is not in allowed redirect URLs in callback', { redirectUri });
      throw new AppError(
        `${redirectUri} is not in the allowed redirect URLs`,
        400,
        ERROR_CODES.INVALID_INPUT,
        'Please add this URL to the allowed redirect URLs in the authentication configuration.'
      );
    }

    if (!codeChallenge) {
      throw new AppError('code_challenge is required in state', 400, ERROR_CODES.INVALID_INPUT);
    }

    try {
      // Validate provider using OAuthProvidersSchema
      const providerValidation = oAuthProvidersSchema.safeParse(provider);
      if (!providerValidation.success) {
        throw new AppError(
          `Unsupported OAuth provider: ${provider}. Supported providers: ${oAuthProvidersSchema.options.join(', ')}`,
          400,
          ERROR_CODES.INVALID_INPUT
        );
      }

      const validatedProvider = providerValidation.data;

      const result = await authService.handleOAuthCallback(validatedProvider, {
        code: code || undefined,
        token: token || undefined,
        state: state || undefined,
      });

      // Create exchange code for PKCE flow (instead of exposing tokens in URL)
      // Only store minimal data - user and token are fetched fresh on exchange
      const exchangeCode = oAuthPKCEService.createCode({
        userId: result.user.id,
        codeChallenge,
        provider: validatedProvider,
      });

      // Redirect with only the exchange code (no sensitive tokens in URL)
      const successUrl = new URL(redirectUri);
      successUrl.searchParams.set('insforge_code', exchangeCode);
      return res.redirect(successUrl.toString());
    } catch (error) {
      logger.error('OAuth callback error', {
        error: error instanceof Error ? error.message : error,
        stack: error instanceof Error ? error.stack : undefined,
        provider: req.params.provider,
        hasCode: !!code,
        hasState: !!state,
        hasToken: !!token,
      });

      const errorMessage = error instanceof Error ? error.message : 'OAuth Authentication Failed';

      // Redirect with error in URL parameters
      const errorUrl = new URL(redirectUri);
      errorUrl.searchParams.set('error', errorMessage);
      return res.redirect(errorUrl.toString());
    }
  } catch (error) {
    logger.error('OAuth callback error', { error });
    next(error);
  }
};

// GET /api/auth/oauth/:provider/callback - OAuth provider callback (most providers)
router.get('/:provider/callback', handleOAuthCallback);

// POST /api/auth/oauth/:provider/callback - OAuth provider callback (Apple uses POST with form_post)
router.post('/:provider/callback', handleOAuthCallback);

// POST /api/auth/oauth/exchange - Exchange OAuth code for tokens (PKCE flow)
// Query params: client_type (optional) - 'web' (default), 'mobile', 'desktop', or 'server'
router.post('/exchange', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const clientType = parseClientType(req.query.client_type);

    const validationResult = oAuthCodeExchangeRequestSchema.safeParse(req.body);
    if (!validationResult.success) {
      throw new AppError(
        validationResult.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '),
        400,
        ERROR_CODES.INVALID_INPUT
      );
    }

    const { code, code_verifier } = validationResult.data;
    const result = await oAuthPKCEService.exchangeCode(code, code_verifier);

    const tokenManager = TokenManager.getInstance();
    const socket = SocketManager.getInstance();
    socket.broadcastToRoom(
      'role:project_admin',
      ServerEvents.DATA_UPDATE,
      { resource: DataUpdateResourceType.USERS },
      'system'
    );

    if (clientType === 'web') {
      const { refreshToken, csrfToken } = tokenManager.generateRefreshTokenWithCsrf(
        result.user.id,
        'user'
      );
      setRefreshTokenCookie(res, refreshToken);

      successResponse(res, {
        accessToken: result.accessToken,
        user: result.user,
        csrfToken,
      });
    } else {
      const refreshToken = tokenManager.generateRefreshToken(result.user.id, 'user');
      successResponse(res, {
        accessToken: result.accessToken,
        user: result.user,
        refreshToken,
      });
    }
  } catch (error) {
    logger.error('OAuth exchange error', { error });
    next(error);
  }
});

export default router;
