import { randomUUID } from 'node:crypto';
import { lookup } from 'node:dns/promises';
import { isIP } from 'node:net';
import { copyFile, mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { NextRequest, NextResponse } from 'next/server';

/**
 * Peer upload directories (frontend and storefront) that must mirror the
 * admin's own `public/uploads/`. After every successful upload, the file
 * is copied to each peer so that product pages render images regardless
 * of which app the URL is fetched from.
 */
const PEER_UPLOAD_DIRS = [
  path.join(process.cwd(), '..', '..', 'frontend', 'public', 'uploads'),
  path.join(process.cwd(), '..', '..', 'storefront', 'public', 'uploads'),
];

/** Thumbnail widths pre-generated for lists (40px) and previews (200px). */
const THUMBNAIL_WIDTHS = [40, 200];
/**
 * Maximum height ratio (h/w) for cached thumbnails. The thumbnail is scaled
 * to fit inside a `width × width * MAX_HEIGHT_RATIO` bounding box without
 * cropping. A non-square source produces a non-square thumbnail that preserves
 * the original aspect ratio, so the frontend can render it with
 * `object-contain` inside a matching box.
 */
const MAX_HEIGHT_RATIO = 1.4;

async function mirrorToPeers(filePath: string): Promise<void> {
  await Promise.allSettled(
    PEER_UPLOAD_DIRS.map(async (dir) => {
      await mkdir(dir, { recursive: true });
      await copyFile(filePath, path.join(dir, path.basename(filePath)));
    }),
  );
}

/**
 * Generate the 40px/200px thumbnails for every upload directory right after
 * the file lands on disk, so list and preview URLs (`/uploads/40/<file>` and
 * `/uploads/200/<file>`) resolve without waiting for the batch script.
 * Failures are non-fatal: the dynamic `/uploads/[...path]` handler regenerates
 * missing thumbnails on demand.
 */
async function generateThumbnails(buffer: Buffer, filename: string): Promise<void> {
  let sharp: (input: Buffer) => import('sharp').Sharp;
  try {
    ({ default: sharp } = await import('sharp'));
  } catch {
    return;
  }

  const targets = [path.join(process.cwd(), 'public', 'uploads'), ...PEER_UPLOAD_DIRS];
  await Promise.allSettled(
    targets.flatMap((dir) =>
      THUMBNAIL_WIDTHS.map(async (width) => {
        const thumbDir = path.join(dir, String(width));
        await mkdir(thumbDir, { recursive: true });
        const maxHeight = Math.round(width * MAX_HEIGHT_RATIO);
        const resized = await sharp(buffer)
          .resize({ width, height: maxHeight, fit: 'inside', withoutEnlargement: true })
          .toBuffer();
        await writeFile(path.join(thumbDir, filename), resized);
      }),
    ),
  );
}

const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
const MAX_SIZE = 10 * 1024 * 1024;
const EXTENSION_BY_TYPE: Readonly<Record<string, string>> = {
  'image/jpeg': 'jpg',
  'image/png': 'png',
  'image/webp': 'webp',
  'image/avif': 'avif',
  'image/gif': 'gif',
};

export async function POST(request: NextRequest) {
  if (!(await isAuthenticatedBackofficeRequest(request))) {
    return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
  }

  try {
    if (request.headers.get('content-type')?.includes('application/json')) {
      const body = (await request.json()) as { url?: unknown };
      if (typeof body.url !== 'string' || !body.url.trim()) {
        return NextResponse.json({ error: 'URL de imagen obligatoria' }, { status: 400 });
      }
      const remote = await downloadRemoteImage(body.url.trim());
      const saved = await saveUpload(remote.buffer, remote.extension);
      return NextResponse.json(saved);
    }

    const formData = await request.formData();
    const file = formData.get('file');

    if (!(file instanceof File)) {
      return NextResponse.json({ error: 'No file provided' }, { status: 400 });
    }

    const extension = EXTENSION_BY_TYPE[file.type];
    if (!extension) {
      return NextResponse.json(
        { error: `Tipo no permitido. Usa: ${Object.keys(EXTENSION_BY_TYPE).join(', ')}` },
        { status: 400 },
      );
    }

    if (file.size <= 0 || file.size > MAX_SIZE) {
      return NextResponse.json({ error: 'El archivo debe ocupar entre 1 byte y 10MB' }, { status: 400 });
    }

    const buffer = Buffer.from(await file.arrayBuffer());
    if (!hasExpectedImageSignature(buffer, file.type)) {
      return NextResponse.json({ error: 'El contenido no coincide con un formato de imagen válido' }, { status: 400 });
    }

    const saved = await saveUpload(buffer, extension);
    return NextResponse.json({ ...saved, size: file.size });
  } catch (error) {
    console.error('Upload error:', error);
    return NextResponse.json({ error: 'Error al subir el archivo' }, { status: 500 });
  }
}

async function saveUpload(buffer: Buffer, extension: string): Promise<{ url: string; filename: string }> {
  const filename = `${randomUUID()}.${extension}`;
  const uploadDir = path.join(process.cwd(), 'public', 'uploads');
  const filePath = path.join(uploadDir, filename);
  await mkdir(uploadDir, { recursive: true });
  await writeFile(filePath, buffer, { flag: 'wx' });
  await mirrorToPeers(filePath);
  await generateThumbnails(buffer, filename);
  return { url: `/uploads/${filename}`, filename };
}

async function downloadRemoteImage(rawUrl: string): Promise<{ buffer: Buffer; extension: string }> {
  let currentUrl = rawUrl;
  for (let attempt = 0; attempt < 4; attempt += 1) {
    await assertPublicRemoteUrl(currentUrl);
    const response = await fetch(currentUrl, {
      redirect: 'manual',
      headers: { Accept: 'image/*', 'User-Agent': 'MercadoDeVida image importer' },
      signal: AbortSignal.timeout(15_000),
    });
    if (response.status >= 300 && response.status < 400) {
      const location = response.headers.get('location');
      if (!location) throw new Error('La imagen remota redirige sin destino');
      currentUrl = new URL(location, currentUrl).toString();
      continue;
    }
    if (!response.ok) throw new Error(`La imagen remota respondió ${response.status}`);
    const contentType = (response.headers.get('content-type') ?? '').split(';')[0].toLowerCase();
    const extension = EXTENSION_BY_TYPE[contentType];
    if (!extension) throw new Error('La URL no devuelve una imagen compatible');
    const declaredSize = Number(response.headers.get('content-length') ?? 0);
    if (declaredSize > MAX_SIZE) throw new Error('La imagen supera el límite de 10MB');
    const buffer = Buffer.from(await response.arrayBuffer());
    if (buffer.length === 0 || buffer.length > MAX_SIZE) throw new Error('La imagen supera el límite de 10MB');
    if (!hasExpectedImageSignature(buffer, contentType)) throw new Error('El contenido remoto no es una imagen válida');
    return { buffer, extension };
  }
  throw new Error('Demasiadas redirecciones en la URL de imagen');
}

async function assertPublicRemoteUrl(rawUrl: string): Promise<void> {
  let parsed: URL;
  try { parsed = new URL(rawUrl); } catch { throw new Error('URL de imagen no válida'); }
  if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) {
    throw new Error('Solo se permiten URLs HTTP o HTTPS públicas');
  }
  const hostname = parsed.hostname.toLowerCase();
  if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local')) {
    throw new Error('No se permiten hosts locales');
  }
  const addresses = await lookup(hostname, { all: true });
  if (addresses.some(({ address }) => isPrivateAddress(address))) {
    throw new Error('No se permiten direcciones privadas o internas');
  }
}

function isPrivateAddress(address: string): boolean {
  if (isIP(address) === 4) {
    const octets = address.split('.').map(Number);
    return octets[0] === 10 || octets[0] === 127 || octets[0] === 0 ||
      (octets[0] === 169 && octets[1] === 254) ||
      (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) ||
      (octets[0] === 192 && octets[1] === 168);
  }
  const normalized = address.toLowerCase();
  return normalized === '::1' || normalized.startsWith('fc') || normalized.startsWith('fd') || normalized.startsWith('fe80:');
}

async function isAuthenticatedBackofficeRequest(request: NextRequest): Promise<boolean> {
  const cookies = request.headers.get('cookie');
  if (!cookies) return false;

  try {
    const response = await fetch(`${API}/backoffice/auth/me`, {
      headers: { Cookie: cookies },
      cache: 'no-store',
    });
    if (!response.ok) return false;
    const body = (await response.json()) as { id?: unknown; role?: unknown; user?: null };
    return (
      typeof body.id === 'string' &&
      (body.role === 'admin' || body.role === 'editor') &&
      body.user !== null
    );
  } catch {
    return false;
  }
}

function hasExpectedImageSignature(buffer: Buffer, type: string): boolean {
  if (type === 'image/jpeg') {
    return buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
  }
  if (type === 'image/png') {
    return buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex'));
  }
  if (type === 'image/gif') {
    const signature = buffer.subarray(0, 6).toString('ascii');
    return signature === 'GIF87a' || signature === 'GIF89a';
  }
  if (type === 'image/webp') {
    return (
      buffer.length >= 12 &&
      buffer.subarray(0, 4).toString('ascii') === 'RIFF' &&
      buffer.subarray(8, 12).toString('ascii') === 'WEBP'
    );
  }
  if (type === 'image/avif') {
    if (buffer.length < 12 || buffer.subarray(4, 8).toString('ascii') !== 'ftyp') return false;
    const brands = buffer.subarray(8, Math.min(buffer.length, 32)).toString('ascii');
    return brands.includes('avif') || brands.includes('avis');
  }
  return false;
}
