import * as Blockly from 'blockly';
import { pythonGenerator } from 'blockly/python';
import { arduinoGenerator } from './generators/arduino.js';
import './generators/arduino_builtins.js';
import './blocks/esp32_blocks.js';
import './blocks/esp32_generators.js';
import './blocks/arduino_generators.js';
import {
  getDeviceId,
  setDeviceId,
  getDevice,
  canFlashInBrowser,
  buildToolbox,
  getAllDevices,
} from './devices/registry.js';
import {
  setRefreshCallback,
  setDeviceListRefreshCallback,
  loadAllSavedAddons,
  installAddonFromFile,
  removeAddon,
  getInstalledAddons,
} from './addons/loader.js';
import { connect, disconnect, isConnected, getPort, onData, writeString } from './serial/connection.js';
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
import { flashFirmware } from './serial/flasher.js';
import { flashPicoFirmware } from './serial/picoFlasher.js';
import { flashFileToDrive } from './serial/driveFlasher.js';
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
import { initToolboxCustomizer, toggleCustomizeMode, refreshCustomizer } from './ui/toolboxCustomizer.js';
import {
  initProjectsDialog,
  refreshAll as refreshProjects,
  loadWorkspaceFromDevice,
  saveCurrentWorkspaceToDevice,
} from './ui/projectsDialog.js';
import { initRobotPanel, syncRobotPanelDevice } from './ui/robotPanel.js';
import { uploadHex, BOARDS } from './arduino/stk500.js';
import { initLoginDialog } from './ui/loginDialog.js';
import { getUser, getToken, refreshMe, onAuthChange } from './auth/client.js';
import { connectStudent, disconnectStudent, sendWorkspace } from './ws/studentClient.js';
import './style.css';

// ─── Blockly Workspace ───────────────────────────────────

// Load saved addons before building toolbox so their categories are included
loadAllSavedAddons();

const workspace = Blockly.inject('blockly-div', {
  toolbox: buildToolbox(getDeviceId()),
  theme: Blockly.Themes.Dark,
  grid: { spacing: 25, length: 3, colour: '#333', snap: true },
  zoom: { controls: true, wheel: true, startScale: 0.9, maxScale: 3, minScale: 0.3, scaleSpeed: 1.2 },
  trashcan: true,
  renderer: 'zelos',
});

// Now that workspace exists, set the refresh callback for addons
setRefreshCallback(() => {
  workspace.updateToolbox(buildToolbox(getDeviceId()));
});

// Rebuild the device <select> from the registry (called when addons register devices)
function rebuildDeviceSelect() {
  const devices = getAllDevices();
  const current = getDeviceId();
  const select = document.getElementById('device-select');
  select.innerHTML = '';
  for (const [id, profile] of Object.entries(devices)) {
    const opt = document.createElement('option');
    opt.value = id;
    opt.textContent = profile.label;
    select.appendChild(opt);
  }
  select.value = current;
}

setDeviceListRefreshCallback(rebuildDeviceSelect);

// Toolbox customizer (show/hide categories & blocks)
initToolboxCustomizer(refreshToolbox);
document.getElementById('btn-customize')?.addEventListener('click', toggleCustomizeMode);

// ─── Generator Selection ─────────────────────────────────

function isArduinoDevice() {
  const device = getDevice();
  return device && device.language === 'arduino';
}

function getActiveGenerator() {
  return isArduinoDevice() ? arduinoGenerator : pythonGenerator;
}

function getGeneratedCode() {
  return getActiveGenerator().workspaceToCode(workspace);
}

// ─── Live Code Preview ───────────────────────────────────

const codeOutput = document.getElementById('code-output');

function updateCodePreview() {
  const code = getGeneratedCode();
  const placeholder = isArduinoDevice()
    ? '// Drag blocks to generate Arduino code'
    : '# Drag blocks to generate MicroPython code';
  codeOutput.textContent = code || placeholder;
}

workspace.addChangeListener((event) => {
  if (event.isUiEvent) return;
  updateCodePreview();
});

updateCodePreview();

// ─── Workspace Persistence (localStorage) ────────────────

const STORAGE_KEY = 'esp32block_workspace';

function saveWorkspace() {
  const state = Blockly.serialization.workspaces.save(workspace);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}

function loadWorkspace() {
  const json = localStorage.getItem(STORAGE_KEY);
  if (json) {
    try {
      const state = JSON.parse(json);
      Blockly.serialization.workspaces.load(state, workspace);
    } catch (_) {
      /* corrupted state, ignore */
    }
  }
}

let suppressBroadcast = false;
let broadcastTimer = null;

function scheduleBroadcast() {
  if (suppressBroadcast) return;
  if (broadcastTimer) return;
  broadcastTimer = setTimeout(() => {
    broadcastTimer = null;
    if (suppressBroadcast) return;
    try {
      const state = Blockly.serialization.workspaces.save(workspace);
      sendWorkspace(state);
    } catch { /* ignore */ }
  }, 500);
}

workspace.addChangeListener((event) => {
  if (event.isUiEvent) return;
  saveWorkspace();
  scheduleBroadcast();
});

loadWorkspace();

// ─── Push-code toast helper ──────────────────────────────

const pushToast = document.getElementById('push-toast');
let pushToastTimer = null;

function showPushToast(message) {
  if (!pushToast) return;
  pushToast.textContent = message;
  pushToast.classList.remove('hidden');
  if (pushToastTimer) clearTimeout(pushToastTimer);
  pushToastTimer = setTimeout(() => pushToast.classList.add('hidden'), 3500);
}

function applyPushedState(state, fromUser) {
  if (!state) return;
  suppressBroadcast = true;
  try {
    Blockly.serialization.workspaces.load(state, workspace);
    updateCodePreview();
    saveWorkspace();
  } catch (err) {
    appendToTerminal(`\nFailed to apply pushed blocks: ${err.message}\n`);
  } finally {
    setTimeout(() => { suppressBroadcast = false; }, 250);
  }
  showPushToast(`Teacher${fromUser ? ` (${fromUser})` : ''} pushed new blocks to your workspace.`);
}

// ─── Auth / realtime wiring ──────────────────────────────

function syncRealtimeConnection() {
  const user = getUser();
  const token = getToken();
  if (!user || !token) {
    disconnectStudent();
    return;
  }
  if (user.role !== 'student') {
    disconnectStudent();
    return;
  }
  connectStudent({
    token,
    onOpen: () => {
      try {
        const state = Blockly.serialization.workspaces.save(workspace);
        sendWorkspace(state);
      } catch { /* ignore */ }
    },
    onPush: (state, from) => applyPushedState(state, from),
  });
}

initLoginDialog();
onAuthChange(() => syncRealtimeConnection());
refreshMe().then(() => syncRealtimeConnection()).catch(() => { /* ignore */ });

// ─── Resize Handling ─────────────────────────────────────

function onResize() {
  const blocklyArea = document.getElementById('blockly-area');
  const blocklyDiv = document.getElementById('blockly-div');
  if (blocklyArea && blocklyDiv) {
    blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
    blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
    Blockly.svgResize(workspace);
  }
}

window.addEventListener('resize', onResize);
onResize();
initResizablePanels();
initPanelToggles();
initProjectTabs();
setProjectsPanelCallbacks({
  onExpand: () => refreshProjects(),
});

// ─── UI State Helpers ────────────────────────────────────

const deviceSelect = document.getElementById('device-select');
const btnConnect = document.getElementById('btn-connect');
const btnFlash = document.getElementById('btn-flash');
const btnRun = document.getElementById('btn-run');
const btnStop = document.getElementById('btn-stop');
const btnSave = document.getElementById('btn-save');
const btnLoad = document.getElementById('btn-load');
const statusEl = document.getElementById('connection-status');
const terminalInput = document.getElementById('terminal-input');
const sendOverlayEl = document.getElementById('send-overlay');
const sendModalTitle = document.getElementById('send-modal-title');
const sendProgressFill = document.getElementById('send-progress-fill');
const sendProgressText = document.getElementById('send-progress-text');

function showSendProgress(title = 'Sending code to device') {
  sendModalTitle.textContent = title;
  sendProgressFill.style.width = '0%';
  sendProgressText.textContent = '0%';
  sendOverlayEl.classList.remove('hidden');
}

function updateSendProgress(sent, total) {
  const pct = total > 0 ? Math.round((sent / total) * 100) : 0;
  sendProgressFill.style.width = pct + '%';
  sendProgressText.textContent = pct + '%';
}

function hideSendProgress() {
  sendOverlayEl.classList.add('hidden');
  sendProgressFill.style.width = '0%';
}

// ─── Arduino-specific UI state ───────────────────────────

function updateDeviceUI() {
  const arduino = isArduinoDevice();

  if (arduino) {
    btnRun.querySelector('.icon').innerHTML = '&#9655;';
    btnRun.childNodes[btnRun.childNodes.length - 1].textContent = ' Upload .hex';
    btnRun.title = 'Upload a compiled .hex file to Arduino via WebSerial';
    btnRun.disabled = false;

    btnSave.querySelector('.icon').innerHTML = '&#128190;';
    btnSave.childNodes[btnSave.childNodes.length - 1].textContent = ' Download .ino';
    btnSave.title = 'Download generated code as .ino file';
    btnSave.disabled = false;
    btnLoad.querySelector('.icon').innerHTML = '&#128229;';
    btnLoad.childNodes[btnLoad.childNodes.length - 1].textContent = ' Load';
    btnLoad.title = 'Load is unavailable for Arduino devices';
    btnLoad.disabled = true;

    btnConnect.querySelector('.icon').innerHTML = '&#9654;';
    btnConnect.childNodes[btnConnect.childNodes.length - 1].textContent = ' Serial Monitor';
    btnConnect.title = 'Open serial monitor via WebSerial';

    btnFlash.classList.add('hidden');
    btnStop.disabled = false;
  } else {
    btnRun.querySelector('.icon').innerHTML = '&#9655;';
    btnRun.childNodes[btnRun.childNodes.length - 1].textContent = ' Run';
    btnRun.title = 'Upload and run code';
    btnRun.disabled = !isConnected();

    btnSave.querySelector('.icon').innerHTML = '&#128190;';
    btnSave.childNodes[btnSave.childNodes.length - 1].textContent = ' Save';
    btnSave.title = 'Save code to device as main.py';
    btnSave.disabled = !isConnected();
    btnLoad.querySelector('.icon').innerHTML = '&#128229;';
    btnLoad.childNodes[btnLoad.childNodes.length - 1].textContent = ' Load';
    btnLoad.title = 'Load block layout from main.blk';
    btnLoad.disabled = !isConnected();

    btnConnect.querySelector('.icon').innerHTML = '&#9654;';
    btnConnect.childNodes[btnConnect.childNodes.length - 1].textContent = isConnected() ? ' Disconnect' : ' Connect';
    btnConnect.title = 'Connect to device via Web Serial';

    btnFlash.classList.remove('hidden');
    const flashMethod = getDevice()?.firmware?.flashMethod;
    btnFlash.title = (canFlashInBrowser() || flashMethod === 'webusb' || flashMethod === 'microbit')
      ? 'Flash MicroPython firmware'
      : 'Download firmware (drag to device)';

    btnStop.disabled = !isConnected();
  }
}

// Sync device dropdown with stored device
deviceSelect.value = getDeviceId();
deviceSelect.addEventListener('change', () => {
  setDeviceId(deviceSelect.value);
  refreshToolbox();
  refreshCustomizer();
  syncRobotPanelDevice();
  updateCodePreview();
  updateDeviceUI();
});
updateDeviceUI();

function refreshToolbox() {
  workspace.updateToolbox(buildToolbox(getDeviceId()));
}

function setConnectedUI(connected) {
  if (isArduinoDevice()) {
    btnConnect.innerHTML = connected
      ? '<span class="icon">&#9211;</span> Disconnect Monitor'
      : '<span class="icon">&#9654;</span> Serial Monitor';
    terminalInput.disabled = !connected;
    statusEl.textContent = connected ? 'Monitoring' : 'Disconnected';
    statusEl.className = connected ? 'status-connected' : 'status-disconnected';
  } else {
    btnConnect.innerHTML = connected
      ? '<span class="icon">&#9211;</span> Disconnect'
      : '<span class="icon">&#9654;</span> Connect';
    btnRun.disabled = !connected;
    btnStop.disabled = !connected;
    btnSave.disabled = !connected;
    btnLoad.disabled = !connected;
    terminalInput.disabled = !connected;
    statusEl.textContent = connected ? 'Connected' : 'Disconnected';
    statusEl.className = connected ? 'status-connected' : 'status-disconnected';
  }
}

// ─── Serial Capture (reusable promise-based) ─────────────

let captureState = null;

onData((text) => {
  if (!captureState) {
    appendToTerminal(text);
    return;
  }

  const { startMarker, endMarker } = captureState;
  captureState.raw += text;
  const raw = captureState.raw;

  const startIdx = raw.indexOf(startMarker);

  if (startIdx === -1) {
    const keep = startMarker.length - 1;
    if (raw.length > keep) {
      appendToTerminal(raw.substring(0, raw.length - keep));
      captureState.raw = raw.substring(raw.length - keep);
    }
    return;
  }

  const contentStart = startIdx + startMarker.length;
  const endIdx = raw.indexOf(endMarker, contentStart);

  if (endIdx === -1) {
    if (!captureState.flushedPre && startIdx > 0) {
      appendToTerminal(raw.substring(0, startIdx));
      captureState.flushedPre = true;
    }
    return;
  }

  const content = raw.substring(contentStart, endIdx);
  const beforeStart = startIdx > 0 && !captureState.flushedPre
    ? raw.substring(0, startIdx) : '';
  const afterEnd = raw.substring(endIdx + endMarker.length);
  const resolve = captureState.resolve;

  captureState = null;

  if (beforeStart) appendToTerminal(beforeStart);
  if (afterEnd.trim()) appendToTerminal(afterEnd);

  resolve(content);
});

function captureDeviceOutput(script) {
  return new Promise((resolve, reject) => {
    const ts = Date.now();
    const sm = `__CAP_S_${ts}__`;
    const em = `__CAP_E_${ts}__`;

    captureState = {
      startMarker: sm,
      endMarker: em,
      raw: '',
      flushedPre: false,
      resolve,
    };

    const wrapped = `print('${sm}',end='')\n${script}\nprint('${em}',end='')`;
    executeCode(wrapped).catch(err => {
      captureState = null;
      reject(err);
    });

    setTimeout(() => {
      if (captureState?.resolve === resolve) {
        captureState = null;
        reject(new Error('Timeout waiting for device response'));
      }
    }, 10000);
  });
}

// ─── Toolbar Buttons ─────────────────────────────────────

btnConnect.addEventListener('click', async () => {
  try {
    if (isConnected()) {
      await disconnect();
      setConnectedUI(false);
      appendToTerminal('\n--- Disconnected ---\n');
    } else {
      await connect();
      setConnectedUI(true);
      appendToTerminal('--- Connected ---\n');
    }
  } catch (err) {
    appendToTerminal(`\nConnection error: ${err.message}\n`);
  }
});

const flashOverlay = document.getElementById('flash-overlay');
const flashLog = document.getElementById('flash-log');
const flashFill = document.getElementById('flash-progress-fill');
const flashPctText = document.getElementById('flash-progress-text');
const flashCloseBtn = document.getElementById('flash-close');
const esp32FlashOverlay = document.getElementById('esp32-flash-overlay');
const esp32FlashClose = document.getElementById('esp32-flash-close');
const esp32FlashStart = document.getElementById('esp32-flash-start');
const esp32FlashVariant = document.getElementById('esp32-variant-select');
const esp32FlashStatus = document.getElementById('esp32-flash-status');
const microbitFlashOverlay = document.getElementById('microbit-flash-overlay');
const microbitFlashClose = document.getElementById('microbit-flash-close');
const microbitFlashStart = document.getElementById('microbit-flash-start');
const microbitFlashVariant = document.getElementById('microbit-variant-select');
const microbitFlashStatus = document.getElementById('microbit-flash-status');

const FW_BASE = import.meta.env.BASE_URL + 'firmware/';

const ESP32_FIRMWARE_OPTIONS = {
  esp32: {
    label: 'ESP32',
    url: FW_BASE + 'ESP32_GENERIC-20260406-v1.28.0.bin',
    flashAddress: 0x1000,
  },
  esp32s2: {
    label: 'ESP32-S2',
    url: FW_BASE + 'ESP32_GENERIC_S2-20260406-v1.28.0.bin',
    flashAddress: 0x1000,
    resetMode: 'no_reset',
  },
  esp32s3: {
    label: 'ESP32-S3',
    url: FW_BASE + 'ESP32_GENERIC_S3-20251209-v1.27.0.bin',
    flashAddress: 0x0,
  },
  esp32c3: {
    label: 'ESP32-C3',
    url: FW_BASE + 'ESP32_GENERIC_C3-20260406-v1.28.0.bin',
    flashAddress: 0x0,
  },
};

const MICROBIT_FIRMWARE_OPTIONS = {
  v1: {
    label: 'micro:bit v1',
    url: FW_BASE + 'micropython-microbit-v1.1.1.hex',
  },
  v2: {
    label: 'micro:bit v2',
    url: FW_BASE + 'micropython-microbit-v2.1.1.hex',
  },
};

function showFlashOverlay() {
  flashLog.textContent = '';
  flashFill.style.width = '0%';
  flashPctText.textContent = '0%';
  flashCloseBtn.classList.add('hidden');
  flashOverlay.classList.remove('hidden');
}

function appendFlashLog(msg) {
  flashLog.textContent += msg;
  flashLog.scrollTop = flashLog.scrollHeight;
}

function setFlashProgress(pct) {
  flashFill.style.width = pct + '%';
  flashPctText.textContent = pct + '%';
}

flashCloseBtn.addEventListener('click', () => {
  flashOverlay.classList.add('hidden');
});

function openEsp32FlashChooser() {
  if (!esp32FlashOverlay) return;
  if (esp32FlashStatus) {
    esp32FlashStatus.textContent = '';
    esp32FlashStatus.className = 'hex-upload-status';
  }
  esp32FlashOverlay.classList.remove('hidden');
}

function closeEsp32FlashChooser() {
  if (!esp32FlashOverlay) return;
  esp32FlashOverlay.classList.add('hidden');
}

function openMicrobitFlashChooser() {
  if (!microbitFlashOverlay) return;
  if (microbitFlashStatus) {
    microbitFlashStatus.textContent = '';
    microbitFlashStatus.className = 'hex-upload-status';
  }
  microbitFlashOverlay.classList.remove('hidden');
}

function closeMicrobitFlashChooser() {
  if (!microbitFlashOverlay) return;
  microbitFlashOverlay.classList.add('hidden');
}

esp32FlashClose?.addEventListener('click', closeEsp32FlashChooser);

esp32FlashOverlay?.addEventListener('click', (event) => {
  if (event.target === esp32FlashOverlay) closeEsp32FlashChooser();
});
microbitFlashClose?.addEventListener('click', closeMicrobitFlashChooser);
microbitFlashOverlay?.addEventListener('click', (event) => {
  if (event.target === microbitFlashOverlay) closeMicrobitFlashChooser();
});

btnFlash.addEventListener('click', async () => {
  if (isArduinoDevice()) return;

  const device = getDevice();
  const fw = device.firmware;
  if (!fw) return;

  if (fw.flashMethod === 'webusb') {
    if (isConnected()) {
      await disconnect();
      setConnectedUI(false);
    }
    showFlashOverlay();
    try {
      await flashPicoFirmware(
        (msg) => appendFlashLog(msg),
        (pct) => setFlashProgress(pct),
        { firmwareUrl: fw.url },
      );
      setFlashProgress(100);
      appendFlashLog('\nFlash complete! You can now Connect to use the device.\n');
    } catch (err) {
      appendFlashLog(`\nFlash error: ${err.message}\n`);
    } finally {
      flashCloseBtn.classList.remove('hidden');
    }
    return;
  }

  if (fw.flashMethod === 'microbit') {
    openMicrobitFlashChooser();
    return;
  }

  if (canFlashInBrowser()) {
    openEsp32FlashChooser();
    return;
  }

  if (isConnected()) {
    await disconnect();
    setConnectedUI(false);
  }

  {
    const a = document.createElement('a');
    a.href = fw.url;
    a.download = fw.url.split('/').pop();
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    appendToTerminal(`\n--- Firmware: ${fw.label} ---\n`);
    appendToTerminal(`Downloading: ${a.download}\n`);
    if (fw.instructions) {
      appendToTerminal(`${fw.instructions}\n`);
    }
    appendToTerminal('After flashing, connect here to run code.\n');
  }
});

microbitFlashStart?.addEventListener('click', async () => {
  const variantKey = microbitFlashVariant?.value || 'v2';
  const variant = MICROBIT_FIRMWARE_OPTIONS[variantKey];
  if (!variant) {
    if (microbitFlashStatus) {
      microbitFlashStatus.textContent = 'Invalid micro:bit version selected.';
      microbitFlashStatus.className = 'hex-upload-status status-err';
    }
    return;
  }

  closeMicrobitFlashChooser();
  showFlashOverlay();
  appendFlashLog(`Selected target: ${variant.label}\n`);
  appendFlashLog('Put your micro:bit in bootloader mode if needed, then select the MICROBIT drive.\n');

  try {
    await flashFileToDrive(
      (msg) => appendFlashLog(msg),
      (pct) => setFlashProgress(pct),
      {
        firmwareUrl: variant.url,
        outputName: 'firmware.hex',
        driveHint: 'MICROBIT drive',
      },
    );
    setFlashProgress(100);
    appendFlashLog('\nFlash complete! You can now Connect to use the device.\n');
  } catch (err) {
    appendFlashLog(`\nFlash error: ${err.message}\n`);
  } finally {
    flashCloseBtn.classList.remove('hidden');
  }
});

esp32FlashStart?.addEventListener('click', async () => {
  if (isConnected()) {
    await disconnect();
    setConnectedUI(false);
    appendToTerminal('\n--- Disconnected for firmware flash ---\n');
  }

  const variantKey = esp32FlashVariant?.value || 'esp32s3';
  const variant = ESP32_FIRMWARE_OPTIONS[variantKey];
  if (!variant) {
    if (esp32FlashStatus) {
      esp32FlashStatus.textContent = 'Invalid board family selected.';
      esp32FlashStatus.className = 'hex-upload-status status-err';
    }
    return;
  }

  closeEsp32FlashChooser();
  showFlashOverlay();
  appendFlashLog(`Selected target: ${variant.label}\n`);

  try {
    await flashFirmware(
      (msg) => appendFlashLog(msg),
      (pct) => setFlashProgress(pct),
      {
        firmwareUrl: variant.url,
        targetLabel: variant.label,
        flashAddress: variant.flashAddress ?? 0x0,
        resetMode: variant.resetMode || 'default_reset',
      },
    );
    setFlashProgress(100);
    appendFlashLog('\nFlash complete! You can now Connect to use the device.\n');
  } catch (err) {
    appendFlashLog(`\nFlash error: ${err.message}\n`);
  } finally {
    flashCloseBtn.classList.remove('hidden');
  }
});

btnRun.addEventListener('click', async () => {
  if (isArduinoDevice()) {
    document.getElementById('hex-upload-overlay').classList.remove('hidden');
    document.getElementById('hex-upload-status').textContent = '';
    return;
  }

  const code = getGeneratedCode();
  if (!code.trim()) {
    appendToTerminal('\nNo code to run. Add some blocks!\n');
    return;
  }
  appendToTerminal('\n>>> Running...\n');
  showSendProgress('Sending code to device');
  try {
    await executeCode(code, {
      onProgress: (sent, total) => updateSendProgress(sent, total),
    });
  } catch (err) {
    appendToTerminal(`\nRun error: ${err.message}\n`);
  } finally {
    hideSendProgress();
  }
});

btnStop.addEventListener('click', async () => {
  try {
    await stopExecution();
    appendToTerminal('\n--- Stopped ---\n');
  } catch (err) {
    appendToTerminal(`\nStop error: ${err.message}\n`);
  }
});

btnSave.addEventListener('click', async () => {
  if (isArduinoDevice()) {
    const code = getGeneratedCode();
    if (!code.trim()) {
      appendToTerminal('\nNo code to download.\n');
      return;
    }
    const blob = new Blob([code], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'sketch.ino';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    appendToTerminal('\n--- Downloaded sketch.ino ---\n');
    return;
  }

  const code = getGeneratedCode();
  if (!code.trim()) {
    appendToTerminal('\nNo code to save.\n');
    return;
  }
  appendToTerminal('\nSaving to device as main.py...\n');
  showSendProgress('Saving to device');
  try {
    await saveToDevice(code, 'main.py', {
      onProgress: (sent, total) => updateSendProgress(sent, total),
    });
    const layoutFile = await saveCurrentWorkspaceToDevice('main');
    if (layoutFile) {
      appendToTerminal(`Saved Blockly layout to ${layoutFile}\n`);
      refreshProjects();
    }
  } catch (err) {
    appendToTerminal(`\nSave error: ${err.message}\n`);
  } finally {
    hideSendProgress();
  }
});

btnLoad.addEventListener('click', async () => {
  if (isArduinoDevice()) {
    appendToTerminal('\nLoad is not available for Arduino devices.\n');
    return;
  }
  appendToTerminal('\nLoading Blockly layout from device...\n');
  try {
    const layoutFile = await loadWorkspaceFromDevice('main');
    if (!layoutFile) {
      appendToTerminal('Load skipped: no layout name available.\n');
      return;
    }
    updateCodePreview();
    appendToTerminal(`Loaded Blockly layout from ${layoutFile}\n`);
    refreshProjects();
  } catch (err) {
    appendToTerminal(`\nLoad error: ${err.message}\n`);
  }
});

// ─── Projects Dialog ────────────────────────────────────

initProjectsDialog({
  workspace,
  captureDeviceOutput,
  executeCode,
  writeFileToDevice,
  isConnected,
});

initRobotPanel({ workspace, getDeviceId });

terminalInput.addEventListener('keydown', async (e) => {
  if (e.key === 'Enter') {
    const text = terminalInput.value;
    terminalInput.value = '';
    await writeString(text + '\r\n');
  }
});

// ─── Hex Upload Modal (Arduino STK500) ───────────────────

const hexOverlay = document.getElementById('hex-upload-overlay');
const hexCloseBtn = document.getElementById('hex-upload-close');
const hexUploadBtn = document.getElementById('hex-upload-btn');
const hexFileInput = document.getElementById('hex-file-input');
const hexBoardSelect = document.getElementById('hex-board-select');
const hexStatus = document.getElementById('hex-upload-status');

if (hexCloseBtn) {
  hexCloseBtn.addEventListener('click', () => {
    hexOverlay.classList.add('hidden');
  });
}

if (hexOverlay) {
  hexOverlay.addEventListener('click', (e) => {
    if (e.target === hexOverlay) hexOverlay.classList.add('hidden');
  });
}

if (hexUploadBtn) {
  hexUploadBtn.addEventListener('click', async () => {
    const file = hexFileInput.files[0];
    if (!file) {
      hexStatus.textContent = 'Select a .hex file first.';
      hexStatus.className = 'hex-upload-status status-err';
      return;
    }

    const boardKey = hexBoardSelect.value;
    hexOverlay.classList.add('hidden');

    // Grab the existing serial port before tearing down the text-mode connection
    const existingPort = getPort();
    const wasConnected = isConnected();

    if (wasConnected) {
      await disconnect();
      setConnectedUI(false);
      appendToTerminal('\n--- Disconnected for upload ---\n');
    }

    const hexString = await file.text();
    appendToTerminal(`\n--- Uploading ${file.name} (${boardKey})... ---\n`);
    showSendProgress('Uploading .hex');
    try {
      await uploadHex(hexString, {
        board: boardKey,
        port: existingPort || undefined,
        onProgress: (status, pct) => {
          sendProgressFill.style.width = Math.round(pct) + '%';
          sendProgressText.textContent = status;
          appendToTerminal(`  ${status} (${Math.round(pct)}%)\n`);
        },
      });
      appendToTerminal('--- Upload complete! ---\n');
    } catch (err) {
      appendToTerminal(`\nUpload error: ${err.message}\n`);
    } finally {
      hideSendProgress();
    }

    // Resume serial monitor on the same port (small delay for the board to reboot)
    if (existingPort) {
      await new Promise(r => setTimeout(r, 1500));
      try {
        await connect(115200, existingPort);
        setConnectedUI(true);
        appendToTerminal('--- Serial monitor resumed ---\n');
      } catch (err) {
        appendToTerminal(`--- Could not resume monitor: ${err.message} ---\n`);
      }
    }
  });
}

// ─── Addons Manager UI ──────────────────────────────────

const btnAddons = document.getElementById('btn-addons');
const addonsOverlay = document.getElementById('addons-overlay');
const addonsClose = document.getElementById('addons-close');
const addonFileInput = document.getElementById('addon-file-input');
const addonInstallBtn = document.getElementById('addon-install-btn');
const addonStatus = document.getElementById('addon-status');
const addonsList = document.getElementById('addons-list');

function renderAddonsList() {
  const addons = getInstalledAddons();
  addonsList.innerHTML = '';
  if (!addons.length) {
    addonsList.innerHTML = '<li class="addons-empty">No addons installed</li>';
    return;
  }
  for (const addon of addons) {
    const li = document.createElement('li');
    li.className = 'addons-item';
    const nameSpan = document.createElement('span');
    nameSpan.className = 'addons-item-name';
    nameSpan.textContent = addon.name;
    const removeBtn = document.createElement('button');
    removeBtn.className = 'addons-item-remove';
    removeBtn.textContent = 'Remove';
    removeBtn.title = 'Remove addon (reload required)';
    removeBtn.addEventListener('click', () => {
      removeAddon(addon.name);
      renderAddonsList();
      addonStatus.textContent = `"${addon.name}" removed. Reload the page to apply.`;
      addonStatus.className = 'addons-status status-warn';
    });
    li.appendChild(nameSpan);
    li.appendChild(removeBtn);
    addonsList.appendChild(li);
  }
}

btnAddons.addEventListener('click', () => {
  addonsOverlay.classList.remove('hidden');
  renderAddonsList();
  addonStatus.textContent = '';
  addonStatus.className = 'addons-status';
});

addonsClose.addEventListener('click', () => {
  addonsOverlay.classList.add('hidden');
});

addonsOverlay.addEventListener('click', (e) => {
  if (e.target === addonsOverlay) addonsOverlay.classList.add('hidden');
});

addonInstallBtn.addEventListener('click', async () => {
  const file = addonFileInput.files[0];
  if (!file) {
    addonStatus.textContent = 'Select a .js file first.';
    addonStatus.className = 'addons-status status-err';
    return;
  }
  try {
    const name = await installAddonFromFile(file);
    addonStatus.textContent = `"${name}" installed successfully!`;
    addonStatus.className = 'addons-status status-ok';
    renderAddonsList();
    addonFileInput.value = '';
  } catch (err) {
    addonStatus.textContent = `Error: ${err.message}`;
    addonStatus.className = 'addons-status status-err';
  }
});
