/** @odoo-module */

import { patch } from "@web/core/utils/patch";
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
import { Order } from "@point_of_sale/app/store/models";

const ESC = 0x1b;
const GS = 0x1d;
const CR = 0x0d;
const LF = 0x0a;
const DEFAULT_COLUMNS = 42;
const END_FEED_LINES = 8;
let qzPrintQueue = Promise.resolve();

function columnsFromConfig(config) {
    const value = Number.parseInt(config?.qz_paper_width || DEFAULT_COLUMNS, 10);
    return Number.isFinite(value) ? value : DEFAULT_COLUMNS;
}

function getBillingPrinterName(config) {
    return config?.qz_billing_printer_name || config?.qz_printer_name || "";
}

function paperWidthMm(config) {
    return String(config?.qz_paper_width || DEFAULT_COLUMNS) === "48" ? 80 : 58;
}

function enqueueQzPrint(task) {
    const next = qzPrintQueue.catch(() => undefined).then(task);
    qzPrintQueue = next.catch(() => undefined);
    return next;
}

async function findConfiguredPrinter(printerName) {
    const name = cleanText(printerName);
    if (!name) {
        throw new Error("No QZ printer name is configured.");
    }
    return qz.printers.find(name);
}

function cleanText(value) {
    return String(value ?? "")
        .normalize("NFKD")
        .replace(/[\u0300-\u036f]/g, "")
        .replace(/[“”]/g, '"')
        .replace(/[‘’]/g, "'")
        .replace(/[–—]/g, "-")
        .replace(/\u00a0/g, " ")
        .replace(/[^\x09\x0a\x0d\x20-\x7e]/g, "")
        .replace(/[ \t]+/g, " ")
        .trim();
}

function getOrderTable(order) {
    return order?.getTable?.() || order?.table;
}

function wrapText(text, width) {
    const clean = cleanText(text);
    if (clean.length <= width) {
        return [clean];
    }
    const wrapped = [];
    let rest = clean;
    while (rest.length > width) {
        let splitAt = rest.lastIndexOf(" ", width);
        if (splitAt <= 0) {
            splitAt = width;
        }
        wrapped.push(rest.slice(0, splitAt).trimEnd());
        rest = rest.slice(splitAt).trimStart();
    }
    if (rest) {
        wrapped.push(rest);
    }
    return wrapped;
}

function bytesToBase64(bytes) {
    let binary = "";
    const chunkSize = 0x8000;
    for (let i = 0; i < bytes.length; i += chunkSize) {
        binary += String.fromCharCode(...bytes.slice(i, i + chunkSize));
    }
    return btoa(binary);
}

function absoluteUrl(value) {
    if (!value || value.startsWith("data:") || value.startsWith("blob:")) {
        return value;
    }
    return new URL(value, window.location.origin).href;
}

async function imageToDataUrl(src) {
    const url = absoluteUrl(src);
    if (!url || url.startsWith("data:") || url.startsWith("blob:")) {
        return url;
    }
    const response = await fetch(url, { credentials: "include" });
    if (!response.ok) {
        throw new Error(`Unable to load receipt image: ${url}`);
    }
    const blob = await response.blob();
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
        reader.readAsDataURL(blob);
    });
}

function collectPrintableStyles() {
    return Array.from(document.querySelectorAll("link[rel='stylesheet'], style"))
        .map((node) => {
            if (node.tagName === "LINK") {
                return `<link rel="stylesheet" href="${absoluteUrl(node.getAttribute("href"))}">`;
            }
            return node.outerHTML;
        })
        .join("\n");
}

async function buildHtmlReceipt(receiptElement, pos) {
    const config = pos?.config || {};
    const widthMm = paperWidthMm(config);
    const clone = receiptElement.cloneNode(true);
    await Promise.all(Array.from(clone.querySelectorAll("img[src]")).map(async (node) => {
        node.setAttribute("src", await imageToDataUrl(node.getAttribute("src")));
    }));
    for (const node of clone.querySelectorAll("[href]")) {
        node.setAttribute("href", absoluteUrl(node.getAttribute("href")));
    }

    return `<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <base href="${window.location.origin}/">
    ${collectPrintableStyles()}
    <style>
        @page { size: ${widthMm}mm auto; margin: 0; }
        html, body {
            width: ${widthMm}mm;
            margin: 0;
            padding: 0;
            background: #fff;
        }
        body {
            -webkit-print-color-adjust: exact;
            print-color-adjust: exact;
        }
        .pos-receipt {
            width: ${widthMm}mm !important;
            max-width: ${widthMm}mm !important;
            box-sizing: border-box;
            margin: 0 auto !important;
        }
    </style>
</head>
<body>${clone.outerHTML}</body>
</html>`;
}

function findReceiptElement(screen) {
    return screen.el?.querySelector?.(".pos-receipt")
        || document.querySelector(".receipt-screen .pos-receipt")
        || document.querySelector(".pos-receipt");
}

class EscPosBuilder {
    constructor(columns) {
        this.columns = columns;
        this.bytes = [];
    }

    raw(...values) {
        this.bytes.push(...values);
    }

    text(value) {
        for (const char of cleanText(value)) {
            this.bytes.push(char.charCodeAt(0) & 0xff);
        }
    }

    line(value = "") {
        this.text(value);
        this.bytes.push(CR, LF);
    }

    lines(values) {
        for (const value of values) {
            this.line(value);
        }
    }

    init() {
        this.raw(ESC, 0x40);
    }

    align(value) {
        const modes = { left: 0, center: 1, right: 2 };
        this.raw(ESC, 0x61, modes[value] ?? 0);
    }

    bold(enabled) {
        this.raw(ESC, 0x45, enabled ? 1 : 0);
    }

    size(value) {
        const sizes = { normal: 0x00, doubleHeight: 0x01, doubleWidth: 0x10, double: 0x11 };
        this.raw(GS, 0x21, sizes[value] ?? 0x00);
    }

    feed(lines = 1) {
        this.raw(ESC, 0x64, Math.max(0, Math.min(8, lines)));
    }

    cut(mode = "partial") {
        this.raw(GS, 0x56, mode === "full" ? 0x41 : 0x42, 0x00);
    }

    separator(char = "-") {
        this.line(char.repeat(this.columns));
    }

    qrCode(value) {
        const data = cleanText(value);
        if (!data) {
            return;
        }
        const bytes = Array.from(data).map((char) => char.charCodeAt(0) & 0xff);
        const storeLength = bytes.length + 3;
        this.align("center");
        this.raw(GS, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00);
        this.raw(GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, 0x06);
        this.raw(GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, 0x31);
        this.raw(
            GS, 0x28, 0x6b,
            storeLength & 0xff,
            (storeLength >> 8) & 0xff,
            0x31, 0x50, 0x30,
            ...bytes
        );
        this.raw(GS, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30);
    }
}

function addWrappedLine(builder, left, right = "", indent = 0) {
    const width = builder.columns;
    const rightText = cleanText(right);
    const leftWidth = rightText ? width - rightText.length - 1 : width;
    const wrapped = wrapText(left, Math.max(8, leftWidth - indent));
    if (rightText) {
        builder.line(leftRight(`${" ".repeat(indent)}${wrapped[0]}`, rightText, width));
        for (const extra of wrapped.slice(1)) {
            builder.line(`${" ".repeat(indent)}${extra}`);
        }
    } else {
        for (const line of wrapped) {
            builder.line(`${" ".repeat(indent)}${line}`);
        }
    }
}

patch(ReceiptScreen.prototype, {
    async printReceipt() {
        const printerName = getBillingPrinterName(this.pos.config);
        if (this.pos.config.use_qz_printer && printerName) {
            try {
                if (!window.qz) {
                    console.error("QZ Tray library not loaded.");
                    return false;
                }

                if (!qz.websocket.isActive()) {
                    await qz.websocket.connect({ retries: 2, delay: 1 });
                }

                const receiptElement = findReceiptElement(this);
                if (!receiptElement) {
                    throw new Error("The Odoo receipt preview was not found. Billing print was stopped to avoid printing fallback data.");
                }
                const widthMm = paperWidthMm(this.pos.config);
                const printer = await findConfiguredPrinter(printerName);
                const config = qz.configs.create(printer, {
                    copies: 1,
                    margins: 0,
                    units: "mm",
                    size: { width: widthMm },
                    rasterize: true,
                    scaleContent: true,
                    spool: { size: 1 },
                });
                const html = await buildHtmlReceipt(receiptElement, this.pos);
                await enqueueQzPrint(() => qz.print(config, [{
                    type: "pixel",
                    format: "html",
                    flavor: "plain",
                    data: html,
                }]));

                if (this.currentOrder) {
                    this.currentOrder._printed = true;
                }
                return true;
            } catch (err) {
                console.error("QZ Tray Print Error:", err);
                this.env.services.popup.add(ErrorPopup, {
                    title: "QZ Tray Printer Error",
                    body: err?.message || "Failed to print the rendered Odoo receipt through QZ Tray.",
                });
                return false;
            }
        }
        return super.printReceipt(...arguments);
    },
});

async function buildEscPosKitchenTicket(order, pos, orderChange, cancelled = false) {
    const config = pos?.config || {};
    const columns = columnsFromConfig(config);
    const builder = new EscPosBuilder(columns);
    const table = order.getTable?.() || order.table;
    const newLines = orderChange?.new || [];
    const cancelledLines = orderChange?.cancelled || [];

    builder.init();
    builder.align("center");
    builder.bold(true);
    builder.size("doubleHeight");
    builder.line("KITCHEN ORDER");
    builder.size("normal");
    builder.line("");

    builder.align("left");
    if (order.name) builder.line(`Order: ${order.name}`);
    if (table?.name) builder.line(`Table: ${table.name}`);
    if (order.customer_count) builder.line(`Guests: ${order.customer_count}`);
    builder.line(`Time: ${new Date().toLocaleTimeString()}`);
    if (cancelled) {
        builder.bold(true);
        builder.line("ORDER CANCELLED");
        builder.bold(false);
    }
    builder.separator("=");

    function printSection(title, lines) {
        if (!lines.length) {
            return;
        }
        builder.bold(true);
        builder.line(title);
        builder.bold(false);
        for (const line of lines) {
            const qty = Number(line.quantity || 0);
            const qtyText = Number.isInteger(qty) ? String(qty) : qty.toFixed(2).replace(/\.?0+$/, "");
            addWrappedLine(builder, `${qtyText} x ${line.name}`);
            if (line.note) {
                addWrappedLine(builder, `Note: ${line.note}`, "", 2);
            }
        }
        builder.line("");
    }

    printSection("NEW ITEMS", newLines);
    printSection("CANCELLED ITEMS", cancelledLines);

    builder.feed(END_FEED_LINES);
    if (config.qz_enable_cutter !== false) {
        builder.cut(config.qz_cut_mode || "partial");
    }
    builder.feed(1);
    return bytesToBase64(builder.bytes);
}

patch(Order.prototype, {
    async printChanges(cancelled) {
        const printerName = this.pos.config.qz_kitchen_printer_name;
        if (this.pos.config.use_qz_printer && printerName) {
            const orderChange = this.changesToOrder(cancelled);
            if (!orderChange.new.length && !orderChange.cancelled.length) {
                return true;
            }
            try {
                if (!window.qz) {
                    console.error("QZ Tray library not loaded.");
                    return false;
                }
                if (!qz.websocket.isActive()) {
                    await qz.websocket.connect({ retries: 2, delay: 1 });
                }
                const printer = await findConfiguredPrinter(printerName);
                const config = qz.configs.create(printer, {
                    encoding: "CP437",
                    copies: 1,
                    spool: { size: 1 },
                });
                const payload = await buildEscPosKitchenTicket(this, this.pos, orderChange, cancelled);
                await enqueueQzPrint(() => qz.print(config, [{
                    type: "raw",
                    format: "command",
                    flavor: "base64",
                    data: payload,
                }]));
                return true;
            } catch (err) {
                console.error("QZ Tray Kitchen Print Error:", err);
                return false;
            }
        }
        return super.printChanges(...arguments);
    },
});
