import { Injectable, Logger } from '@nestjs/common';
import sharp from 'sharp';
import { AiProviderService } from 'src/ai-dashboard/ai-provider/ai-provider.service';
import { BuscadorSerperService } from './buscador-serper.service';
import { armarPartesVision, parsearEvaluacion, PROMPT_VISION } from './pipeline/evaluador-vision';
import { armarEntradaNormalizacion, parsearNormalizacion, PROMPT_NORMALIZAR, queriesFallback } from './pipeline/normalizador-queries';
import { analizarImagen, candidataDesdeSerper, descargarImagen, marcarDuplicadas, MAX_DESCARGAS, ordenarParaVision } from './pipeline/prefiltro-imagenes';
import { calcularScore, seleccionarParaGuardar } from './pipeline/puntaje';
import type { CandidataImagen, EvaluacionVision, ProductoParaBuscar, QueriesProducto } from './pipeline/tipos';

export const FEATURE_IMAGENES = 'IMAGENES';

export interface ContextoBusqueda {
  empresaId: string;
  busquedaId: string;
  usuarioId: string | null;
}

export interface SugerenciaNueva {
  buffer: Buffer;
  fuente_url: string;
  fuente_dominio: string;
  fuente_titulo: string;
  ancho: number;
  alto: number;
  vision: EvaluacionVision;
  score: number;
}

const CONCURRENCIA_DESCARGAS = 4;

async function pool<T, R>(items: T[], n: number, fn: (t: T) => Promise<R>): Promise<R[]> {
  const out: R[] = new Array(items.length);
  let idx = 0;
  await Promise.all(
    Array.from({ length: Math.min(n, items.length) }, async () => {
      while (idx < items.length) {
        const i = idx++;
        out[i] = await fn(items[i]);
      }
    }),
  );
  return out;
}

/** Pipeline de un producto: queries → Serper → descarga/prefiltro → visión → selección. Sin acceso a DB ni storage. */
@Injectable()
export class PipelineProductoService {
  private readonly logger = new Logger(PipelineProductoService.name);
  /** Sobrescribible en tests. */
  private descargar: (url: string) => Promise<Buffer | null> = (url) => descargarImagen(url);

  constructor(
    private readonly serper: BuscadorSerperService,
    private readonly ai: AiProviderService,
  ) {}

  private meta(ctx: ContextoBusqueda) {
    return { feature: FEATURE_IMAGENES, usuarioId: ctx.usuarioId, referenciaId: ctx.busquedaId };
  }

  async normalizarLote(ctx: ContextoBusqueda, productos: ProductoParaBuscar[]): Promise<Map<string, QueriesProducto>> {
    let mapa = new Map<string, QueriesProducto>();
    try {
      const res = await this.ai.completar(ctx.empresaId, armarEntradaNormalizacion(productos), PROMPT_NORMALIZAR, this.meta(ctx));
      mapa = parsearNormalizacion(res.content);
    } catch (e) {
      this.logger.warn(`Normalización falló (empresa ${ctx.empresaId}): ${(e as Error).message}; se usa fallback`);
    }
    for (const p of productos) if (!mapa.has(p.id)) mapa.set(p.id, queriesFallback(p));
    return mapa;
  }

  async procesarProducto(
    ctx: ContextoBusqueda,
    producto: ProductoParaBuscar,
    norm: QueriesProducto,
  ): Promise<{ sugerencias: SugerenciaNueva[]; queriesNuevas: number }> {
    // 1) Serper: candidatas únicas por URL.
    const candidatas: CandidataImagen[] = [];
    const vistas = new Set<string>();
    let queriesNuevas = 0;
    for (const q of norm.queries) {
      const { resultados, desdeCache } = await this.serper.buscar(q);
      if (!desdeCache) queriesNuevas++;
      for (const r of resultados) {
        if (vistas.has(r.imageUrl)) continue;
        vistas.add(r.imageUrl);
        candidatas.push(candidataDesdeSerper(r, norm.marca));
      }
    }

    // 2) Descarga + análisis de las mejores según prefiltro.
    candidatas.sort((a, b) => b.prefiltro - a.prefiltro);
    const aDescargar = candidatas.slice(0, MAX_DESCARGAS);
    candidatas.slice(MAX_DESCARGAS).forEach((c) => (c.descarte = 'fuera del top de prefiltro'));
    await pool(aDescargar, CONCURRENCIA_DESCARGAS, async (c) => {
      const buf = await this.descargar(c.url);
      if (!buf) return void (c.descarte = 'no se pudo descargar');
      const an = await analizarImagen(buf);
      if ('descarte' in an) return void (c.descarte = an.descarte);
      Object.assign(c, an, { buffer: buf });
    });
    marcarDuplicadas(aDescargar);
    const paraVision = ordenarParaVision(aDescargar);
    if (!paraVision.length) return { sugerencias: [], queriesNuevas };

    // 3) Visión (una llamada; un reintento si el JSON no parsea).
    const imgs = await Promise.all(
      paraVision.map(async (c) => ({
        jpegBase64: (await sharp(c.buffer!).resize(512, 512, { fit: 'inside' }).flatten({ background: '#ffffff' }).jpeg({ quality: 78 }).toBuffer()).toString('base64'),
        dominio: c.dominio,
        titulo: c.titulo,
      })),
    );
    const partes = armarPartesVision(producto, norm, imgs);
    let evals: (EvaluacionVision | null)[] = [];
    for (let intento = 0; intento < 2; intento++) {
      const res = await this.ai.completarConImagenes(ctx.empresaId, PROMPT_VISION, partes, this.meta(ctx));
      evals = parsearEvaluacion(res.content, paraVision.length);
      if (evals.some(Boolean)) break;
    }
    paraVision.forEach((c, i) => {
      const v = evals[i];
      if (!v) return void (c.descarte = 'sin evaluación');
      c.vision = v;
      c.score = calcularScore(v);
    });

    // 4) Selección final.
    const sugerencias = seleccionarParaGuardar(paraVision).map((c) => ({
      buffer: c.buffer!,
      fuente_url: c.url,
      fuente_dominio: c.dominio,
      fuente_titulo: c.titulo,
      ancho: c.ancho,
      alto: c.alto,
      vision: c.vision!,
      score: c.score!,
    }));
    return { sugerencias, queriesNuevas };
  }
}
