import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { ReadonlyRepository } from "../common/readonly-repository";
import { TenantRepositoryAccessor } from "../tenancy/tenant-repository.accessor";
import { OtFacturacion } from "./entities/ot-facturacion.entity";
import { TallerParametrosService } from "../taller-parametros/taller-parametros.service";
import { FacturacionPosConfig, OrdenPosLinea, PostouchClientService } from "./postouch-client.service";

export type OtParaFacturar = {
  id: number;
  numeroOt: string;
  // Una tienda especifica, nunca "el tenant" en general -- computadora en
  // POSTouch resuelve a UNA tienda/caja puntual, asi que sin tiendaId no hay
  // forma segura de saber a cual enviar la orden.
  tiendaId: number | null;
  clienteTarjeta: string;
  cliente: {
    nombre: string;
    nit: string;
    telefono?: string;
    email?: string;
    direccion?: string;
  };
  cotizacion: {
    lineas: Array<{
      plu: string;
      descripcion: string;
      // Base SIN IVA (asi la guarda cotizaciones.service) y total CON IVA de la
      // linea. Cual de los dos viaja a POSTouch lo decide
      // facturacion_pos.precio_incluye_iva de la tienda.
      precioUnitario: number;
      total: number;
      ivaPorcentaje: number;
      cantidad: number;
      esServicio: boolean;
      // Lineas creadas en el taller (TLLP- y MANUAL-): POSTouch no las conoce,
      // asi que no viajan con su PLU sino consolidadas bajo los dos PLU
      // genericos del ERP (productos y servicios) de la tienda.
      origenLocal?: boolean;
    }>;
  };
};

export type FacturaCreadaPos = {
  tienda: number;
  caja: number;
  tipo: string;
  serie: string;
  transac: number;
};

export type EstadoPagoPos = "pagada" | "pendiente" | "anulada" | "no_encontrada";

export type ConsultaPagoPos = {
  estado: EstadoPagoPos;
  total: number;
  pagado: number;
};

export type ResultadoEnvioPos =
  | { exito: true; factura: FacturaCreadaPos }
  // incierto: POSTouch pudo haber creado la factura; quien reintente debe
  // verificar primero. noAplica: la OT simplemente no se factura por POS.
  | { exito: false; error: string; incierto?: boolean; noAplica?: boolean };

type LineaOt = OtParaFacturar["cotizacion"]["lineas"][number];

// Precio por linea que viaja a POSTouch: con precioIncluyeIva es el final con
// IVA (si no, el total cotizado no cuadraria con lo facturado).
function importeDeLinea(linea: LineaOt, precioIncluyeIva: boolean): number {
  return precioIncluyeIva ? linea.total : linea.precioUnitario * linea.cantidad;
}

// Las lineas del ERP conservan su PLU, cantidad y precio. Las creadas en el
// taller se suman en una sola linea por PLU generico (cantidad 1): la orden y
// los comprobantes del taller mantienen el detalle, la factura de POSTouch
// lleva el total de productos y el total de servicios.
export function consolidarLineasParaPos(
  lineas: LineaOt[],
  config: Pick<FacturacionPosConfig, "precioIncluyeIva" | "pluProductosTaller" | "pluServiciosTaller">,
): OrdenPosLinea[] {
  const resultado: OrdenPosLinea[] = [];
  const delTaller: { productos: LineaOt[]; servicios: LineaOt[] } = { productos: [], servicios: [] };

  for (const linea of lineas) {
    if (!linea.origenLocal) {
      resultado.push({
        plu: linea.plu,
        cantidad: linea.cantidad,
        precio:
          config.precioIncluyeIva && linea.cantidad > 0 ? linea.total / linea.cantidad : linea.precioUnitario,
        totalEsperado: config.precioIncluyeIva ? linea.total : undefined,
      });
    } else if (linea.esServicio) {
      delTaller.servicios.push(linea);
    } else {
      delTaller.productos.push(linea);
    }
  }

  const grupos: Array<{ lineas: LineaOt[]; plu: string | undefined; que: string; parametro: string }> = [
    {
      lineas: delTaller.productos,
      plu: config.pluProductosTaller,
      que: "productos",
      parametro: "PLU genérico de productos del taller",
    },
    {
      lineas: delTaller.servicios,
      plu: config.pluServiciosTaller,
      que: "servicios",
      parametro: "PLU genérico de servicios del taller",
    },
  ];

  for (const grupo of grupos) {
    if (grupo.lineas.length === 0) continue;
    if (!grupo.plu) {
      throw new BadRequestException(
        `Esta OT tiene ${grupo.que} creados en el taller y la tienda no tiene configurado el ${grupo.parametro}. Configúralo en Configuración → Facturación POS y reintenta. No se envió nada a POSTouch.`,
      );
    }
    const importe = grupo.lineas.reduce((suma, l) => suma + importeDeLinea(l, config.precioIncluyeIva), 0);
    const importeRedondeado = Math.round(importe * 100) / 100;
    resultado.push({
      plu: grupo.plu,
      cantidad: 1,
      precio: importeRedondeado,
      totalEsperado: config.precioIncluyeIva ? importeRedondeado : undefined,
    });
  }

  return resultado;
}

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

  constructor(
    private readonly tenantAccessor: TenantRepositoryAccessor,
    private readonly postouchClientService: PostouchClientService,
    private readonly tallerParametros: TallerParametrosService,
  ) {}

  // Por TIENDA, no por tenant -- ver comentario en TallerParametro entity /
  // spec §3.3 sobre por que facturacion_pos vive con ambito='tienda'. Ya no
  // lee columnas de maetie directo: taller_parametros es la fuente de verdad
  // desde este cambio (Task 1 migro los datos existentes).
  private async getConfigForTienda(tiendaId: number): Promise<FacturacionPosConfig | null> {
    const habilitada = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.habilitada");
    if (!habilitada) {
      return null;
    }

    const url = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.url");
    const username = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.username");
    const password = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.password");
    const computadora = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.computadora");
    const precioDecimales = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.precio_decimales");

    if (!url) {
      throw new BadRequestException(
        `La tienda ${tiendaId} tiene facturacionPosHabilitada pero no tiene facturacionPosUrl configurada`,
      );
    }
    if (!username) {
      throw new BadRequestException(
        `La tienda ${tiendaId} tiene facturacionPosHabilitada pero no tiene facturacionPosUsername configurado`,
      );
    }
    if (!password) {
      throw new BadRequestException(
        `La tienda ${tiendaId} tiene facturacionPosHabilitada pero no tiene contraseña configurada`,
      );
    }
    if (!computadora) {
      throw new BadRequestException(
        `La tienda ${tiendaId} tiene facturacionPosHabilitada pero no tiene facturacionPosComputadora configurada`,
      );
    }
    if (!precioDecimales && precioDecimales !== 0) {
      throw new BadRequestException(
        `La tienda ${tiendaId} tiene facturacionPosHabilitada pero no tiene facturacionPosPrecioDecimales configurado`,
      );
    }

    const precioIncluyeIva = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.precio_incluye_iva");
    const pluProductosTaller = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.plu_productos_taller");
    const pluServiciosTaller = await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.plu_servicios_taller");

    return {
      habilitada: true,
      url,
      username,
      password,
      computadora,
      precioDecimales,
      precioIncluyeIva,
      pluProductosTaller: pluProductosTaller || undefined,
      pluServiciosTaller: pluServiciosTaller || undefined,
    };
  }

  // La facturacion POS aplica a una OT solo si el taller la usa Y esa tienda
  // la tiene habilitada. Un taller sin POS (o una tienda sin caja asignada)
  // no debe ver errores ni banners de facturacion: no es una falla, es que
  // ese flujo no existe para ellos.
  async facturacionAplica(tiendaId: number | null | undefined): Promise<boolean> {
    if (tiendaId === null || tiendaId === undefined) return false;
    if (!(await this.tallerParametros.getBooleanConFallback("facturacion.usa_facturacion"))) {
      return false;
    }
    try {
      return Boolean(await this.tallerParametros.getParaTienda(tiendaId, "facturacion_pos.habilitada"));
    } catch (error: any) {
      this.logger.warn(`No se pudo leer facturacion_pos.habilitada de la tienda ${tiendaId}: ${error?.message ?? error}`);
      return false;
    }
  }

  // NO guarda nada local: quien llama (OtsService) registra la factura y mueve
  // la OT a FACTURADA en UNA transaccion, para que "POSTouch ya la tiene" y
  // "el taller lo sabe" no puedan separarse por una falla en medio.
  async enviarOrdenTerminada(ot: OtParaFacturar): Promise<ResultadoEnvioPos> {
    try {
      if (!(await this.facturacionAplica(ot.tiendaId))) {
        return {
          exito: false,
          noAplica: true,
          error: "La facturación POS no aplica a esta OT (apagada para el taller o para la tienda) -- Configuración → Facturación.",
        };
      }

      const config = await this.getConfigForTienda(ot.tiendaId as number);

      if (!config) {
        return { exito: false, noAplica: true, error: "Facturacion POS no habilitada para esta tienda" };
      }

      // Lo que viene del ERP viaja con su PLU real (descuenta inventario y sale
      // con su descripcion). Lo creado en el taller no existe en el ERP: se
      // factura bajo los dos PLU genericos de la tienda, una linea por grupo.
      const lineas = consolidarLineasParaPos(ot.cotizacion.lineas, config);

      const orden = {
        id: ot.id,
        cliente: {
          tarjeta: ot.clienteTarjeta,
          nombre: ot.cliente.nombre,
          nit: ot.cliente.nit,
          telefono: ot.cliente.telefono,
          email: ot.cliente.email,
          direccion: ot.cliente.direccion,
        },
        lineas,
      };

      const factura = await this.enviarYConciliar(config, orden);
      return { exito: true, factura };
    } catch (error: any) {
      this.logger.error(
        `Error enviando OT ${ot.numeroOt} a POSTouch: ${error?.message ?? error}`,
      );
      return {
        exito: false,
        error: error?.message ?? "Error desconocido enviando a POSTouch",
        incierto: Boolean(error?.incierto),
      };
    }
  }

  // Envia la orden y, si POSTouch contesta algo que no se puede leer, busca la
  // factura en su propia bitacora de pedidos en vez de dejar a la OT "incierta".
  //
  // Con una caja de tipo domicilio (la que usa el endpoint create_ordenes_edngt)
  // POSTouch trabaja en dos tiempos y siempre por el id de la orden: la primera
  // vez crea un PEDIDO; si le llega la MISMA orden otra vez, lo confirma a
  // factura y devuelve esa. Es idempotente: nunca crea dos facturas para el mismo
  // id (comprobado en el demo el 12-oct-2026: la orden repetida confirmo el pedido
  // PE0101-1 a la factura CI0101-3156 sin duplicar). Por eso, si queda un pedido
  // pendiente, se reenvia UNA vez y se vuelve a mirar. Dos envios como maximo.
  private async enviarYConciliar(config: FacturacionPosConfig, orden: Parameters<PostouchClientService["enviarOrden"]>[1]): Promise<FacturaCreadaPos> {
    let ultimoError: any;

    for (let envio = 1; envio <= 2; envio++) {
      try {
        return await this.postouchClientService.enviarOrden(config, orden);
      } catch (error: any) {
        // Un rechazo explicito no dejo nada en POSTouch: no hay nada que buscar.
        if (!error?.incierto) throw error;
        ultimoError = error;
      }

      const enPos = await this.buscarEnBitacoraPos(orden.id);
      if (enPos?.estado === "factura") return enPos.factura;
      // Sin fila (caja normal, POSTouch reventó antes de registrarla) o sin
      // poder leer la tabla: no hay con que reconciliar, sigue incierto.
      if (enPos?.estado !== "pedido") break;
    }

    throw ultimoError;
  }

  // Solo SELECT. pedidos_bitacora la llena POSTouch con webifica_order_id = el id
  // de la orden que le mandamos (el de la OT). Si la tabla no existe o no se
  // puede leer (el modulo de domicilios puede vivir en otra base), devuelve null:
  // el llamador conserva el error incierto original, nunca lo empeora.
  private async buscarEnBitacoraPos(
    ordenId: number,
  ): Promise<{ estado: "factura"; factura: FacturaCreadaPos } | { estado: "pedido" } | null> {
    try {
      const lectura = new ReadonlyRepository(this.tenantAccessor.repositoryFor(OtFacturacion));
      const filas = await lectura.query<Array<{ tienda: number | string; caja: number | string; tipo: number | string; serie: string | null; transac: number | string | null }>>(
        `SELECT tienda, caja, tipo, serie, transac
         FROM pedidos_bitacora
         WHERE webifica_order_id = ?
         ORDER BY id DESC
         LIMIT 1`,
        [ordenId],
      );

      if (!Array.isArray(filas) || filas.length === 0) return null;

      const fila = filas[0];
      const serie = String(fila.serie ?? "").trim();
      const transac = Number(fila.transac ?? 0);
      // Pedido sin confirmar: POSTouch lo deja con serie vacia y transac 0.
      if (serie === "" || !(transac > 0)) return { estado: "pedido" };

      return {
        estado: "factura",
        factura: {
          tienda: Number(fila.tienda),
          caja: Number(fila.caja),
          tipo: String(fila.tipo),
          serie,
          transac,
        },
      };
    } catch (error: any) {
      this.logger.warn(`No se pudo leer pedidos_bitacora para la orden ${ordenId}: ${error?.message ?? error}`);
      return null;
    }
  }

  // Pago de una factura ya enviada, leido DIRECTO de las tablas de POSTouch
  // (solo SELECT). Usa la misma regla que POSTouch aplica internamente
  // (AccionesSobreTransaccion: "Debe LIQUIDAR la factura antes de..."): una
  // venta enviada con forma de pago "por liquidar" tiene una fila en hdetmed
  // cuyo `media` es el de formas_pago.porliquidar = 1. Al liquidarla POSTouch
  // cambia esa fila a media=1 con monto=0 y agrega las filas de los pagos
  // reales. O sea: pendiente <=> todavia existe una fila de ese medio; no se
  // compara contra el numero 99 que viaja en el payload, porque en la base
  // queda el `media` real configurado para "por liquidar" en cada tienda.
  async consultarPago(factura: Pick<OtFacturacion, "tienda" | "caja" | "tipo" | "serie" | "transac">): Promise<ConsultaPagoPos> {
    const lectura = new ReadonlyRepository(this.tenantAccessor.repositoryFor(OtFacturacion));
    const porLiquidar = "SELECT media FROM formas_pago WHERE porliquidar = 1";
    const claveDoc = "d.tienda = m.tienda AND d.caja = m.caja AND d.tipo = m.tipo AND d.serie = m.serie AND d.transac = m.transac";

    const filas = await lectura.query<Array<{ total: number | string | null; anulacion: number | string | null; pendientes: number | string; pagado: number | string }>>(
      `SELECT m.total AS total, m.anulacion AS anulacion,
        (SELECT COUNT(*) FROM hdetmed d WHERE ${claveDoc} AND d.media IN (${porLiquidar})) AS pendientes,
        (SELECT COALESCE(SUM(d.monto), 0) FROM hdetmed d WHERE ${claveDoc} AND d.media NOT IN (${porLiquidar})) AS pagado
       FROM hmaetra m
       WHERE m.tienda = ? AND m.caja = ? AND m.tipo = ? AND m.serie = ? AND m.transac = ?
       LIMIT 1`,
      [factura.tienda, factura.caja, factura.tipo, factura.serie, factura.transac],
    );

    if (!Array.isArray(filas) || filas.length === 0) {
      return { estado: "no_encontrada", total: 0, pagado: 0 };
    }

    const fila = filas[0];
    const total = Number(fila.total ?? 0);
    const pagado = Number(fila.pagado ?? 0);
    const anulada = Number(fila.anulacion ?? 0) !== 0;
    const pendiente = Number(fila.pendientes) > 0;

    if (anulada) return { estado: "anulada", total, pagado };
    // Sin fila por liquidar Y con pagos reales que cubren el total (tolerancia de
    // un centavo, la misma que usa POSTouch). Sin pagos reales no se da por
    // pagada: una venta sin ninguna forma de pago no esta cobrada.
    if (!pendiente && pagado > 0 && pagado + 0.01 >= total) {
      return { estado: "pagada", total, pagado };
    }
    return { estado: "pendiente", total, pagado };
  }
}
