// lib/exporters/endpoint-validator.mjs
// Validate OTLP/HTTP endpoint URLs for the OTel exporter.
//
// CWE-918 (Server-Side Request Forgery) mitigation: reject loopback, RFC-1918,
// link-local (cloud metadata 169.254.169.254), and require HTTPS for non-loopback.
// Operator opt-in for private endpoints via VOYAGE_OTEL_ALLOW_PRIVATE=1
// (legitimate home-lab / docker-compose operator scenario).

import { ok, fail, issue } from '../util/result.mjs';

const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost', '0.0.0.0']);
const LINK_LOCAL_PREFIXES = ['169.254.', 'fe80:'];

function isRfc1918(host) {
  // 10.0.0.0/8
  if (/^10\./.test(host)) return true;
  // 172.16.0.0/12
  if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(host)) return true;
  // 192.168.0.0/16
  if (/^192\.168\./.test(host)) return true;
  return false;
}

function isLoopback(host) {
  return LOOPBACK_HOSTS.has(host);
}

function isLinkLocal(host) {
  return LINK_LOCAL_PREFIXES.some(p => host.startsWith(p));
}

/**
 * Validate an OTLP/HTTP endpoint URL.
 *
 * @param {string} url
 * @param {{env?: object}} [opts]
 * @returns {import('../util/result.mjs').Result}
 */
export function validateOtlpEndpoint(url, opts = {}) {
  const env = opts.env || process.env;
  const allowPrivate = env.VOYAGE_OTEL_ALLOW_PRIVATE === '1';

  if (typeof url !== 'string' || url.length === 0) {
    return fail(issue('ENDPOINT_EMPTY', 'Endpoint must be a non-empty string'));
  }

  let parsed;
  try { parsed = new URL(url); }
  catch (e) {
    return fail(issue('ENDPOINT_PARSE_ERROR', `Invalid URL: ${e.message}`));
  }

  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    return fail(issue('ENDPOINT_BAD_PROTOCOL',
      `Endpoint protocol must be http or https, got ${parsed.protocol}`));
  }

  // Strip brackets from IPv6
  const host = parsed.hostname.replace(/^\[|\]$/g, '');

  // Cloud metadata IP (link-local) — always rejected unless explicit opt-in
  if (isLinkLocal(host) && !allowPrivate) {
    return fail(issue('ENDPOINT_LINK_LOCAL_REJECTED',
      `Link-local endpoint ${host} rejected (potential cloud-metadata access). ` +
      `Set VOYAGE_OTEL_ALLOW_PRIVATE=1 to allow.`));
  }

  // Loopback / RFC-1918 — rejected unless opt-in
  if (isLoopback(host) && !allowPrivate) {
    return fail(issue('ENDPOINT_LOOPBACK_REJECTED',
      `Loopback endpoint ${host} rejected. Set VOYAGE_OTEL_ALLOW_PRIVATE=1 for ` +
      `home-lab / docker-compose scenarios.`));
  }

  if (isRfc1918(host) && !allowPrivate) {
    return fail(issue('ENDPOINT_RFC1918_REJECTED',
      `RFC-1918 private endpoint ${host} rejected. ` +
      `Set VOYAGE_OTEL_ALLOW_PRIVATE=1 for home-lab scenarios.`));
  }

  // For non-loopback, non-private endpoints: require HTTPS
  const isPrivate = isLoopback(host) || isRfc1918(host) || isLinkLocal(host);
  if (!isPrivate && parsed.protocol === 'http:') {
    return fail(issue('ENDPOINT_HTTPS_REQUIRED',
      `Public endpoint ${host} requires https:// (got http://)`));
  }

  return ok({ url: parsed.href, host, isPrivate });
}

export { LOOPBACK_HOSTS, LINK_LOCAL_PREFIXES };
