/**
 * API Token Authentication Middleware
 *
 * Validates external API access tokens (separate from session JWT auth).
 * These tokens allow external applications to access Admin APIs
 * using `Authorization: Bearer <api_access_token>`.
 *
 * This middleware DOES NOT modify the existing JWT authentication system.
 * It provides an alternative authentication path for API consumers.
 */

import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
import { getModels } from '../models';

// Extend Express Request type for API token
declare global {
  namespace Express {
    interface Request {
      apiToken?: any; // IApiToken document
    }
  }
}

/**
 * Authenticate API token from Authorization header.
 * Looks up the token by prefix, then verifies via bcrypt comparison.
 * Attaches the user and token document to the request.
 */
export const authenticateApiToken = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  // Skip for CORS preflight
  if (req.method === 'OPTIONS') {
    next();
    return;
  }

  try {
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      res.status(401).json({ error: 'Access denied. No API token provided.' });
      return;
    }

    const rawToken = authHeader.substring(7);
    if (!rawToken || rawToken.length < 16) {
      res.status(401).json({ error: 'Invalid API token format.' });
      return;
    }

    // Extract prefix (first 8 chars) for efficient lookup
    const tokenPrefix = rawToken.substring(0, 8);

    const { ApiToken, User } = getModels();

    // Find active tokens with matching prefix
    const candidates = await ApiToken.find({
      accessTokenPrefix: tokenPrefix,
      status: 'active',
    }).lean();

    // Compare against each candidate using bcrypt
    let matchedToken: any = null;
    for (const candidate of candidates) {
      const isMatch = await bcrypt.compare(rawToken, candidate.accessTokenHash);
      if (isMatch) {
        matchedToken = candidate;
        break;
      }
    }

    if (!matchedToken) {
      res.status(401).json({ error: 'Invalid or expired API token.' });
      return;
    }

    // Check expiry
    if (matchedToken.expiresAt && new Date(matchedToken.expiresAt) < new Date()) {
      // Mark as expired
      await ApiToken.findByIdAndUpdate(matchedToken._id, { status: 'expired' });
      res.status(401).json({ error: 'API token has expired. Please regenerate.' });
      return;
    }

    // Load user
    const user = await User.findById(matchedToken.userId);
    if (!user) {
      res.status(401).json({ error: 'User associated with this token no longer exists.' });
      return;
    }

    // Attach to request
    req.user = user;
    req.apiToken = matchedToken;

    // Update lastUsedAt (fire and forget)
    ApiToken.findByIdAndUpdate(matchedToken._id, { lastUsedAt: new Date() }).catch(() => {});

    next();
  } catch (error) {
    console.error('API token authentication error:', error);
    res.status(500).json({ error: 'API token authentication failed' });
  }
};

/**
 * Check if the API token has the required scope(s).
 * Must be used AFTER authenticateApiToken middleware.
 */
export const requireApiScope = (...scopes: string[]) => {
  return (req: Request, res: Response, next: NextFunction): void => {
    if (!req.apiToken) {
      res.status(401).json({ error: 'API token required.' });
      return;
    }

    const tokenScopes: string[] = req.apiToken.scopes || [];
    const hasRequiredScope = scopes.some(scope => tokenScopes.includes(scope));

    if (!hasRequiredScope) {
      res.status(403).json({ error: `Insufficient scope. Required: ${scopes.join(' or ')}` });
      return;
    }

    next();
  };
};