/**
 * Server-side image proxy API endpoint
 * Proxies images from untrusted domains to prevent user IP exposure
 * Uses HMAC signatures to prevent abuse
 */

import {
  checkRateLimit,
  recordRequest,
  getCookieId,
  generateNonce,
  createNonceCookie,
} from '../../utils/rateLimit.ts';

// Secret key for signing URLs - MUST be set in production
const SIGNING_SECRET = import.meta.env.IMAGE_PROXY_SECRET;
if (!SIGNING_SECRET && import.meta.env.PROD) {
  throw new Error('CRITICAL: IMAGE_PROXY_SECRET environment variable is not set in production!');
} else if (!SIGNING_SECRET) {
  console.error('WARNING: IMAGE_PROXY_SECRET environment variable is not set!');
}

// Private IP ranges to block (SSRF protection)
const PRIVATE_IP_PATTERNS = [
  /^127\./, // localhost
  /^10\./, // Class A private
  /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Class B private
  /^192\.168\./, // Class C private
  /^169\.254\./, // Link-local
  /^0\./, // Current network
  /^224\./, // Multicast
  /^255\./, // Broadcast
  /^localhost$/i,
  /^\[?::1\]?$/, // IPv6 localhost
  /^\[?fe80:/i, // IPv6 link-local
  /^\[?fc00:/i, // IPv6 private
  /^\[?fd00:/i, // IPv6 private
];

function isPrivateUrl(urlString: string): boolean {
  try {
    const url = new URL(urlString);
    const hostname = url.hostname;
    return PRIVATE_IP_PATTERNS.some(pattern => pattern.test(hostname));
  } catch {
    return true; // Block invalid URLs
  }
}

// Max image size to proxy (25MB - needed for animated GIFs)
const MAX_IMAGE_SIZE = 25 * 1024 * 1024;

// Allowed content types
const ALLOWED_CONTENT_TYPES = [
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp',
  'image/svg+xml',
  'image/bmp',
  'image/x-icon',
  'image/vnd.microsoft.icon',
  'image/avif',
  'image/apng',
];

// Image extensions for fallback content-type detection
const IMAGE_EXTENSIONS: Record<string, string> = {
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.png': 'image/png',
  '.gif': 'image/gif',
  '.webp': 'image/webp',
  '.svg': 'image/svg+xml',
  '.bmp': 'image/bmp',
  '.ico': 'image/x-icon',
  '.avif': 'image/avif',
  '.apng': 'image/apng',
};

/**
 * Get content type from URL extension
 */
function getContentTypeFromUrl(url: string): string | null {
  try {
    const pathname = new URL(url).pathname.toLowerCase();
    for (const [ext, type] of Object.entries(IMAGE_EXTENSIONS)) {
      if (pathname.endsWith(ext)) {
        return type;
      }
    }
  } catch {}
  return null;
}

/**
 * Generate HMAC signature for a URL
 */
export async function signImageUrl(imageUrl: string): Promise<string> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(SIGNING_SECRET),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(imageUrl));
  const signatureHex = Array.from(new Uint8Array(signature))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  return signatureHex;
}

/**
 * Verify HMAC signature for a URL
 */
async function verifySignature(imageUrl, signature) {
  const expectedSignature = await signImageUrl(imageUrl);
  return signature === expectedSignature;
}

/**
 * Create a signed proxy URL for an image
 */
export async function createSignedProxyUrl(imageUrl) {
  const signature = await signImageUrl(imageUrl);
  return `/api/image-proxy?url=${encodeURIComponent(imageUrl)}&sig=${signature}`;
}

export async function GET({ request }) {
  // Rate limit check
  const rateCheck = checkRateLimit(request, {
    limit: 20,
    windowMs: 1000,
    burstLimit: 100,
    burstWindowMs: 10_000,
  });

  let cookieId = getCookieId(request);
  const hadCookie = !!cookieId;
  if (!cookieId) {
    cookieId = generateNonce();
  }

  if (!rateCheck.allowed) {
    const response = new Response('Rate limit exceeded', {
      status: 429,
      headers: { 'Retry-After': '1' },
    });
    if (!hadCookie) {
      response.headers.set('Set-Cookie', createNonceCookie(cookieId));
    }
    return response;
  }

  recordRequest(request, 1000);

  const url = new URL(request.url);
  const imageUrl = url.searchParams.get('url');
  const signature = url.searchParams.get('sig');

  if (!imageUrl) {
    return new Response('Missing url parameter', { status: 400 });
  }

  if (!signature) {
    return new Response('Missing signature', { status: 403 });
  }

  // Require signing secret to be configured
  if (!SIGNING_SECRET) {
    return new Response('Server misconfigured', { status: 500 });
  }

  // Verify the signature
  const isValid = await verifySignature(imageUrl, signature);
  if (!isValid) {
    return new Response('Invalid signature', { status: 403 });
  }

  // Validate URL format
  let parsedUrl;
  try {
    parsedUrl = new URL(imageUrl);
    if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
      throw new Error('Invalid protocol');
    }
  } catch {
    return new Response('Invalid URL', { status: 400 });
  }

  // SSRF protection: block private/internal IPs
  if (isPrivateUrl(imageUrl)) {
    return new Response('URL not allowed', { status: 403 });
  }

  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 10000);

    const response = await fetch(imageUrl, {
      method: 'GET',
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; ImageProxy/1.0)',
        'Accept': 'image/*',
      },
      signal: controller.signal,
      redirect: 'follow',
    });

    clearTimeout(timeout);

    if (!response.ok) {
      return new Response('Failed to fetch image', { status: 502 });
    }

    let contentType = response.headers.get('content-type') || '';
    const contentLength = parseInt(response.headers.get('content-length') || '0', 10);

    // Validate content type - must be an image
    let isAllowedType = ALLOWED_CONTENT_TYPES.some(type => contentType.startsWith(type));
    
    // If server returned generic binary type, try to infer from URL extension
    if (!isAllowedType && (contentType.includes('octet-stream') || contentType === '')) {
      const inferredType = getContentTypeFromUrl(imageUrl);
      if (inferredType) {
        contentType = inferredType;
        isAllowedType = true;
      }
    }
    
    if (!isAllowedType) {
      console.error(`[image-proxy] Invalid content type: ${contentType} for URL: ${imageUrl}`);
      return new Response(`Invalid content type: ${contentType}`, { status: 400 });
    }

    // Check size limit
    if (contentLength > MAX_IMAGE_SIZE) {
      return new Response('Image too large', { status: 413 });
    }

    // Stream the image through
    const imageData = await response.arrayBuffer();
    
    // Double-check size after download
    if (imageData.byteLength > MAX_IMAGE_SIZE) {
      return new Response('Image too large', { status: 413 });
    }

    const proxyResponse = new Response(imageData, {
      status: 200,
      headers: {
        'Content-Type': contentType,
        'Content-Length': imageData.byteLength.toString(),
        'Cache-Control': 'public, max-age=86400', // Cache for 1 day
        'X-Content-Type-Options': 'nosniff',
      },
    });

    if (!hadCookie) {
      proxyResponse.headers.set('Set-Cookie', createNonceCookie(cookieId));
    }

    return proxyResponse;

  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    console.error('[image-proxy] Error fetching image:', message);
    return new Response('Failed to fetch image', { status: 500 });
  }
}
