import { FastifyInstance } from 'fastify';

// ── Domain allowlist ────────────────────────────────────────────────────────
// Only these hosts may be proxied. Keeps the proxy from being used as an SSRF
// vector against internal services (metadata endpoints, cloud metadata, etc.).
const ALLOWED_HOSTS = new Set([
  'coverartarchive.org',
  'lastfm.freetls.fastly.net',
  'i.scdn.co',
  'images.genius.com',
  'commons.wikimedia.org',
  'e.snmc.io',
  // Cover Art Archive 302s to the apex host, which then redirects on to an
  // ia*.us.archive.org node. The suffix below covers the node, not the apex.
  'archive.org',
]);

// Wildcard suffixes — any subdomain of these is allowed.
//
// The CDNs below are where enrichment actually stores artwork: of 572 albums
// with a cover, 126 sit on mzstatic (iTunes), 115 on dzcdn (Deezer) and 26 on
// discogs, and every coverartarchive.org URL 302s to an ia*.us.archive.org
// node. Without these the proxy answered 403 for every cover in the library.
const ALLOWED_SUFFIXES = [
  '.coverartarchive.org',
  '.musicbrainz.org',
  '.archive.org',
  '.mzstatic.com',
  '.dzcdn.net',
  '.discogs.com',
];

function isAllowed(hostname: string): boolean {
  if (ALLOWED_HOSTS.has(hostname)) return true;
  return ALLOWED_SUFFIXES.some(suffix => hostname.endsWith(suffix));
}

/**
 * Image proxy — fetches external artwork URLs server-side and returns them
 * with aggressive caching headers so the browser never re-fetches from
 * Discogs / Cover Art Archive on repeat page loads.
 */
export default async function imagesRoutes(fastify: FastifyInstance) {
  fastify.get('/images/proxy', async (request, reply) => {
    const { url } = request.query as { url?: string };
    if (!url) {
      return reply.code(400).send({ error: 'url query parameter is required' });
    }

    // Only proxy http(s) URLs — don't be an open proxy for file:// etc.
    if (!url.startsWith('http://') && !url.startsWith('https://')) {
      return reply.code(400).send({ error: 'Only http/https URLs are supported' });
    }

    let parsed: URL;
    try {
      parsed = new URL(url);
    } catch {
      return reply.code(400).send({ error: 'Invalid URL' });
    }

    if (!isAllowed(parsed.hostname)) {
      request.log.warn({ hostname: parsed.hostname, url }, 'Image proxy blocked — host not in allowlist');
      return reply.code(403).send({ error: 'Domain not allowed' });
    }

    try {
      const response = await fetch(url, {
        signal: AbortSignal.timeout(10_000),
        // Follow up to 5 redirects but validate each hop's domain
        redirect: 'manual',
      });

      // Handle redirect — validate the redirect target too
      if (response.status >= 300 && response.status < 400) {
        const location = response.headers.get('location');
        if (!location) {
          return reply.code(502).send({ error: 'Redirect with no location' });
        }
        let redirectParsed: URL;
        try {
          redirectParsed = new URL(location);
        } catch {
          return reply.code(502).send({ error: 'Invalid redirect URL' });
        }
        if (!isAllowed(redirectParsed.hostname)) {
          request.log.warn({ hostname: redirectParsed.hostname, location }, 'Image proxy blocked — redirect target not in allowlist');
          return reply.code(403).send({ error: 'Redirect target not allowed' });
        }
        // Re-fetch the redirect target
        const redirectResponse = await fetch(location, {
          signal: AbortSignal.timeout(10_000),
        });
        if (!redirectResponse.ok) {
          return reply.code(redirectResponse.status).send({ error: `Upstream returned ${redirectResponse.status}` });
        }
        const redirectBuffer = await redirectResponse.arrayBuffer();
        const redirectContentType = redirectResponse.headers.get('content-type') || 'image/jpeg';
        return reply
          .headers({
            'Content-Type': redirectContentType,
            'Cache-Control': 'public, max-age=31536000, immutable',
            'Content-Length': redirectBuffer.byteLength,
          })
          .send(Buffer.from(redirectBuffer));
      }

      if (!response.ok) {
        return reply.code(response.status).send({ error: `Upstream returned ${response.status}` });
      }

      const buffer = await response.arrayBuffer();
      const contentType = response.headers.get('content-type') || 'image/jpeg';

      // Cache aggressively — artwork URLs are immutable (Discogs, Cover Art
      // Archive etc. use content-addressed paths). 1 year.
      return reply
        .headers({
          'Content-Type': contentType,
          'Cache-Control': 'public, max-age=31536000, immutable',
          'Content-Length': buffer.byteLength,
        })
        .send(Buffer.from(buffer));
    } catch (err: any) {
      if (err?.name === 'TimeoutError' || err?.code === 'UND_ERR_CONNECT_TIMEOUT') {
        return reply.code(504).send({ error: 'Upstream timed out' });
      }
      request.log.error({ err, url }, 'Image proxy failed');
      return reply.code(502).send({ error: 'Failed to fetch image' });
    }
  });
}
