const SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const RX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
const TX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';

const encoder = new TextEncoder();
const decoder = new TextDecoder();

let device = null;
let server = null;
let rxCharacteristic = null;
let txCharacteristic = null;
let lineBuffer = '';
let commandQueue = Promise.resolve();

const lineListeners = new Set();

function emitLine(line) {
  for (const listener of lineListeners) {
    listener(line);
  }
}

function handleNotification(event) {
  const dataView = event.target.value;
  const chunk = decoder.decode(dataView, { stream: true });
  lineBuffer += chunk;

  while (true) {
    const delimiterIndex = lineBuffer.indexOf('\r\n');
    if (delimiterIndex < 0) {
      break;
    }

    const line = lineBuffer.slice(0, delimiterIndex);
    lineBuffer = lineBuffer.slice(delimiterIndex + 2);
    emitLine(line);
  }
}

function handleDisconnected() {
  server = null;
  rxCharacteristic = null;
  if (txCharacteristic) {
    txCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification);
  }
  txCharacteristic = null;
}

async function writeToRxCharacteristic(payloadBytes) {
  if (!rxCharacteristic) {
    throw new Error('EggDuino is not connected');
  }

  if (typeof rxCharacteristic.writeValueWithoutResponse === 'function') {
    await rxCharacteristic.writeValueWithoutResponse(payloadBytes);
    return;
  }

  if (typeof rxCharacteristic.writeValueWithResponse === 'function') {
    await rxCharacteristic.writeValueWithResponse(payloadBytes);
    return;
  }

  await rxCharacteristic.writeValue(payloadBytes);
}

function normalizeCommand(command) {
  return command.endsWith('\r') ? command : `${command}\r`;
}

function enqueueCommand(fn) {
  const run = commandQueue.then(fn, fn);
  commandQueue = run.catch(() => {});
  return run;
}

export async function connectEggDuino() {
  if (server?.connected && rxCharacteristic && txCharacteristic) {
    return;
  }

  try {
    device = await navigator.bluetooth.requestDevice({
      filters: [{ services: [SERVICE_UUID] }],
      optionalServices: [SERVICE_UUID],
    });
  } catch (error) {
    throw new Error(`BLE request failed: ${error.message}`);
  }

  device.addEventListener('gattserverdisconnected', handleDisconnected);

  try {
    server = await device.gatt.connect();
  } catch (error) {
    throw new Error(`BLE gatt connect failed: ${error.message}`);
  }

  let service;
  try {
    service = await server.getPrimaryService(SERVICE_UUID);
  } catch (error) {
    throw new Error(`BLE service lookup failed: ${error.message}`);
  }

  try {
    rxCharacteristic = await service.getCharacteristic(RX_UUID);
    txCharacteristic = await service.getCharacteristic(TX_UUID);
  } catch (error) {
    throw new Error(`BLE characteristic lookup failed: ${error.message}`);
  }

  try {
    await txCharacteristic.startNotifications();
    txCharacteristic.addEventListener('characteristicvaluechanged', handleNotification);
  } catch (error) {
    throw new Error(`BLE notifications setup failed: ${error.message}`);
  }
}

export function disconnectEggDuino() {
  if (!device) {
    return;
  }

  if (txCharacteristic) {
    txCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification);
  }

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

  handleDisconnected();
}

export async function sendEggBotCommand(command) {
  const normalized = normalizeCommand(command);
  await writeToRxCharacteristic(encoder.encode(normalized));
}

export async function sendEggBotCommandExpectOk(command, timeoutMs = 2000) {
  return enqueueCommand(() => new Promise(async (resolve, reject) => {
    const collectedLines = [];

    const timeout = setTimeout(() => {
      lineListeners.delete(onResponseLine);
      reject(new Error('EggBot response timeout'));
    }, timeoutMs);

    const onResponseLine = (line) => {
      collectedLines.push(line);

      if (line === 'OK') {
        clearTimeout(timeout);
        lineListeners.delete(onResponseLine);
        resolve(collectedLines.slice(0, -1));
      }

      if (line === 'unknown CMD') {
        clearTimeout(timeout);
        lineListeners.delete(onResponseLine);
        reject(new Error(`EggBot error: ${collectedLines.join(' | ')}`));
      }
    };

    lineListeners.add(onResponseLine);

    try {
      await sendEggBotCommand(command);
    } catch (error) {
      clearTimeout(timeout);
      lineListeners.delete(onResponseLine);
      reject(error);
    }
  }));
}

export function onLine(callback) {
  lineListeners.add(callback);
}

export function offLine(callback) {
  lineListeners.delete(callback);
}
