package com.atom.kiosk.plugins.printer;

import java.nio.charset.StandardCharsets;

import android.graphics.Paint;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Picture;
import android.graphics.Typeface;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.util.Base64;
import android.util.Log;

import com.dantsu.escposprinter.exceptions.EscPosConnectionException;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import com.dantsu.escposprinter.EscPosPrinter;
import com.dantsu.escposprinter.connection.usb.UsbConnection;
import com.dantsu.escposprinter.connection.usb.UsbPrintersConnections;
import com.dantsu.escposprinter.textparser.PrinterTextParserImg;

// OmniDriver SDK
import android.os.Bundle;
import com.sdksuite.omnidriver.OmniDriver;
import com.sdksuite.omnidriver.OmniConnection;
import com.sdksuite.omnidriver.aidl.printer.Align;
import com.sdksuite.omnidriver.api.OnPrintListener;
import com.sdksuite.omnidriver.api.Printer;

import com.caverock.androidsvg.SVG;

@CapacitorPlugin(name = "UsbPrinter", permissions = {
        @Permission(strings = {}, alias = "usb")
})
public class UsbPrinterPlugin extends Plugin {
    // ---- USB fields ----
    private UsbManager usbManager;
    private UsbDeviceConnection connection;
    private UsbInterface usbInterface;
    private UsbEndpoint endpoint;

    // ---- OmniDriver fields ----
    private OmniDriver mOmniDriver;
    private boolean omniDriverReady = false;

    // ---- Static optimization: Vietnamese character map ----
    private static final Map<Character, Character> VIETNAMESE_MAP = initVietnameseMap();

    // ---- Regex patterns cache ----
    private static final Pattern QR_PATTERN = Pattern.compile(".*<qrcode.*?>.*?</qrcode>.*");
    private static final Pattern BARCODE_PATTERN = Pattern.compile(".*<barcode.*>.*</barcode>.*");
    private static final Pattern IMG_BASE64_PATTERN = Pattern.compile(".*<img_base64>.*</img_base64>.*");
    private static final Pattern QR_SIZE_PATTERN = Pattern.compile("<qrcode[^>]*?size=['\"]([^'\"]+)['\"][^>]*>");
    private static final Pattern TAG_PATTERN = Pattern.compile("<[^>]+>");

    private static Map<Character, Character> initVietnameseMap() {
        Map<Character, Character> map = new HashMap<>();
        char[][] mapping = {
                {'à','a'}, {'á','a'}, {'ạ','a'}, {'ả','a'}, {'ã','a'},
                {'â','a'}, {'ầ','a'}, {'ấ','a'}, {'ậ','a'}, {'ẩ','a'}, {'ẫ','a'},
                {'ă','a'}, {'ằ','a'}, {'ắ','a'}, {'ặ','a'}, {'ẳ','a'}, {'ẵ','a'},
                {'è','e'}, {'é','e'}, {'ẹ','e'}, {'ẻ','e'}, {'ẽ','e'},
                {'ê','e'}, {'ề','e'}, {'ế','e'}, {'ệ','e'}, {'ể','e'}, {'ễ','e'},
                {'ì','i'}, {'í','i'}, {'ị','i'}, {'ỉ','i'}, {'ĩ','i'},
                {'ò','o'}, {'ó','o'}, {'ọ','o'}, {'ỏ','o'}, {'õ','o'},
                {'ô','o'}, {'ồ','o'}, {'ố','o'}, {'ộ','o'}, {'ổ','o'}, {'ȍ','o'},
                {'ơ','o'}, {'ờ','o'}, {'ớ','o'}, {'ợ','o'}, {'ở','o'}, {'ỡ','o'},
                {'ù','u'}, {'ú','u'}, {'ụ','u'}, {'ủ','u'}, {'ũ','u'},
                {'ư','u'}, {'ừ','u'}, {'ứ','u'}, {'ự','u'}, {'ử','u'}, {'ữ','u'},
                {'ỳ','y'}, {'ý','y'}, {'ỵ','y'}, {'ỷ','y'}, {'ỹ','y'},
                {'đ','d'},
                {'À','A'}, {'Á','A'}, {'Ạ','A'}, {'Ả','A'}, {'Ã','A'},
                {'Â','A'}, {'Ầ','A'}, {'Ấ','A'}, {'Ậ','A'}, {'Ẩ','A'}, {'Ẫ','A'},
                {'Ă','A'}, {'Ằ','A'}, {'Ắ','A'}, {'Ặ','A'}, {'Ẳ','A'}, {'Ẵ','A'},
                {'È','E'}, {'É','E'}, {'Ẹ','E'}, {'Ẻ','E'}, {'Ẽ','E'},
                {'Ê','E'}, {'Ề','E'}, {'Ế','E'}, {'Ệ','E'}, {'Ể','E'}, {'Ễ','E'},
                {'Ì','I'}, {'Í','I'}, {'Ị','I'}, {'Ỉ','I'}, {'Ĩ','I'},
                {'Ò','O'}, {'Ó','O'}, {'Ọ','O'}, {'Ỏ','O'}, {'Õ','O'},
                {'Ô','O'}, {'Ồ','O'}, {'Ố','O'}, {'Ộ','O'}, {'Ổ','O'}, {'Ỗ','O'},
                {'Ơ','O'}, {'Ờ','O'}, {'Ớ','O'}, {'Ợ','O'}, {'Ở','O'}, {'Ỡ','O'},
                {'Ù','U'}, {'Ú','U'}, {'Ụ','U'}, {'Ủ','U'}, {'Ũ','U'},
                {'Ư','U'}, {'Ừ','U'}, {'Ứ','U'}, {'Ự','U'}, {'Ử','U'}, {'Ữ','U'},
                {'Ỳ','Y'}, {'Ý','Y'}, {'Ỵ','Y'}, {'Ỷ','Y'}, {'Ỹ','Y'},
                {'Đ','D'}
        };
        for (char[] m : mapping) {
            map.put(m[0], m[1]);
        }
        return map;
    }


    // ===========================================================
    // 🔌 OMNIDRIVER — INIT
    // ===========================================================
    @PluginMethod
    public void initOmniDriver(PluginCall call) {
        initOmniDriverInternal(new OmniInitCallback() {
            @Override
            public void onSuccess() {
                call.resolve();
            }
            @Override
            public void onError(String errorMsg) {
                call.reject(errorMsg);
            }
        });
    }

    private interface OmniInitCallback {
        void onSuccess();
        void onError(String errorMsg);
    }

    private void initOmniDriverInternal(final OmniInitCallback callback) {
        if (omniDriverReady && mOmniDriver != null) {
            callback.onSuccess();
            return;
        }
        Context context = getContext();
        mOmniDriver = OmniDriver.me(context);
        mOmniDriver.init(new OmniConnection() {
            @Override
            public void onConnected() {
                omniDriverReady = true;
                Log.i("UsbPrinter", "✅ OmniDriver connected");
                callback.onSuccess();
            }

            @Override
            public void onDisconnected(int error) {
                omniDriverReady = false;
                Log.e("UsbPrinter", "❌ OmniDriver disconnected, error=" + error);
                callback.onError("OmniDriver disconnected, error=" + error);
            }
        });
    }

    // ===========================================================
    // 🖨️ OMNIDRIVER — PRINT (internal)
    // ===========================================================
    private void printViaOmniDriver(String text, String logoBase64, PluginCall call) {
        if (!omniDriverReady || mOmniDriver == null) {
            call.reject("OmniDriver not initialized. Call initOmniDriver() first.");
            return;
        }

        String formatText = vietnameseToLatinManual(text);

        Printer printer = null;
        try {
            printer = mOmniDriver.getPrinter(new Bundle());
            printer.openDevice(1);

            String[] lines = formatText.split("\n");
            for (String line : lines) {
                String trimmed = line.trim();
                if (trimmed.isEmpty()) {
                    printer.feedLine(1);
                    continue;
                }

                int align = Align.LEFT;
                String content = trimmed;

                if (trimmed.startsWith("[C]")) {
                    align = Align.CENTER;
                    content = trimmed.substring(3).trim();
                } else if (trimmed.startsWith("[L]")) {
                    align = Align.LEFT;
                    content = trimmed.substring(3).trim();
                } else if (trimmed.startsWith("[R]")) {
                    align = Align.RIGHT;
                    content = trimmed.substring(3).trim();
                }

                // Nếu có qr code
                if (QR_PATTERN.matcher(content).matches()) {
                    String data = content.replaceAll(".*<qrcode[^>]*>(.*?)</qrcode>.*", "$1");
                    int qrSize = 200;
                    try {
                        java.util.regex.Matcher m = QR_SIZE_PATTERN.matcher(content);
                        if (m.find()) {
                            qrSize = Integer.parseInt(m.group(1))*10;
                        }
                        printer.addQrCode(qrSize, 2, data, align, 0);
                        Log.i("UsbPrinter", "✅ [OmniDriver] addQrCode: size=" + qrSize + " data=" + data);
                    } catch (Exception e) {
                        Log.e("UsbPrinter", "❌ [OmniDriver] addQrCode failed", e);
                    }
                    continue;
                }

                // Nếu có barcode
                if (BARCODE_PATTERN.matcher(content).matches()) {
                    String data = content.replaceAll(".*<barcode[^>]*>(.*?)</barcode>.*", "$1");
                    int bcType = 10; // CODE128
                    int bcWidth = 300;
                    int bcHeight = 80;
                    try {
                        String typeStr = content.replaceAll(".*<barcode[^>]*type=['\"]([^'\"]+)['\"].*", "$1");
                        if (typeStr.contains("ean")) bcType = 2; // Ví dụ type 2
                        String hStr = content.replaceAll(".*<barcode[^>]*height=['\"](\\d+)['\"].*", "$1");
                        if (!hStr.equals(content)) bcHeight = Integer.parseInt(hStr) * 8;
                    } catch (Exception ignored) {}
                    try {
                        // Tương tự, tránh truyền tham số mang giá trị "0" hoặc quá nhỏ
                        // Thử: addBarCode(align, type, data, width, height) 
                        printer.addBarCode(align, bcType, data, bcWidth, bcHeight);
                    } catch (Exception e) {
                        Log.e("UsbPrinter", "❌ [OmniDriver] addBarCode failed", e);
                    }
                    continue;
                }

                // Thử in ảnh nếu có thẻ <img_base64>
                if (IMG_BASE64_PATTERN.matcher(content).matches()) {
                    String b64 = content.replaceAll(".*<img_base64[^>]*>(.*?)</img_base64>.*", "$1");
                    try {
                        if (b64.startsWith("data")) {
                            b64 = b64.substring(b64.indexOf(",") + 1);
                        }
                        byte[] imgBytes = android.util.Base64.decode(b64, android.util.Base64.DEFAULT);
                        // int i: align, int i1: 0
                        printer.addImage(imgBytes, align, 0);
                    } catch (Exception e) {}
                    continue;
                }

                // Thử in ảnh truyền thống (bỏ qua hex string của EscPos vì OmniDriver không đọc được hex raster)
                if (content.matches(".*<img>.*</img>.*")) {
                    content = content.replaceAll("<img>.*?</img>", "");
                }

                // Strip inline HTML-like tags (<b>, </b>, <u>, etc.) cho định dạng thông thường
                content = TAG_PATTERN.matcher(content).replaceAll("");

                if (!content.isEmpty()) {
                    printer.addText(content, align, 0);
                }
            }

            printer.feedLine(3);
            printer.cutPaper(); // Cắt giấy cho máy Landi hỗ trợ dao cắt tự động

            final Printer finalPrinter = printer;
            printer.startPrint(new OnPrintListener() {
                @Override
                public void onSuccess() {
                    Log.i("UsbPrinter", "✅ OmniDriver print success");
                    try { finalPrinter.closeDevice(); } catch (Exception ignored) {}
                    call.resolve();
                }

                @Override
                public void onFail(int error) {
                    Log.e("UsbPrinter", "❌ OmniDriver print failed, error=" + error);
                    try { finalPrinter.closeDevice(); } catch (Exception ignored) {}
                    fallbackToUsbPrint(text, logoBase64, call, "OmniDriver print failed, error=" + error, null);
                }
            });

        } catch (Exception e) {
            Log.e("UsbPrinter", "❌ OmniDriverException: " + e.getMessage(), e);
            if (printer != null) {
                try { printer.closeDevice(); } catch (Exception ignored) {}
            }
            fallbackToUsbPrint(text, logoBase64, call, "OmniDriver service error: " + e.getMessage(), e);
        }
    }

    private void fallbackToUsbPrint(String text, String logoBase64, PluginCall call, String reason, Exception error) {
        Log.w("UsbPrinter", "⚠️ Falling back to USB ESC/POS. Reason: " + reason, error);

        if (connection == null || endpoint == null) {
            call.reject(reason + " and USB printer is not connected.");
            return;
        }

        try {
            printViaUsbEscPos(text, logoBase64, call);
        } catch (Exception fallbackError) {
            Log.e("UsbPrinter", "❌ USB fallback failed: " + fallbackError.getMessage(), fallbackError);
            call.reject(reason + ". USB fallback failed: " + fallbackError.getMessage());
        }
    }

    @PluginMethod
    public void connect(final PluginCall call) {
        initOmniDriverInternal(new OmniInitCallback() {
            @Override
            public void onSuccess() {
                Log.i("UsbPrinter", "✅ Connected via OmniDriver");
                connectUsbFallback(call, true);
            }

            @Override
            public void onError(String errorMsg) {
                Log.w("UsbPrinter", "⚠️ OmniDriver init failed: " + errorMsg);
                connectUsbFallback(call, false);
            }
        });
    }

    private void connectUsbFallback(final PluginCall call, final boolean omniSucceeded) {
        Context context = getContext();
        usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);

        if (usbManager == null) {
            handleConnectResult(call, omniSucceeded, false, "USB Manager not available");
            return;
        }

        for (UsbDevice device : usbManager.getDeviceList().values()) {
            Log.i("UsbPrinter", "Found USB device: " + device.getDeviceName());

            for (int i = 0; i < device.getInterfaceCount(); i++) {
                UsbInterface intf = device.getInterface(i);

                UsbEndpoint outEp = null;
                for (int j = 0; j < intf.getEndpointCount(); j++) {
                    UsbEndpoint ep = intf.getEndpoint(j);
                    if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK &&
                            ep.getDirection() == UsbConstants.USB_DIR_OUT) {
                        outEp = ep;
                        break;
                    }
                }

                if (outEp == null) continue;

                usbInterface = intf;
                endpoint = outEp;

                if (!usbManager.hasPermission(device)) {
                    Log.i("UsbPrinter", "Requesting USB permission for: " + device.getDeviceName());

                    PendingIntent permissionIntent = PendingIntent.getBroadcast(
                            context, 0,
                            new Intent(context.getPackageName() + ".USB_PERMISSION"),
                            android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
                                    ? PendingIntent.FLAG_MUTABLE
                                    : 0);

                    BroadcastReceiver usbReceiver = new BroadcastReceiver() {
                        @Override
                        public void onReceive(Context ctx, Intent intent) {
                            if ((ctx.getPackageName() + ".USB_PERMISSION").equals(intent.getAction())) {
                                synchronized (this) {
                                    UsbDevice dev = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                                        if (dev != null) {
                                            connection = usbManager.openDevice(dev);
                                            if (connection != null) {
                                                connection.claimInterface(usbInterface, true);
                                                Log.i("UsbPrinter", "✅ Connected to USB printer " + dev.getDeviceName());
                                                handleConnectResult(call, omniSucceeded, true, null);
                                            } else {
                                                handleConnectResult(call, omniSucceeded, false, "Failed to open USB connection");
                                            }
                                        }
                                    } else {
                                        handleConnectResult(call, omniSucceeded, false, "USB permission denied");
                                    }
                                    ctx.unregisterReceiver(this);
                                }
                            }
                        }
                    };

                    context.registerReceiver(
                            usbReceiver,
                            new IntentFilter(context.getPackageName() + ".USB_PERMISSION"));
                    usbManager.requestPermission(device, permissionIntent);
                    return;
                }

                connection = usbManager.openDevice(device);
                if (connection != null) {
                    connection.claimInterface(usbInterface, true);
                    Log.i("UsbPrinter", "✅ Connected to USB printer " + device.getDeviceName());
                    handleConnectResult(call, omniSucceeded, true, null);
                    return;
                } else {
                    handleConnectResult(call, omniSucceeded, false, "Failed to open USB device");
                    return;
                }
            }
        }

        handleConnectResult(call, omniSucceeded, false, "No USB printer found with OUT endpoint");
    }

    private void handleConnectResult(PluginCall call, boolean omniSucceeded, boolean usbSucceeded, String usbErrorMsg) {
        if (omniSucceeded || usbSucceeded) {
            JSObject ret = new JSObject();
            if (omniSucceeded && usbSucceeded) {
                ret.put("type", "both");
            } else if (omniSucceeded) {
                ret.put("type", "omni");
            } else {
                ret.put("type", "usb");
            }
            ret.put("omni", omniSucceeded);
            ret.put("usb", usbSucceeded);
            if (!usbSucceeded && usbErrorMsg != null) {
                ret.put("usbError", usbErrorMsg);
            }
            call.resolve(ret);
        } else {
            call.reject("Failed to connect to both OmniDriver and USB. USB Error: " + usbErrorMsg);
        }
    }


    private Bitmap decodeBase64ToBitmap(String base64) {
        try {
            if (base64.startsWith("data")) {
                base64 = base64.substring(base64.indexOf(",") + 1);
            }

            byte[] decoded = Base64.decode(base64, Base64.DEFAULT);
            return BitmapFactory.decodeByteArray(decoded, 0, decoded.length);
        } catch (Exception e) {
            return null;
        }
    }


    public static String vietnameseToLatinManual(String text) {
        StringBuilder sb = new StringBuilder();
        for (char c : text.toCharArray()) {
            sb.append(VIETNAMESE_MAP.getOrDefault(c, c));
        }
        return sb.toString();
    }




    // ===========================================================
    // 🖨️ USB ESC/POS — PRINT (internal)
    // ===========================================================
    private void printViaUsbEscPos(String text, String logoBase64, PluginCall call) throws EscPosConnectionException {
        if (connection == null || endpoint == null) {
            Log.e("UsbPrinter", "❌ Not connected. Call connect() first.");
            call.reject("Not connected. Call connect() first.");
            return;
        }

        UsbConnection usbConnection =
                UsbPrintersConnections.selectFirstConnected(this.getContext());

        if (usbConnection == null) {
            Log.e("UsbPrinter", "❌ No USB printer found");
            call.reject("No USB printer found");
            return;
        }

        EscPosPrinter printer = new EscPosPrinter(
                usbConnection, 203, 72f, 48
        );

        try {
            text = processLogo(text, logoBase64, printer);
            String safeText = vietnameseToLatinManual(text);
            if (Log.isLoggable("UsbPrinter", Log.INFO)) {
                Log.i("UsbPrinter", "📄 [USB] Printing: " + safeText.substring(0, Math.min(80, safeText.length())));
            }
            printer.printFormattedTextAndCut(safeText);
            Log.i("UsbPrinter", "✅ [USB] printText success");
            call.resolve();
        } catch (Exception e) {
            Log.e("UsbPrinter", "❌ [USB] printText failed: " + e.getMessage(), e);
            call.reject("printText failed: " + e.getMessage());
        }
    }

    // ===========================================================
    // 🖨️ PRINT TEXT — routes to OmniDriver or USB ESC/POS
    //    Pass useOmniDriver=true in the call to use OmniDriver.
    // ===========================================================
    @PluginMethod
    public void printText(PluginCall call) {
        String text = call.getString("text");
        String logoBase64 = call.getString("logoBase64");

        com.getcapacitor.JSObject headerObj = call.getObject("header", new com.getcapacitor.JSObject());
        String headerName = headerObj.getString("name", "");
        String headerAddress = headerObj.getString("address", "");

        com.getcapacitor.JSObject qrCodeObj = call.getObject("qrCode", new com.getcapacitor.JSObject());
        String qrContent = qrCodeObj.getString("content", "");
        int qrSize = qrCodeObj.getInteger("size", 20);

        boolean explicitUseOmni = Boolean.TRUE.equals(call.getBoolean("useOmniDriver", false));
        boolean useOmni = omniDriverReady || explicitUseOmni;

        Log.i("UsbPrinter", "🖨️ printText called, useOmni=" + useOmni
                + ", text length=" + (text != null ? text.length() : 0));

        if (text == null || text.isEmpty()) {
            call.reject("Missing text");
            return;
        }


        try {
            if (useOmni) {
                // Nhánh 1: OmniDriver
                if (text.contains("{LOGO}") && logoBase64 != null && !logoBase64.isEmpty()) {
                    text = text.replace("{LOGO}", "<img>" + logoBase64 + "</img>");
                }

                if (text.contains("{QR_CODE}") && !qrContent.isEmpty()) {
                    String qrTag = "[C]<qrcode size='" + qrSize + "'>" + qrContent + "</qrcode>";
                    text = text.replace("{QR_CODE}", qrTag);
                }

                printViaOmniDriver(text, logoBase64, call);
            } else {
                // Nhánh 2: USB ESC/POS
                if (connection == null || endpoint == null) {
                    Log.e("UsbPrinter", "❌ Not connected. Call connect() first.");
                    call.reject("Not connected. Call connect() first.");
                    return;
                }

                UsbConnection usbConnection = UsbPrintersConnections.selectFirstConnected(this.getContext());
                if (usbConnection == null) {
                    Log.e("UsbPrinter", "❌ No USB printer found");
                    call.reject("No USB printer found");
                    return;
                }

                EscPosPrinter printer = new EscPosPrinter(usbConnection, 203, 72f, 48);


                if (text != null && text.contains("{QR_CODE}") && !qrContent.isEmpty()) {
                    String qrTag = "[C]<qrcode size='" + qrSize + "'>" + qrContent + "</qrcode>";
                    text = text.replace("{QR_CODE}", qrTag);
                }

                text = processLogo(text, logoBase64, printer);

                String safeText = vietnameseToLatinManual(text);
                if (Log.isLoggable("UsbPrinter", Log.INFO)) {
                    Log.i("UsbPrinter", "📄 [USB] Printing: " + safeText.substring(0, Math.min(80, safeText.length())));
                }

                printer.printFormattedTextAndCut(safeText);
                Log.i("UsbPrinter", "✅ [USB] printText success");
                call.resolve();
            }
        } catch (Exception e) {
            Log.e("UsbPrinter", "❌ printText failed", e);
            call.reject("printText failed: " + e.getMessage());
        }
    }

    /**
     * Converts DantSu-formatted text ([C], [L], [R] tags) into raw ESC/POS bytes.
     * This bypasses DantSu's USB class detection, allowing it to work on
     * devices like Landi C20Pro whose built-in printer doesn't declare USB_CLASS_PRINTER.
     */
    private byte[] buildEscPosFromFormattedText(String formattedText) throws java.io.IOException {
        java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();

        // ESC @ - Initialize printer
        bos.write(new byte[]{0x1B, 0x40});
        // ESC a 0 - Left align by default
        bos.write(new byte[]{0x1B, 0x61, 0x00});

        String[] lines = formattedText.split("\n");
        for (String line : lines) {
            String trimmed = line.trim();

            byte align = 0x00; // left
            String content = trimmed;

            if (trimmed.startsWith("[C]")) {
                align = 0x01; // center
                content = trimmed.substring(3).trim();
            } else if (trimmed.startsWith("[L]")) {
                align = 0x00; // left
                content = trimmed.substring(3).trim();
            } else if (trimmed.startsWith("[R]")) {
                align = 0x02; // right
                content = trimmed.substring(3).trim();
            }

            // Strip any remaining inline tags like <b>, </b>, <u>, etc.
            content = TAG_PATTERN.matcher(content).replaceAll("");

            // ESC a - Set alignment
            bos.write(new byte[]{0x1B, 0x61, align});

            if (!content.isEmpty()) {
                bos.write(content.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1));
            }
            bos.write('\n');
        }

        // Feed 3 lines before cut
        bos.write(new byte[]{0x1B, 0x64, 0x03});
        // GS V 0 - Full cut
        bos.write(new byte[]{0x1D, 0x56, 0x00});

        return bos.toByteArray();
    }

    private String processLogo(String text, String base64, EscPosPrinter printer) {
        if (base64 == null || !text.contains("{LOGO}")) {
            return text;
        }

        try {
            if (base64.startsWith("data")) {
                base64 = base64.substring(base64.indexOf(",") + 1);
            }

            byte[] decoded = Base64.decode(base64, Base64.DEFAULT);

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
            Bitmap bitmap = BitmapFactory.decodeByteArray(decoded, 0, decoded.length, options);
            if (bitmap == null) {
                Log.e("UsbPrinter", "❌ Bitmap decode failed");
                return text;
            }

            int targetWidth = 300;
            int targetHeight = (int) ((bitmap.getHeight() * targetWidth) / bitmap.getWidth());

            Bitmap scaled = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true);

            // Xử lý transparency: tạo bitmap RGB (không alpha) với white background
            Bitmap processed = Bitmap.createBitmap(scaled.getWidth(), scaled.getHeight(), Bitmap.Config.RGB_565);
            Canvas canvas = new Canvas(processed);
            canvas.drawColor(Color.WHITE); // Vẽ white background trước
            canvas.drawBitmap(scaled, 0, 0, null); // Vẽ logo lên

            String hex = PrinterTextParserImg.bitmapToHexadecimalString(printer, processed);

            if (!scaled.isRecycled() && scaled != bitmap) {
                scaled.recycle();
            }
            if (!processed.isRecycled()) {
                processed.recycle();
            }

            String imgTag = "<img>" + hex + "</img>";

            return text.replace("{LOGO}", imgTag);

        } catch (Exception e) {
            Log.e("UsbPrinter", "❌ Logo parse failed", e);
            return text;
        }
    }

    @PluginMethod
    public void printBase64(PluginCall call) {
        String base64 = call.getString("data");
        if (base64 == null || base64.isEmpty()) {
            call.reject("Missing base64 data");
            return;
        }

        try {
            final int printerDPI = 203;
            final float printableWidthMM = 72f;
            final int targetW = Math.round((printableWidthMM * printerDPI) / 25.4f); // ~576

            Bitmap decodedBitmap;
            if (base64.startsWith("data:image/svg+xml;base64,")) {
                String svgBase64 = base64.replaceFirst("^data:image/svg\\+xml;base64,", "");
                byte[] svgBytes = Base64.decode(svgBase64, Base64.NO_WRAP);
                String svgString = new String(svgBytes, java.nio.charset.StandardCharsets.UTF_8);

                SVG svg = SVG.getFromString(svgString);
                Picture p = svg.renderToPicture();
                float scale = (targetW * 1.2f) / (float) p.getWidth();
                float offsetX = -(targetW * (scale - 1f) * 0.35f);
                int targetH = Math.max(1, (int) (p.getHeight() * scale));

                decodedBitmap = Bitmap.createBitmap(targetW, targetH, Bitmap.Config.RGB_565);
                Canvas cvs = new Canvas(decodedBitmap);
                cvs.drawColor(Color.WHITE);
                cvs.translate(offsetX, 0);
                cvs.scale(scale, scale);
                cvs.drawPicture(p);
            } else {
                String clean = base64
                        .replaceFirst("^data:image/png;base64,", "")
                        .replaceFirst("^data:image/jpeg;base64,", "")
                        .replaceFirst("^data:image/jpg;base64,", "");
                byte[] bytes = Base64.decode(clean, Base64.NO_WRAP);

                BitmapFactory.Options bounds = new BitmapFactory.Options();
                bounds.inJustDecodeBounds = true;
                BitmapFactory.decodeByteArray(bytes, 0, bytes.length, bounds);

                int inSample = 1;
                int srcW = Math.max(1, bounds.outWidth);
                while ((srcW / (inSample * 2)) > targetW * 2) inSample <<= 1;

                BitmapFactory.Options opts = new BitmapFactory.Options();
                opts.inSampleSize = inSample;
                opts.inPreferredConfig = Bitmap.Config.RGB_565; // nhanh & nhẹ
                decodedBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opts);
                if (decodedBitmap == null) {
                    call.reject("Failed to decode raster image");
                    return;
                }
            }

            long t1 = android.os.SystemClock.uptimeMillis();

            Bitmap work = decodedBitmap;
            if (work.getWidth() != targetW) {
                int newH = Math.max(1, (work.getHeight() * targetW) / work.getWidth());
                work = Bitmap.createScaledBitmap(work, targetW, newH, false); // no filter => nhanh
                if (work != decodedBitmap && !decodedBitmap.isRecycled()) decodedBitmap.recycle();
            }

            int w8 = (work.getWidth() >> 3) << 3;
            if (w8 != work.getWidth()) {
                work = Bitmap.createBitmap(work, 0, 0, w8, work.getHeight());
            }

            final int WHITE_TH = 245;
            int top = 0, bottom = work.getHeight() - 1;
            while (top < work.getHeight() && isWhiteRow(work, top, WHITE_TH)) top++;

            int whiteBottomCount = 0;
            while (bottom > top && isWhiteRow(work, bottom, WHITE_TH)) {
                bottom--;
                whiteBottomCount++;
                if (whiteBottomCount > 100) break;
            };

            if (top > 0 || bottom < work.getHeight() - 1) {
                int newH = bottom - top + 1;
                work = Bitmap.createBitmap(work, 0, top, work.getWidth(), Math.max(1, newH));
            }

            final int width = work.getWidth();
            final int height = work.getHeight();

            work = boostContrastFast(work, 1.28f, -18f);
            work = toPureMonochrome(work, 190);

            final int charsPerLine = 48;
            UsbConnection usbConnection = UsbPrintersConnections.selectFirstConnected(this.getContext());
            if (usbConnection == null) {
                call.reject("No printer connected");
                return;
            }
            EscPosPrinter printer = new EscPosPrinter(usbConnection, printerDPI, printableWidthMM, charsPerLine);
            try {
                usbConnection.write(new byte[]{0x1B, 0x37, 12, (byte)120, 40});

                usbConnection.write(new byte[]{0x12, 0x23, 12});

                usbConnection.write(new byte[]{0x1D, 0x45, 4});

            } catch (Exception e) {
                Log.w("UsbPrinterFast", "⚠️ Density command skipped: " + e.getMessage());
            }

            final int SLICE = 256;
            StringBuilder sb = new StringBuilder(height * 6);
            for (int y = 0; y < height; y += SLICE) {
                int h = Math.min(SLICE, height - y);
                Bitmap slice = Bitmap.createBitmap(work, 0, y, width, h);
                sb.append("[C]<img>")
                        .append(PrinterTextParserImg.bitmapToHexadecimalString(printer, slice))
                        .append("</img>\n");
                slice.recycle();
            }
            printer.printFormattedTextAndCut(sb.toString());

            if (!work.isRecycled()) {
                work.recycle();
            }

            call.resolve();

        } catch (Exception e) {
            Log.e("UsbPrinter", "❌ PrintBase64 failed", e);
            call.reject("Print failed: " + e.getMessage());
        }
    }

    private boolean isWhiteRow(Bitmap bmp, int y, int whiteTh) {
        final int w = bmp.getWidth();
        int[] row = new int[w];
        bmp.getPixels(row, 0, w, 0, y, w, 1);
        for (int x = 0; x < w; x++) {
            int c = row[x];
            int r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF;
            int gray = (r * 38 + g * 75 + b * 15) >> 7; // 0..255
            if (gray < whiteTh) return false; // có mực -> không trắng
        }
        return true;
    }

    private Bitmap toPureMonochrome(Bitmap src, int threshold) {
        int w = src.getWidth(), h = src.getHeight();
        Bitmap out = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        int[] pixels = new int[w];
        for (int y = 0; y < h; y++) {
            src.getPixels(pixels, 0, w, 0, y, w, 1);
            for (int x = 0; x < w; x++) {
                int c = pixels[x];
                int r = (c >> 16) & 0xFF;
                int g = (c >> 8) & 0xFF;
                int b = c & 0xFF;
                // công thức gray nhẹ: nhanh hơn 0.3R+0.59G+0.11B
                int gray = (r * 38 + g * 75 + b * 15) >> 7;
                pixels[x] = (gray < threshold)
                        ? Color.BLACK
                        : Color.WHITE;
            }
            out.setPixels(pixels, 0, w, 0, y, w, 1);
        }
        return out;
    }

    private Bitmap boostContrastFast(Bitmap src, float contrast, float brightness) {
        Bitmap out = Bitmap.createBitmap(src.getWidth(), src.getHeight(),
                (src.getConfig() == Bitmap.Config.ARGB_8888) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(out);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setDither(true);
        p.setFilterBitmap(false);
        float c = contrast;
        float b = brightness;
        ColorMatrix cm = new ColorMatrix(new float[]{
                c, 0, 0, 0, b,
                0, c, 0, 0, b,
                0, 0, c, 0, b,
                0, 0, 0, 1, 0
        });
        p.setColorFilter(new ColorMatrixColorFilter(cm));
        canvas.drawBitmap(src, 0, 0, p);
        return out;
    }

    // --------------------------
    // 🔄 INTERNAL - PAD WIDTH
    //
    private Bitmap padBitmapWidth(Bitmap src) {
        int width = src.getWidth();
        int paddedWidth = (width + 7) & (~7); // làm tròn lên bội số 8
        if (paddedWidth == width) return src;

        Bitmap padded = Bitmap.createBitmap(paddedWidth, src.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(padded);
        canvas.drawColor(Color.WHITE);
        canvas.drawBitmap(src, 0, 0, null);
        return padded;
    }

    // --------------------------
    // 🔄 INTERNAL - SEND DATA
    // --------------------------
    private void sendToPrinter(byte[] data) {
        final int CHUNK = 4096;
        int offset = 0;
        while (offset < data.length) {
            int len = Math.min(CHUNK, data.length - offset);
            int result = connection.bulkTransfer(endpoint, data, offset, len, 2000);
            if (result < 0) {
                Log.e("UsbPrinter", "Bulk transfer failed at offset=" + offset);
                break;
            }
            offset += len;
        }
        Log.i("UsbPrinter", "Sent total bytes=" + data.length);
    }

    // --------------------------
    // ❌ DISCONNECT
    // --------------------------
    @PluginMethod
    public void disconnect(PluginCall call) {
        if (connection != null) {
            try {
                connection.releaseInterface(usbInterface);
                connection.close();
            } catch (Exception ignored) {}
            connection = null;
        }
        call.resolve();
    }

    // --------------------------
    // 🔄 INTERNAL - CONVERT TO BINARY
    // ---
    private Bitmap convertToBinary(Bitmap src) {
        int width = src.getWidth();
        int height = src.getHeight();
        Bitmap bw = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int pixel = src.getPixel(x, y);
                int gray = (Color.red(pixel) + Color.green(pixel) + Color.blue(pixel)) / 3;
                bw.setPixel(x, y, gray < 128 ? Color.BLACK : Color.WHITE);
            }
        }
        return bw;
    }
}
