import { Injectable } from '@nestjs/common';
import { envs } from 'src/config/envs';
import { Consulta, ErpReadonlyService } from 'src/erp-readonly/erp-readonly.service';
import { Sesion } from 'src/sesion/sesion.types';
import { diagnosticarDocumento, DocumentoParaDiagnostico, Veredicto } from './diagnostico/diagnosticar-documento';
import { formatearReferencia, normalizarReferencia, ReferenciaDocumento } from './normalizar-referencia';
import { PRIVILEGIO_VER, TipoDocumento, tiposVisibles } from './tipos-documento';

/**
 * Herramientas del catálogo (plan §6.1). Todas:
 *   - corren dentro de ErpReadonlyService.runScoped con la empresa y el usuario de la SESIÓN;
 *   - usan SQL fijo sobre las vistas de soporte_ro (el modelo nunca escribe SQL);
 *   - verifican el privilegio del usuario y su alcance por sucursal.
 * Ninguna recibe empresa, RUC ni usuario como parámetro.
 */

export type FallaCatalogo =
  | { ok: false; motivo: 'sin_permiso'; detalle: string }
  | { ok: false; motivo: 'referencia_invalida'; detalle: string }
  | { ok: false; motivo: 'no_encontrado'; detalle: string };

export interface DocumentoResumen {
  tipo: TipoDocumento;
  id: string;
  nro_documento: string;
  fecha_emision: Date | null;
  estado_sifen: string | null;
  cdc: string | null;
}

export interface FilaDocumento extends DocumentoParaDiagnostico {
  id: string;
  establecimiento: string;
  punto_expedicion: string;
  numero: string;
  nro_documento: string;
  fecha_emision: Date | null;
  cdc: string | null;
  fecha_firma_sifen: Date | null;
  fecha_registro_sifen: Date | null;
  total: string | null;
}

export interface FilaEvento {
  id: string;
  cdc: string | null;
  tipo: string | null;
  estado: string | null;
  enviado_sifen: boolean | null;
  fecha_envio_sifen: Date | null;
  created_at: Date | null;
  cod_respuesta: string | null;
  mensaje: string | null;
  estado_respuesta: string | null;
  fecha_retorno: Date | null;
}

export interface FilaAlerta {
  id: string;
  tipo: string;
  criticidad: string;
  titulo: string;
  descripcion: string;
  accion_sugerida: string | null;
  modulo: string | null;
  clave: string | null;
  privilegio_requerido: string | null;
  ruta: string | null;
  sucursal_id: string | null;
  entidad_tipo: string | null;
  entidad_id: string | null;
  created_at: Date;
}

export interface FilaEmpresa {
  id: string;
  ruc: string;
  dv: string;
  razon_social: string | null;
  nombre_fantasia: string | null;
  tipo: string | null;
  sifen_activo: boolean | null;
  activa: boolean | null;
  timbrado_numero: string | null;
  timbrado_fecha_inicio: Date | null;
  timbrado_ambiente: string | null;
  certificado_vencimiento: Date | null;
  suscripcion_estado: string | null;
  suscripcion_fecha_fin: Date | null;
}

export interface FragmentoGuia {
  titulo: string;
  screen_key: string | null;
  contenido: string;
  relevancia: number;
}

const COLUMNAS_DOCUMENTO = `
  tipo, id, establecimiento, punto_expedicion, numero, nro_documento, fecha_emision, estado,
  estado_sifen, mensaje_sifen, tipo_aprobacion, cdc, nro_lote, fecha_envio_sifen, fecha_firma_sifen,
  fecha_registro_sifen, estado_envio_email, evento_aplicado, estado_evento, mensaje_evento,
  fecha_evento, total, created_at, updated_at`;

const MAX_RESULTADOS = 10;
const MAX_LISTADO = 20;
const MAX_CARACTERES_GUIA = 1_500;

@Injectable()
export class CatalogoService {
  constructor(private readonly erp: ErpReadonlyService) {}

  private enAlcance<T>(sesion: Sesion, fn: (consulta: Consulta) => Promise<T>): Promise<T> {
    return this.erp.runScoped(alcanceDe(sesion), fn);
  }

  /** Tipos que el usuario puede ver, opcionalmente acotados a uno. */
  private tiposPermitidos(sesion: Sesion, tipo?: TipoDocumento): TipoDocumento[] | FallaCatalogo {
    const visibles = tiposVisibles(sesion.privilegios);
    if (tipo) {
      return visibles.includes(tipo)
        ? [tipo]
        : { ok: false, motivo: 'sin_permiso', detalle: `Falta el privilegio ${PRIVILEGIO_VER[tipo]}` };
    }
    return visibles.length > 0
      ? visibles
      : { ok: false, motivo: 'sin_permiso', detalle: 'El usuario no tiene permiso para ver documentos' };
  }

  // ── q_buscar_documento ───────────────────────────────────────────────

  async buscarDocumento(
    sesion: Sesion,
    texto: string,
    tipo?: TipoDocumento,
  ): Promise<{ ok: true; referencia: string; candidatos: DocumentoResumen[] } | FallaCatalogo> {
    const ref = normalizarReferencia(texto);
    if (!ref) {
      return { ok: false, motivo: 'referencia_invalida', detalle: 'No se reconoce un número de documento ni un CDC' };
    }
    const tipos = this.tiposPermitidos(sesion, tipo);
    if (!Array.isArray(tipos)) return tipos;

    const { filtro, params } = filtroReferencia(ref, 3);
    const candidatos = await this.enAlcance(sesion, (consulta) =>
      consulta<DocumentoResumen>(
        `SELECT tipo, id, nro_documento, fecha_emision, estado_sifen, cdc
           FROM soporte_ro.v_documentos
          WHERE tipo = ANY($1::text[])
            AND ($2::text[] IS NULL OR establecimiento = ANY($2::text[]))
            AND ${filtro}
          ORDER BY fecha_emision DESC NULLS LAST
          LIMIT ${MAX_RESULTADOS}`,
        [tipos, establecimientosDe(sesion), ...params],
      ),
    );
    if (candidatos.length === 0) {
      return { ok: false, motivo: 'no_encontrado', detalle: `Sin documentos para ${formatearReferencia(ref)}` };
    }
    return { ok: true, referencia: formatearReferencia(ref), candidatos };
  }

  // ── q_estado_documento (+ q_estado_evento, q_envio_correo) ──────────────

  async estadoDocumento(
    sesion: Sesion,
    tipo: TipoDocumento,
    id: string,
    ahora = new Date(),
  ): Promise<
    | {
        ok: true;
        documento: FilaDocumento;
        veredicto: Veredicto;
        eventos: FilaEvento[];
        alertas: FilaAlerta[];
      }
    | FallaCatalogo
  > {
    const tipos = this.tiposPermitidos(sesion, tipo);
    if (!Array.isArray(tipos)) return tipos;

    return this.enAlcance(sesion, async (consulta) => {
      const [documento] = await consulta<FilaDocumento>(
        `SELECT ${COLUMNAS_DOCUMENTO}
           FROM soporte_ro.v_documentos
          WHERE tipo = $1 AND id = $2::uuid
            AND ($3::text[] IS NULL OR establecimiento = ANY($3::text[]))`,
        [tipo, id, establecimientosDe(sesion)],
      );
      if (!documento) {
        return { ok: false as const, motivo: 'no_encontrado' as const, detalle: 'Documento no encontrado' };
      }

      const [empresa] = await consulta<Pick<FilaEmpresa, 'sifen_activo'>>(
        `SELECT sifen_activo FROM soporte_ro.v_empresa`,
      );
      const eventos = documento.cdc
        ? await consulta<FilaEvento>(
            `SELECT * FROM soporte_ro.v_eventos WHERE cdc = $1 ORDER BY created_at DESC NULLS LAST LIMIT 5`,
            [documento.cdc],
          )
        : [];
      const alertas = filtrarAlertas(
        sesion,
        await consulta<FilaAlerta>(
          `SELECT * FROM soporte_ro.v_alertas_abiertas WHERE entidad_id = $1 ORDER BY created_at DESC`,
          [documento.id],
        ),
      );

      const veredicto = diagnosticarDocumento(documento, {
        ahora,
        horasParaTrabado: envs.horasParaTrabado,
        sifenActivo: empresa?.sifen_activo ?? null,
      });
      return { ok: true as const, documento, veredicto, eventos, alertas };
    });
  }

  // ── q_listar_documentos ──────────────────────────────────────────────

  async listarDocumentos(
    sesion: Sesion,
    filtros: { tipo?: TipoDocumento; estadosSifen?: string[]; desde?: Date; hasta?: Date },
  ): Promise<{ ok: true; documentos: DocumentoResumen[]; truncado: boolean } | FallaCatalogo> {
    const tipos = this.tiposPermitidos(sesion, filtros.tipo);
    if (!Array.isArray(tipos)) return tipos;

    const filas = await this.enAlcance(sesion, (consulta) =>
      consulta<DocumentoResumen>(
        `SELECT tipo, id, nro_documento, fecha_emision, estado_sifen, cdc
           FROM soporte_ro.v_documentos
          WHERE tipo = ANY($1::text[])
            AND ($2::text[] IS NULL OR establecimiento = ANY($2::text[]))
            AND ($3::text[] IS NULL OR lower(coalesce(estado_sifen, '')) = ANY($3::text[]))
            AND ($4::timestamp IS NULL OR fecha_emision >= $4)
            AND ($5::timestamp IS NULL OR fecha_emision < $5)
          ORDER BY fecha_emision DESC NULLS LAST
          LIMIT ${MAX_LISTADO + 1}`,
        [
          tipos,
          establecimientosDe(sesion),
          filtros.estadosSifen?.length ? filtros.estadosSifen.map((e) => e.toLowerCase()) : null,
          filtros.desde ?? null,
          filtros.hasta ?? null,
        ],
      ),
    );
    return { ok: true, documentos: filas.slice(0, MAX_LISTADO), truncado: filas.length > MAX_LISTADO };
  }

  // ── q_alertas_abiertas ───────────────────────────────────────────────

  async alertasAbiertas(sesion: Sesion, modulo?: string): Promise<{ ok: true; alertas: FilaAlerta[] }> {
    const filas = await this.enAlcance(sesion, (consulta) =>
      consulta<FilaAlerta>(
        `SELECT * FROM soporte_ro.v_alertas_abiertas
          WHERE ($1::text IS NULL OR modulo = $1)
          ORDER BY created_at DESC
          LIMIT 50`,
        [modulo ?? null],
      ),
    );
    return { ok: true, alertas: filtrarAlertas(sesion, filas).slice(0, MAX_LISTADO) };
  }

  // ── q_salud_empresa ──────────────────────────────────────────────────

  async saludEmpresa(sesion: Sesion, ahora = new Date()) {
    const [empresa] = await this.enAlcance(sesion, (consulta) =>
      consulta<FilaEmpresa>(`SELECT * FROM soporte_ro.v_empresa`),
    );
    if (!empresa) return { ok: false as const, motivo: 'no_encontrado' as const, detalle: 'Empresa no encontrada' };

    const venc = empresa.certificado_vencimiento ? new Date(empresa.certificado_vencimiento) : null;
    const diasCertificado = venc ? Math.floor((venc.getTime() - ahora.getTime()) / 86_400_000) : null;
    return {
      ok: true as const,
      empresa: {
        razon_social: empresa.razon_social,
        ruc: `${empresa.ruc}-${empresa.dv}`,
        sifen_activo: empresa.sifen_activo,
        timbrado: empresa.timbrado_numero,
        ambiente: empresa.timbrado_ambiente,
        suscripcion_estado: empresa.suscripcion_estado,
        suscripcion_fecha_fin: empresa.suscripcion_fecha_fin,
      },
      certificado: {
        vencimiento: venc,
        dias_para_vencer: diasCertificado,
        estado:
          diasCertificado === null
            ? ('sin_dato' as const)
            : diasCertificado < 0
              ? ('vencido' as const)
              : diasCertificado <= 30
                ? ('por_vencer' as const)
                : ('vigente' as const),
      },
    };
  }

  // ── q_buscar_guia ────────────────────────────────────────────────────

  async buscarGuia(sesion: Sesion, pregunta: string, screenKey?: string): Promise<FragmentoGuia[]> {
    const texto = pregunta.trim().slice(0, 500);
    if (!texto) return [];
    // OR entre los términos: una pregunta natural con AND casi nunca matchea.
    const filas = await this.enAlcance(sesion, (consulta) =>
      consulta<FragmentoGuia>(
        `WITH q AS (
           SELECT to_tsquery('spanish', NULLIF(replace(plainto_tsquery('spanish', $1)::text, '&', '|'), '')) AS q
         )
         SELECT g.titulo, g.screen_key, g.contenido,
                ts_rank(to_tsvector('spanish', g.titulo || ' ' || g.contenido), q.q)
                  * CASE WHEN $2::text IS NOT NULL AND g.screen_key = $2 THEN 1.5 ELSE 1 END AS relevancia
           FROM soporte_ro.v_guias g, q
          WHERE q.q IS NOT NULL
            AND to_tsvector('spanish', g.titulo || ' ' || g.contenido) @@ q.q
          ORDER BY relevancia DESC
          LIMIT 5`,
        [texto, screenKey ?? null],
      ),
    );
    return filas.map((f) => ({
      ...f,
      relevancia: Number(f.relevancia),
      contenido:
        f.contenido.length > MAX_CARACTERES_GUIA ? `${f.contenido.slice(0, MAX_CARACTERES_GUIA)}…` : f.contenido,
    }));
  }
}

/** El alcance completo de la sesión, que runScoped firma. */
export function alcanceDe(sesion: Sesion) {
  return {
    usuarioId: sesion.usuarioId,
    empresaId: sesion.empresaId,
    sucursales: sesion.alcance ? sesion.alcance.sucursalIds : null,
    establecimientos: sesion.alcance ? sesion.alcance.establecimientos : null,
    privilegios: sesion.privilegios,
  };
}

function establecimientosDe(sesion: Sesion): string[] | null {
  return sesion.alcance ? [...sesion.alcance.establecimientos] : null;
}

/**
 * Alertas que el usuario puede ver: por privilegio requerido y por sucursal.
 * Sin atajo para superadmin: sus privilegios ya vienen calculados con el plan.
 */
export function filtrarAlertas(sesion: Sesion, alertas: FilaAlerta[]): FilaAlerta[] {
  return alertas.filter(
    (a) =>
      (!a.privilegio_requerido || sesion.privilegios.has(a.privilegio_requerido)) &&
      (!a.sucursal_id || !sesion.alcance || sesion.alcance.sucursalIds.includes(a.sucursal_id)),
  );
}

/** Filtro SQL para una referencia, con placeholders a partir de $desde. */
export function filtroReferencia(ref: ReferenciaDocumento, desde: number): { filtro: string; params: string[] } {
  if (ref.clase === 'cdc') {
    return { filtro: `cdc = $${desde}`, params: [ref.cdc] };
  }
  const params = [ref.numero];
  let filtro = `ltrim(numero, '0') = ltrim($${desde}, '0')`;
  if (ref.establecimiento && ref.puntoExpedicion) {
    filtro += ` AND establecimiento = $${desde + 1} AND punto_expedicion = $${desde + 2}`;
    params.push(ref.establecimiento, ref.puntoExpedicion);
  }
  return { filtro, params };
}
