/** @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;

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 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 formatMoney(value, currency) {
    const amount = Number(value || 0).toFixed(2);
    const symbol = cleanText(currency?.symbol || "$");
    return currency?.position === "after" ? `${amount} ${symbol}` : `${symbol}${amount}`;
}

function center(text, width) {
    const value = cleanText(text);
    const padding = Math.max(0, Math.floor((width - value.length) / 2));
    return `${" ".repeat(padding)}${value}`;
}

function leftRight(left, right, width) {
    const lhs = cleanText(left);
    const rhs = cleanText(right);
    const space = Math.max(1, width - lhs.length - rhs.length);
    return `${lhs}${" ".repeat(space)}${rhs}`;
}

function safeUpper(value) {
    return cleanText(value).toUpperCase();
}

function formatPhone(value) {
    const clean = cleanText(value);
    const digits = clean.replace(/\D/g, "");
    const local = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits;
    if (local.length === 10) {
        return `${local.slice(0, 3)}-${local.slice(3, 6)}-${local.slice(6)}`;
    }
    return clean;
}

function getCompanyAddress(company) {
    const parts = [
        company.street,
        company.street2,
        [company.city, company.state_id?.[1] || company.state_id?.name, company.zip].filter(Boolean).join(" "),
    ].map(cleanText).filter(Boolean);
    return parts.join(", ");
}

function formatPlacedDate(value) {
    const date = value ? new Date(value) : new Date();
    if (Number.isNaN(date.getTime())) {
        return cleanText(value);
    }

    const weekday = date.toLocaleDateString("en-CA", { weekday: "short" });
    const month = date.toLocaleDateString("en-CA", { month: "short" });
    const day = date.getDate();
    const suffix = day % 10 === 1 && day !== 11 ? "st" : day % 10 === 2 && day !== 12 ? "nd" : day % 10 === 3 && day !== 13 ? "rd" : "th";
    const time = date.toLocaleTimeString("en-CA", {
        hour: "numeric",
        minute: "2-digit",
        hour12: true,
    }).replace(/\s/g, "");
    return `${weekday}, ${month} ${day}${suffix}, ${time}`;
}

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

function getOrderDisplayName(receiptData, order) {
    return cleanText(receiptData?.name || order?.name || "");
}

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);
}

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);
    }
}

async function getLogoRasterBytes(pos, columns) {
    if (pos?.config?.qz_print_logo === false || !pos?.company_logo_base64) {
        return [];
    }

    const image = await new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.src = pos.company_logo_base64;
    });

    const maxWidth = Math.min(columns * 8, 384);
    const maxHeight = 120;
    const ratio = Math.min(maxWidth / image.width, maxHeight / image.height, 1);
    const width = Math.max(8, Math.floor((image.width * ratio) / 8) * 8);
    const height = Math.max(1, Math.floor(image.height * ratio));
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d", { willReadFrequently: true });
    ctx.fillStyle = "#fff";
    ctx.fillRect(0, 0, width, height);
    ctx.drawImage(image, 0, 0, width, height);

    const pixels = ctx.getImageData(0, 0, width, height).data;
    const widthBytes = Math.ceil(width / 8);
    const data = [];
    for (let y = 0; y < height; y++) {
        for (let xb = 0; xb < widthBytes; xb++) {
            let byte = 0;
            for (let bit = 0; bit < 8; bit++) {
                const x = xb * 8 + bit;
                const index = (y * width + x) * 4;
                const alpha = pixels[index + 3];
                const luminance = pixels[index] * 0.299 + pixels[index + 1] * 0.587 + pixels[index + 2] * 0.114;
                if (alpha > 32 && luminance < 170) {
                    byte |= 0x80 >> bit;
                }
            }
            data.push(byte);
        }
    }

    return [
        ESC, 0x61, 0x01,
        GS, 0x76, 0x30, 0x00,
        widthBytes & 0xff, (widthBytes >> 8) & 0xff,
        height & 0xff, (height >> 8) & 0xff,
        ...data,
        CR, LF,
        ESC, 0x61, 0x00,
    ];
}

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}`);
        }
    }
}

function addOrderLines(builder, order, currency) {
    builder.bold(true);
    builder.line(leftRight("Item", "Price", builder.columns));
    builder.bold(false);

    for (const line of order?.get_orderlines?.() || []) {
        const product = line.get_product?.();
        const name = product?.display_name || product?.name || line.get_full_product_name?.() || "";
        const qty = Number(line.get_quantity?.() || 0);
        const qtyText = Number.isInteger(qty) ? String(qty) : qty.toFixed(2).replace(/\.?0+$/, "");
        const total = formatMoney(line.get_price_with_tax?.() || 0, currency);
        addWrappedLine(builder, `${qtyText} x ${name}`, total);

        const unitPrice = line.get_unit_display_price?.();
        if (qty !== 1 && unitPrice !== undefined) {
            builder.line(`  @ ${formatMoney(unitPrice, currency)} each`);
        }
        const note = line.get_customer_note?.();
        if (note) {
            addWrappedLine(builder, `Note: ${note}`, "", 2);
        }
    }
}

function addTaxSummary(builder, receiptData, currency) {
    const taxes = receiptData?.tax_details || [];
    if (!taxes.length) {
        return;
    }
    for (const tax of taxes) {
        const label = tax?.tax?.name || (tax?.tax?.amount ? `${tax.tax.amount}%` : "Tax");
        builder.line(leftRight(label, formatMoney(tax.amount || 0, currency), builder.columns));
    }
}

function addPaymentLines(builder, receiptData, currency) {
    const paymentLines = receiptData?.paymentlines || [];
    if (!paymentLines.length) {
        return;
    }
    builder.separator("-");
    for (const payment of paymentLines) {
        builder.line(leftRight(payment.name, formatMoney(payment.amount, currency), builder.columns));
    }
}

async function buildEscPosReceipt(order, pos) {
    const config = pos?.config || {};
    const columns = columnsFromConfig(config);
    const currency = pos?.currency;
    const company = pos?.company || {};
    const receiptData = order?.export_for_printing?.() || {};
    const builder = new EscPosBuilder(columns);
    const table = getOrderTable(order);
    const companyName = cleanText(company.name || "Chennora");
    const fulfilmentLabel = safeUpper(receiptData.fulfilment_type_label || receiptData.fulfilment_type || "DINE-IN");
    const orderName = getOrderDisplayName(receiptData, order);
    const qrValue = orderName || `${companyName} ${formatMoney(order?.get_total_with_tax?.(), currency)}`;

    builder.init();
    builder.raw(...(await getLogoRasterBytes(pos, columns)));
    builder.align("center");
    builder.bold(true);
    builder.line(companyName);
    builder.bold(false);
    builder.line("");

    const address = getCompanyAddress(company);
    if (address) {
        builder.lines(wrapText(`Address: ${address}`, columns).map((line) => center(line, columns)));
    }
    if (company.email) {
        builder.line(center(`Email: ${company.email}`, columns));
    }
    if (company.phone) {
        builder.line(center(`Phone: ${formatPhone(company.phone)}`, columns));
    }

    builder.line("");
    builder.bold(true);
    builder.line(order?._printed ? "*** Duplicate Bill (1) ***" : "*** Bill ***");
    if (orderName) {
        builder.line(`Invoice ${orderName}`);
    }
    builder.line(fulfilmentLabel);
    builder.bold(false);
    builder.line("");

    builder.align("left");
    builder.separator("-");
    builder.line(leftRight("Placed", formatPlacedDate(receiptData.date), columns));
    builder.separator("-");
    builder.line(leftRight("Cashier", companyName, columns));
    if (order?.customer_count) {
        builder.line(leftRight("Guests Served", order.customer_count, columns));
    }
    if (table?.name) {
        builder.line(leftRight("Table", table.name, columns));
    }

    builder.separator("=");
    addOrderLines(builder, order, currency);
    builder.separator("=");
    builder.line(leftRight("Price", formatMoney(order?.get_total_without_tax?.(), currency), columns));
    const tax = order?.get_total_tax?.() || 0;
    if (tax) {
        const firstTax = receiptData?.tax_details?.[0]?.tax;
        const taxLabel = firstTax?.name || (firstTax?.amount ? `HST (${firstTax.amount}%)` : "Tax");
        builder.line(leftRight(taxLabel, formatMoney(tax, currency), columns));
    }
    if (receiptData.total_discount) {
        builder.line(leftRight("Discounts", formatMoney(receiptData.total_discount, currency), columns));
    }
    builder.bold(true);
    builder.line(leftRight("Grand Total", formatMoney(order?.get_total_with_tax?.(), currency), columns));
    builder.bold(false);

    addPaymentLines(builder, receiptData, currency);
    const change = receiptData.change || order?.get_change?.() || 0;
    if (change > 0) {
        builder.bold(true);
        builder.line(leftRight("Change", formatMoney(change, currency), columns));
        builder.bold(false);
    }
    addTaxSummary(builder, receiptData, currency);

    if (config.receipt_footer) {
        builder.separator("-");
        builder.align("center");
        builder.lines(wrapText(config.receipt_footer, columns).map((line) => center(line, columns)));
        builder.align("left");
    }

    builder.separator("-");
    builder.align("center");
    builder.bold(true);
    builder.line("* PAID *");
    builder.bold(false);
    builder.line("");
    builder.qrCode(qrValue);
    builder.line("");
    builder.line("Powered by dind360");
    builder.align("left");
    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(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 config = qz.configs.create(printerName, {
                    encoding: "CP437",
                    copies: 1,
                    spool: { size: 1 },
                });
                const payload = await buildEscPosReceipt(this.currentOrder, this.pos);
                await qz.print(config, [{
                    type: "raw",
                    format: "command",
                    flavor: "base64",
                    data: payload,
                }]);

                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: "Failed to print through QZ Tray. Check that QZ Tray is running, the printer name is correct, and the printer supports ESC/POS raw commands.",
                });
                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 config = qz.configs.create(printerName, {
                    encoding: "CP437",
                    copies: 1,
                    spool: { size: 1 },
                });
                const payload = await buildEscPosKitchenTicket(this, this.pos, orderChange, cancelled);
                await 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);
    },
});
