import { BadRequestException, Injectable, Logger } from "@nestjs/common";

// Config resuelta para UNA tienda especifica -- ver comentario en
// TenantConfig entity / Tienda entity sobre por que esto es por tienda y no
// por tenant.
export type FacturacionPosConfig = {
  habilitada: boolean;
  url: string;
  username: string;
  password: string;
  computadora: string;
  precioDecimales: number;
  // Si el precio de maeplu/POSTouch ya incluye IVA (lo normal en retail). Se
  // configura por tienda porque falta confirmarlo contra el sandbox real.
  precioIncluyeIva: boolean;
  // PLU del ERP bajo los que se facturan las lineas creadas en el taller
  // (TLLP-/MANUAL-), que POSTouch no conoce. Se crean una vez en el ERP.
  pluProductosTaller?: string;
  pluServiciosTaller?: string;
};

export type OrdenPosLinea = {
  plu: string;
  cantidad: number;
  precio: number;
  // Total cotizado de la linea (con IVA). Si viene, enviarOrden verifica que
  // los montos que va a mandar a POSTouch cuadren con lo cotizado ANTES de
  // llamar: facturar un monto distinto al aprobado por el cliente no se
  // puede deshacer desde aca.
  totalEsperado?: number;
};

export type OrdenPosCliente = {
  tarjeta?: string;
  nombre: string;
  nit: string;
  telefono?: string;
  email?: string;
  direccion?: string;
};

export type OrdenPosInput = {
  // Debe ser numerico: POSTouch persiste este valor en PedidosBitacora.webificaOrderId,
  // columna `float` (PedidosBitacora.php:277) — un string (p.ej. "OT-20260909-001")
  // se trunca a 0 y colisiona con cualquier otra orden.
  id: number;
  cliente: OrdenPosCliente;
  lineas: OrdenPosLinea[];
};

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

// Redondeo a los decimales exactos configurados para la tienda destino
// (ParametrosGlobales.getPrecioDecimales() en POSTouch) — la tolerancia de
// esa validacion es de solo Q0.005 por linea (Transaccion.php:1955-1977),
// asi que un redondeo distinto al de la tienda hace que se rechace la
// orden completa antes de facturar.
export function redondear(valor: number, decimales: number): number {
  const factor = 10 ** decimales;
  return Math.round(Number((valor * factor).toPrecision(15))) / factor;
}

const MEDIA_POR_LIQUIDAR = 99;

// Una llamada colgada bloquearia la peticion HTTP del usuario (cambio de
// estado de la OT) hasta el timeout por defecto de undici, que son minutos.
const TIMEOUT_POSTOUCH_MS = 30_000;

// Redondear por linea puede alejar el total facturado unos centavos del
// cotizado (p. ej. 100.00 entre 3 unidades). Se tolera lo que POSTouch mismo
// tolera por linea (Q0.005 por unidad, minimo un centavo); mas que eso es un
// precio mal configurado, no redondeo.
function toleranciaCuadre(cantidad: number): number {
  return Math.max(0.01, 0.005 * Math.abs(cantidad)) + 1e-9;
}

// Error de "no sabemos si la factura se creo". Reintentar a ciegas puede
// duplicar la venta, asi que el mensaje manda a verificar en POSTouch primero.
export class PostouchRespuestaIncierta extends BadRequestException {
  readonly incierto = true;
}

// POSTouch es PHP: un warning/notice antes del JSON deja un 200 cuyo cuerpo no
// parsea completo aunque la factura SI se creo. Se extrae el primer objeto
// JSON balanceado en vez de descartar la respuesta entera.
export function extraerJson(texto: string): any {
  if (!texto) return {};
  try {
    return JSON.parse(texto);
  } catch {
    // sigue abajo
  }
  const inicio = texto.indexOf("{");
  if (inicio < 0) return {};
  let profundidad = 0;
  let enCadena = false;
  let escapado = false;
  for (let i = inicio; i < texto.length; i++) {
    const c = texto[i];
    if (enCadena) {
      if (escapado) escapado = false;
      else if (c === "\\") escapado = true;
      else if (c === '"') enCadena = false;
      continue;
    }
    if (c === '"') enCadena = true;
    else if (c === "{") profundidad++;
    else if (c === "}") {
      profundidad--;
      if (profundidad === 0) {
        try {
          return JSON.parse(texto.slice(inicio, i + 1));
        } catch {
          return {};
        }
      }
    }
  }
  return {};
}

const CODIGOS_SIN_LLEGAR = new Set(["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"]);

// fetch de Node reporta casi todo como "fetch failed"; la causa real esta en
// error.cause. Devuelve el mensaje legible y si la orden pudo haber llegado.
function describirFalloDeRed(error: any): { mensaje: string; pudoLlegar: boolean } {
  const causa = error?.cause;
  const codigo = String(causa?.code ?? error?.code ?? "");
  const esTimeout =
    error?.name === "TimeoutError" ||
    error?.name === "AbortError" ||
    codigo === "UND_ERR_HEADERS_TIMEOUT" ||
    codigo === "UND_ERR_BODY_TIMEOUT";
  if (esTimeout) {
    return {
      mensaje: `POSTouch no respondió en ${TIMEOUT_POSTOUCH_MS / 1000} s`,
      pudoLlegar: true,
    };
  }
  const detalle = causa?.message || error?.message || "error de red";
  return {
    mensaje: `No se pudo contactar POSTouch (${detalle})`,
    pudoLlegar: !CODIGOS_SIN_LLEGAR.has(codigo),
  };
}

export type CredencialesPosParaProbar = Pick<FacturacionPosConfig, "url" | "username" | "password" | "computadora">;

export type ResultadoProbarConexion = { ok: true } | { ok: false; detalle: string };

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

  async enviarOrden(
    config: FacturacionPosConfig,
    orden: OrdenPosInput,
  ): Promise<OrdenPosCreada> {
    const productos = orden.lineas.map((linea) => {
      const precio = redondear(linea.precio, config.precioDecimales);
      const monto = redondear(precio * linea.cantidad, config.precioDecimales);
      return {
        plu: linea.plu,
        cantidad: linea.cantidad,
        precio,
        monto,
        descuentoItemPorcentaje: 0,
        comboNumero: 0,
        pluPadre: "",
      };
    });

    const total = redondear(
      productos.reduce((sum, p) => sum + p.monto, 0),
      config.precioDecimales,
    );

    orden.lineas.forEach((linea, i) => {
      if (linea.totalEsperado === undefined) return;
      const diferencia = Math.abs(productos[i].monto - linea.totalEsperado);
      if (diferencia > toleranciaCuadre(linea.cantidad)) {
        throw new BadRequestException(
          `El monto a facturar de la línea ${linea.plu} (Q${productos[i].monto.toFixed(2)}) no cuadra con lo cotizado (Q${linea.totalEsperado.toFixed(2)}). Revisa los decimales de precio de la tienda en Configuración → Facturación. No se envió nada a POSTouch.`,
        );
      }
    });

    const payload = {
      _username: config.username,
      _password: config.password,
      computadora: config.computadora,
      ordenes: [
        {
          id: orden.id,
          clienteTarjeta: orden.cliente.tarjeta ?? "",
          clienteNombre: orden.cliente.nombre,
          clienteTelefono: orden.cliente.telefono ?? "",
          clienteEmail: orden.cliente.email ?? "",
          clienteNIT: orden.cliente.nit,
          clienteDireccion2: orden.cliente.direccion ?? "",
          clienteCiudad: "",
          clienteDepartamen: "0",
          formasPago: [
            { media: MEDIA_POR_LIQUIDAR, emisor: 0, codigo: 0, monto: total },
          ],
          productos,
        },
      ],
    };

    let response: Response;
    try {
      response = await fetch(config.url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(TIMEOUT_POSTOUCH_MS),
      });
    } catch (error: any) {
      const { mensaje, pudoLlegar } = describirFalloDeRed(error);
      if (pudoLlegar) {
        throw new PostouchRespuestaIncierta(
          `${mensaje}. La orden pudo haberse creado: verifica en POSTouch antes de reintentar para no duplicar la factura.`,
        );
      }
      throw new BadRequestException(`${mensaje}. No se envió la orden.`);
    }

    let text: string;
    try {
      text = await response.text();
    } catch (error: any) {
      const { mensaje } = describirFalloDeRed(error);
      throw new PostouchRespuestaIncierta(
        `${mensaje} al leer la respuesta. La orden pudo haberse creado: verifica en POSTouch antes de reintentar para no duplicar la factura.`,
      );
    }
    const data = extraerJson(text);

    // Un {"error": ...} es un rechazo explicito de POSTouch (usuario, PLU,
    // formas de pago...), con el HTTP que sea: este POSTouch contesta 500 hasta
    // para un usuario inexistente. Nunca llego a facturar.
    if (data?.error) {
      throw new BadRequestException(`POSTouch rechazo la orden: ${data.error}`);
    }

    if (!response.ok) {
      const status = typeof response.status === "number" ? response.status : undefined;
      const etiqueta = status === undefined ? "sin codigo de estado" : `HTTP ${status}`;
      const extracto = text.slice(0, 300);

      // Un 4xx sin {error} (credenciales, ruta, permisos) lo para el servidor
      // antes de procesar la orden: es un fallo limpio y se puede reintentar.
      if (status !== undefined && status >= 400 && status < 500) {
        throw new BadRequestException(
          `POSTouch respondio con error al crear la orden (${etiqueta}): ${extracto}`,
        );
      }

      // Un 5xx (o un estado ilegible) SIN {error} es PHP reventando a mitad de
      // camino, y puede ser DESPUES de facturar: el 12-oct-2026 el demo creo la
      // factura CI0101-3155 y devolvio 500 con el cuerpo vacio porque fallo el
      // paso posterior (la caja no era de tipo domicilio). Contarlo como fallo
      // invita a reintentar y duplicar la venta.
      throw new PostouchRespuestaIncierta(
        `POSTouch respondió con error (${etiqueta}) y pudo haber creado la factura antes de fallar. Verifica en POSTouch antes de reintentar para no duplicarla. Respuesta: ${extracto || "(vacía)"}`,
      );
    }

    const factura = data?.facturas?.[0];
    const campos = ["tienda", "caja", "tipo", "serie", "transac"] as const;
    if (!factura || campos.some((c) => factura[c] === undefined || factura[c] === null || factura[c] === "")) {
      // Respuesta 200 pero sin factura legible: lo mas probable es que POSTouch
      // SI la haya creado. Por eso no se dice "fallo", se dice "verifica".
      throw new PostouchRespuestaIncierta(
        `POSTouch respondió pero no se pudo leer la factura creada. Verifica en POSTouch antes de reintentar para no duplicarla. Respuesta: ${text.slice(0, 300)}`,
      );
    }

    return {
      tienda: factura.tienda,
      caja: factura.caja,
      tipo: factura.tipo,
      serie: factura.serie,
      transac: factura.transac,
    };
  }

  // Sonda segura por construccion: ordenes:[] verificado contra el codigo
  // real de POSTouch (DomicilioController::createOrdenesEdngtAction llama
  // login() ANTES de tocar `ordenes`; con `ordenes:[]` el foreach de
  // Domicilio::createOrdenesEdngt no corre ni una vez -- cero PLUs, cero
  // clientes, cero facturas). Independiente de enviarOrden: nunca cae a el
  // bajo ninguna rama de error, nunca asume exito ante una respuesta que no
  // coincide exactamente con la esperada.
  async probarConexion(config: CredencialesPosParaProbar): Promise<ResultadoProbarConexion> {
    const payload = {
      _username: config.username,
      _password: config.password,
      computadora: config.computadora,
      ordenes: [] as unknown[],
    };

    let response: Response;
    try {
      response = await fetch(config.url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(TIMEOUT_POSTOUCH_MS),
      });
    } catch (error: any) {
      const { mensaje } = describirFalloDeRed(error);
      this.logDiagnosticoProbarConexion(payload, null, `Error de red: ${error?.message ?? error}`);
      return { ok: false, detalle: mensaje };
    }

    const text = await response.text();
    let data: any;
    try {
      data = text ? JSON.parse(text) : {};
    } catch {
      this.logDiagnosticoProbarConexion(payload, text, "Respuesta no es JSON valido");
      return { ok: false, detalle: "No se pudo verificar la respuesta de POSTouch" };
    }

    if (!response.ok) {
      this.logDiagnosticoProbarConexion(payload, text, `HTTP ${response.status}`);
      return { ok: false, detalle: `POSTouch respondió con error: ${text.slice(0, 300)}` };
    }

    if (data?.error) {
      this.logDiagnosticoProbarConexion(payload, text, "data.error presente");
      return { ok: false, detalle: `POSTouch rechazó la conexión: ${data.error}` };
    }

    const esRespuestaEsperada =
      data && typeof data === "object" && data.procesadas === 0 && Array.isArray(data.facturas) && data.facturas.length === 0;

    if (!esRespuestaEsperada) {
      this.logDiagnosticoProbarConexion(payload, text, "Forma de respuesta inesperada");
      return { ok: false, detalle: "No se pudo verificar la respuesta de POSTouch" };
    }

    this.logDiagnosticoProbarConexion(payload, text, "ok");
    return { ok: true };
  }

  private logDiagnosticoProbarConexion(
    payload: Record<string, unknown>,
    respuesta: string | null,
    resultado: string,
  ): void {
    const payloadSeguro = { ...payload, _password: "***" };
    this.logger.debug(
      `[probarConexion] payload=${JSON.stringify(payloadSeguro)} respuesta=${(respuesta ?? "").slice(0, 300)} resultado=${resultado}`,
    );
  }
}
