import { NextRequest, NextResponse } from 'next/server';
import { randomBytes } from 'crypto';

// CSRF token configuration
const CSRF_TOKEN_LENGTH = 32;
const CSRF_COOKIE_NAME = 'csrf-token';
const CSRF_HEADER_NAME = 'x-csrf-token';
const CSRF_FORM_FIELD_NAME = 'csrf_token';

/**
 * Generate a cryptographically secure CSRF token
 */
export function generateCSRFToken(): string {
  return randomBytes(CSRF_TOKEN_LENGTH).toString('hex');
}

/**
 * Create a CSRF token and set it in a cookie
 */
export function createCSRFToken(): { token: string; cookie: string } {
  const token = generateCSRFToken();
  let cookie = `${CSRF_COOKIE_NAME}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`; // 24 hours
  
  // Add secure flag in production
  if (process.env.NODE_ENV === 'production' && process.env.NEXT_PUBLIC_APP_URL?.startsWith('https://')) {
    cookie += '; Secure';
  }
  
  return { token, cookie };
}

/**
 * Get CSRF token from cookie
 */
export function getCSRFCookieToken(request: Request | NextRequest): string | null {
  // Check if it's a NextRequest with cookies property
  if ('cookies' in request && typeof request.cookies.get === 'function') {
    const cookie = request.cookies.get(CSRF_COOKIE_NAME);
    return cookie?.value || null;
  }
  
  // Fall back to parsing Cookie header for standard Request
  const cookieHeader = request.headers.get('cookie');
  if (!cookieHeader) return null;
  
  const cookies = cookieHeader.split(';');
  for (const cookie of cookies) {
    const [name, value] = cookie.trim().split('=');
    if (name === CSRF_COOKIE_NAME) {
      return value;
    }
  }
  return null;
}

/**
 * Get CSRF token from request (header or form data)
 */
export function getCSRFRequestToken(request: Request | NextRequest): string | null {
  // Check header first
  const headerToken = request.headers.get(CSRF_HEADER_NAME);
  if (headerToken) return headerToken;
  
  // For form data, check in the request body (will need to parse appropriately)
  // This is handled in the validation function for JSON requests
  return null;
}

/**
 * Validate CSRF token from request
 * For JSON requests: checks x-csrf-token header
 * For form requests: checks form field
 */
export async function validateCSRFToken(request: Request | NextRequest): Promise<boolean> {
  const cookieToken = getCSRFCookieToken(request);
  
  if (!cookieToken) {
    console.error('CSRF validation failed: No token in cookie');
    return false;
  }
  
  let requestToken: string | null = null;
  
  // For JSON requests
  if (request.headers.get('content-type')?.includes('application/json')) {
    requestToken = request.headers.get(CSRF_HEADER_NAME);
    
    // If no header, check in JSON body
    if (!requestToken) {
      try {
        const body = await request.clone().json();
        requestToken = body[CSRF_FORM_FIELD_NAME] || null;
      } catch {
        // Ignore JSON parsing errors
      }
    }
  }
  
  // For form data requests
  else if (request.headers.get('content-type')?.includes('multipart/form-data')) {
    try {
      const formData = await request.clone().formData();
      requestToken = formData.get(CSRF_FORM_FIELD_NAME) as string;
    } catch {
      // Ignore form parsing errors
    }
  }
  
  if (!requestToken) {
    console.error('CSRF validation failed: No token in request');
    return false;
  }
  
  // Constant-time comparison to prevent timing attacks
  const tokensMatch = timingSafeEqual(cookieToken, requestToken);
  
  if (!tokensMatch) {
    console.error('CSRF validation failed: Token mismatch');
    return false;
  }
  
  return true;
}

/**
 * Timing-safe string comparison
 */
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  
  return result === 0;
}

/**
 * Check if request is same-origin
 */
export function isSameOrigin(request: Request | NextRequest): boolean {
  const origin = request.headers.get('origin');
  const referer = request.headers.get('referer');
  
  if (!origin && !referer) {
    // No origin/referer headers - reject the request to prevent CSRF bypass
    return false;
  }
  
  const checkUrl = origin || referer;
  if (!checkUrl) return false;
  
  try {
    const parsedUrl = new URL(checkUrl);
    const appUrl = new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000');
    
    return parsedUrl.origin === appUrl.origin;
  } catch {
    return false;
  }
}

/**
 * CSRF protection middleware
 * Use this for routes that need CSRF protection
 */
export function withCSRFProtection<T extends NextRequest>(
  handler: (request: T) => Promise<NextResponse | Response>,
  options?: {
    requireSameOrigin?: boolean;
    skipValidation?: boolean;
  }
): (request: T) => Promise<NextResponse | Response> {
  return async (request: T) => {
    // Skip CSRF for GET, HEAD, OPTIONS requests
    if (['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
      return handler(request);
    }
    
    // Check if request is same-origin (optional, but recommended)
    if (options?.requireSameOrigin !== false && !isSameOrigin(request)) {
      return NextResponse.json(
        { error: 'Cross-origin request not allowed' },
        { status: 403 }
      );
    }
    
    // Validate CSRF token (unless explicitly skipped)
    if (!options?.skipValidation) {
      const isValid = await validateCSRFToken(request);
      if (!isValid) {
        return NextResponse.json(
          { error: 'Invalid or missing CSRF token' },
          { status: 403 }
        );
      }
    }
    
    return handler(request);
  };
}

/**
 * Create a CSRF token endpoint
 * This should be called when a user session starts
 */
export async function createCSRTEndpoint(request: NextRequest): Promise<NextResponse> {
  if (request.method !== 'GET') {
    return NextResponse.json({ error: 'Method not allowed' }, { status: 405 });
  }
  
  const { token, cookie } = createCSRFToken();
  
  const response = NextResponse.json({ token });
  response.headers.append('Set-Cookie', cookie);
  
  return response;
}