import sharp from 'sharp';
import type { CandidataImagen, ResultadoSerper } from './tipos';

export const MIN_LADO_PX = 400;
/** Cuántas candidatas (únicas por URL) se descargan por producto. */
export const MAX_DESCARGAS = 20;
/** Cuántas van al LLM de visión en una sola llamada. */
export const MAX_A_VISION = 10;
/** Distancia de Hamming (dHash de 64 bits) a partir de la cual dos imágenes son la misma. */
export const DISTANCIA_DUPLICADO = 6;

const TIMEOUT_DESCARGA_MS = 12_000;
const MAX_BYTES = 10 * 1024 * 1024;
const USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124 Safari/537.36';

/** Sitios que suelen superponer logos/precios (marca de agua de tienda). */
const DOMINIOS_PENALIZADOS = ['mercadolibre', 'mlstatic', 'aliexpress', 'alicdn', 'shopee', 'pinterest', 'facebook', 'instagram', 'youtube', 'tiktok'];

const soloAlnum = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');

export function puntajePrefiltro(r: ResultadoSerper, marca: string | null): number {
  let s = 0;
  const dom = (r.domain ?? '').toLowerCase();
  const m = marca ? soloAlnum(marca) : '';
  if (m && soloAlnum(dom).includes(m)) s += 3; // sitio del fabricante
  if (DOMINIOS_PENALIZADOS.some((d) => dom.includes(d))) s -= 2;
  if (Math.min(r.imageWidth ?? 0, r.imageHeight ?? 0) >= 800) s += 1;
  return s;
}

export function candidataDesdeSerper(r: ResultadoSerper, marca: string | null): CandidataImagen {
  return {
    url: r.imageUrl,
    dominio: r.domain ?? '',
    titulo: r.title ?? '',
    ancho: r.imageWidth ?? 0,
    alto: r.imageHeight ?? 0,
    prefiltro: puntajePrefiltro(r, marca),
  };
}

export async function descargarImagen(url: string, fetchFn: typeof fetch = fetch): Promise<Buffer | null> {
  try {
    const res = await fetchFn(url, {
      signal: AbortSignal.timeout(TIMEOUT_DESCARGA_MS),
      headers: { 'User-Agent': USER_AGENT, Accept: 'image/*,*/*;q=0.8' },
    });
    if (!res.ok) return null;
    if (!(res.headers.get('content-type') ?? '').startsWith('image/')) return null;
    const buf = Buffer.from(await res.arrayBuffer());
    return buf.length > MAX_BYTES ? null : buf;
  } catch {
    return null;
  }
}

/** Proporción de píxeles casi blancos en el anillo exterior (2 px sobre 64×64). */
async function bordeBlanco(img: sharp.Sharp): Promise<number> {
  const { data, info } = await img.clone().resize(64, 64, { fit: 'fill' }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
  let total = 0;
  let blancos = 0;
  for (let y = 0; y < info.height; y++) {
    for (let x = 0; x < info.width; x++) {
      if (y > 1 && y < info.height - 2 && x > 1 && x < info.width - 2) continue;
      const i = (y * info.width + x) * info.channels;
      total++;
      if (data[i] >= 232 && data[i + 1] >= 232 && data[i + 2] >= 232) blancos++;
    }
  }
  return blancos / total;
}

async function dhash(img: sharp.Sharp): Promise<string> {
  const { data } = await img.clone().grayscale().resize(9, 8, { fit: 'fill' }).raw().toBuffer({ resolveWithObject: true });
  let bits = '';
  for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) bits += data[y * 9 + x] < data[y * 9 + x + 1] ? '1' : '0';
  return bits;
}

export async function analizarImagen(
  buffer: Buffer,
): Promise<{ ancho: number; alto: number; bordeBlanco: number; dhash: string } | { descarte: string }> {
  try {
    const img = sharp(buffer, { failOn: 'none' });
    const meta = await img.metadata();
    if (!meta.width || !meta.height) return { descarte: 'sin metadata' };
    if (Math.min(meta.width, meta.height) < MIN_LADO_PX) return { descarte: `chica (${meta.width}x${meta.height})` };
    const ratio = meta.width / meta.height;
    if (ratio < 0.5 || ratio > 2) return { descarte: `proporción rara (${ratio.toFixed(2)})` };
    return { ancho: meta.width, alto: meta.height, bordeBlanco: await bordeBlanco(img), dhash: await dhash(img) };
  } catch (e) {
    return { descarte: `error imagen: ${(e as Error).message.slice(0, 60)}` };
  }
}

export function hamming(a: string, b: string): number {
  let d = 0;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) d++;
  return d;
}

/** Marca `descarte='duplicada'` en las candidatas vivas perceptualmente iguales a una anterior. */
export function marcarDuplicadas(cands: CandidataImagen[]): void {
  const vistas: string[] = [];
  for (const c of cands) {
    if (c.descarte || !c.dhash) continue;
    if (vistas.some((h) => hamming(h, c.dhash!) <= DISTANCIA_DUPLICADO)) c.descarte = 'duplicada';
    else vistas.push(c.dhash);
  }
}

/** Candidatas vivas ordenadas (borde más blanco primero, prefiltro como desempate); las que sobran quedan marcadas. */
export function ordenarParaVision(cands: CandidataImagen[]): CandidataImagen[] {
  const vivas = cands
    .filter((c) => !c.descarte && c.buffer)
    .sort((a, b) => (b.bordeBlanco ?? 0) - (a.bordeBlanco ?? 0) + (b.prefiltro - a.prefiltro) * 0.1);
  vivas.slice(MAX_A_VISION).forEach((c) => (c.descarte = 'fuera del top para visión'));
  return vivas.slice(0, MAX_A_VISION);
}
