import { NextResponse } from 'next/server';
import { unstable_cache } from 'next/cache';
import { readFile } from 'fs/promises';
import { resolve, sep } from 'path';
import { lookup as dnsLookup } from 'dns/promises';
import { isIP } from 'net';
import { db, platformSettings } from '@server/db/drizzle';
import { inArray } from 'drizzle-orm';

const PUBLIC_DIR = resolve(process.cwd(), 'public');
const UPLOADS_ROOT = resolve(PUBLIC_DIR, 'uploads');
const BRAND_FAVICON_PATH = resolve(PUBLIC_DIR, 'favicon-brand.png');
const FALLBACK_PATH = resolve(PUBLIC_DIR, 'favicon-fallback.ico');
const FALLBACK_CONTENT_TYPE = 'image/x-icon';

const MAX_BYTES = 512 * 1024; // 512 KB hard cap
const REMOTE_TIMEOUT_MS = 4000;

const ALLOWED_MIME = new Set([
  'image/x-icon',
  'image/vnd.microsoft.icon',
  'image/png',
  'image/jpeg',
  'image/gif',
  'image/webp',
  'image/svg+xml',
]);

const EXT_TO_MIME: Record<string, string> = {
  ico: 'image/x-icon',
  png: 'image/png',
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg',
  gif: 'image/gif',
  webp: 'image/webp',
  svg: 'image/svg+xml',
};

function mimeFromUrl(url: string): string {
  const m = url.split('?')[0].match(/\.([a-z0-9]+)$/i);
  if (!m) return FALLBACK_CONTENT_TYPE;
  return EXT_TO_MIME[m[1].toLowerCase()] ?? FALLBACK_CONTENT_TYPE;
}

function normalizeMime(raw: string | null, urlForExt: string): string {
  const m = (raw ?? '').split(';')[0].trim().toLowerCase();
  if (m && ALLOWED_MIME.has(m)) return m;
  return mimeFromUrl(urlForExt);
}

function coerceUrl(v: unknown): string | null {
  if (typeof v === 'string' && v) return v;
  if (v && typeof v === 'object' && !Array.isArray(v)) {
    const first = Object.values(v as Record<string, unknown>)[0];
    if (typeof first === 'string' && first) return first;
  }
  return null;
}

// Resolve in priority order: explicit favicon → site logo → static fallback.
// Falling through to the logo means buyers who only upload a brand mark
// also get the right tab icon for free.
const loadFaviconUrl = unstable_cache(
  async (): Promise<string | null> => {
    try {
      const rows = await db
        .select({ key: platformSettings.key, value: platformSettings.value })
        .from(platformSettings)
        .where(inArray(platformSettings.key, ['site_favicon_url', 'site_logo_url']));
      const byKey: Record<string, unknown> = {};
      for (const r of rows) byKey[r.key] = r.value;
      return coerceUrl(byKey.site_favicon_url) ?? coerceUrl(byKey.site_logo_url);
    } catch {
      return null;
    }
  },
  ['favicon-url-v2'],
  { revalidate: 60, tags: ['branding'] },
);

function isPrivateIPv4(ip: string): boolean {
  const parts = ip.split('.').map(Number);
  if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return true;
  const [a, b] = parts;
  if (a === 10) return true;
  if (a === 127) return true;
  if (a === 0) return true;
  if (a === 169 && b === 254) return true; // link-local + AWS metadata
  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
  if (a >= 224) return true; // multicast / reserved
  return false;
}

function isPrivateIPv6(ip: string): boolean {
  const lower = ip.toLowerCase();
  if (lower === '::1' || lower === '::') return true;
  if (lower.startsWith('fc') || lower.startsWith('fd')) return true; // ULA
  if (lower.startsWith('fe80')) return true; // link-local
  if (lower.startsWith('::ffff:')) {
    const v4 = lower.slice(7);
    if (isIP(v4) === 4) return isPrivateIPv4(v4);
  }
  return false;
}

async function isSafeRemoteHost(hostname: string): Promise<boolean> {
  // Block obvious local names
  const lower = hostname.toLowerCase();
  if (lower === 'localhost' || lower.endsWith('.localhost') || lower.endsWith('.internal')) {
    return false;
  }
  if (isIP(hostname)) {
    return isIP(hostname) === 4 ? !isPrivateIPv4(hostname) : !isPrivateIPv6(hostname);
  }
  try {
    const addrs = await dnsLookup(hostname, { all: true });
    if (addrs.length === 0) return false;
    for (const a of addrs) {
      if (a.family === 4 && isPrivateIPv4(a.address)) return false;
      if (a.family === 6 && isPrivateIPv6(a.address)) return false;
    }
    return true;
  } catch {
    return false;
  }
}

async function loadLocalUpload(relativeUrl: string): Promise<{ bytes: Buffer; mime: string } | null> {
  try {
    const cleaned = relativeUrl.split('?')[0].split('#')[0];
    if (!cleaned.startsWith('/uploads/')) return null;
    const abs = resolve(PUBLIC_DIR, cleaned.replace(/^\/+/, ''));
    // Ensure path stays under public/uploads
    if (abs !== UPLOADS_ROOT && !abs.startsWith(UPLOADS_ROOT + sep)) return null;
    const bytes = await readFile(abs);
    if (bytes.byteLength > MAX_BYTES) return null;
    return { bytes, mime: mimeFromUrl(cleaned) };
  } catch {
    return null;
  }
}

async function loadRemote(rawUrl: string): Promise<{ bytes: Buffer; mime: string } | null> {
  let parsed: URL;
  try {
    parsed = new URL(rawUrl);
  } catch {
    return null;
  }
  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return null;
  if (parsed.username || parsed.password) return null;
  if (!(await isSafeRemoteHost(parsed.hostname))) return null;

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), REMOTE_TIMEOUT_MS);
  try {
    const res = await fetch(parsed.toString(), {
      cache: 'no-store',
      redirect: 'error', // do not follow redirects (would re-open SSRF surface)
      signal: controller.signal,
    });
    if (!res.ok) return null;

    const declaredLen = Number(res.headers.get('content-length') ?? '0');
    if (declaredLen && declaredLen > MAX_BYTES) return null;

    const mime = normalizeMime(res.headers.get('content-type'), parsed.pathname);
    if (!ALLOWED_MIME.has(mime)) return null;

    // Stream with a hard size cap to avoid memory blowup on missing/lying content-length
    const reader = res.body?.getReader();
    if (!reader) return null;
    const chunks: Uint8Array[] = [];
    let total = 0;
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      if (value) {
        total += value.byteLength;
        if (total > MAX_BYTES) {
          try { await reader.cancel(); } catch {}
          return null;
        }
        chunks.push(value);
      }
    }
    return { bytes: Buffer.concat(chunks.map((c) => Buffer.from(c))), mime };
  } catch {
    return null;
  } finally {
    clearTimeout(timer);
  }
}

async function loadFallback(): Promise<{ bytes: Buffer; mime: string }> {
  try {
    const bytes = await readFile(BRAND_FAVICON_PATH);
    return { bytes, mime: 'image/png' };
  } catch {
    const bytes = await readFile(FALLBACK_PATH);
    return { bytes, mime: FALLBACK_CONTENT_TYPE };
  }
}

// Accepts an optional `?v=<hash>` query so the root layout can cache-bust the
// browser's favicon entry whenever branding changes. The hash itself is not
// used to pick which icon to serve — that always comes from platform_settings
// — but having distinct URLs per version forces the browser to fetch fresh.
//
// Cache strategy (version-aware):
//   ?v=HASH present → immutable (browser may cache forever; hash change = new URL)
//   no ?v=          → no-cache (Chrome's automatic /favicon.ico fetch always fresh)
//
// The route is intentionally dynamic (reads request URL) so Next.js never puts
// the response in the Full Route Cache — each request executes the handler and
// uses the unstable_cache-backed DB read for efficiency.
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const hasVersion = searchParams.has('v');

  const faviconUrl = await loadFaviconUrl();
  let payload: { bytes: Buffer; mime: string } | null = null;

  if (faviconUrl) {
    if (/^https?:\/\//i.test(faviconUrl)) {
      payload = await loadRemote(faviconUrl);
    } else if (faviconUrl.startsWith('/')) {
      payload = await loadLocalUpload(faviconUrl);
    }
  }

  if (!payload) {
    try {
      payload = await loadFallback();
    } catch {
      return new NextResponse(null, { status: 404 });
    }
  }

  return new NextResponse(new Uint8Array(payload.bytes), {
    status: 200,
    headers: {
      'Content-Type': payload.mime,
      'Cache-Control': hasVersion
        ? 'public, max-age=31536000, immutable'
        : 'no-cache, must-revalidate',
    },
  });
}
