import Anthropic from '@anthropic-ai/sdk';
import { CatalogoService } from 'src/catalogo/catalogo.service';
import { ConsultorService, ResultadoConsulta } from 'src/consultas/consultor.service';
import { UsoTokens } from 'src/llm/precios';
import { EnrutadorManualService } from 'src/manual/enrutador.service';
import { ManualService, ResultadoGuia, ResultadoManual } from 'src/manual/manual.service';
import { esTipoDocumento, NOMBRE_TIPO, TipoDocumento } from 'src/catalogo/tipos-documento';
import { Sesion } from 'src/sesion/sesion.types';
import { proyectarAlerta, proyectarDocumento } from './proyeccion';
import { DocumentoRef, EstadoBot } from './tipos';

/**
 * Herramientas del redactor (plan §6.1). Ninguna recibe empresa, RUC ni usuario:
 * el alcance sale de la sesión. Los documentos se nombran por código (c1, c2…),
 * nunca por id interno.
 *
 * El cierre del turno NO es una herramienta: ver cierre.ts.
 *
 * `strict: true` en todas: el API valida el input contra el esquema. No se usa
 * eager_input_streaming porque apaga esa validación y los inputs son mínimos.
 */
const TIPOS_O_CUALQUIERA = ['FACTURA', 'NOTA_CREDITO', 'NOTA_REMISION', 'CUALQUIERA'];
const ESTADOS_SIFEN = [
  'Rechazado',
  'Error',
  'Pendiente',
  'Documento firmado',
  'Enviado',
  'Aprobado',
  'Cancelado',
  'Inutilizado',
];

export const DEFINICIONES: Anthropic.Beta.BetaTool[] = [
  {
    name: 'q_estado_documento',
    description:
      'Estado y diagnóstico de UN documento electrónico de la empresa (factura, nota de crédito o remisión). ' +
      'Si la referencia coincide con varios documentos devuelve los candidatos con su código para que el cliente elija.',
    strict: true,
    input_schema: {
      type: 'object',
      properties: {
        referencia: {
          type: 'string',
          description:
            'Número (001-001-0000123 o 123), CDC de 44 dígitos, o código de un documento ya mostrado (c1, c2…).',
        },
        tipo: { type: 'string', enum: TIPOS_O_CUALQUIERA },
      },
      required: ['referencia', 'tipo'],
      additionalProperties: false,
    },
  },
  {
    name: 'q_listar_documentos',
    description:
      'Lista documentos de la empresa por estado en SIFEN y antigüedad. Sirve para ubicar un documento cuando el cliente no sabe el número.',
    strict: true,
    input_schema: {
      type: 'object',
      properties: {
        estados: { type: 'array', items: { type: 'string', enum: ESTADOS_SIFEN } },
        dias: { type: 'integer', description: 'Cuántos días hacia atrás (1 a 90).' },
        tipo: { type: 'string', enum: TIPOS_O_CUALQUIERA },
      },
      required: ['estados', 'dias', 'tipo'],
      additionalProperties: false,
    },
  },
  {
    name: 'q_alertas_abiertas',
    description: 'Alertas abiertas de la empresa que el usuario puede ver, con la acción sugerida de cada una.',
    strict: true,
    input_schema: { type: 'object', properties: {}, required: [], additionalProperties: false },
  },
  {
    name: 'q_salud_empresa',
    description:
      'Estado general de la empresa: facturación electrónica activa, timbrado, ambiente, certificado digital y suscripción.',
    strict: true,
    input_schema: { type: 'object', properties: {}, required: [], additionalProperties: false },
  },
  {
    name: 'q_buscar_guia',
    description:
      'Cómo se usa Novasis: devuelve las pantallas que tienen que ver con la pregunta (dónde están, desde dónde se abren, ' +
      'y los textos reales que ve el usuario: campos, botones, opciones, ayudas y mensajes), las reglas del sistema ' +
      '(mensajes de validación) y fragmentos de ayuda. Para "cómo se hace", "dónde está" o "por qué no me deja".',
    strict: true,
    input_schema: {
      type: 'object',
      properties: { pregunta: { type: 'string' } },
      required: ['pregunta'],
      additionalProperties: false,
    },
  },
  {
    name: 'q_leer_codigo',
    description:
      'Lee un archivo del sistema (de los que lista q_buscar_guia en "archivos") para confirmar un detalle: cuándo aparece una ' +
      'opción, qué condición bloquea una acción, qué pasa al confirmar. Con `buscar`, devuelve solo las zonas donde aparece ese ' +
      'texto. Es para entender el sistema: nunca muestres código, nombres de archivos ni de variables al cliente.',
    strict: true,
    input_schema: {
      type: 'object',
      properties: {
        archivo: {
          type: 'string',
          description: 'Ruta tal como la lista q_buscar_guia (ej. frontend/src/pages/Ventas.jsx).',
        },
        buscar: {
          type: 'string',
          description: 'Texto a ubicar en el archivo (ej. "Crédito", "permitir_venta_credito").',
        },
      },
      required: ['archivo', 'buscar'],
      additionalProperties: false,
    },
  },
  {
    name: 'q_buscar_codigo',
    description:
      'Busca un texto en TODO el código del sistema (como un grep): para rastrear de dónde sale un mensaje de error, ' +
      'qué condición dispara un comportamiento o si algo es un posible error del sistema. Usá el texto EXACTO del error ' +
      '(o una parte distintiva) o el nombre de un campo. Devuelve archivo y línea: seguí con q_leer_codigo para leer el ' +
      'contexto. Es para entender el sistema: nunca muestres código, archivos ni variables al cliente.',
    strict: true,
    input_schema: {
      type: 'object',
      properties: {
        buscar: { type: 'string', description: 'Texto a buscar (mensaje de error, campo, palabra clave).' },
        carpeta: {
          anyOf: [{ type: 'string', enum: ['frontend', 'backend'] }, { type: 'null' }],
          description:
            'Limitar a una carpeta; null busca en las dos. backend = reglas y validaciones; frontend = pantallas.',
        },
      },
      required: ['buscar', 'carpeta'],
      additionalProperties: false,
    },
  },
  {
    name: 'q_consulta_negocio',
    description:
      'Consulta los datos de operaciones de la empresa (ventas, cobranzas, clientes, compras, gastos, stock, caja, tesorería, ' +
      'contabilidad, RRHH, CRM) y devuelve las filas. Recibe la pregunta en lenguaje natural, completa y autocontenida ' +
      '(ej. "ventas del mes pasado por vendedor", no "¿y el anterior?").',
    strict: true,
    input_schema: {
      type: 'object',
      properties: { pregunta: { type: 'string' } },
      required: ['pregunta'],
      additionalProperties: false,
    },
  },
  {
    name: 'q_mis_tickets',
    description: 'Consultas anteriores del usuario con este asistente y en qué terminaron.',
    strict: true,
    input_schema: { type: 'object', properties: {}, required: [], additionalProperties: false },
  },
];

/** Etiqueta del progreso que ve el cliente mientras corre cada herramienta. */
export const PROGRESO: Record<string, string> = {
  q_estado_documento: 'Consultando el documento…',
  q_listar_documentos: 'Buscando tus documentos…',
  q_alertas_abiertas: 'Revisando las alertas de tu empresa…',
  q_salud_empresa: 'Revisando la configuración de tu empresa…',
  // Nada de mecánica interna ("guías", "base de datos"): el cliente habla con un asistente.
  q_buscar_guia: 'Pensando…',
  q_leer_codigo: 'Revisando cómo funciona…',
  q_buscar_codigo: 'Revisando cómo funciona…',
  q_mis_tickets: 'Revisando tus consultas anteriores…',
  q_consulta_negocio: 'Consultando los datos de tu empresa…',
};

export interface RegistroTraza {
  herramienta: string;
  parametros: unknown;
  respuesta: unknown;
  proyeccion: unknown;
  duracionMs: number;
  error?: string;
  fueraDeAlcance?: boolean;
}

export interface MisTicketsFuente {
  ultimos(
    sesion: Sesion,
  ): Promise<Array<{ numero: number; titulo: string; resultado: string | null; created_at: Date }>>;
}

/**
 * Ejecuta las herramientas de UN turno. Mantiene el estado de la máquina
 * (documento activo, candidatos) y registra una traza por llamada.
 */
export class HerramientasTurno {
  /** Diagnóstico del último documento consultado en este turno. */
  veredicto: EstadoBot['ultimoVeredicto'] | null = null;
  /** Tokens que gastaron las herramientas que llaman a un modelo (el consultor). */
  readonly usos: Array<{ modelo: string; uso: UsoTokens }> = [];

  constructor(
    private readonly catalogo: CatalogoService,
    private readonly sesion: Sesion,
    readonly estado: EstadoBot,
    private readonly tickets: MisTicketsFuente,
    private readonly trazar: (t: RegistroTraza) => void,
    private readonly negocio?: {
      consultor: ConsultorService;
      contexto: string[];
      manual?: ManualService;
      enrutador?: EnrutadorManualService;
    },
  ) {}

  /** Devuelve el contenido del tool_result (JSON) y si fue un error. */
  async ejecutar(nombre: string, input: unknown): Promise<{ contenido: string; error: boolean }> {
    const inicio = Date.now();
    const params = (input && typeof input === 'object' ? input : {}) as Record<string, unknown>;
    try {
      const { respuesta, proyeccion, fueraDeAlcance } = await this.despachar(nombre, params);
      this.trazar({
        herramienta: nombre,
        parametros: params,
        respuesta,
        proyeccion,
        duracionMs: Date.now() - inicio,
        fueraDeAlcance,
      });
      return { contenido: JSON.stringify(proyeccion), error: false };
    } catch (err) {
      const mensaje = err instanceof Error ? err.message : String(err);
      this.trazar({
        herramienta: nombre,
        parametros: params,
        respuesta: null,
        proyeccion: null,
        duracionMs: Date.now() - inicio,
        error: mensaje,
      });
      return { contenido: JSON.stringify({ error: 'No se pudo consultar en este momento.' }), error: true };
    }
  }

  private async despachar(
    nombre: string,
    p: Record<string, unknown>,
  ): Promise<{ respuesta: unknown; proyeccion: unknown; fueraDeAlcance?: boolean }> {
    switch (nombre) {
      case 'q_estado_documento':
        return this.estadoDocumento(texto(p.referencia), tipoDe(p.tipo));
      case 'q_listar_documentos':
        return this.listar(p);
      case 'q_alertas_abiertas': {
        const r = await this.catalogo.alertasAbiertas(this.sesion);
        return { respuesta: r, proyeccion: { alertas: r.alertas.map(proyectarAlerta) } };
      }
      case 'q_salud_empresa': {
        const r = await this.catalogo.saludEmpresa(this.sesion);
        return {
          respuesta: r,
          proyeccion: r.ok ? { empresa: r.empresa, certificado: r.certificado } : { error: r.detalle },
        };
      }
      case 'q_buscar_guia': {
        const pregunta = texto(p.pregunta);
        const { pantallas, guias } = await this.pantallasDelManual(pregunta);
        const fragmentos = await this.catalogo.buscarGuia(this.sesion, pregunta);
        const proyeccion = {
          ...(guias.length ? { guias_paso_a_paso: guias } : {}),
          pantallas,
          ayuda: fragmentos.map((f) => ({ tema: f.titulo, contenido: f.contenido })),
          ...(pantallas.length || fragmentos.length ? {} : { nota: 'No hay información sobre esto.' }),
        };
        return {
          respuesta: { pantallas: pantallas.map((x) => x.pantalla), guias: guias.map((g) => g.marca), fragmentos },
          proyeccion,
        };
      }
      case 'q_buscar_codigo': {
        const carpeta = p.carpeta === 'frontend' || p.carpeta === 'backend' ? p.carpeta : undefined;
        const r = this.negocio?.manual?.buscarCodigo(texto(p.buscar), carpeta) ?? { coincidencias: [], hay_mas: false };
        return {
          respuesta: { coincidencias: r.coincidencias.length, hay_mas: r.hay_mas },
          proyeccion: r.coincidencias.length
            ? r
            : { ...r, nota: 'No aparece en el código. Probá con otra parte del texto.' },
        };
      }
      case 'q_leer_codigo': {
        const r = this.negocio?.manual?.leerCodigo(texto(p.archivo), texto(p.buscar)) ?? { error: 'no disponible' };
        return { respuesta: 'error' in r ? r : { archivo: r.archivo }, proyeccion: r };
      }
      case 'q_consulta_negocio': {
        if (!this.negocio) throw new Error('Consultor no disponible');
        const r = await this.negocio.consultor.consultar({
          sesion: this.sesion,
          pregunta: texto(p.pregunta),
          contexto: this.negocio.contexto,
        });
        this.usos.push(...r.usos);
        if (r.estado === 'consultas') this.estado.huboConsultaReal = true;
        const respuesta = {
          estado: r.estado,
          explicacion: r.explicacion,
          resultados: r.resultados,
          intentos: r.intentos,
        };
        return { respuesta, proyeccion: proyectarConsulta(r) };
      }
      case 'q_mis_tickets': {
        const t = await this.tickets.ultimos(this.sesion);
        return {
          respuesta: t,
          proyeccion: {
            tickets: t.map((x) => ({
              numero: `SOP-${x.numero}`,
              fecha: x.created_at.toISOString().slice(0, 10),
              tema: x.titulo,
              resultado: x.resultado,
            })),
          },
        };
      }
      default:
        throw new Error(`Herramienta desconocida: ${nombre}`);
    }
  }

  /**
   * Pantallas del manual para una pregunta de uso: las elige el enrutador (Haiku sobre el
   * índice) y se sacan sus pasajes; si el enrutador no responde, búsqueda por palabras.
   */
  private async pantallasDelManual(
    pregunta: string,
  ): Promise<{ pantallas: ResultadoManual[]; guias: ResultadoGuia[] }> {
    const manual = this.negocio?.manual;
    if (!manual?.disponible) return { pantallas: [], guias: [] };
    const ruteo = await this.negocio?.enrutador?.elegir(pregunta, this.negocio.contexto);
    if (ruteo?.uso) this.usos.push({ modelo: ruteo.modelo, uso: ruteo.uso });
    const guias = manual.guiasPara(ruteo?.ids ?? [], `${pregunta} ${ruteo?.terminos.join(' ') ?? ''}`);
    if (!ruteo?.ids.some((i) => !i.startsWith('guia:'))) return { pantallas: manual.buscar(pregunta), guias };
    // El enrutador a veces trae solo la configuración, o solo la pantalla donde se hace: se
    // completa con el mejor resultado por palabras del tipo que falte (máximo 5 pantallas).
    const consulta = `${pregunta} ${ruteo.terminos.join(' ')}`;
    const ids = ruteo.ids.filter((i) => !i.startsWith('guia:'));
    // Sin rutas con parámetros (/viajes/:id): necesitan un registro concreto, no son un punto de partida.
    const porPalabras = manual
      .buscar(consulta, 8)
      .map((r) => r.id)
      .filter((i) => !i.includes('/:'));
    const falta = (prefijo: string) => !ids.some((i) => i.startsWith(prefijo));
    for (const prefijo of ['ruta:', 'config:']) {
      const extra = falta(prefijo) ? porPalabras.find((i) => i.startsWith(prefijo)) : undefined;
      if (extra && ids.length < 5) ids.push(extra);
    }
    return { pantallas: manual.detalle(ids, consulta), guias };
  }

  /** Busca (por número, CDC o código) y diagnostica. Público: lo usa también la consulta previa al redactor. */
  async estadoDocumento(
    referencia: string,
    tipo: TipoDocumento | undefined,
  ): Promise<{ respuesta: unknown; proyeccion: unknown; fueraDeAlcance?: boolean }> {
    this.estado.huboConsultaReal = true;
    const ref = referencia.trim().toLowerCase();
    let elegido: DocumentoRef | undefined;
    if (/^c\d+$/.test(ref)) {
      // Un código de documento ya mostrado. Nunca se reinterpreta como número ("c9" no es el 9).
      const conocidos = [
        ...(this.estado.candidatos ?? []),
        ...(this.estado.documentoActivo ? [this.estado.documentoActivo] : []),
      ];
      elegido = conocidos.find((c) => c.codigo === ref);
      if (!elegido) {
        return { respuesta: { codigo: ref }, proyeccion: { encontrado: false, motivo: 'codigo_desconocido' } };
      }
    }
    if (!elegido) {
      const busqueda = await this.catalogo.buscarDocumento(this.sesion, referencia, tipo);
      if (!busqueda.ok) {
        if (busqueda.motivo === 'no_encontrado') this.estado.pedidosDeReferencia += 1;
        if (busqueda.motivo === 'referencia_invalida') this.estado.pedidosDeReferencia += 1;
        return {
          respuesta: busqueda,
          proyeccion: { encontrado: false, motivo: busqueda.motivo },
          fueraDeAlcance: busqueda.motivo === 'no_encontrado' && mencionaOtroRuc(referencia, this.sesion),
        };
      }
      const refs = busqueda.candidatos.map((c) => this.registrarDocumento(c.tipo, c.id, c.nro_documento));
      if (refs.length > 1) {
        this.estado.candidatos = refs;
        return {
          respuesta: busqueda,
          proyeccion: {
            encontrado: true,
            varios: true,
            candidatos: busqueda.candidatos.map((c, i) => ({
              codigo: refs[i].codigo,
              tipo: NOMBRE_TIPO[c.tipo],
              numero: c.nro_documento,
              fecha_emision: c.fecha_emision ? new Date(c.fecha_emision).toISOString().slice(0, 10) : null,
              estado_sifen: c.estado_sifen,
            })),
            indicacion: 'Preguntale al cliente cuál es, mostrando tipo, número y fecha.',
          },
        };
      }
      elegido = refs[0];
    }

    const r = await this.catalogo.estadoDocumento(this.sesion, elegido.tipo, elegido.id);
    if (!r.ok) {
      return { respuesta: r, proyeccion: { encontrado: false, motivo: r.motivo } };
    }
    this.estado.documentoActivo = elegido;
    this.estado.candidatos = undefined;
    const v = r.veredicto;
    this.veredicto = { situacion: v.situacion, resultado: v.resultado, motivo: v.motivo };
    this.estado.ultimoVeredicto = this.veredicto;
    const accion = v.accionCliente ?? v.evento?.accionCliente;
    this.estado.accionPendiente =
      accion && accion !== 'esperar' ? { accion, documento: elegido, situacionAntes: v.situacion } : undefined;
    return { respuesta: r, proyeccion: proyectarDocumento(elegido.codigo, r.documento, v, r.alertas) };
  }

  private async listar(p: Record<string, unknown>) {
    const dias = Math.min(90, Math.max(1, Math.trunc(Number(p.dias) || 7)));
    const estados = Array.isArray(p.estados) ? p.estados.filter((e): e is string => typeof e === 'string') : [];
    const desde = new Date(Date.now() - dias * 86_400_000);
    const r = await this.catalogo.listarDocumentos(this.sesion, {
      tipo: tipoDe(p.tipo),
      estadosSifen: estados.length ? estados : undefined,
      desde,
    });
    if (!r.ok) return { respuesta: r, proyeccion: { motivo: r.motivo } };
    this.estado.huboConsultaReal = true;
    const refs = r.documentos.map((d) => this.registrarDocumento(d.tipo, d.id, d.nro_documento));
    this.estado.candidatos = refs;
    return {
      respuesta: r,
      proyeccion: {
        documentos: r.documentos.map((d, i) => ({
          codigo: refs[i].codigo,
          tipo: NOMBRE_TIPO[d.tipo],
          numero: d.nro_documento,
          fecha_emision: d.fecha_emision ? new Date(d.fecha_emision).toISOString().slice(0, 10) : null,
          estado_sifen: d.estado_sifen ?? 'sin enviar',
        })),
        hay_mas: r.truncado,
      },
    };
  }

  /** Mismo documento, mismo código en todo el hilo. */
  private registrarDocumento(tipo: TipoDocumento, id: string, nro: string): DocumentoRef {
    const conocidos = [
      ...(this.estado.candidatos ?? []),
      ...(this.estado.documentoActivo ? [this.estado.documentoActivo] : []),
    ];
    const previo = conocidos.find((d) => d.id === id);
    if (previo) return previo;
    const ref = { codigo: `c${this.estado.siguienteCodigo}`, tipo, id, nro };
    this.estado.siguienteCodigo += 1;
    return ref;
  }
}

/** Lo que ve el redactor de una consulta de negocio: sin el SQL. */
export function proyectarConsulta(r: ResultadoConsulta): unknown {
  if (r.estado === 'error') return { error: 'No se pudo consultar en este momento.' };
  if (r.estado !== 'consultas') return { estado: r.estado, explicacion: r.explicacion };
  return {
    estado: 'ok',
    explicacion: r.explicacion,
    resultados: r.resultados.map((x) =>
      x.error
        ? { proposito: x.proposito, error: 'Esta parte no se pudo consultar.' }
        : {
            proposito: x.proposito,
            columnas: x.columnas,
            filas: x.filas,
            ...(x.filas.length === 0 ? { nota: 'sin resultados' } : {}),
            ...(x.hay_mas ? { hay_mas: true } : {}),
          },
    ),
  };
}

function texto(v: unknown): string {
  return typeof v === 'string' ? v : '';
}

function tipoDe(v: unknown): TipoDocumento | undefined {
  return esTipoDocumento(v) ? v : undefined;
}

/** Cable trampa (§6.6): el texto trae un RUC que no es el de la empresa de la sesión. */
export function mencionaOtroRuc(texto: string, sesion: Sesion): boolean {
  const rucs = texto.match(/\b\d{6,8}-\d\b/g) ?? [];
  return rucs.some((r) => sesion.empresaRuc && !r.startsWith(sesion.empresaRuc));
}
