import { z } from 'zod';

import { baseListQuerySchema } from '@/lib/api/list-query';
import { WEBHOOK_EVENTS } from '@/lib/services/webhook-event-map';

// ─── SSRF guards ──────────────────────────────────────────────────────────────

const BLOCKED_HOSTNAME_SUFFIXES = [
  '.internal',
  '.local',
  '.localhost',
  '.lan',
  '.intranet',
  '.corp',
  '.home',
  '.private',
];
const BLOCKED_HOSTNAME_LITERALS = new Set([
  'localhost',
  '0.0.0.0',
  '255.255.255.255',
  // IPv6 metadata host (Azure/GCP); literal with brackets handled below
  'metadata.google.internal',
  'metadata.azure.internal',
]);

/** Returns true if `host` is an IPv4 address in any disallowed range. */
function isBlockedIpv4(host: string): boolean {
  const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  if (!m) return false;
  const oct = m.slice(1, 5).map(Number);
  if (oct.some((o) => o < 0 || o > 255)) return true; // malformed → treat as blocked
  const [a, b, c, d] = oct as [number, number, number, number];
  if (a === 10) return true; // 10/8 RFC1918
  if (a === 127) return true; // 127/8 loopback
  if (a === 169 && b === 254) return true; // 169.254/16 link-local + AWS IMDS
  if (a === 172 && b >= 16 && b <= 31) return true; // 172.16/12 RFC1918
  if (a === 192 && b === 168) return true; // 192.168/16 RFC1918
  if (a === 100 && b >= 64 && b <= 127) return true; // 100.64/10 CGNAT
  if (a === 0) return true; // 0/8 zero
  if (a >= 224) return true; // multicast / reserved
  // outbound-webhook-auditor M1: Oracle Cloud metadata endpoint
  // (192.0.0.192) - was missing from the original denylist.
  if (a === 192 && b === 0 && c === 0 && d === 192) return true;
  return false;
}

/**
 * Extracts the embedded IPv4 dotted-quad from an IPv4-mapped IPv6 literal.
 *
 * L28: Node's URL parser normalizes `[::ffff:127.0.0.1]` to the hex form
 * `[::ffff:7f00:1]`, so the old `h.slice(7)` (which assumed a dotted-quad
 * tail like `::ffff:127.0.0.1`) produced `7f00:1` and never matched
 * `isBlockedIpv4`'s dotted-quad regex — the mapped-range block was dead.
 * We now handle BOTH encodings: a literal dotted-quad tail, and the
 * two-hextet hex tail Node actually emits, reconstructing the dotted-quad
 * from the hextets before range-checking.
 */
function extractMappedIpv4(h: string): string | null {
  if (!h.startsWith('::ffff:')) return null;
  const tail = h.slice(7);
  // Form 1: `::ffff:127.0.0.1` (dotted-quad tail survived parsing)
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(tail)) return tail;
  // Form 2: `::ffff:7f00:1` (Node-normalized two-hextet hex tail).
  const hextets = tail.split(':');
  if (hextets.length !== 2) return null;
  if (!hextets.every((g) => /^[0-9a-f]{1,4}$/.test(g))) return null;
  const high = parseInt(hextets[0]!, 16);
  const low = parseInt(hextets[1]!, 16);
  if (Number.isNaN(high) || Number.isNaN(low)) return null;
  return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
}

/** Returns true if `host` is an IPv6 literal in any disallowed range. */
function isBlockedIpv6(host: string): boolean {
  // strip brackets if present
  const h = host.replace(/^\[/, '').replace(/\]$/, '').toLowerCase();
  if (h === '::1') return true; // loopback
  if (h === '::') return true; // unspecified
  if (h.startsWith('fe80:') || h.startsWith('fe80::')) return true; // link-local
  if (/^f[cd][0-9a-f]{2}:/.test(h)) return true; // fc00::/7 unique-local
  // IPv4-mapped (::ffff:0:0/96) — block when the embedded v4 is private/
  // reserved. Handles both the dotted-quad and Node's normalized hex form.
  const mapped = extractMappedIpv4(h);
  if (mapped !== null) {
    return isBlockedIpv4(mapped);
  }
  return false;
}

/**
 * Reject webhook URLs whose hostname targets a private/internal/loopback/
 * link-local destination. The webhook worker `fetch`es the URL and writes
 * a slice of the response body into `webhook_deliveries.response_body`,
 * which is later returned by the deliveries listing endpoint - making any
 * SSRF here an information-disclosure read primitive against any internal
 * service the worker can reach. Does NOT defend against DNS rebinding;
 * the worker performs its own re-resolution at dispatch time.
 */
export function isLocalOrPrivateHost(rawUrl: string): boolean {
  let parsed: URL;
  try {
    parsed = new URL(rawUrl);
  } catch {
    return true;
  }
  const host = parsed.hostname.toLowerCase();
  if (BLOCKED_HOSTNAME_LITERALS.has(host)) return true;
  if (BLOCKED_HOSTNAME_SUFFIXES.some((s) => host === s.slice(1) || host.endsWith(s))) {
    return true;
  }
  if (host.startsWith('[') || host.includes(':')) {
    if (isBlockedIpv6(host)) return true;
  }
  if (isBlockedIpv4(host)) return true;
  return false;
}

const urlSchema = z
  .string()
  .url('Must be a valid HTTPS URL')
  .refine((u) => u.startsWith('https://'), 'Webhook URL must use HTTPS')
  .refine((u) => !isLocalOrPrivateHost(u), 'Webhook URL host is not allowed');

// ─── Create ───────────────────────────────────────────────────────────────────

export const createWebhookSchema = z.object({
  name: z.string().min(1).max(200),
  url: urlSchema,
  events: z.array(z.enum(WEBHOOK_EVENTS)).min(1, 'At least one event must be selected'),
  isActive: z.boolean().default(true),
});

// ─── Update ───────────────────────────────────────────────────────────────────

export const updateWebhookSchema = z.object({
  name: z.string().min(1).max(200).optional(),
  url: urlSchema.optional(),
  events: z.array(z.enum(WEBHOOK_EVENTS)).min(1).optional(),
  isActive: z.boolean().optional(),
});

// ─── List Deliveries ──────────────────────────────────────────────────────────

export const listDeliveriesSchema = baseListQuerySchema.extend({
  status: z.enum(['pending', 'success', 'failed', 'dead_letter']).optional(),
});

// ─── Types ────────────────────────────────────────────────────────────────────

export type CreateWebhookInput = z.infer<typeof createWebhookSchema>;
export type UpdateWebhookInput = z.infer<typeof updateWebhookSchema>;
export type ListDeliveriesInput = z.infer<typeof listDeliveriesSchema>;
