/**
 * api.mjs — Centralized REST + WebSocket + SSE client
 */

import store from './store.mjs';
import eventBus, { EVENTS } from './event-bus.mjs';

class ApiClient {
  constructor() {
    this._token = null;
    this._baseUrl = '';
    this._ws = null;
    this._wsReconnectDelay = 1000;
    this._wsMaxDelay = 30000;
    this._wsReconnectTimer = null;
    this._intentionalClose = false;
    this._chatSessionId = null;
  }

  init() {
    // Auth token from meta tag injected by server.mjs
    const meta = document.querySelector('meta[name="auth-token"]');
    this._token = meta?.content || new URLSearchParams(location.search).get('token') || '';
    this._baseUrl = location.origin;
  }

  // --- REST helpers ---

  async _fetch(path, opts = {}) {
    const url = `${this._baseUrl}${path}`;
    const headers = {
      'Authorization': `Bearer ${this._token}`,
      ...(opts.headers || {})
    };
    if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
      headers['Content-Type'] = 'application/json';
      opts.body = JSON.stringify(opts.body);
    }
    const res = await fetch(url, { ...opts, headers });
    if (!res.ok) {
      const text = await res.text().catch(() => '');
      throw new Error(`API ${opts.method || 'GET'} ${path}: ${res.status} ${text}`);
    }
    const ct = res.headers.get('content-type') || '';
    return ct.includes('json') ? res.json() : res.text();
  }

  get(path) { return this._fetch(path); }
  post(path, body) { return this._fetch(path, { method: 'POST', body }); }
  put(path, body) { return this._fetch(path, { method: 'PUT', body }); }
  del(path) { return this._fetch(path, { method: 'DELETE' }); }

  // --- Chat ---

  clearChatSession() { this._chatSessionId = null; }

  async clearAllChat() {
    return this.post('/api/chat/clear-all', {});
  }

  async sendMessage(message) {
    return this.post('/api/chat', { message });
  }

  async streamMessage(message) {
    const controller = new AbortController();
    store.dispatch('SET_STREAM_ABORT', controller);
    store.dispatch('SET_STREAMING', true);

    try {
      const res = await fetch(`${this._baseUrl}/api/chat/stream`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this._token}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ message, sessionId: this._chatSessionId }),
        signal: controller.signal
      });

      if (!res.ok) {
        // Fallback to non-streaming
        store.dispatch('SET_STREAMING', false);
        return this.sendMessage(message);
      }

      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      let accumulated = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop();

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));
              // Capture session ID for continuity across messages
              if (data.sessionId && !this._chatSessionId) {
                this._chatSessionId = data.sessionId;
              }
              if (data.error) {
                // Server-side error (429 rate limit, budget exhausted, etc.)
                eventBus.emit(EVENTS.CHAT_STREAM_ERROR, { error: data.error });
              }
              if (data.text) {
                accumulated = data.text;
                store.dispatch('APPEND_STREAM', accumulated);
                eventBus.emit(EVENTS.CHAT_STREAM_CHUNK, { text: accumulated });
              }
              if (data.id) {
                // Stream complete
                eventBus.emit(EVENTS.CHAT_STREAM_END, { id: data.id, text: accumulated });
              }
            } catch { /* ignore malformed */ }
          }
        }
      }
    } catch (err) {
      if (err.name !== 'AbortError') {
        console.error('[api] Stream error:', err);
        // Fallback to non-streaming
        store.dispatch('SET_STREAMING', false);
        return this.sendMessage(message);
      }
    } finally {
      store.dispatch('SET_STREAMING', false);
      store.dispatch('SET_STREAM_ABORT', null);
    }
  }

  cancelStream() {
    const controller = store.getState().chat.streamAbortController;
    if (controller) controller.abort();
  }

  // --- Data endpoints ---

  getEvents(limit = 50) { return this.get(`/api/events?limit=${limit}`); }
  getConfig() { return this.get('/api/config'); }
  updateConfig(updates) { return this.put('/api/config', updates); }
  getUsage() { return this.get('/api/usage'); }
  getTasks() { return this.get('/api/tasks'); }
  cancelTask(id) { return this.post(`/api/tasks/${id}/cancel`); }
  pauseLoop() { return this.post('/api/loop/pause'); }
  resumeLoop() { return this.post('/api/loop/resume'); }

  // Persona
  getPersona() { return this.get('/api/persona'); }
  updatePersona(updates) { return this.put('/api/persona', updates); }

  // Skills
  getSkillsPalette() { return this.get('/api/skills/palette'); }
  toggleSkill(name, enabled) { return this.post(`/api/skills/${encodeURIComponent(name)}/toggle`, { enabled }); }

  // Workspaces
  getWorkspaces() { return this.get('/api/workspaces'); }
  createWorkspace(data) { return this.post('/api/workspaces', data); }
  activateWorkspace(id) { return this.put(`/api/workspaces/${id}/activate`); }
  deleteWorkspace(id) { return this.del(`/api/workspaces/${id}`); }

  // Knowledge Graph
  searchEntities(q) { return this.get(`/api/kg/entities?q=${encodeURIComponent(q)}`); }
  getEntityRelations(id) { return this.get(`/api/kg/entities/${id}/relations`); }
  getKGStats() { return this.get('/api/kg/stats'); }
  getKGGraph(limit = 150) { return this.get(`/api/kg/graph?limit=${limit}`); }
  exportKG() { return this.get('/api/kg/export'); }
  importKG(data) { return this.post('/api/kg/import', data); }

  // Ingestion
  ingest(data) { return this.post('/api/ingest', data); }
  getIngestionQueue() { return this.get('/api/ingest/queue'); }

  // Voice
  getVoiceStatus() { return this.get('/api/voice/status'); }
  toggleVoice() { return this.post('/api/voice/toggle'); }
  voiceListen() { return this.post('/api/voice/listen'); }
  getGrokToken() { return this.post('/api/voice/grok-token'); }
  syncGrokConversation(conversation) { return this.post('/api/voice/grok-sync', { conversation }); }
  executeGrokTool(tool, args) { return this.post('/api/voice/grok-tool', { tool, args }); }

  // Cognitive
  getCognitiveLogs(module, limit = 20) {
    const params = new URLSearchParams({ limit });
    if (module) params.set('module', module);
    return this.get(`/api/cognitive/logs?${params}`);
  }

  // Tools
  getTools() { return this.get('/api/tools'); }

  // Scheduled Tasks
  getSchedules() { return this.get('/api/schedules'); }
  createSchedule(data) { return this.post('/api/schedules', data); }
  updateSchedule(id, data) { return this.put(`/api/schedules/${id}`, data); }
  deleteSchedule(id) { return this.del(`/api/schedules/${id}`); }

  // Files
  getWorkspaceFiles() { return this.get('/api/workspace-files'); }
  getWorkspaceRoot() { return this.get('/api/workspace-root'); }
  downloadFile(name) {
    const url = `${this._baseUrl}/api/workspace-files/${encodeURIComponent(name)}/download?token=${this._token}`;
    const a = document.createElement('a');
    a.href = url;
    a.download = name;
    a.click();
  }

  // Update check
  checkForUpdates(force = false) { return this.get(`/api/update-check${force ? '?force=true' : ''}`); }

  // --- WebSocket ---

  connectWebSocket() {
    if (this._ws?.readyState === WebSocket.OPEN) return;
    this._intentionalClose = false;

    const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    const url = `${proto}//${location.host}/ws?token=${this._token}`;

    try {
      this._ws = new WebSocket(url);
    } catch (err) {
      console.error('[api] WebSocket creation error:', err);
      this._scheduleReconnect();
      return;
    }

    this._ws.onopen = () => {
      console.log('[api] WebSocket connected');
      this._wsReconnectDelay = 1000;
      eventBus.emit(EVENTS.WS_CONNECTED);
    };

    this._ws.onclose = () => {
      console.log('[api] WebSocket closed');
      eventBus.emit(EVENTS.WS_DISCONNECTED);
      if (!this._intentionalClose) this._scheduleReconnect();
    };

    this._ws.onerror = (err) => {
      console.error('[api] WebSocket error:', err);
    };

    this._ws.onmessage = (evt) => {
      try {
        const msg = JSON.parse(evt.data);
        this._handleWsMessage(msg);
      } catch { /* ignore malformed */ }
    };
  }

  disconnectWebSocket() {
    this._intentionalClose = true;
    clearTimeout(this._wsReconnectTimer);
    if (this._ws) {
      this._ws.close();
      this._ws = null;
    }
  }

  _scheduleReconnect() {
    clearTimeout(this._wsReconnectTimer);
    this._wsReconnectTimer = setTimeout(() => {
      console.log(`[api] Reconnecting WebSocket (delay: ${this._wsReconnectDelay}ms)`);
      this.connectWebSocket();
    }, this._wsReconnectDelay);
    this._wsReconnectDelay = Math.min(this._wsReconnectDelay * 2, this._wsMaxDelay);
  }

  _handleWsMessage(msg) {
    eventBus.emit(EVENTS.WS_MESSAGE, msg);

    switch (msg.type) {
      case 'status':
        store.dispatch('SET_AGENT_STATUS', msg.data || msg);
        eventBus.emit(EVENTS.AGENT_STATUS, msg.data || msg);
        break;

      case 'event': {
        // DB events use 'type' for the event kind (e.g. type:'chat'),
        // while broadcastActivity uses 'kind'. Normalise to 'kind'.
        const evtKind = msg.data?.kind || msg.data?.type;
        const evtSource = msg.data?.source;

        // Agent chat replies → add to chat store + emit for TTS
        if (evtKind === 'chat' && evtSource === 'agent') {
          console.log('[api] Agent chat reply via WS:', (msg.data?.content || '').slice(0, 80));
          store.dispatch('ADD_MESSAGE', {
            role: 'agent',
            content: msg.data.content,
            timestamp: msg.data.timestamp || msg.data.created_at || Date.now()
          });
          eventBus.emit(EVENTS.CHAT_MESSAGE, {
            ...msg.data,
            role: 'agent',
            source: 'agent'
          });
        }

        // User chat messages arriving via WS (e.g. from voice-activation server-side inject
        // or another browser tab). Skip if already added locally by voice-orb or chat.mjs.
        if (evtKind === 'chat' && evtSource === 'user') {
          const existing = store.getState().chat.messages;
          const content = msg.data?.content;
          const isDuplicate = existing.some(m =>
            m.role === 'user' && m.content === content &&
            (Date.now() - (m.timestamp || 0)) < 10000
          );
          if (!isDuplicate) {
            store.dispatch('ADD_MESSAGE', {
              role: 'user',
              content: msg.data.content,
              timestamp: msg.data.timestamp || msg.data.created_at || Date.now()
            });
          }
        }

        // Grok voice messages relayed to main chat via send_message tool.
        // Display with the correct role but don't trigger agent processing
        // (the DB source is 'grok-voice', not 'user', so agent ignores them).
        if (evtKind === 'chat' && evtSource === 'grok-voice') {
          const meta = typeof msg.data?.metadata === 'string'
            ? JSON.parse(msg.data.metadata) : msg.data?.metadata;
          const displayRole = meta?.displayRole || 'agent';
          store.dispatch('ADD_MESSAGE', {
            role: displayRole,
            content: msg.data.content,
            timestamp: msg.data.timestamp || msg.data.created_at || Date.now(),
            source: 'grok-voice'
          });
        }

        store.dispatch('ADD_ACTIVITY', {
          kind: evtKind,
          source: evtSource,
          content: msg.data?.content,
          timestamp: msg.data?.timestamp || msg.data?.created_at || Date.now(),
          metadata: msg.data?.metadata
        });
        eventBus.emit(EVENTS.ACTIVITY_EVENT, msg.data);
        break;
      }

      case 'stream':
        store.dispatch('APPEND_STREAM', msg.data?.text || '');
        eventBus.emit(EVENTS.CHAT_STREAM_CHUNK, msg.data);
        break;

      case 'activity':
        store.dispatch('ADD_ACTIVITY', msg.data);
        eventBus.emit(EVENTS.ACTIVITY_EVENT, msg.data);
        break;

      case 'skill_start':
        store.dispatch('SET_SKILL_RUNNING', msg.data?.name);
        eventBus.emit(EVENTS.SKILL_START, msg.data);
        break;

      case 'skill_complete':
        store.dispatch('SET_SKILL_RUNNING', null);
        eventBus.emit(EVENTS.SKILL_COMPLETE, msg.data);
        break;

      case 'voice': {
        const voiceState = msg.data?.state || 'idle';
        // Only update store voice status if the browser isn't actively
        // using Web Speech. The voice-orb manages its own state when active.
        // Server states like 'detecting', 'injecting' are server-internal
        // and shouldn't change the orb UI.
        const currentStatus = store.getState().voice.status;
        const browserIsActive = currentStatus === 'listening' || currentStatus === 'processing' || currentStatus === 'speaking';
        if (!browserIsActive) {
          store.dispatch('SET_VOICE_STATUS', voiceState);
        }
        eventBus.emit(EVENTS.VOICE_STATE_CHANGE, msg.data);

        // When server-side wake word triggers listening, switch to chat view
        // and notify the user so they know the agent heard the wake word.
        if (voiceState === 'listening' && !browserIsActive) {
          store.dispatch('SET_VIEW', 'chat');
          eventBus.emit(EVENTS.NOTIFICATION, {
            message: 'Wake word detected \u2014 listening\u2026',
            type: 'info'
          });
        }
        break;
      }

      default:
        break;
    }
  }
}

// Singleton
const api = new ApiClient();
export default api;
