import { jsPDF } from "jspdf";
import QRCode from "qrcode";
import JsBarcode from "jsbarcode";
import { ticketBarcodePayload, ticketQrPayload } from "@/utils/laundryScanInput";
import { formatFechaEntregaDisplay, promisedFechaEntregaIso } from "@/utils/fechaEntrega";

const MM_TO_PT = 72 / 25.4;
const PAGE_W_PT = 80 * MM_TO_PT;

/**
 * URL pública de seguimiento (misma lógica que el ticket del servidor).
 */
export function buildSeguimientoUrl(folioUnico) {
  const base =
    (typeof import.meta !== "undefined" && import.meta.env?.VITE_APP_URL) ||
    (typeof window !== "undefined" ? window.location.origin : "");
  const root = String(base || "").replace(/\/$/, "").replace(/\/api$/i, "");
  return `${root}/seguimiento/${encodeURIComponent(folioUnico)}`;
}

/** Genera el QR en el navegador (sin depender de servicios externos como api.qrserver.com). */
async function qrImageDataUrl(payload) {
  try {
    return await QRCode.toDataURL(payload, {
      width: 200,
      margin: 1,
      errorCorrectionLevel: "M",
    });
  } catch {
    throw new Error("No se pudo generar el código QR.");
  }
}

/** Genera código de barras Code128 en navegador (data URL PNG). */
function barcodeImageDataUrl(payload, opts = {}) {
  const text = String(payload ?? "").trim();
  if (!text) return "";
  const width = typeof opts.width === "number" && opts.width > 0 ? opts.width : 2;
  const height = typeof opts.height === "number" && opts.height > 0 ? opts.height : 64;
  try {
    const canvas = document.createElement("canvas");
    JsBarcode(canvas, text, {
      format: "CODE128",
      width,
      height,
      margin: 6,
      displayValue: false,
      background: "#ffffff",
      lineColor: "#111111",
    });
    return canvas.toDataURL("image/png");
  } catch {
    return "";
  }
}

/**
 * QR para escaneo en lavandería (mismo valor que `codigo_qr` en BD: POST escanear-qr / laundry/scans).
 * @param {string|{folio_unico?: string, codigo_qr?: string}} folioOrOrden Folio o objeto orden (con codigo_qr).
 * @param {{ width?: number }} [opts] width en px del QR (por defecto 160)
 * @returns {Promise<string>} data URL o cadena vacía si falla
 */
export async function buildSeguimientoQrDataUrl(folioOrOrden, opts = {}) {
  const payload =
    folioOrOrden && typeof folioOrOrden === "object"
      ? ticketQrPayload(folioOrOrden)
      : String(folioOrOrden ?? "").trim();
  if (!payload) {
    return "";
  }
  const width = typeof opts.width === "number" && opts.width > 0 ? opts.width : 160;
  try {
    return await QRCode.toDataURL(payload, {
      width,
      margin: 1,
      errorCorrectionLevel: "M",
    });
  } catch {
    return "";
  }
}

/**
 * Código de barras para escaneo en lavandería (Code128).
 * @param {string|{folio_unico?: string, codigo_qr?: string}} folioOrOrden
 * @param {{ width?: number, height?: number }} [opts]
 * @returns {Promise<string>}
 */
export async function buildSeguimientoBarcodeDataUrl(folioOrOrden, opts = {}) {
  const payload =
    folioOrOrden && typeof folioOrOrden === "object"
      ? ticketBarcodePayload(folioOrOrden)
      : String(folioOrOrden ?? "").trim();
  if (!payload) return "";
  return barcodeImageDataUrl(payload, opts);
}

/** Misma cadena que App\Support\TicketObservationLines::TITLE_OBSERVACIONES_POR_PRENDA (C4). */
export const TICKET_TITLE_OBSERVACIONES_POR_PRENDA = "Observaciones por tipo de prenda";

/**
 * Agrupa observaciones de línea por nombre de tipo de prenda (o producto/servicio como respaldo).
 * @returns {Array<{ title: string, lines: string[] }>}
 */
export function buildGarmentObservationSections(orden) {
  const detalles = orden?.detalles || [];
  /** @type {Map<string, string[]>} */
  const map = new Map();
  detalles.forEach((d) => {
    if (d.is_informational) return;
    const raw = d.observations != null ? String(d.observations).trim() : "";
    if (!raw) return;
    const title =
      d.garment_type?.name || d.laundry_sale_product?.name || d.producto?.nombre || d.etapa?.name || "Ítem";
    if (!map.has(title)) map.set(title, []);
    map.get(title).push(raw);
  });
  return Array.from(map.entries()).map(([title, lines]) => ({ title, lines }));
}

/**
 * Instrucciones generales y detalle de ropa (orden), fuera del bloque por prenda.
 * @returns {Array<{ label: string, text: string }>}
 */
export function buildGeneralObservationLines(orden) {
  const out = [];
  if (orden?.instrucciones && String(orden.instrucciones).trim()) {
    out.push({ label: "Instrucciones", text: String(orden.instrucciones).trim() });
  }
  const detallesRopa = orden?.detalles_ropa != null ? String(orden.detalles_ropa).trim() : "";
  if (detallesRopa && !/^ver detalle en ítems$/iu.test(detallesRopa)) {
    out.push({ label: "Ropa / detalle", text: detallesRopa });
  }
  return out;
}

export function ticketObservationsHasAny(orden) {
  return buildGarmentObservationSections(orden).length > 0 || buildGeneralObservationLines(orden).length > 0;
}

/**
 * @deprecated Prefer buildGarmentObservationSections + buildGeneralObservationLines (C4).
 */
export function buildObservationsLines(orden) {
  const flat = [];
  buildGarmentObservationSections(orden).forEach(({ title, lines }) => {
    lines.forEach((line) => flat.push(`${title}: ${line}`));
  });
  buildGeneralObservationLines(orden).forEach(({ label, text }) => {
    flat.push(`${label}: ${text}`);
  });
  return flat;
}

const BILLING_BY_WEIGHT = "by_weight";

function partitionTicketDetalles(detalles) {
  const list = Array.isArray(detalles) ? detalles : [];
  const chargeable = list.filter((d) => !d.is_informational);
  const cargas = chargeable.filter((d) => (d.billing_mode || "") === BILLING_BY_WEIGHT);
  const servicios = chargeable.filter((d) => (d.billing_mode || "") !== BILLING_BY_WEIGHT);
  const contenido = list.filter((d) => !!d.is_informational);
  return { cargas, servicios, contenido };
}

function ticketLineConcepto(d) {
  if (d.is_informational) {
    const product = d.laundry_sale_product?.name || "";
    const garment = d.garment_type?.name || "";
    const qty = Number(d.cantidad || 0).toFixed(0);
    const base = [product, garment].filter(Boolean).join(" → ");
    return base ? `${base} x${qty}` : "Contenido";
  }
  if (d.producto?.nombre) return String(d.producto.nombre);
  if (d.laundry_sale_product?.name) {
    const name = String(d.laundry_sale_product.name);
    const extra = Number(d.extra_cost ?? 0);
    if (extra > 0) return `${name} (+ $${extra.toFixed(2)} extra)`;
    return name;
  }
  const parts = [d.garment_type?.name, d.etapa?.name, d.load_type?.name].filter(Boolean);
  return parts.length ? parts.join(" · ") : "Ítem";
}

function detalleRopaSublines(orden) {
  const raw = String(orden?.detalles_ropa || "").trim();
  if (!raw) return [];
  return raw
    .split(/\r\n|\n|\r/)
    .map((l) => l.trim())
    .filter(Boolean);
}

function sumSubtotal(rows) {
  return rows.reduce((a, d) => a + Number(d.subtotal ?? 0), 0);
}

function formatTicketDate(iso) {
  if (!iso) return "";
  try {
    const d = new Date(iso);
    if (Number.isNaN(d.getTime())) return "";
    const pad = (n) => String(n).padStart(2, "0");
    return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`;
  } catch {
    return "";
  }
}

function splitToLines(doc, text, maxWidth) {
  const t = String(text ?? "");
  if (typeof doc.splitTextToSize !== "function") {
    return t ? [t] : [""];
  }
  const out = doc.splitTextToSize(t, maxWidth);
  return Array.isArray(out) ? out : [String(out)];
}

function linesAreEffectivelyEmpty(lines) {
  return !lines.some((ln) => String(ln ?? "").length);
}

/**
 * Texto izquierda con ajuste de ancho; devuelve la nueva coordenada Y debajo del bloque.
 * Usa una sola llamada a text() + getTextDimensions para que el avance vertical coincida con jsPDF
 * (evita solapamiento al mezclar baseline: "top" con saltos manuales).
 * @param {import('jspdf').jsPDF} doc
 */
function drawWrappedLeft(doc, text, x, y, maxWidth, extraGap = 4) {
  const lines = splitToLines(doc, text, maxWidth);
  if (linesAreEffectivelyEmpty(lines)) return y + extraGap;
  doc.text(lines, x, y);
  const dim = doc.getTextDimensions(lines);
  return y + dim.h + extraGap;
}

/** Texto centrado, varias líneas; devuelve Y final. */
function drawWrappedCenter(doc, text, centerX, y, maxWidth, extraGap = 6) {
  const lines = splitToLines(doc, text, maxWidth);
  if (linesAreEffectivelyEmpty(lines)) return y + extraGap;
  doc.text(lines, centerX, y, { align: "center" });
  const dim = doc.getTextDimensions(lines);
  return y + dim.h + extraGap;
}

/** Una línea alineada a la derecha. */
function drawRightLine(doc, text, rightX, y) {
  const line = String(text);
  doc.text(line, rightX, y, { align: "right" });
  const dim = doc.getTextDimensions(line);
  return y + dim.h + 4;
}

function drawTicketPage(doc, orden, barcodeDataUrl, yStart) {
  let y = yStart;
  const margin = 14;
  const maxW = PAGE_W_PT - margin * 2;

  doc.setLineHeightFactor(1.45);

  doc.setFont("helvetica", "bold");
  doc.setFontSize(9);
  const exp = formatTicketDate(orden.created_at);
  y = drawWrappedLeft(doc, `TICKET: ${orden.folio_unico}  |  EXP: ${exp}`, margin, y, maxW, 8);

  doc.setFont("helvetica", "normal");
  doc.setFontSize(8);
  const cliente = String(orden.cliente_nombre || "").slice(0, 120);
  y = drawWrappedLeft(doc, `Cliente: ${cliente}`, margin, y, maxW);

  const pl = orden.sla_tipo_servicio || orden.slaTipoServicio;
  if (pl?.name) {
    y = drawWrappedLeft(doc, `Prioridad: ${String(pl.name)}`, margin, y, maxW);
  } else if (orden.is_priority) {
    y = drawWrappedLeft(doc, "Prioridad: Urgente (marcada)", margin, y, maxW);
  }
  const pagos = Array.isArray(orden?.pagos) && orden.pagos.length ? orden.pagos : null;
  if (pagos) {
    pagos.forEach((p) => {
      const line = `Pago: ${String(p.metodo_pago)}  $${Number(p.monto ?? 0).toFixed(2)}`;
      y = drawWrappedLeft(doc, line, margin, y, maxW);
    });
  } else if (orden.metodo_pago) {
    y = drawWrappedLeft(doc, `Pago: ${String(orden.metodo_pago)}`, margin, y, maxW);
  }
  y += 2;

  const detalles = orden?.detalles || [];
  const { cargas, servicios, contenido } = partitionTicketDetalles(detalles);
  const ropaLines = detalleRopaSublines(orden);

  const drawSection = (title, rows, showRopaUnderFirst) => {
    if (!rows.length) return;
    doc.setFont("helvetica", "bold");
    doc.setFontSize(8);
    y = drawWrappedCenter(doc, title, PAGE_W_PT / 2, y, maxW - 10, 6);
    doc.setFont("helvetica", "normal");
    doc.setFontSize(6.5);
    y = drawWrappedLeft(doc, "CONCEPTO / P.UNIT / CANT / DESC / IMP", margin, y, maxW, 2);
    rows.forEach((d, idx) => {
      const concept = ticketLineConcepto(d);
      const nums = `$${Number(d.precio_unitario || 0).toFixed(2)}  ${Number(d.cantidad || 0).toFixed(2)}  $0  $${Number(d.subtotal ?? 0).toFixed(2)}`;
      y = drawWrappedLeft(doc, `${concept}  ${nums}`, margin, y, maxW, 2);
      if (showRopaUnderFirst && idx === 0 && ropaLines.length) {
        ropaLines.forEach((sl) => {
          y = drawWrappedLeft(doc, `→ ${sl}`, margin + 4, y, maxW - 4, 1);
        });
      }
    });
    doc.setFont("helvetica", "bold");
    doc.setFontSize(7.5);
    y = drawRightLine(doc, `$${sumSubtotal(rows).toFixed(2)}`, PAGE_W_PT - margin, y);
  };

  drawSection("CARGAS MÍNIMAS", cargas, true);
  drawSection("SERVICIOS GENERALES", servicios, false);

  if (contenido.length) {
    doc.setFont("helvetica", "bold");
    doc.setFontSize(8);
    y = drawWrappedCenter(doc, "CONTENIDO DEL SERVICIO", PAGE_W_PT / 2, y, maxW - 10, 6);
    doc.setFont("helvetica", "normal");
    doc.setFontSize(6.5);
    contenido.forEach((d) => {
      const concept = ticketLineConcepto(d);
      y = drawWrappedLeft(doc, `${concept}  (${Number(d.cantidad || 0).toFixed(0)} pzs)`, margin, y, maxW, 2);
      const obs = d.observations != null ? String(d.observations).trim() : "";
      if (obs) {
        y = drawWrappedLeft(doc, `→ ${obs}`, margin + 4, y, maxW - 4, 1);
      }
    });
    y += 2;
  }

  if (!detalles.length) {
    doc.setFont("helvetica", "normal");
    doc.setFontSize(7);
    y = drawWrappedLeft(doc, `${orden.tipo_servicio || "Servicio"} — $${Number(orden.total || 0).toFixed(2)}`, margin, y, maxW);
  }

  const imp = Number(orden.impuestos ?? 0);
  const total = Number(orden.total || 0);
  const saldo = total;
  doc.setFont("helvetica", "normal");
  doc.setFontSize(7);
  y = drawWrappedLeft(doc, `Registros: ${detalles.length}    Impuestos: ${imp.toFixed(2)}`, margin, y, maxW);
  doc.setFont("helvetica", "bold");
  doc.setFontSize(9);
  y = drawRightLine(doc, `TOTAL: $${total.toFixed(2)}`, PAGE_W_PT - margin, y);
  doc.setFont("helvetica", "normal");
  doc.setFontSize(7);
  y = drawWrappedLeft(doc, `ANTICIPO: $0.00    SALDO: $${saldo.toFixed(2)}`, margin, y, maxW);
  y = drawWrappedLeft(doc, "NO DEJO ANTICIPO", margin, y, maxW);
  y = drawWrappedLeft(doc, `CLIENTE: ${cliente}`, margin, y, maxW);
  const entregaStr = formatFechaEntregaDisplay(promisedFechaEntregaIso(orden));
  y = drawWrappedLeft(doc, `SE ENTREGA: ${entregaStr}`, margin, y, maxW);
  y = drawWrappedLeft(doc, `PENDIENTE DE PAGAR: $${saldo.toFixed(2)}`, margin, y, maxW, 6);
  doc.setFont("helvetica", "bold");
  doc.setFontSize(8);
  y = drawWrappedCenter(doc, "¡GRACIAS POR SU PREFERENCIA!", PAGE_W_PT / 2, y, maxW - 8, 6);
  doc.setFont("helvetica", "normal");
  doc.setFontSize(7);
  const rx = orden.recepcionista?.name || "N/D";
  y = drawWrappedCenter(doc, `LE ATENDIÓ: ${String(rx).toUpperCase()}`, PAGE_W_PT / 2, y, maxW - 8, 10);

  if (ticketObservationsHasAny(orden)) {
    const garmentSections = buildGarmentObservationSections(orden);
    const generalLines = buildGeneralObservationLines(orden);
    doc.setFontSize(8);
    if (garmentSections.length) {
      doc.setFont("helvetica", "bold");
      y = drawWrappedLeft(doc, TICKET_TITLE_OBSERVACIONES_POR_PRENDA, margin, y, maxW, 4);
      garmentSections.forEach(({ title, lines }) => {
        doc.setFont("helvetica", "bold");
        y = drawWrappedLeft(doc, `${title}:`, margin, y, maxW, 2);
        doc.setFont("helvetica", "normal");
        lines.forEach((line) => {
          y = drawWrappedLeft(doc, `• ${line}`, margin + 4, y, maxW - 4, 2);
        });
        y += 2;
      });
    }
    doc.setFont("helvetica", "normal");
    generalLines.forEach(({ label, text }) => {
      y = drawWrappedLeft(doc, `${label}: ${text}`, margin, y, maxW, 4);
    });
    y += 6;
  }

  doc.setFont("helvetica", "bold");
  doc.setFontSize(7);
  y = drawWrappedCenter(doc, "POLITICAS DEL SERVICIO", PAGE_W_PT / 2, y, maxW - 8, 8);
  doc.setFont("helvetica", "normal");
  doc.setFontSize(6);
  const politicas = [
    "1) LA CARGA MÍNIMA ES DE 3 KG. PARA SERVICIOS NORMAL, EXPRESS y SÚPER EXPRESS.",
    "2) EL TICKET ES INDISPENSABLE PARA RECOGER SUS PRENDAS.",
    "3) LOS CAMBIOS O RECLAMACIONES POR PIEZAS FALTANTES O DAÑOS DEBERÁN HACERSE A MÁS TARDAR 3 DÍAS DESPUÉS DE LA ENTREGA.",
    "4) TIEMPO MÁXIMO EN BODEGA ES DE 8 DÍAS A PARTIR DE LA FECHA DE ENTREGA DE LA PRENDA, DESPUÉS DE ESE TIEMPO LA LAVANDERÍA QUEDA LIBERADA DE TODA RESPONSABILIDAD.",
  ];
  politicas.forEach((t) => {
    y = drawWrappedLeft(doc, t, margin, y, maxW, 8);
  });
  y += 4;
  doc.setFont("helvetica", "bold");
  doc.setFontSize(6.5);
  y = drawWrappedCenter(doc, "ESTE TICKET NO ES UN COMPROBANTE FISCAL", PAGE_W_PT / 2, y, maxW - 8, 6);
  y = drawWrappedCenter(doc, "COPIA PARA EL CLIENTE", PAGE_W_PT / 2, y, maxW - 8, 12);

  const barcodeW = Math.min(190, PAGE_W_PT - margin * 2);
  const barcodeH = 48;
  const xBarcode = (PAGE_W_PT - barcodeW) / 2;
  doc.addImage(barcodeDataUrl, "PNG", xBarcode, y, barcodeW, barcodeH);
  y += barcodeH + 8;
  doc.setFont("helvetica", "normal");
  doc.setFontSize(7);
  y = drawWrappedCenter(doc, "Escanee en Lavandería → Escanear", PAGE_W_PT / 2, y, maxW - 8, 4);
  return y;
}

function ticketBarcodeDataUrlForOrden(orden) {
  const barcodePayload = ticketBarcodePayload(orden);
  if (!barcodePayload) {
    throw new Error("Orden sin folio.");
  }
  const barcodeDataUrl = barcodeImageDataUrl(barcodePayload, { width: 2, height: 64 });
  if (!barcodeDataUrl) {
    throw new Error("No se pudo generar el código de barras.");
  }
  return barcodeDataUrl;
}

/**
 * PDF ticket 80 mm (una o dos copias).
 * @param {object} orden
 * @param {{ copies?: number }} [opts]
 * @returns {Promise<Blob>}
 */
export async function buildTicketPdfBlob(orden, { copies = 1 } = {}) {
  if (!orden?.folio_unico) {
    throw new Error("Orden sin folio.");
  }
  const barcodeDataUrl = ticketBarcodeDataUrlForOrden(orden);
  const probeDoc = new jsPDF({ unit: "pt", format: [PAGE_W_PT, 4000], orientation: "portrait" });
  const yEnd = drawTicketPage(probeDoc, orden, barcodeDataUrl, 36);
  const pageHeight = Math.ceil(yEnd + 24);

  const doc = new jsPDF({ unit: "pt", format: [PAGE_W_PT, pageHeight], orientation: "portrait" });
  drawTicketPage(doc, orden, barcodeDataUrl, 36);

  const totalCopies = Math.max(1, Number(copies) || 1);
  for (let i = 1; i < totalCopies; i += 1) {
    doc.addPage([PAGE_W_PT, pageHeight], "portrait");
    drawTicketPage(doc, orden, barcodeDataUrl, 36);
  }

  return doc.output("blob");
}

/**
 * Imagen PNG del ticket (una copia) para compartir por WhatsApp u otros canales.
 * @param {object} orden
 * @returns {Promise<Blob>}
 */
export async function buildTicketPngBlob(orden) {
  const pdfBlob = await buildTicketPdfBlob(orden, { copies: 1 });
  const pdfjs = await import("pdfjs-dist/build/pdf.mjs");
  pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).href;

  const buffer = await pdfBlob.arrayBuffer();
  const pdf = await pdfjs.getDocument({ data: buffer }).promise;
  const page = await pdf.getPage(1);
  const viewport = page.getViewport({ scale: 2.5 });
  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");
  if (!context) {
    throw new Error("No se pudo generar la imagen del ticket.");
  }

  canvas.width = Math.ceil(viewport.width);
  canvas.height = Math.ceil(viewport.height);
  context.fillStyle = "#ffffff";
  context.fillRect(0, 0, canvas.width, canvas.height);
  await page.render({ canvasContext: context, viewport }).promise;

  return await new Promise((resolve, reject) => {
    canvas.toBlob(
      (blob) => (blob ? resolve(blob) : reject(new Error("No se pudo generar la imagen del ticket."))),
      "image/png",
      0.95,
    );
  });
}

/**
 * Genera un PDF en formato ticket 80 mm con dos páginas idénticas (dos copias del mismo QR).
 * @param {object} orden Orden devuelta por la API (con detalles cargados).
 */
export async function openTicketPdfTwoQrCopies(orden) {
  const blob = await buildTicketPdfBlob(orden, { copies: 2 });
  const blobUrl = URL.createObjectURL(blob);
  return window.open(blobUrl, "_blank");
}
