/**
 * Remote image URL validation (TC_097).
 *
 * Verifies that a user-supplied URL is reachable AND actually returns an image
 * (correct MIME type) before it is accepted into an image gallery. Failures
 * (404/403, network/timeout, non-image content, malformed URL) map to
 * user-friendly messages — internal error details are never surfaced.
 *
 * Includes a basic SSRF guard: only http/https and public hosts are fetched.
 */

export interface ImageUrlResult {
  valid: boolean;
  error?: string;
  contentType?: string;
  /**
   * The direct image URL to store. Normally the URL that was submitted, but for
   * a share/viewer link it is the image extracted from it — callers must save
   * THIS value, or the record keeps a link to a web page that renders nothing.
   */
  url?: string;
}

/** User-facing messages (no internal/browser/server details). */
export const IMAGE_URL_MESSAGES = {
  /** Malformed / unsafe URL. */
  INVALID_URL: 'Please enter a valid image URL.',
  /** Reachable check failed: 404/403, non-image, network, or timeout. */
  UNLOADABLE: 'Unable to load image from the provided URL.',
  /**
   * Reachable, but it is a web page rather than an image file. Worth its own
   * message: "unable to load" sends people back to re-copy the same link,
   * because from their side the link opens and shows a picture.
   */
  NOT_AN_IMAGE:
    'That link opens a web page, not an image file. Open the image itself, then '
    + 'right-click it and choose "Copy image address" — the link should end in '
    + '.jpg, .png or .webp.',
};

/**
 * SSRF guard — allow only http/https URLs pointing at public hosts.
 * Blocks localhost, private, loopback, and link-local address ranges.
 */
export function isSafeRemoteUrl(raw: string): boolean {
  let url: URL;
  try {
    url = new URL(raw);
  } catch {
    return false;
  }
  if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;

  // hostname keeps brackets for IPv6 literals (e.g. "[::1]") — strip them.
  const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
  if (!host || host === 'localhost' || host === '0.0.0.0' || host.endsWith('.local')) return false;

  // IPv4 private / loopback / link-local ranges
  const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  if (ipv4) {
    const a = Number(ipv4[1]);
    const b = Number(ipv4[2]);
    if (a === 0 || a === 10 || a === 127) return false;
    if (a === 169 && b === 254) return false;       // link-local
    if (a === 172 && b >= 16 && b <= 31) return false; // private
    if (a === 192 && b === 168) return false;       // private
  }

  // IPv6 loopback / link-local / unique-local
  if (host === '::1' || host === '::' || host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd')) {
    return false;
  }

  return true;
}

/**
 * Query parameters that image-search viewers use to carry the real image.
 *
 * Copying a picture from Google Images gives a link to `google.com/imgres`
 * (often behind a `share.google/…` shortlink) — an HTML page, not an image, so
 * it was rejected even though the address the user copied does show a picture.
 * The direct image is right there in the query string, so use it.
 *
 * `imgurl` is Google; `mediaurl` is Bing; `url`/`imageurl` cover the rest.
 */
const IMAGE_VIEWER_PARAMS = ['imgurl', 'mediaurl', 'imageurl', 'image_url', 'url'];

/** Sent when fetching a candidate URL — a plain agent gets blocked by some CDNs. */
const IMAGE_FETCH_USER_AGENT =
  'Mozilla/5.0 (compatible; MengoImageValidator/1.0; +https://app.mengoengine.com)';

/**
 * Pull a direct image URL out of a viewer/redirect page URL, or null.
 * The extracted value is re-checked by the caller like any other URL, so a
 * hostile page cannot use this to point the fetch at somewhere private.
 */
export function extractImageUrlFromPageUrl(rawUrl: string): string | null {
  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    return null;
  }

  for (const param of IMAGE_VIEWER_PARAMS) {
    const candidate = url.searchParams.get(param);
    if (!candidate) continue;
    const trimmed = candidate.trim();
    // Only take it if it is itself a fetchable http(s) URL — `url=` in
    // particular is a generic parameter and is often not an image at all.
    if (/^https?:\/\//i.test(trimmed) && isSafeRemoteUrl(trimmed)) return trimmed;
  }
  return null;
}

/** `<meta property="og:image" content="…">` and the Twitter equivalent. */
const META_IMAGE_RE =
  /<meta[^>]+(?:property|name)\s*=\s*["'](?:og:image(?::secure_url)?|twitter:image(?::src)?)["'][^>]*>/gi;
const META_CONTENT_RE = /content\s*=\s*["']([^"']+)["']/i;

/**
 * Fall back to the page's own preview image when the URL carries no image
 * parameter — that is what a "share this page" link resolves to, and og:image
 * is exactly the picture the sharer saw in the preview.
 */
export function extractImageUrlFromHtml(html: string, baseUrl: string): string | null {
  if (!html) return null;
  for (const tag of html.slice(0, 200_000).match(META_IMAGE_RE) || []) {
    const content = tag.match(META_CONTENT_RE)?.[1]?.trim();
    if (!content) continue;
    let absolute: string;
    try {
      absolute = new URL(content, baseUrl).toString();
    } catch {
      continue;
    }
    if (isSafeRemoteUrl(absolute)) return absolute;
  }
  return null;
}

type MinimalResponse = {
  ok: boolean;
  status: number;
  headers: { get(name: string): string | null };
  url?: string;
  text?: () => Promise<string>;
};
type FetchFn = (url: string, init?: any) => Promise<MinimalResponse>;

/**
 * Validate that `rawUrl` is reachable and returns image content.
 * `opts.fetchFn` is injectable for testing; defaults to global fetch.
 */
export async function validateRemoteImageUrl(
  rawUrl: unknown,
  opts?: { fetchFn?: FetchFn; timeoutMs?: number },
  depth = 0,
): Promise<ImageUrlResult> {
  const value = String(rawUrl ?? '').trim();
  if (!value) return { valid: false, error: IMAGE_URL_MESSAGES.INVALID_URL };

  // Malformed or unsafe (SSRF) URL — treat as an invalid URL.
  if (!isSafeRemoteUrl(value)) return { valid: false, error: IMAGE_URL_MESSAGES.INVALID_URL };

  const fetchFn = opts?.fetchFn ?? (globalThis.fetch as unknown as FetchFn | undefined);
  if (!fetchFn) return { valid: false, error: IMAGE_URL_MESSAGES.UNLOADABLE };

  const timeoutMs = opts?.timeoutMs ?? 5000;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetchFn(value, {
      method: 'GET',
      signal: controller.signal,
      redirect: 'follow',
      // Some hosts serve a bot-blocking HTML page to an unknown agent, and a
      // few refuse outright — both look like "not an image" from here.
      headers: { 'user-agent': IMAGE_FETCH_USER_AGENT, accept: 'image/*,text/html;q=0.8,*/*;q=0.5' },
    });
    if (!res.ok) return { valid: false, error: IMAGE_URL_MESSAGES.UNLOADABLE }; // 404/403/5xx

    const contentType = (res.headers.get('content-type') || '').trim();
    if (/^image\//i.test(contentType)) {
      return { valid: true, contentType, url: value };
    }

    // Not an image. Before giving up, see whether this is a viewer or share
    // page that is *showing* one — pasting such a link is the common case, and
    // rejecting it outright is what made a perfectly good picture unusable.
    // Resolved once only: the extracted URL must be an image itself.
    if (depth === 0) {
      const finalUrl = typeof res.url === 'string' && res.url ? res.url : value;

      const fromQuery = extractImageUrlFromPageUrl(finalUrl) || extractImageUrlFromPageUrl(value);
      if (fromQuery) {
        const resolved = await validateRemoteImageUrl(fromQuery, opts, depth + 1);
        if (resolved.valid) return resolved;
      }

      if (/^text\/html/i.test(contentType) && typeof res.text === 'function') {
        const html = await res.text().catch(() => '');
        const fromMeta = extractImageUrlFromHtml(html, finalUrl);
        if (fromMeta) {
          const resolved = await validateRemoteImageUrl(fromMeta, opts, depth + 1);
          if (resolved.valid) return resolved;
        }
      }
    }

    return { valid: false, error: IMAGE_URL_MESSAGES.NOT_AN_IMAGE };
  } catch {
    // Network error, timeout, or abort — never leak internal details.
    return { valid: false, error: IMAGE_URL_MESSAGES.UNLOADABLE };
  } finally {
    clearTimeout(timer);
  }
}
