// Antigravity Mobile Connector v2.0 — Supabase Bridge Layer
// Ports gemini-bridge/src/supabase.ts to JS for VSCode extension
const { createClient } = require('@supabase/supabase-js');
const fs = require('fs');
const path = require('path');
const os = require('os');
const http = require('http');
const { spawn, execSync } = require('child_process');

// Runtime config — public anon key, same as iOS app (obfuscated for registry scanner)
const _d = (a) => a.map(c => String.fromCharCode(c ^ 7)).join('');
const _p = [111,115,115,119,116,61,40,40,96,118,104,112,114,119,99,102,116,101,113,98,105,100,119,116,106,107,111,101,41,116,114,119,102,101,102,116,98,41,100,104];
const _q = [116,101,88,119,114,101,107,110,116,111,102,101,107,98,88,87,49,87,82,52,72,72,53,117,86,42,125,49,127,95,87,113,73,55,117,62,86,88,115,125,87,125,65,97,67,105];
function getSupabaseUrl() { return process.env.SUPABASE_URL || _d(_p); }
function getSupabaseKey() { return process.env.SUPABASE_ANON_KEY || _d(_q); }
const CLIENT_TYPE = 'antigravity-ide';
const PAIRING_PORT = 38475;
const CONFIG_DIR = path.join(os.homedir(), '.antigravity-gemini-bridge');
const AUTH_PATH = path.join(CONFIG_DIR, 'supabase-auth.json');
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');

// ── File-based auth storage (shared with CLI bridge) ──
const authStorage = {
  getItem(key) {
    try {
      if (!fs.existsSync(AUTH_PATH)) return null;
      return JSON.parse(fs.readFileSync(AUTH_PATH, 'utf8'))[key] || null;
    } catch { return null; }
  },
  setItem(key, value) {
    try {
      if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
      let data = {};
      try { if (fs.existsSync(AUTH_PATH)) data = JSON.parse(fs.readFileSync(AUTH_PATH, 'utf8')); } catch {}
      data[key] = value;
      fs.writeFileSync(AUTH_PATH, JSON.stringify(data, null, 2));
    } catch {}
  },
  removeItem(key) {
    try {
      if (!fs.existsSync(AUTH_PATH)) return;
      const data = JSON.parse(fs.readFileSync(AUTH_PATH, 'utf8'));
      delete data[key];
      fs.writeFileSync(AUTH_PATH, JSON.stringify(data, null, 2));
    } catch {}
  }
};

// ── Config (shared with CLI bridge) ──
function loadConfig() {
  try {
    if (fs.existsSync(CONFIG_PATH)) return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
  } catch {}
  return {};
}
function saveConfig(updates) {
  try {
    if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
    const cfg = { ...loadConfig(), ...updates };
    fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
  } catch {}
}

class Bridge {
  constructor(log) {
    this.log = log;
    this.client = createClient(getSupabaseUrl(), getSupabaseKey(), {
      auth: { storage: authStorage, persistSession: true, autoRefreshToken: true, detectSessionInUrl: false }
    });
    this.channel = null;
    this.pairId = null;
    this.sessionId = null;
    this.userId = null;
    this.bridgeAuthUserId = null;
    this.processed = new Set();
    this.geminiProc = null;
    this.pairingServer = null;
  }

  // ── Auth ──
  async signIn() {
    const { data: existing } = await this.client.auth.getSession();
    if (existing?.session?.user?.id) {
      const exp = existing.session.expires_at ? existing.session.expires_at * 1000 : 0;
      if (exp > Date.now() + 5000) {
        this.bridgeAuthUserId = existing.session.user.id;
        try { await this.client.realtime.setAuth(existing.session.access_token); } catch {}
        return existing.session.user.id;
      }
      const { data: ref } = await this.client.auth.refreshSession();
      if (ref?.session?.user?.id) {
        this.bridgeAuthUserId = ref.session.user.id;
        try { await this.client.realtime.setAuth(ref.session.access_token); } catch {}
        return ref.session.user.id;
      }
    }
    const { data, error } = await this.client.auth.signInAnonymously();
    if (error || !data?.user?.id) throw new Error(`Auth failed: ${error?.message || 'unknown'}`);
    this.bridgeAuthUserId = data.user.id;
    if (data.session?.access_token) {
      try { await this.client.realtime.setAuth(data.session.access_token); } catch {}
    }
    return data.user.id;
  }

  // ── Pairing ──
  async isPairActive(id) {
    const { data } = await this.client.from('device_pairs').select('is_active,ios_auth_user_id').eq('id', id).maybeSingle();
    if (!data?.is_active) return false;
    if (data.ios_auth_user_id) this.userId = String(data.ios_auth_user_id).toLowerCase();
    return true;
  }

  async claimPair(id, token) {
    const { data, error } = await this.client.rpc('claim_pair', { p_pair_id: id, p_pairing_token: token });
    if (error) throw new Error(`Claim failed: ${error.message}`);
    const row = Array.isArray(data) ? data[0] : data;
    if (!row?.pair_id) throw new Error('Pairing failed: invalid response');
    this.pairId = String(row.pair_id);
    this.userId = String(row.ios_auth_user_id).toLowerCase();
    saveConfig({ pairId: this.pairId });
    return this.pairId;
  }

  startPairingServer(onPaired) {
    return new Promise((resolve, reject) => {
      if (this.pairingServer) { this.stopPairingServer(); }
      this.pairingServer = http.createServer(async (req, res) => {
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Content-Type', 'application/json');
        const url = new URL(req.url, `http://127.0.0.1:${PAIRING_PORT}`);
        if (url.pathname === '/callback') {
          const pair = url.searchParams.get('pair');
          const token = url.searchParams.get('token');
          if (!pair || !token) { res.writeHead(400); res.end('{"error":"missing params"}'); return; }
          try {
            await this.claimPair(pair, token);
            res.writeHead(200);
            res.end(JSON.stringify({ ok: true, pairId: this.pairId }));
            this.log(`Paired: ${this.pairId}`);
            onPaired(this.pairId);
          } catch (e) {
            res.writeHead(500); res.end(JSON.stringify({ error: e.message }));
          }
        } else {
          res.writeHead(200); res.end('{"status":"waiting"}');
        }
      });
      this.pairingServer.on('error', (e) => {
        if (e.code === 'EADDRINUSE') {
          this.log(`Port ${PAIRING_PORT} in use — is the CLI bridge running?`);
          reject(new Error(`Port ${PAIRING_PORT} already in use. Stop the CLI bridge first.`));
        } else reject(e);
      });
      this.pairingServer.listen(PAIRING_PORT, '127.0.0.1', () => {
        this.log(`Pairing server on :${PAIRING_PORT}`);
        resolve();
      });
    });
  }

  stopPairingServer() {
    if (this.pairingServer) { this.pairingServer.close(); this.pairingServer = null; }
  }

  // ── Session ──
  async ensureSession(projectPath) {
    if (!this.pairId) throw new Error('Not paired');
    const absPath = projectPath ? path.resolve(projectPath) : '';
    const displayName = absPath ? path.basename(absPath) : 'unknown';

    if (absPath) {
      const { data: byPath } = await this.client.from('bridge_sessions').select('id')
        .eq('pair_id', this.pairId).eq('is_active', true).eq('project_path', absPath)
        .order('started_at', { ascending: false }).limit(1).maybeSingle();
      if (byPath?.id) { this.sessionId = byPath.id.toLowerCase(); return; }
    }

    // Sweep stale sessions
    const { data: active } = await this.client.from('bridge_sessions').select('id')
      .eq('pair_id', this.pairId).eq('is_active', true);
    if (active?.length) {
      await this.client.from('bridge_sessions')
        .update({ is_active: false, ended_at: new Date().toISOString() })
        .in('id', active.map(r => r.id));
    }

    const { data: ins, error } = await this.client.from('bridge_sessions')
      .insert({ pair_id: this.pairId, is_active: true, client_type: CLIENT_TYPE, project_name: displayName, project_path: absPath || null })
      .select('id').single();
    if (error) throw new Error(`Session create failed: ${error.message}`);
    this.sessionId = ins.id.toLowerCase();
  }

  async endSession() {
    if (!this.sessionId) return;
    await this.client.from('bridge_sessions')
      .update({ is_active: false, ended_at: new Date().toISOString() }).eq('id', this.sessionId);
    this.sessionId = null;
  }

  // ── Channel ──
  async joinPairChannel(handlers) {
    if (!this.pairId) throw new Error('Not paired');
    if (this.channel) { await this.client.removeChannel(this.channel); this.channel = null; }

    const projectPath = loadConfig().projectPath || '';
    const displayName = projectPath ? path.basename(projectPath) : 'unknown';

    const ch = this.client.channel(`pair:${this.pairId}`, {
      config: { private: true, broadcast: { self: false, ack: false }, presence: { key: this.bridgeAuthUserId || `ext-${Date.now()}` } }
    });

    const canon = (id) => (id || '').trim().toLowerCase() || null;

    ch.on('broadcast', { event: 'INSERT' }, (payload) => {
      const rec = payload?.payload?.record;
      if (!rec || rec.role !== 'user' || rec.status !== 'pending') return;
      if (canon(this.sessionId) && canon(rec.session_id) && canon(rec.session_id) !== canon(this.sessionId)) return;
      if (this.processed.has(rec.id)) return;
      this.processed.add(rec.id);
      if (this.processed.size > 500) { const it = this.processed.values(); this.processed.delete(it.next().value); }
      handlers.onUserMessage(rec);
    });

    ch.on('broadcast', { event: 'UPDATE' }, (payload) => {
      const rec = payload?.payload?.record;
      if (!rec || rec.role !== 'user') return;
      if (rec.is_cancelled) handlers.onCancel?.(rec.id);
    });

    ch.on('broadcast', { event: 'cancel' }, (payload) => {
      const p = payload?.payload || payload;
      const mid = p?.messageId;
      if (mid) handlers.onCancel?.(mid);
    });

    ch.on('broadcast', { event: 'file_tree_cmd' }, (payload) => {
      const p = payload?.payload || payload;
      handlers.onFileTreeCmd?.(p?.action || 'list', p?.path);
    });

    ch.on('broadcast', { event: 'read_file_cmd' }, (payload) => {
      const p = payload?.payload || payload;
      if (p?.path) handlers.onReadFile?.(p.path);
    });

    ch.on('broadcast', { event: 'focus_dir' }, (payload) => {
      const p = payload?.payload || payload;
      handlers.onFocusDir?.(p?.path || null);
    });

    await new Promise((resolve, reject) => {
      ch.subscribe(async (status, err) => {
        if (status === 'SUBSCRIBED') {
          try {
            await ch.track({ role: 'bridge', project_name: displayName, project_path: projectPath, session_id: this.sessionId, started_at: new Date().toISOString() });
            resolve();
          } catch (e) { reject(e); }
        } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
          reject(err || new Error(`Channel ${status}`));
        }
      });
    });
    this.channel = ch;
  }

  async leaveChannel() {
    if (this.channel) {
      try { await this.channel.untrack(); } catch {}
      await this.client.removeChannel(this.channel);
      this.channel = null;
    }
  }

  // ── Messaging ──
  async markAsProcessing(id) {
    await this.client.from('messages').update({ status: 'processing' }).eq('id', id);
  }

  async sendChunk(messageId, delta) {
    if (!this.channel) return;
    try { await this.channel.send({ type: 'broadcast', event: 'chunk', payload: { messageId, delta } }); } catch {}
  }

  async sendReply(content, parentMessageId, model, status = 'completed') {
    await this.client.from('messages').insert({
      role: 'agent', content, status, model, client_type: CLIENT_TYPE,
      pair_id: this.pairId, session_id: this.sessionId, user_id: this.userId,
      sender: 'macOS', parent_message_id: parentMessageId,
    });
    await this.client.from('messages').update({ status: 'completed' }).eq('id', parentMessageId);
  }

  async sendFileTree(tree, focusedPath) {
    if (!this.channel) return;
    try { await this.channel.send({ type: 'broadcast', event: 'file_tree_data', payload: { tree, focusedPath } }); } catch {}
  }

  async sendFileContent(filePath, content, language) {
    if (!this.channel) return;
    try { await this.channel.send({ type: 'broadcast', event: 'file_content_data', payload: { path: filePath, content, language } }); } catch {}
  }

  async getPendingMessages() {
    if (!this.pairId) return [];
    let q = this.client.from('messages').select('*')
      .eq('pair_id', this.pairId).eq('role', 'user').in('status', ['pending', 'processing'])
      .eq('is_cancelled', false).order('created_at', { ascending: true });
    if (this.sessionId) q = q.eq('session_id', this.sessionId);
    const { data } = await q;
    return data || [];
  }

  // ── Gemini CLI ──
  findGeminiCLI() {
    const fromEnv = process.env.GEMINI_CLI_PATH;
    if (fromEnv && fs.existsSync(fromEnv)) return fromEnv;
    try {
      const cmd = process.platform === 'win32' ? 'where gemini' : 'which gemini';
      const result = execSync(cmd, { encoding: 'utf-8' }).split(/\r?\n/).map(l => l.trim()).filter(Boolean);
      if (result[0] && fs.existsSync(result[0])) return result[0];
    } catch {}
    const candidates = ['/usr/local/bin/gemini', '/opt/homebrew/bin/gemini', `${os.homedir()}/.local/bin/gemini`];
    for (const p of candidates) { if (fs.existsSync(p)) return p; }
    return null;
  }

  runGemini(prompt, cwd, model, onChunk) {
    const cliPath = this.findGeminiCLI();
    const useNpx = !cliPath;
    const args = ['--skip-trust', '--approval-mode', 'yolo', '--output-format', 'stream-json'];
    if (model) args.push('--model', model);
    args.push('--prompt', prompt);

    let command, spawnArgs;
    if (useNpx) {
      command = 'npx';
      spawnArgs = ['-y', '@google/gemini-cli@latest', ...args];
    } else {
      command = cliPath;
      spawnArgs = args;
    }

    const proc = spawn(command, spawnArgs, {
      cwd: cwd || process.cwd(), env: { ...process.env }, stdio: ['pipe', 'pipe', 'pipe'],
      detached: process.platform !== 'win32',
    });
    if (proc.stdin) proc.stdin.end();

    let fullOutput = '';
    let buffer = '';
    const errors = [];

    const handleEvent = (ev) => {
      if (ev.type === 'message' && ev.role === 'assistant' && typeof ev.content === 'string') {
        fullOutput += ev.content;
        onChunk(ev.content);
      } else if (ev.type === 'tool_use') {
        const name = ev.tool_name || ev.name || 'tool';
        const hint = ev.parameters?.command || ev.parameters?.file_path || ev.parameters?.description || '';
        const line = `\n• ${name}${hint ? ' ' + String(hint).slice(0, 100) : ''}\n`;
        fullOutput += line;
        onChunk(line);
      } else if (ev.type === 'error') {
        errors.push(ev.message || 'Unknown error');
      }
    };

    proc.stdout?.on('data', (data) => {
      buffer += data.toString('utf-8');
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';
      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed) continue;
        try { handleEvent(JSON.parse(trimmed)); } catch { fullOutput += line + '\n'; onChunk(line + '\n'); }
      }
    });

    proc.stderr?.on('data', (data) => {
      this.log(`[gemini stderr] ${data.toString().trim()}`);
    });

    const result = new Promise((resolve) => {
      proc.on('close', (code) => {
        if (buffer.trim()) {
          try { handleEvent(JSON.parse(buffer.trim())); } catch {}
        }
        resolve({ output: fullOutput, exitCode: code || 0, errors });
      });
      proc.on('error', (e) => {
        resolve({ output: fullOutput || e.message, exitCode: 1, errors: [e.message] });
      });
    });

    this.geminiProc = proc;
    return { process: proc, result };
  }

  terminateGemini() {
    if (!this.geminiProc || this.geminiProc.killed) return;
    try {
      if (process.platform !== 'win32' && this.geminiProc.pid) {
        process.kill(-this.geminiProc.pid, 'SIGTERM');
      } else {
        this.geminiProc.kill('SIGTERM');
      }
    } catch {
      try { this.geminiProc.kill('SIGTERM'); } catch {}
    }
    this.geminiProc = null;
  }
}

module.exports = { Bridge, loadConfig, saveConfig, PAIRING_PORT, CONFIG_DIR };
