/* ==========================================================================
   Core: store, transport, notifications, formatting.

   Every URL here is derived from window.location, so the dashboard works from
   whatever address you happened to open it on - laptop, phone, tablet, or a
   hostname. Nothing is hardcoded.
   ========================================================================== */

export const API = `${location.origin}`;
export const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;

/* -- Store ---------------------------------------------------------------- */

class Store {
  constructor() {
    this.state = null;        // live robot state
    this.spec = null;         // constants from the backend
    this.settings = null;
    this.plugins = { plugins: [], errors: [] };
    this.bridge = null;
    this.network = null;
    this.server = null;
    this.events = [];
    this.connected = false;
    this._subs = new Map();
    this._nextId = 1;
  }

  on(channel, fn) {
    if (!this._subs.has(channel)) this._subs.set(channel, new Map());
    const id = this._nextId++;
    this._subs.get(channel).set(id, fn);
    return () => this._subs.get(channel)?.delete(id);
  }

  emit(channel, payload) {
    const subs = this._subs.get(channel);
    if (!subs) return;
    for (const fn of subs.values()) {
      try { fn(payload); } catch (err) { console.error(`[${channel}]`, err); }
    }
  }
}

export const store = new Store();

/* -- HTTP ----------------------------------------------------------------- */

export async function api(path, options = {}) {
  const { method = 'GET', body, timeout = 15000 } = options;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    const res = await fetch(`${API}${path}`, {
      method,
      headers: body ? { 'Content-Type': 'application/json' } : undefined,
      body: body ? JSON.stringify(body) : undefined,
      signal: controller.signal,
    });

    const text = await res.text();
    let data = null;
    try { data = text ? JSON.parse(text) : null; } catch { data = { detail: text }; }

    if (!res.ok) {
      const message = data?.detail || data?.message || `${res.status} ${res.statusText}`;
      const error = new Error(typeof message === 'string' ? message : JSON.stringify(message));
      error.status = res.status;
      error.data = data;
      throw error;
    }
    return data;
  } catch (err) {
    if (err.name === 'AbortError') throw new Error(`Request timed out after ${timeout / 1000}s`);
    throw err;
  } finally {
    clearTimeout(timer);
  }
}

export const get  = (path)       => api(path);
export const post = (path, body) => api(path, { method: 'POST', body: body ?? {} });

/**
 * POST that reports its own outcome as a toast. Returns the payload on success
 * and null on failure, so callers can `if (!await command(...)) return;`.
 */
export async function command(path, body, { silent = false, successTitle } = {}) {
  try {
    const data = await post(path, body);
    if (!silent) toast(successTitle || 'Done', data?.message || '', 'good');
    return data;
  } catch (err) {
    if (!silent) toast('Command failed', err.message, 'critical');
    return null;
  }
}

/* -- WebSocket ------------------------------------------------------------ */

class Socket {
  constructor() {
    this.ws = null;
    this.attempts = 0;
    this.closing = false;
    this._pending = null;
    this._pendingTimer = null;
  }

  connect() {
    if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) return;
    this.closing = false;

    try {
      this.ws = new WebSocket(WS_URL);
    } catch (err) {
      this._retry();
      return;
    }

    this.ws.onopen = () => {
      this.attempts = 0;
      store.connected = true;
      store.emit('link', { connected: true });
    };

    this.ws.onmessage = (event) => {
      let message;
      try { message = JSON.parse(event.data); } catch { return; }
      this._route(message);
    };

    this.ws.onclose = () => {
      store.connected = false;
      store.emit('link', { connected: false });
      if (!this.closing) this._retry();
    };

    this.ws.onerror = () => { /* onclose handles recovery */ };
  }

  _route(message) {
    const { type, data } = message;
    switch (type) {
      case 'state':
        store.state = data;
        store.emit('state', data);
        break;
      case 'event':
        store.events.push(data);
        if (store.events.length > 500) store.events.splice(0, store.events.length - 500);
        store.emit('event', data);
        if (data.level === 'error') toast('Robot', data.message, 'critical');
        else if (data.level === 'warn') toast('Robot', data.message, 'warning');
        break;
      case 'plugins':
        store.plugins = data;
        store.emit('plugins', data);
        break;
      case 'settings':
        store.settings = data;
        store.emit('settings', data);
        break;
      case 'scan_progress':
        store.emit('scan_progress', data);
        break;
      case 'command_error':
        store.emit('command_error', data);
        break;
      default:
        store.emit(type, data);
    }
  }

  _retry() {
    // Back off to 8 s so a robot that is off overnight does not spam the network.
    const delay = Math.min(8000, 500 * Math.pow(1.6, this.attempts++));
    setTimeout(() => this.connect(), delay);
  }

  send(type, data) {
    if (this.ws?.readyState !== WebSocket.OPEN) return false;
    this.ws.send(JSON.stringify({ type, data }));
    return true;
  }

  /**
   * Rate-limited send for the joystick. Coalesces to ~25 Hz so dragging never
   * floods the socket, but always delivers the final position.
   */
  sendThrottled(type, data, interval = 40) {
    this._pending = { type, data };
    if (this._pendingTimer) return;
    const flush = () => {
      if (!this._pending) { this._pendingTimer = null; return; }
      const { type: t, data: d } = this._pending;
      this._pending = null;
      this.send(t, d);
      this._pendingTimer = setTimeout(flush, interval);
    };
    flush();
  }
}

export const socket = new Socket();

/* -- Toasts --------------------------------------------------------------- */

const toastHost = () => document.getElementById('toasts');

export function toast(title, message = '', tone = 'default', ttl = 4200) {
  const host = toastHost();
  if (!host) return;

  const node = document.createElement('div');
  node.className = 'toast';
  node.dataset.tone = tone;
  node.innerHTML = `
    <div class="toast-body">
      <div class="toast-title"></div>
      ${message ? '<div class="toast-msg"></div>' : ''}
    </div>`;
  node.querySelector('.toast-title').textContent = title;
  if (message) node.querySelector('.toast-msg').textContent = message;

  host.appendChild(node);
  while (host.children.length > 5) host.firstElementChild.remove();

  setTimeout(() => {
    node.classList.add('leaving');
    setTimeout(() => node.remove(), 200);
  }, ttl);
}

/* -- Confirm modal -------------------------------------------------------- */

export function confirmDialog(title, body, { confirmLabel = 'Confirm', danger = true } = {}) {
  return new Promise((resolve) => {
    const backdrop = document.getElementById('modal-backdrop');
    const confirmBtn = document.getElementById('modal-confirm');
    const cancelBtn = document.getElementById('modal-cancel');

    document.getElementById('modal-title').textContent = title;
    document.getElementById('modal-body').textContent = body;
    confirmBtn.textContent = confirmLabel;
    confirmBtn.className = danger ? 'btn btn-danger' : 'btn btn-primary';
    backdrop.hidden = false;
    confirmBtn.focus();

    const finish = (value) => {
      backdrop.hidden = true;
      confirmBtn.removeEventListener('click', onYes);
      cancelBtn.removeEventListener('click', onNo);
      backdrop.removeEventListener('click', onBackdrop);
      document.removeEventListener('keydown', onKey);
      resolve(value);
    };
    const onYes = () => finish(true);
    const onNo = () => finish(false);
    const onBackdrop = (e) => { if (e.target === backdrop) finish(false); };
    const onKey = (e) => { if (e.key === 'Escape') finish(false); };

    confirmBtn.addEventListener('click', onYes);
    cancelBtn.addEventListener('click', onNo);
    backdrop.addEventListener('click', onBackdrop);
    document.addEventListener('keydown', onKey);
  });
}

/* -- Formatting ----------------------------------------------------------- */

export const RAD2DEG = 180 / Math.PI;
export const DEG2RAD = Math.PI / 180;

export function num(value, precision = 2, fallback = '—') {
  if (value === null || value === undefined || Number.isNaN(Number(value))) return fallback;
  return Number(value).toFixed(precision);
}

export function compact(value) {
  if (value === null || value === undefined) return '—';
  const abs = Math.abs(value);
  if (abs >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
  if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
  if (abs >= 1e4) return `${(value / 1e3).toFixed(1)}K`;
  return value.toLocaleString(undefined, { maximumFractionDigits: 0 });
}

export function clockTime(ts) {
  const d = new Date(ts * 1000);
  return d.toLocaleTimeString([], { hour12: false });
}

export function duration(seconds) {
  if (!seconds || seconds < 0) return '—';
  const s = Math.floor(seconds % 60);
  const m = Math.floor((seconds / 60) % 60);
  const h = Math.floor(seconds / 3600);
  if (h) return `${h}h ${m}m`;
  if (m) return `${m}m ${s}s`;
  return `${s}s`;
}

export function batteryTone(pct) {
  if (pct === null || pct === undefined) return 'default';
  if (pct <= 10) return 'critical';
  if (pct <= 25) return 'warning';
  return 'good';
}

export function tempTone(celsius, warn = 55, crit = 70) {
  if (celsius === null || celsius === undefined) return 'default';
  if (celsius >= crit) return 'critical';
  if (celsius >= warn) return 'warning';
  return 'good';
}

export function throttle(fn, ms) {
  let last = 0, timer = null, queued = null;
  return (...args) => {
    const now = Date.now();
    queued = args;
    if (now - last >= ms) { last = now; fn(...queued); queued = null; return; }
    if (timer) return;
    timer = setTimeout(() => {
      timer = null; last = Date.now();
      if (queued) { fn(...queued); queued = null; }
    }, ms - (now - last));
  };
}

export function debounce(fn, ms) {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

/* Series colours, read from CSS so the theme toggle stays authoritative. */
export function seriesColor(index) {
  const styles = getComputedStyle(document.documentElement);
  return styles.getPropertyValue(`--series-${(index % 8) + 1}`).trim() || '#3987e5';
}

export function cssVar(name) {
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
