/**
 * SSRF-safe image fetch — HTTPS/HTTP only, host allowlist, private-IP block.
 */

import { captureCaught } from '@/server/observability/capture.server.js';

const MAX_REDIRECTS = 3;
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;

const BLOCKED_HOSTNAMES = new Set([
  'localhost',
  'metadata.google.internal',
  'metadata.google',
  'kubernetes.default.svc',
]);

function parseIpv4(host: string): number[] | null {
  const parts = host.split('.');
  if (parts.length !== 4) return null;
  const octets = parts.map((p) => Number(p));
  if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return null;
  return octets;
}

function isPrivateOrReservedHost(hostname: string): boolean {
  const lower = hostname.toLowerCase();
  if (BLOCKED_HOSTNAMES.has(lower)) return true;
  if (
    lower === '::1' ||
    lower.startsWith('fe80:') ||
    lower.startsWith('fc') ||
    lower.startsWith('fd')
  )
    return true;

  const v4 = parseIpv4(lower);
  if (!v4) return false;
  const a = v4[0]!;
  const b = v4[1]!;
  if (a === 0) return true;
  if (a === 10) return true;
  if (a === 127) return true;
  if (a === 169 && b === 254) return true;
  if (a === 172 && b >= 16 && b <= 31) return true;
  if (a === 192 && b === 168) return true;
  if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
  return false;
}

function isAllowedHost(hostname: string, siteUrl?: string): boolean {
  const lower = hostname.toLowerCase();
  if (lower === 'imagedelivery.net' || lower.endsWith('.imagedelivery.net')) return true;
  if (siteUrl) {
    try {
      const siteHost = new URL(siteUrl).hostname.toLowerCase();
      if (lower === siteHost || lower.endsWith(`.${siteHost}`)) return true;
    } catch (err) {
      captureCaught(err, {
        scope: 'server.security.safe-fetch-image.site-url',
        severity: 'info',
      });
    }
  }
  return false;
}

function assertSafeImageUrl(url: string, siteUrl?: string): URL {
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch (err) {
    captureCaught(err, { scope: 'server.security.safe-fetch-image.parse', severity: 'info' });
    throw new Error('Invalid image URL', { cause: err });
  }
  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
    throw new Error('Image URL must use http or https');
  }
  const host = parsed.hostname.toLowerCase();
  if (isPrivateOrReservedHost(host)) {
    throw new Error('Image URL host not allowed');
  }
  if (!isAllowedHost(host, siteUrl)) {
    throw new Error('Image URL host not in allowlist');
  }
  return parsed;
}

export async function safeFetchImage(
  imageUrl: string,
  opts?: { siteUrl?: string; maxBytes?: number; signal?: AbortSignal },
): Promise<{ imageBytes: ArrayBuffer; mimeType: string }> {
  const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
  let currentUrl = imageUrl;

  for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
    assertSafeImageUrl(currentUrl, opts?.siteUrl);
    const res = await fetch(currentUrl, {
      signal: opts?.signal,
      redirect: 'manual',
    });

    if (res.status >= 300 && res.status < 400) {
      const location = res.headers.get('location');
      if (!location || hop === MAX_REDIRECTS) {
        throw new Error('Image fetch redirect limit exceeded');
      }
      currentUrl = new URL(location, currentUrl).toString();
      continue;
    }

    if (!res.ok) {
      throw new Error(`Image fetch failed: ${res.status}`);
    }

    const contentLength = res.headers.get('content-length');
    if (contentLength && parseInt(contentLength, 10) > maxBytes) {
      throw new Error('Image too large');
    }

    const imageBytes = await res.arrayBuffer();
    if (imageBytes.byteLength > maxBytes) {
      throw new Error('Image too large');
    }

    const mimeType = res.headers.get('content-type')?.split(';')[0]?.trim() || 'image/jpeg';
    return { imageBytes, mimeType };
  }

  throw new Error('Image fetch failed');
}
