import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { AiProviderService } from 'src/ai-dashboard/ai-provider/ai-provider.service';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { ContabilidadConciliacionService } from './contabilidad-conciliacion.service';
import { RecontabilizacionService } from './recontabilizacion.service';

/**
 * Sugerencia de categoría con IA para movimientos de tesorería cargados con una
 * categoría genérica ("Otro Ingreso", "Otro Egreso", importación de extracto).
 *
 * Esas categorías mandan el movimiento a una cuenta que no es la suya: el
 * préstamo de 240 millones de DOBA terminó en Anticipos a proveedores. La IA lee
 * la descripción ("PRESTAMO BB A DOBA SA", "PAGO ESSAP") y propone la categoría
 * de la empresa que corresponde. Nunca decide sola: la persona revisa y aplica.
 *
 * Aplicar cambia la categoría del detalle y rehace el asiento (recontabilización,
 * o regeneración si el movimiento no tenía asiento).
 */

export const CODIGOS_CATEGORIA_GENERICA = ['INGRESO_OTRO', 'EGRESO_OTRO', 'IMPORT_EXTRACTO_INGRESO', 'IMPORT_EXTRACTO_EGRESO'];

const LOTE_IA = 20;
const MAX_POR_PEDIDO = 100;

export type AccionSugerida = 'RECATEGORIZAR' | 'CARGAR_COMO_GASTO' | 'REVISAR';

export interface SugerenciaCategoria {
  movimiento_id: string;
  detalle_id: string;
  fecha: string;
  tipo: string;
  monto: number;
  moneda: string;
  cuenta: string;
  descripcion: string;
  categoria_actual: string;
  accion: AccionSugerida;
  categoria_sugerida_id: string | null;
  categoria_sugerida: string | null;
  cuenta_contable_sugerida: string | null;
  confianza: number;
  motivo: string;
}

interface CategoriaOpcion {
  id: string;
  nombre: string;
  tipo: string;
  cuenta: string | null;
}

const SYSTEM_PROMPT = `Sos un asistente contable de una empresa paraguaya. Clasificás movimientos bancarios y de caja que alguien cargó con una categoría genérica ("Otro ingreso", "Otro egreso", importación de extracto) en la categoría correcta de la empresa.

Reglas:
- Usá solo categorías de la lista, y solo del mismo tipo que el movimiento (INGRESO con INGRESO o AMBOS; EGRESO con EGRESO o AMBOS).
- Si es un gasto que normalmente tiene factura (servicios como ESSAP, ANDE, Tigo, Personal, compras de insumos, combustible, estacionamiento, honorarios, SENACSA), la acción es CARGAR_COMO_GASTO: debe registrarse en el módulo de Gastos para recuperar el IVA. Igual sugerí la categoría más cercana.
- Préstamos: el desembolso que recibe la empresa y la cuota que paga van a la categoría de ese préstamo (por banco o por nombre). Los intereses del préstamo van a una categoría de intereses.
- Comisiones, débitos e IVA de gastos del banco van a la categoría de gastos bancarios.
- Transferencias entre cuentas propias de la empresa no son ingresos ni egresos: acción REVISAR (deben cargarse como transferencia entre cuentas). Lo mismo las compras y ventas de divisas ("OPER. CAMBIOS", "COMPRA USD", "VENTA DIVISA"): son un pase de guaraníes a la cuenta en dólares o al revés.
- Si la descripción no alcanza para decidir, acción REVISAR con categoria_id null. Es mejor no sugerir que sugerir mal.
- Si entendés qué es pero la empresa no tiene una categoría adecuada, acción REVISAR y en el motivo proponé el nombre de la categoría a crear (por ejemplo "crear categoría de ingreso Desembolso préstamo BB").
- confianza: número entre 0 y 1.
- motivo: una frase corta en español que explique la sugerencia.

Respondé SOLO con un array JSON, sin texto alrededor, un objeto por movimiento:
[{"id":"<id del movimiento>","accion":"RECATEGORIZAR|CARGAR_COMO_GASTO|REVISAR","categoria_id":"<id o null>","confianza":0.0,"motivo":"..."}]`;

@Injectable()
export class SugerenciaCategoriaService {
  private readonly logger = new Logger(SugerenciaCategoriaService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly ai: AiProviderService,
    private readonly audit: AuditService,
    private readonly conciliacion: ContabilidadConciliacionService,
    private readonly recontabilizacion: RecontabilizacionService,
  ) {}

  /**
   * Solo importan los movimientos cuyo asiento sale de la categoría: los cargados
   * a mano y las líneas de extracto importadas. Los que genera otro módulo
   * (gastos, órdenes de pago, recibos…) se asientan por su documento y la
   * categoría no cambia nada. Y los de fecha anterior a la activación los cubre
   * el asiento de apertura.
   */
  private async movimientosQueImportan(empresaId: string) {
    const activadoEl = await this.conciliacion.fechaActivacionContabilidad(empresaId);
    return {
      empresa_id: empresaId,
      estado: { not: 'ANULADO' },
      OR: [{ origen_tipo: null }, { origen_tipo: 'IMPORT_EXTRACTO' }],
      ...(activadoEl ? { fecha: { gte: activadoEl } } : {}),
    };
  }

  /** Categorías de la empresa a las que se puede reclasificar, con su cuenta contable. */
  private async opciones(empresaId: string): Promise<CategoriaOpcion[]> {
    const [categorias, mapeos] = await Promise.all([
      this.prisma.tes_categorias.findMany({
        where: {
          OR: [{ empresa_id: empresaId }, { empresa_id: null }],
          activo: true,
          tipo: { in: ['INGRESO', 'EGRESO', 'AMBOS'] },
          NOT: { codigo_interno: { in: CODIGOS_CATEGORIA_GENERICA } },
        },
        select: { id: true, nombre: true, tipo: true, cuenta_contable_id: true },
        orderBy: { nombre: 'asc' },
      }),
      this.prisma.tes_categoria_cuenta_contable.findMany({
        where: { empresa_id: empresaId },
        select: { categoria_id: true, cuenta_contable_id: true },
      }),
    ]);
    const porCategoria = new Map(mapeos.map((m) => [m.categoria_id, m.cuenta_contable_id]));
    const ids = categorias
      .map((c) => porCategoria.get(c.id) ?? c.cuenta_contable_id)
      .filter((id): id is string => !!id);
    const cuentas = ids.length
      ? await this.prisma.cont_plan_cuentas.findMany({ where: { id: { in: ids } }, select: { id: true, codigo: true, descripcion: true } })
      : [];
    const cuentaPorId = new Map(cuentas.map((c) => [c.id, `${c.codigo} ${c.descripcion}`]));
    // Solo las que tienen cuenta: una categoría sin cuenta no genera asiento.
    return categorias
      .map((c) => ({ id: c.id, nombre: c.nombre, tipo: c.tipo, cuenta: cuentaPorId.get(porCategoria.get(c.id) ?? c.cuenta_contable_id ?? '') ?? null }))
      .filter((c) => c.cuenta);
  }

  async sugerir(empresaId: string, opciones: { limite?: number; usuarioId?: string } = {}): Promise<{
    total_genericos: number;
    sugerencias: SugerenciaCategoria[];
  }> {
    const limite = Math.min(Math.max(opciones.limite ?? 40, 1), MAX_POR_PEDIDO);
    const where = {
      categoria: { codigo_interno: { in: CODIGOS_CATEGORIA_GENERICA } },
      movimiento: await this.movimientosQueImportan(empresaId),
    };
    const [total, detalles, categorias] = await Promise.all([
      this.prisma.tes_movimiento_det.count({ where }),
      this.prisma.tes_movimiento_det.findMany({
        where,
        select: {
          id: true,
          monto: true,
          descripcion: true,
          categoria: { select: { nombre: true } },
          movimiento: {
            select: {
              id: true,
              fecha: true,
              tipo: true,
              descripcion: true,
              referencia: true,
              cuenta: { select: { nombre: true, moneda: true } },
            },
          },
        },
        // Los de más plata primero: son los que más mueven el balance.
        orderBy: { monto: 'desc' },
        take: limite,
      }),
      this.opciones(empresaId),
    ]);
    if (!detalles.length) return { total_genericos: total, sugerencias: [] };
    if (!categorias.length) {
      throw new BadRequestException('No hay categorías de tesorería con cuenta contable para sugerir. Configurá las categorías primero.');
    }
    const categoriaPorId = new Map(categorias.map((c) => [c.id, c]));

    const sugerencias: SugerenciaCategoria[] = [];
    for (let i = 0; i < detalles.length; i += LOTE_IA) {
      const lote = detalles.slice(i, i + LOTE_IA);
      const prompt = [
        'Categorías disponibles (id | nombre | tipo | cuenta contable):',
        ...categorias.map((c) => `${c.id} | ${c.nombre} | ${c.tipo} | ${c.cuenta}`),
        '',
        'Movimientos a clasificar (id | fecha | tipo | monto | cuenta | descripción):',
        ...lote.map(
          (d) =>
            `${d.movimiento.id} | ${d.movimiento.fecha.toISOString().substring(0, 10)} | ${d.movimiento.tipo} | ${d.movimiento.cuenta.moneda} ${Number(d.monto)} | ${d.movimiento.cuenta.nombre} | ${[d.descripcion, d.movimiento.descripcion, d.movimiento.referencia].filter(Boolean).join(' · ')}`,
        ),
      ].join('\n');

      let respuesta: Array<Record<string, unknown>> = [];
      try {
        const r = await this.ai.completar(empresaId, prompt, SYSTEM_PROMPT, {
          feature: 'TESORERIA',
          usuarioId: opciones.usuarioId ?? null,
        });
        // El proveedor devuelve "[Sin API key configurada]" en vez de lanzar.
        if (/^\[(Sin API key|Proveedor)/.test(r.content)) {
          throw new BadRequestException('La empresa no tiene IA configurada (Configuración › IA).');
        }
        respuesta = parsearArray(r.content);
      } catch (err) {
        if (err instanceof BadRequestException) throw err;
        const msg = err instanceof Error ? err.message : String(err);
        this.logger.warn(`Sugerencia de categoría falló para un lote: ${msg}`);
        throw new BadRequestException(`No se pudo consultar a la IA: ${msg}`);
      }
      const porId = new Map(respuesta.map((x) => [String(x.id), x]));

      for (const d of lote) {
        const x = porId.get(d.movimiento.id);
        const tipoMov = d.movimiento.tipo;
        let categoriaId = typeof x?.categoria_id === 'string' ? x.categoria_id : null;
        const cat = categoriaId ? categoriaPorId.get(categoriaId) : undefined;
        // Una categoría inventada o del tipo contrario se descarta: no se aplica a ciegas.
        if (!cat || (cat.tipo !== 'AMBOS' && cat.tipo !== tipoMov)) categoriaId = null;
        let accion = (['RECATEGORIZAR', 'CARGAR_COMO_GASTO', 'REVISAR'].includes(String(x?.accion)) ? x?.accion : 'REVISAR') as AccionSugerida;
        if (accion === 'RECATEGORIZAR' && !categoriaId) accion = 'REVISAR';
        const final = categoriaId ? categoriaPorId.get(categoriaId) : undefined;
        sugerencias.push({
          movimiento_id: d.movimiento.id,
          detalle_id: d.id,
          fecha: d.movimiento.fecha.toISOString().substring(0, 10),
          tipo: tipoMov,
          monto: Number(d.monto),
          moneda: d.movimiento.cuenta.moneda,
          cuenta: d.movimiento.cuenta.nombre,
          descripcion: [d.descripcion, d.movimiento.descripcion].filter(Boolean).join(' · ') || '(sin descripción)',
          categoria_actual: d.categoria?.nombre ?? '',
          accion,
          categoria_sugerida_id: categoriaId,
          categoria_sugerida: final?.nombre ?? null,
          cuenta_contable_sugerida: final?.cuenta ?? null,
          confianza: Math.max(0, Math.min(1, Number(x?.confianza ?? 0))),
          motivo: typeof x?.motivo === 'string' ? x.motivo : x ? '' : 'La IA no devolvió una sugerencia para este movimiento.',
        });
      }
    }
    return { total_genericos: total, sugerencias };
  }

  /**
   * Cambia la categoría de los detalles indicados y rehace el asiento del
   * movimiento. Solo acepta detalles que hoy tienen categoría genérica y una
   * categoría destino válida para el tipo del movimiento.
   */
  async aplicar(
    empresaId: string,
    items: Array<{ detalle_id: string; categoria_id: string }>,
    usuarioId?: string,
  ): Promise<{ ok: number; fallidos: number; items: Array<{ detalle_id: string; ok: boolean; error?: string }> }> {
    if (!items?.length) throw new BadRequestException('Indicá qué movimientos reclasificar');
    if (items.length > MAX_POR_PEDIDO) throw new BadRequestException(`Máximo ${MAX_POR_PEDIDO} por pedido`);
    const categorias = new Map((await this.opciones(empresaId)).map((c) => [c.id, c]));

    const resultados: Array<{ detalle_id: string; ok: boolean; error?: string }> = [];
    for (const it of items) {
      const det = await this.prisma.tes_movimiento_det.findFirst({
        where: { id: it.detalle_id, movimiento: await this.movimientosQueImportan(empresaId) },
        select: {
          id: true,
          categoria: { select: { codigo_interno: true, nombre: true } },
          movimiento: { select: { id: true, tipo: true, estado: true } },
        },
      });
      const destino = categorias.get(it.categoria_id);
      if (!det) {
        resultados.push({
          detalle_id: it.detalle_id,
          ok: false,
          error: 'No se reclasifica: no existe, lo generó otro módulo o es anterior a la activación de Contabilidad',
        });
        continue;
      }
      if (!CODIGOS_CATEGORIA_GENERICA.includes(det.categoria?.codigo_interno ?? '')) {
        resultados.push({ detalle_id: it.detalle_id, ok: false, error: 'Ya no tiene categoría genérica' });
        continue;
      }
      if (!destino || (destino.tipo !== 'AMBOS' && destino.tipo !== det.movimiento.tipo)) {
        resultados.push({ detalle_id: it.detalle_id, ok: false, error: 'Categoría destino no válida para este movimiento' });
        continue;
      }

      await this.prisma.tes_movimiento_det.update({ where: { id: det.id }, data: { categoria_id: destino.id } });

      // Rehacer el asiento: si tenía, se reemplaza; si no, se genera.
      const movimientoId = det.movimiento.id;
      const conAsiento = await this.prisma.cont_documentos.findFirst({
        where: { empresa_id: empresaId, origen_tipo: 'tes_movimientos', origen_id: movimientoId, estado: 'CONFIRMADO' },
        select: { id: true },
      });
      let error: string | undefined;
      if (conAsiento) {
        const r = await this.recontabilizacion.aplicar(empresaId, 'tes_movimientos', [movimientoId], usuarioId);
        if (!r.items[0]?.ok) error = r.items[0]?.error;
      } else if (det.movimiento.estado === 'CONFIRMADO') {
        const r = await this.conciliacion.regenerarUno(empresaId, 'tes_movimientos', movimientoId, usuarioId);
        if (!r.ok) error = r.error;
      }

      await this.audit.log({
        empresa_id: empresaId,
        user_id: usuarioId,
        action: 'TES_RECLASIFICAR_CATEGORIA',
        entity_type: 'tes_movimiento_det',
        entity_id: det.id,
        descripcion: `Categoría "${det.categoria?.nombre}" → "${destino.nombre}"${error ? ` (asiento: ${error})` : ''}`,
        new_value: { movimiento_id: movimientoId, categoria_id: destino.id },
      });
      // La categoría cambia aunque el asiento falle: el motivo queda visible en
      // Conciliación para corregirlo y regenerar.
      resultados.push({ detalle_id: it.detalle_id, ok: !error, ...(error ? { error: `Categoría cambiada, pero el asiento no: ${error}` } : {}) });
    }
    const ok = resultados.filter((r) => r.ok).length;
    return { ok, fallidos: resultados.length - ok, items: resultados };
  }
}

/** Extrae el primer array JSON de la respuesta (algunos modelos lo envuelven en ```json). */
export function parsearArray(texto: string): Array<Record<string, unknown>> {
  const inicio = texto.indexOf('[');
  const fin = texto.lastIndexOf(']');
  if (inicio < 0 || fin <= inicio) return [];
  try {
    const v = JSON.parse(texto.slice(inicio, fin + 1));
    return Array.isArray(v) ? v.filter((x) => x && typeof x === 'object') : [];
  } catch {
    return [];
  }
}
