import { ref } from 'vue';
import { normalizeEncoderOptions } from '@/lib/thermal-printer/normalize-encoder-options';
import type { PrinterConnection, StoredBluetoothDevice, StoredUsbDevice } from '@/lib/thermal-printer/types';

const USB_STORAGE_KEY = 'thermal-printer.usb-device';
const BLUETOOTH_STORAGE_KEY = 'thermal-printer.bluetooth-device';
const CONNECTION_STORAGE_KEY = 'thermal-printer.active-connection';

const USB_CHUNK_SIZE = 256;
const USB_CHUNK_DELAY_MS = 15;
const USB_FLUSH_DELAY_MS = 200;

type BleProfile = {
    service: string;
    characteristic: string;
    language: string;
    codepageMapping: string;
    chunkSize: number;
    chunkDelayMs: number;
};

const BLE_PROFILES: BleProfile[] = [
    {
        service: '000018f0-0000-1000-8000-00805f9b34fb',
        characteristic: '00002af1-0000-1000-8000-00805f9b34fb',
        language: 'esc-pos',
        codepageMapping: 'xprinter',
        chunkSize: 50,
        chunkDelayMs: 35,
    },
    {
        service: '49535343-fe7d-4ae5-8fa9-9fafd205e455',
        characteristic: '49535343-8841-43f4-a8d4-ecbe34729bb3',
        language: 'esc-pos',
        codepageMapping: 'epson',
        chunkSize: 100,
        chunkDelayMs: 25,
    },
    {
        service: '0000ae30-0000-1000-8000-00805f9b34fb',
        characteristic: '0000ae01-0000-1000-8000-00805f9b34fb',
        language: 'esc-pos',
        codepageMapping: 'epson',
        chunkSize: 100,
        chunkDelayMs: 35,
    },
];

export const connectionState = ref<PrinterConnection | null>(null);

let activeConnection: PrinterConnection | null = null;
let usbPort: SerialPort | null = null;
let bluetoothDevice: BluetoothDevice | null = null;
let bluetoothCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
let activeBleProfile: BleProfile | null = null;
let bluetoothEncoderSettings: StoredBluetoothDevice | null = null;
let printQueue: Promise<void> = Promise.resolve();

function sleep(ms: number): Promise<void> {
    return new Promise((resolve) => {
        window.setTimeout(resolve, ms);
    });
}

function readStoredConnection(): PrinterConnection | null {
    try {
        const raw = localStorage.getItem(CONNECTION_STORAGE_KEY);

        if (raw === 'usb' || raw === 'bluetooth') {
            return raw;
        }
    } catch {
        return null;
    }

    return null;
}

function storeActiveConnection(connection: PrinterConnection | null): void {
    if (connection) {
        localStorage.setItem(CONNECTION_STORAGE_KEY, connection);
    } else {
        localStorage.removeItem(CONNECTION_STORAGE_KEY);
    }
}

function readStoredUsbDevice(): StoredUsbDevice | null {
    try {
        const raw = localStorage.getItem(USB_STORAGE_KEY);

        if (!raw) {
            return null;
        }

        return JSON.parse(raw) as StoredUsbDevice;
    } catch {
        return null;
    }
}

function storeUsbDevice(device: StoredUsbDevice): void {
    localStorage.setItem(USB_STORAGE_KEY, JSON.stringify(device));
}

function readStoredBluetoothDevice(): StoredBluetoothDevice | null {
    try {
        const raw = localStorage.getItem(BLUETOOTH_STORAGE_KEY);

        if (!raw) {
            return null;
        }

        return JSON.parse(raw) as StoredBluetoothDevice;
    } catch {
        return null;
    }
}

function storeBluetoothDevice(device: StoredBluetoothDevice): void {
    localStorage.setItem(BLUETOOTH_STORAGE_KEY, JSON.stringify(device));
}

function clearUsbState(): void {
    usbPort = null;
}

function clearBluetoothState(): void {
    bluetoothDevice = null;
    bluetoothCharacteristic = null;
    activeBleProfile = null;
}

function setActiveConnection(connection: PrinterConnection | null): void {
    activeConnection = connection;
    connectionState.value = connection;
    storeActiveConnection(connection);
}

function markDisconnected(): void {
    setActiveConnection(null);
}

function bindUsbDisconnectHandler(port: SerialPort): void {
    port.addEventListener('disconnect', () => {
        if (activeConnection === 'usb') {
            clearUsbState();
            markDisconnected();
        }
    });
}

async function openUsbPort(port: SerialPort): Promise<void> {
    if (!port.readable && !port.writable) {
        await port.open({
            baudRate: 9600,
            dataBits: 8,
            stopBits: 1,
            parity: 'none',
            flowControl: 'none',
            bufferSize: 255,
        });
    }

    const info = port.getInfo();

    usbPort = port;
    setActiveConnection('usb');
    bindUsbDisconnectHandler(port);

    if (info.usbVendorId && info.usbProductId) {
        storeUsbDevice({
            vendorId: info.usbVendorId,
            productId: info.usbProductId,
        });
    }
}

async function resolveBluetoothPrintChannel(device: BluetoothDevice): Promise<{
    characteristic: BluetoothRemoteGATTCharacteristic;
    profile: BleProfile;
}> {
    const server = device.gatt;

    if (!server) {
        throw new Error('Printer Bluetooth tidak mendukung GATT.');
    }

    if (!server.connected) {
        await server.connect();
    }

    const services = await server.getPrimaryServices().catch(() => []);
    const serviceIds = new Set(services.map((service) => service.uuid));

    for (const profile of BLE_PROFILES) {
        if (!serviceIds.has(profile.service)) {
            continue;
        }

        try {
            const service = await server.getPrimaryService(profile.service);
            const characteristic = await service.getCharacteristic(profile.characteristic);

            return { characteristic, profile };
        } catch {
            continue;
        }
    }

    for (const profile of BLE_PROFILES) {
        try {
            const service = await server.getPrimaryService(profile.service);
            const characteristic = await service.getCharacteristic(profile.characteristic);

            return { characteristic, profile };
        } catch {
            continue;
        }
    }

    throw new Error('Service cetak printer Bluetooth tidak ditemukan.');
}

async function attachBluetoothDevice(device: BluetoothDevice): Promise<void> {
    const { characteristic, profile } = await resolveBluetoothPrintChannel(device);
    const encoderOptions = normalizeEncoderOptions({
        language: profile.language,
        codepageMapping: profile.codepageMapping,
    });

    bluetoothDevice = device;
    bluetoothCharacteristic = characteristic;
    activeBleProfile = profile;
    bluetoothEncoderSettings = {
        id: device.id,
        language: encoderOptions.language,
        codepageMapping: encoderOptions.codepageMapping,
    };

    setActiveConnection('bluetooth');
    storeBluetoothDevice(bluetoothEncoderSettings);

    device.addEventListener('gattserverdisconnected', () => {
        if (activeConnection === 'bluetooth') {
            clearBluetoothState();
            markDisconnected();
        }
    });
}

async function writeBluetoothChunk(
    characteristic: BluetoothRemoteGATTCharacteristic,
    chunk: Uint8Array,
): Promise<void> {
    try {
        await characteristic.writeValueWithResponse(chunk);
    } catch {
        await characteristic.writeValueWithoutResponse(chunk);
        await sleep(20);
    }
}

async function sendUsbData(data: Uint8Array): Promise<void> {
    if (!usbPort?.writable) {
        throw new Error('Port USB printer tidak siap.');
    }

    const writer = usbPort.writable.getWriter();

    try {
        for (let offset = 0; offset < data.length; offset += USB_CHUNK_SIZE) {
            const chunk = data.slice(offset, offset + USB_CHUNK_SIZE);
            await writer.write(chunk);

            if (offset + USB_CHUNK_SIZE < data.length) {
                await sleep(USB_CHUNK_DELAY_MS);
            }
        }

        await writer.ready;
    } finally {
        writer.releaseLock();
    }

    await sleep(USB_FLUSH_DELAY_MS);
}

async function sendBluetoothData(data: Uint8Array): Promise<void> {
    if (!bluetoothCharacteristic || !activeBleProfile) {
        throw new Error('Printer Bluetooth belum siap.');
    }

    const { chunkSize, chunkDelayMs } = activeBleProfile;

    for (let offset = 0; offset < data.length; offset += chunkSize) {
        const chunk = data.slice(offset, offset + chunkSize);
        await writeBluetoothChunk(bluetoothCharacteristic, chunk);

        if (offset + chunkSize < data.length) {
            await sleep(chunkDelayMs);
        }
    }

    await sleep(120);
}

async function reconnectUsb(): Promise<void> {
    const stored = readStoredUsbDevice();
    const ports = await navigator.serial!.getPorts();

    if (!ports.length) {
        throw new Error('Printer USB tidak ditemukan. Hubungkan ulang secara manual.');
    }

    const orderedPorts = [
        ...ports.filter((candidate) => {
            const info = candidate.getInfo();

            return stored?.vendorId === info.usbVendorId
                && stored?.productId === info.usbProductId;
        }),
        ...ports.filter((candidate) => {
            const info = candidate.getInfo();

            return stored?.vendorId !== info.usbVendorId
                || stored?.productId !== info.usbProductId;
        }),
    ];

    clearUsbState();

    let lastError: Error | null = null;

    for (const port of orderedPorts) {
        try {
            await openUsbPort(port);

            return;
        } catch (error) {
            lastError = error instanceof Error
                ? error
                : new Error('Gagal membuka port USB printer.');
        }
    }

    throw lastError ?? new Error('Printer USB tidak ditemukan. Hubungkan ulang secara manual.');
}

async function reconnectBluetooth(): Promise<void> {
    const stored = readStoredBluetoothDevice();

    if (!stored?.id || !navigator.bluetooth?.getDevices) {
        throw new Error('Printer Bluetooth tidak ditemukan. Hubungkan ulang secara manual.');
    }

    const devices = await navigator.bluetooth.getDevices();
    const device = devices.find((candidate) => candidate.id === stored.id);

    if (!device) {
        throw new Error('Printer Bluetooth tidak ditemukan. Hubungkan ulang secara manual.');
    }

    clearBluetoothState();
    await attachBluetoothDevice(device);
}

async function restoreStoredConnection(): Promise<boolean> {
    const storedConnection = readStoredConnection();

    if (!storedConnection) {
        return false;
    }

    try {
        if (storedConnection === 'usb' && isWebSerialSupported()) {
            await reconnectUsb();

            return activeConnection === 'usb';
        }

        if (storedConnection === 'bluetooth' && isWebBluetoothSupported()) {
            await reconnectBluetooth();

            return activeConnection === 'bluetooth';
        }
    } catch {
        clearUsbState();
        clearBluetoothState();
    }

    return false;
}

async function ensureReady(): Promise<void> {
    if (!activeConnection) {
        const restored = await restoreStoredConnection();

        if (!restored) {
            throw new Error('Printer belum terhubung. Hubungkan printer terlebih dahulu.');
        }
    }

    if (activeConnection === 'usb') {
        if (usbPort?.writable) {
            return;
        }

        await reconnectUsb();

        return;
    }

    if (activeConnection === 'bluetooth') {
        if (bluetoothDevice?.gatt?.connected && bluetoothCharacteristic) {
            return;
        }

        await reconnectBluetooth();
    }
}

async function forceReconnect(): Promise<void> {
    const connection = activeConnection ?? readStoredConnection();

    if (connection === 'usb') {
        await reconnectUsb();

        return;
    }

    if (connection === 'bluetooth') {
        await reconnectBluetooth();
    }
}

async function sendPrintDataInternal(data: Uint8Array): Promise<void> {
    if (!activeConnection) {
        throw new Error('Printer belum terhubung. Hubungkan printer terlebih dahulu.');
    }

    await ensureReady();

    if (activeConnection === 'usb') {
        await sendUsbData(data);

        return;
    }

    if (activeConnection === 'bluetooth') {
        await sendBluetoothData(data);

        return;
    }

    throw new Error('Koneksi printer tidak dikenali.');
}

export function getActiveConnection(): PrinterConnection | null {
    return activeConnection;
}

export function getBluetoothEncoderSettings(): StoredBluetoothDevice | null {
    return bluetoothEncoderSettings ?? readStoredBluetoothDevice();
}

export function isWebSerialSupported(): boolean {
    return typeof navigator !== 'undefined' && 'serial' in navigator;
}

export function isWebBluetoothSupported(): boolean {
    return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
}

export async function connectUsb(): Promise<void> {
    if (!isWebSerialSupported()) {
        throw new Error('Browser tidak mendukung Web Serial. Gunakan Chrome/Edge dan sambungkan printer via USB.');
    }

    const port = await navigator.serial!.requestPort();
    clearUsbState();
    await openUsbPort(port);
}

export async function connectBluetooth(): Promise<void> {
    if (!isWebBluetoothSupported()) {
        throw new Error('Browser tidak mendukung Web Bluetooth. Gunakan Chrome/Edge dan aktifkan Bluetooth.');
    }

    const device = await navigator.bluetooth!.requestDevice({
        acceptAllDevices: true,
        optionalServices: BLE_PROFILES.map((profile) => profile.service),
    });

    clearBluetoothState();
    await attachBluetoothDevice(device);
}

export async function restoreConnection(): Promise<boolean> {
    if (activeConnection === 'usb' && usbPort?.writable) {
        return true;
    }

    if (activeConnection === 'bluetooth' && bluetoothDevice?.gatt?.connected && bluetoothCharacteristic) {
        return true;
    }

    return restoreStoredConnection();
}

export async function tryReconnect(): Promise<boolean> {
    return restoreConnection();
}

export function disconnect(): void {
    if (usbPort) {
        void usbPort.close().catch(() => undefined);
    }

    if (bluetoothDevice?.gatt?.connected) {
        bluetoothDevice.gatt.disconnect();
    }

    clearUsbState();
    clearBluetoothState();
    markDisconnected();
}

export async function sendPrintData(data: Uint8Array, maxAttempts = 3): Promise<void> {
    const task = printQueue
        .catch(() => undefined)
        .then(async () => {
            let lastError: Error | null = null;

            for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
                try {
                    await sendPrintDataInternal(data);

                    return;
                } catch (error) {
                    lastError = error instanceof Error
                        ? error
                        : new Error('Gagal mengirim data ke printer.');

                    clearUsbState();
                    clearBluetoothState();

                    if (attempt < maxAttempts) {
                        await sleep(400);

                        try {
                            await forceReconnect();
                        } catch {
                            // Retry will surface the final error.
                        }
                    }
                }
            }

            throw lastError ?? new Error('Gagal mencetak struk.');
        });

    printQueue = task.catch(() => undefined);

    await task;
}
