import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';

const IPC_PREFIX = '@@CUAGENT@@';
const TARGET_STATUS_TIMEOUT_MS = 5000;

function getDesktopPath() {
  const currentPath = process.env.PATH || '';
  const commonPaths = process.platform === 'win32'
    ? []
    : ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
  return [...commonPaths, currentPath].filter(Boolean).join(path.delimiter);
}

function getNodeRuntime(isPackaged) {
  if (isPackaged && process.versions.electron) {
    return { command: process.execPath, env: { ELECTRON_RUN_AS_NODE: '1' } };
  }
  if (process.env.npm_node_execpath) {
    return { command: process.env.npm_node_execpath, env: {} };
  }
  return { command: 'node', env: {} };
}

function toAgentWsUrl(httpUrl) {
  try {
    const parsed = new URL(httpUrl);
    parsed.protocol = parsed.protocol === 'http:' ? 'ws:' : 'wss:';
    parsed.pathname = '/desktop-agent';
    parsed.search = '';
    parsed.hash = '';
    return parsed.toString();
  } catch {
    return null;
  }
}

async function isComputerUseEnabledTarget(httpUrl, apiKey) {
  let statusUrl;
  try {
    statusUrl = new URL('/api/computer-use/status', httpUrl).toString();
  } catch {
    return false;
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), TARGET_STATUS_TIMEOUT_MS);
  try {
    const response = await fetch(statusUrl, {
      signal: controller.signal,
      headers: apiKey ? { 'X-API-Key': apiKey } : undefined,
    });
    const body = await response.json().catch(() => null);
    return response.ok && body?.success !== false && body?.data?.enabled === true;
  } catch {
    return false;
  } finally {
    clearTimeout(timeout);
  }
}

async function filterEnabledComputerUseTargets(targets, apiKey) {
  const checks = await Promise.all(targets.map(async (target) => ({
    target,
    enabled: await isComputerUseEnabledTarget(target, apiKey),
  })));
  return checks.filter((item) => item.enabled).map((item) => item.target);
}

/**
 * Keeps a Computer Use desktop agent connected to running cloud environments
 * while desktop access is enabled.
 */
export class ComputerAgentController {
  constructor({ appRoot, settingsPath, isPackaged = false, getRunningEnvironmentUrls, getApiKey, promptConsent, onChange }) {
    this.appRoot = appRoot;
    this.settingsPath = settingsPath;
    this.isPackaged = isPackaged;
    this.getRunningEnvironmentUrls = getRunningEnvironmentUrls;
    this.getApiKey = getApiKey;
    this.promptConsent = promptConsent;
    this.onChange = onChange;
    this.settings = { enabled: false, consentMode: 'ask' };
    this.child = null;
    this.connectedUrls = new Set();
    this.currentTargets = [];
    this.stdoutBuffer = '';
    this.lastEvent = null;
    this.lastError = null;
  }

  getSettings() {
    return { ...this.settings };
  }

  getState() {
    return {
      enabled: this.settings.enabled,
      consentMode: this.settings.consentMode,
      running: Boolean(this.child),
      connectedCount: this.connectedUrls.size,
      targetCount: this.currentTargets.length,
      targetUrls: [...this.currentTargets],
      lastEvent: this.lastEvent,
      lastError: this.lastError,
    };
  }

  async loadSettings() {
    try {
      const raw = await fs.readFile(this.settingsPath, 'utf8');
      const stored = JSON.parse(raw);
      this.settings = {
        enabled: Boolean(stored.enabled),
        consentMode: stored.consentMode === 'auto' ? 'auto' : 'ask',
      };
    } catch {
      this.settings = { enabled: false, consentMode: 'ask' };
    }
    return this.settings;
  }

  async saveSettings(next) {
    this.settings = {
      enabled: Boolean(next.enabled),
      consentMode: next.consentMode === 'auto' ? 'auto' : 'ask',
    };
    await fs.mkdir(path.dirname(this.settingsPath), { recursive: true });
    await fs.writeFile(this.settingsPath, JSON.stringify(this.settings, null, 2), 'utf8');
    await this.sync();
    this.onChange?.();
    return this.settings;
  }

  async sync() {
    const targets = this.settings.enabled ? (this.getRunningEnvironmentUrls?.() || []) : [];
    const enabledTargets = this.settings.enabled ? await filterEnabledComputerUseTargets(targets, this.getApiKey?.() || '') : [];
    const wsTargets = enabledTargets.map(toAgentWsUrl).filter(Boolean);

    const sameTargets =
      wsTargets.length === this.currentTargets.length &&
      wsTargets.every((url) => this.currentTargets.includes(url));

    if (!this.settings.enabled || wsTargets.length === 0) {
      this.stop();
      this.currentTargets = [];
      this.lastEvent = this.settings.enabled ? 'no-targets' : 'disabled';
      return;
    }

    if (this.child && sameTargets) {
      return;
    }

    this.currentTargets = wsTargets;
    this.lastEvent = 'restarting';
    this.lastError = null;
    this.restart(wsTargets);
  }

  restart(wsTargets) {
    this.stop();

    const agentEntry = process.env.CLOUDCLI_COMPUTER_AGENT_ENTRY
      || path.join(this.appRoot, 'dist-server', 'server', 'computer-use-agent.js');
    const runtime = getNodeRuntime(this.isPackaged);

    this.child = spawn(runtime.command, [agentEntry], {
      cwd: this.appRoot,
      env: {
        ...process.env,
        ...runtime.env,
        PATH: getDesktopPath(),
        CLOUDCLI_DESKTOP_AGENT_URLS: wsTargets.join(','),
        CLOUDCLI_DESKTOP_AGENT_API_KEY: this.getApiKey?.() || '',
        CLOUDCLI_COMPUTER_USE_CONSENT_MODE: this.settings.consentMode,
      },
      stdio: ['pipe', 'pipe', 'pipe'],
      windowsHide: true,
    });

    this.connectedUrls = new Set();

    this.child.once('error', (error) => {
      console.error('[ComputerAgent] failed to start:', error.message);
      this.lastEvent = 'start-error';
      this.lastError = error.message;
      this.child = null;
      this.onChange?.();
    });

    this.child.stdout?.on('data', (chunk) => this.handleStdout(String(chunk)));
    this.child.stderr?.on('data', (chunk) => {
      for (const line of String(chunk).split(/\r?\n/)) {
        if (line.trim()) {
          this.lastError = line.trim();
          console.error('[ComputerAgent]', line);
        }
      }
    });

    this.child.once('exit', (code) => {
      console.log(`[ComputerAgent] exited (code ${code ?? 'null'})`);
      this.lastEvent = `exit:${code ?? 'null'}`;
      this.child = null;
      this.connectedUrls = new Set();
      this.onChange?.();
    });

    this.onChange?.();
  }

  handleStdout(chunk) {
    this.stdoutBuffer += chunk;
    const lines = this.stdoutBuffer.split('\n');
    this.stdoutBuffer = lines.pop() || '';
    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed.startsWith(IPC_PREFIX)) {
        if (trimmed) console.log('[ComputerAgent]', trimmed);
        continue;
      }
      let payload;
      try {
        payload = JSON.parse(trimmed.slice(IPC_PREFIX.length).trim());
      } catch {
        continue;
      }
      void this.handleAgentEvent(payload);
    }
  }

  async handleAgentEvent(payload) {
    switch (payload.type) {
      case 'connected':
        this.connectedUrls.add(payload.url);
        this.lastEvent = 'connected';
        this.lastError = null;
        this.onChange?.();
        break;
      case 'disconnected':
        this.connectedUrls.delete(payload.url);
        this.lastEvent = 'disconnected';
        this.onChange?.();
        if (payload.reason && /computer use.*disabled/i.test(payload.reason)) {
          void this.sync().catch((error) => {
            this.lastError = error instanceof Error ? error.message : 'Failed to sync Computer Use targets.';
            this.onChange?.();
          });
        }
        break;
      case 'starting':
        this.lastEvent = 'starting';
        this.lastError = null;
        this.onChange?.();
        break;
      case 'error':
        this.lastEvent = 'error';
        this.lastError = payload.message || 'Computer agent error.';
        this.onChange?.();
        break;
      case 'consent-request': {
        const allow = await this.promptConsent?.(payload.sessionId);
        this.sendToChild({ type: 'consent-response', sessionId: payload.sessionId, allow: Boolean(allow) });
        break;
      }
      default:
        break;
    }
  }

  sendToChild(message) {
    if (this.child?.stdin?.writable) {
      this.child.stdin.write(`${IPC_PREFIX} ${JSON.stringify(message)}\n`);
    }
  }

  revokeSession(sessionId) {
    this.sendToChild({ type: 'revoke-session', sessionId });
  }

  stop() {
    if (!this.child) return;
    const child = this.child;
    this.child = null;
    this.connectedUrls = new Set();
    try { child.kill('SIGTERM'); } catch { /* noop */ }
  }
}
