#!/usr/bin/env node
/**
 * Upfyn-Code Relay Client
 *
 * Connects your local machine to the hosted Upfyn-Code server.
 * Bridges Claude CLI, terminal, filesystem, and git to the web UI.
 *
 * Usage:
 *   upfynai-code connect --server https://upfynai.thinqmesh.com --key upfyn_xxx
 *   upfynai-code connect  (uses saved config from ~/.upfynai/config.json)
 */

import WebSocket from 'ws';
import os from 'os';
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import {
  c,
  showConnectStartup,
  showConnectionBanner,
  logRelayEvent,
  createSpinner,
} from './cli-ui.js';
import { executeAction, isStreamingAction } from '../shared/agents/index.js';

// Load package.json for version
import { fileURLToPath } from 'url';
const __filename_rc = fileURLToPath(import.meta.url);
const __dirname_rc = path.dirname(__filename_rc);
let VERSION = '0.0.0';
try {
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname_rc, '../package.json'), 'utf8'));
  VERSION = pkg.version;
} catch { /* ignore */ }

const CONFIG_DIR = path.join(os.homedir(), '.upfynai');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
const CLOUD_SERVER = 'https://upfynai-code-production.up.railway.app';

/** Mask internal server URL for display */
function displayUrl(url) {
  if (!url || url === CLOUD_SERVER) return 'Upfyn Cloud (cli.upfyn.com)';
  try { return new URL(url).host; } catch { return url; }
}

function loadConfig() {
  try {
    if (fs.existsSync(CONFIG_FILE)) {
      return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
    }
  } catch (e) { /* ignore */ }
  return {};
}

function saveConfig(config) {
  if (!fs.existsSync(CONFIG_DIR)) {
    fs.mkdirSync(CONFIG_DIR, { recursive: true });
  }
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
}

/**
 * Resolve a binary name to its full path, checking PATH first then local node_modules/.bin
 */
function resolveBinary(name) {
  const isWindows = process.platform === 'win32';
  try {
    const result = execSync(`${isWindows ? 'where' : 'which'} ${name}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
    return result.split('\n')[0].trim();
  } catch {
    const localPath = path.join(__dirname_rc, '../node_modules/.bin', name + (isWindows ? '.cmd' : ''));
    if (fs.existsSync(localPath)) return localPath;
    return name;
  }
}

/**
 * Detect which AI CLI agents are installed on this machine.
 * Used for initial agent-capabilities report (separate from the shared detect agent
 * which returns a different format via relay-response).
 */
function detectInstalledAgents() {
  const isWindows = process.platform === 'win32';
  const whichCmd = isWindows ? 'where' : 'which';
  const localBinDir = path.join(__dirname_rc, '../node_modules/.bin');

  const agentDefs = [
    { name: 'claude', binary: 'claude', label: 'Claude Code' },
    { name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
    { name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
  ];

  const detected = {};
  for (const agent of agentDefs) {
    try {
      const result = execSync(`${whichCmd} ${agent.binary}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
      detected[agent.name] = { installed: true, path: result.split('\n')[0].trim(), label: agent.label };
    } catch {
      const localPath = path.join(localBinDir, agent.binary + (isWindows ? '.cmd' : ''));
      if (fs.existsSync(localPath)) {
        detected[agent.name] = { installed: true, path: localPath, label: agent.label };
      } else {
        detected[agent.name] = { installed: false, label: agent.label };
      }
    }
  }
  return detected;
}

/**
 * Build execution context for shared agents.
 */
function buildAgentContext(requestId, ws) {
  return {
    requestId,
    streamMode: 'simple',
    resolveBinary,
    localBinDir: path.join(__dirname_rc, '../node_modules/.bin'),
    stream: (data) => {
      ws.send(JSON.stringify({ type: 'relay-stream', requestId, data }));
    },
  };
}

/**
 * Handle incoming relay commands from the server.
 * Delegates to shared agent modules.
 */
async function handleRelayCommand(data, ws) {
  const { requestId, action } = data;

  try {
    const ctx = buildAgentContext(requestId, ws);

    if (isStreamingAction(action)) {
      logRelayEvent('>', `${action}: ${(data.command || '').slice(0, 60)}...`, 'cyan');
      const result = await executeAction(action, data, ctx);
      ws.send(JSON.stringify({ type: 'relay-complete', requestId, exitCode: result.exitCode }));
    } else {
      if (action === 'shell-command') logRelayEvent('$', `Shell: ${(data.command || '').slice(0, 50)}`, 'dim');
      else if (action === 'file-read') logRelayEvent('R', `Read: ${data.filePath}`, 'dim');
      else if (action === 'file-write') logRelayEvent('W', `Write: ${data.filePath}`, 'dim');
      else if (action === 'git-operation') logRelayEvent('G', `Git: ${data.gitCommand}`, 'dim');
      else if (action === 'exec') logRelayEvent('$', `Exec: ${(data.command || '').slice(0, 50)}`, 'dim');

      const result = await executeAction(action, data, ctx);
      ws.send(JSON.stringify({ type: 'relay-response', requestId, data: result }));
    }
  } catch (err) {
    ws.send(JSON.stringify({ type: 'relay-response', requestId, error: err.message }));
  }
}

/**
 * Create WebSocket connection with optional API key in handshake
 */
function createRelayConnection(wsUrl, config = {}) {
  const headers = {};
  if (config.anthropicApiKey) headers['x-anthropic-api-key'] = config.anthropicApiKey;
  headers['x-upfyn-version'] = VERSION;
  headers['x-upfyn-machine'] = os.hostname();
  headers['x-upfyn-platform'] = process.platform;
  headers['x-upfyn-cwd'] = process.cwd();
  return new WebSocket(wsUrl, { headers });
}

/**
 * Main connect function (interactive — with animation and logging)
 */
export async function connectToServer(options = {}) {
  const config = loadConfig();
  const serverUrl = options.server || config.server || CLOUD_SERVER;
  const relayKey = options.key || config.relayKey;

  if (!relayKey) {
    console.log('');
    console.log(`  ${c.red('FAIL')} No relay key provided.`);
    console.log('');
    console.log(`  ${c.gray('Get your relay token from the web UI:')}`);
    console.log(`  ${c.dim('1.')} Sign in at ${c.cyan('https://cli.upfyn.com')}`);
    console.log(`  ${c.dim('2.')} Click ${c.bright('Connect')} button`);
    console.log(`  ${c.dim('3.')} Copy the command and run it here`);
    console.log('');
    console.log(`  ${c.gray('Or run:')} ${c.bright('uc connect --server <url> --key upfyn_your_token')}`);
    console.log('');
    process.exit(1);
  }

  saveConfig({ ...config, server: serverUrl, relayKey });

  await showConnectStartup(displayUrl(serverUrl), os.hostname(), os.userInfo().username, VERSION);

  const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);

  const spinner = createSpinner('Connecting to server...');
  spinner.start();

  let reconnectAttempts = 0;
  const MAX_RECONNECT = 10;
  let lastPongTime = Date.now();

  function connect() {
    const ws = createRelayConnection(wsUrl, config);
    lastPongTime = Date.now();

    ws.on('open', () => {
      reconnectAttempts = 0;
      lastPongTime = Date.now();
    });

    ws.on('message', (rawMessage) => {
      try {
        const data = JSON.parse(rawMessage);

        if (data.type === 'relay-connected') {
          spinner.stop('Connected!');
          const nameMatch = data.message?.match(/Connected as (.+?)\./);
          const username = nameMatch ? nameMatch[1] : 'Unknown';
          showConnectionBanner(username, displayUrl(serverUrl));

          const agents = detectInstalledAgents();
          const installed = Object.entries(agents)
            .filter(([, info]) => info.installed)
            .map(([name, info]) => info.label);
          const missing = Object.entries(agents)
            .filter(([, info]) => !info.installed)
            .map(([name, info]) => info.label);

          if (installed.length > 0) logRelayEvent('+', `Agents found: ${installed.join(', ')}`, 'green');
          if (missing.length > 0) logRelayEvent('~', `Not found: ${missing.join(', ')} (install to enable)`, 'yellow');

          ws.send(JSON.stringify({
            type: 'agent-capabilities',
            agents,
            machine: { hostname: os.hostname(), platform: process.platform, cwd: process.cwd() },
          }));

          logRelayEvent('*', 'Relay active -- waiting for commands...', 'green');
          return;
        }

        if (data.type === 'relay-command') {
          handleRelayCommand(data, ws);
          return;
        }

        if (data.type === 'pong' || data.type === 'server-ping') {
          lastPongTime = Date.now();
          if (data.type === 'server-ping') ws.send(JSON.stringify({ type: 'ping' }));
          return;
        }

        if (data.type === 'error') {
          spinner.fail(`Server error: ${data.error}`);
          return;
        }
      } catch (e) {
        logRelayEvent('!', `Parse error: ${e.message}`, 'red');
      }
    });

    ws.on('close', (code) => {
      if (code === 1000) {
        logRelayEvent('-', 'Disconnected gracefully.', 'dim');
        process.exit(0);
      }
      reconnectAttempts++;
      if (reconnectAttempts <= MAX_RECONNECT) {
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
        logRelayEvent('~', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
        setTimeout(connect, delay);
      } else {
        logRelayEvent('X', 'Max reconnection attempts reached. Exiting.', 'red');
        process.exit(1);
      }
    });

    ws.on('error', (err) => {
      if (err.code === 'ECONNREFUSED') {
        spinner.fail(`Cannot reach ${displayUrl(serverUrl)}. Is the server running?`);
      } else if (err.message?.includes('401') || err.message?.includes('Unauthorized')) {
        spinner.fail('Authentication failed (401). Your relay token may be invalid or expired.');
        logRelayEvent('!', 'Get a new token from Settings > Keys & Connect at: https://cli.upfyn.com', 'yellow');
        logRelayEvent('!', 'Then run: uc connect --key <new_token>', 'yellow');
      } else if (err.message?.includes('403') || err.message?.includes('Forbidden')) {
        spinner.fail('Access forbidden (403). Your account may be inactive or subscription expired.');
      } else if (err.message?.includes('504') || err.message?.includes('timeout')) {
        spinner.fail('Server auth timed out. The server may be overloaded — retrying...');
      } else if (err.code === 'ENOTFOUND' || err.code === 'EAI_AGAIN') {
        spinner.fail('DNS resolution failed. Check your internet connection.');
      } else {
        logRelayEvent('!', `WebSocket error: ${err.message || err.code || 'unknown'}`, 'red');
      }
    });

    const heartbeat = setInterval(() => {
      if (ws.readyState !== 1) { clearInterval(heartbeat); return; }
      if (Date.now() - lastPongTime > 75000) {
        clearInterval(heartbeat);
        logRelayEvent('!', 'No heartbeat response — connection stale, reconnecting...', 'yellow');
        ws.terminate();
        return;
      }
      ws.send(JSON.stringify({ type: 'ping' }));
    }, 30000);

    ws.on('close', () => clearInterval(heartbeat));
    ws.on('error', () => clearInterval(heartbeat));
  }

  connect();

  process.on('SIGINT', () => {
    console.log('');
    logRelayEvent('-', 'Disconnecting...', 'dim');
    process.exit(0);
  });
}

/**
 * Background connect function (silent — used when uc launches Claude Code)
 */
export function connectToServerBackground(options = {}) {
  const config = loadConfig();
  const serverUrl = options.server || config.server;
  const relayKey = options.key || config.relayKey;

  if (!serverUrl || !relayKey) return;

  const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);

  let reconnectAttempts = 0;
  const MAX_RECONNECT = 5;
  let lastPongTime = Date.now();

  function connect() {
    const ws = createRelayConnection(wsUrl, config);
    lastPongTime = Date.now();

    ws.on('message', (rawMessage) => {
      try {
        const data = JSON.parse(rawMessage);
        if (data.type === 'relay-connected') {
          // Detect installed agents and report to server (same as interactive connect)
          const agents = detectInstalledAgents();
          ws.send(JSON.stringify({
            type: 'agent-capabilities',
            agents,
            machine: { hostname: os.hostname(), platform: process.platform, cwd: process.cwd() },
          }));
          return;
        }
        if (data.type === 'pong' || data.type === 'server-ping') {
          lastPongTime = Date.now();
          if (data.type === 'server-ping') ws.send(JSON.stringify({ type: 'ping' }));
          return;
        }
        if (data.type === 'relay-command') {
          handleRelayCommand(data, ws);
        }
      } catch { /* ignore */ }
    });

    ws.on('open', () => {
      reconnectAttempts = 0;
      lastPongTime = Date.now();
    });

    ws.on('close', (code) => {
      clearInterval(heartbeat);
      if (code === 1000) return;
      reconnectAttempts++;
      if (reconnectAttempts <= MAX_RECONNECT) {
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
        setTimeout(connect, delay);
      }
    });

    ws.on('error', () => { clearInterval(heartbeat); });

    const heartbeat = setInterval(() => {
      if (ws.readyState !== 1) { clearInterval(heartbeat); return; }
      if (Date.now() - lastPongTime > 75000) { clearInterval(heartbeat); ws.terminate(); return; }
      ws.send(JSON.stringify({ type: 'ping' }));
    }, 30000);
  }

  connect();
}
