// Graceful shutdown
process.on('SIGINT', () => {
  console.log('[pty-sidecar] shutting down...');
  for (const [id, session] of sessions) {
    try { session.term.kill(); } catch { /* ignore */ }
  }
  sessions.clear();
  if (ws) { try { ws.close(); } catch { /* ignore */ } }
  process.exit(0);
});
process.on('SIGTERM', () => process.emit('SIGINT'));

import { existsSync } from 'node:fs';
import { cp, mkdir } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFile } from 'node:child_process';
import WebSocket from 'ws';
import * as pty from 'node-pty';

const run = (cmd, args, opts = {}) =>
  new Promise((resolve) => {
    const proc = execFile(cmd, args, { stdio: 'ignore', ...opts }, () => resolve());
    proc.on('error', () => resolve());
  });

const __dirname = dirname(fileURLToPath(import.meta.url));

const templateDir = join(__dirname, 'templates');

import 'dotenv/config';

const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const REGISTER_URL = `${API_URL}/api/sidecar/register`;

const BUFFER_MAX = 50 * 1024;
const RECONNECT_DELAYS = [200, 500, 1000, 2000, 4000, 8000, 15000];

/** @type {Map<string, { term: import('node-pty').IPty, buffer: string, cols: number, rows: number, initConfig: object }>} */
const sessions = new Map();

// ── Helpers ──

const sendJson = (ws, msg) => {
  try {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(msg));
    }
  } catch {
    // ignore
  }
};

const ensureUserFiles = async (homeDir) => {
  await mkdir(homeDir, { recursive: true });
  await mkdir(join(homeDir, '.config'), { recursive: true });
  await mkdir(join(homeDir, '.local', 'bin'), { recursive: true });

  const zshrcPath = join(homeDir, '.zshrc');
  if (!existsSync(zshrcPath)) {
    await cp(join(templateDir, '.zshrc'), zshrcPath);
  }

  const tmuxconfPath = join(homeDir, '.tmux.conf');
  if (!existsSync(tmuxconfPath)) {
    await cp(join(templateDir, '.tmux.conf'), tmuxconfPath);
  }

  const starshipPath = join(homeDir, '.config', 'starship-officer.toml');
  if (!existsSync(starshipPath)) {
    await cp(join(templateDir, 'starship-officer.toml'), starshipPath);
  }

  const ohMyZshPath = join(homeDir, '.oh-my-zsh');
  if (!existsSync(ohMyZshPath)) {
    await run('git', ['clone', '--depth=1', 'https://github.com/ohmyzsh/ohmyzsh.git', ohMyZshPath]);
  }
};

const appendBuffer = (session, data) => {
  session.buffer += data;
  if (session.buffer.length > BUFFER_MAX) {
    session.buffer = session.buffer.slice(-BUFFER_MAX);
  }
};

// ── Command handler ──

async function handleCommand(ws, msg) {
  switch (msg.type) {
    case 'pty:init': {
      const { sessionId, config } = msg;
      if (!sessionId) return;

      const existing = sessions.get(sessionId);
      console.log(`[pty-sidecar] init sessionId=${sessionId} existing=${!!existing} total=${sessions.size}`);

      if (existing) {
        // Replay buffer
        if (existing.buffer.length > 0) {
          sendJson(ws, { type: 'pty:output', sessionId, data: existing.buffer });
        }

        // Resize PTY to new client dimensions
        const cols = config.cols ?? existing.cols;
        const rows = config.rows ?? existing.rows;
        if (cols > 0 && rows > 0 && (cols !== existing.cols || rows !== existing.rows)) {
          existing.cols = cols;
          existing.rows = rows;
          try {
            existing.term.resize(cols, rows);
          } catch {
            // ignore
          }
        }

        sendJson(ws, { type: 'pty:ready', id: msg.id, sessionId });
        return;
      }

      // New session — spawn PTY
      const shell = config.shell ?? { command: '/bin/bash', args: ['-i'] };
      const cwd = config.cwd ?? process.cwd();
      const homeDir = config.homeDir ?? process.cwd();
      const userLabel = config.userLabel ?? 'officer';
      const cols = config.cols ?? 80;
      const rows = config.rows ?? 24;
      const isHost = !!config.host;

      let spawnCommand;
      let spawnArgs;
      let ptyEnv;

      if (isHost) {
        spawnCommand = shell.command;
        spawnArgs = shell.args ?? [];
        ptyEnv = { ...process.env, TERM: 'xterm-256color', ...(config.env ?? {}) };
      } else {
        // Sandboxed mode: shell config contains the full bwrap command
        spawnCommand = shell.command;
        spawnArgs = shell.args ?? [];

        try {
          await ensureUserFiles(homeDir);
        } catch (err) {
          console.error('[pty-sidecar] ensureUserFiles failed:', err);
        }

        // bwrap sets env vars internally via --setenv, so use minimal host env
        ptyEnv = { TERM: 'xterm-256color' };
      }

      const ptyCwd = isHost ? cwd : undefined;

      let term;
      try {
        term = pty.spawn(spawnCommand, spawnArgs, {
          name: 'xterm-256color',
          cols,
          rows,
          cwd: ptyCwd,
          env: ptyEnv,
        });
      } catch (err) {
        const message = err instanceof Error ? err.message : 'Failed to start terminal';
        sendJson(ws, { type: 'pty:output', sessionId, data: `\r\n[Terminal error] ${message}\r\n` });
        sendJson(ws, { type: 'pty:exit', sessionId, exitCode: 1 });
        return;
      }

      const session = {
        term,
        buffer: '',
        cols,
        rows,
        initConfig: { shell, cwd, homeDir, userLabel },
      };
      sessions.set(sessionId, session);

      term.onData((output) => {
        appendBuffer(session, output);
        sendJson(ws, { type: 'pty:output', sessionId, data: output });
      });

      term.onExit(({ exitCode, signal }) => {
        console.log(`[pty-sidecar] session ${sessionId} exited code=${exitCode} signal=${signal}`);
        sendJson(ws, { type: 'pty:exit', sessionId, exitCode, signal });
        sessions.delete(sessionId);
      });

      sendJson(ws, { type: 'pty:ready', id: msg.id, sessionId });
      return;
    }

    case 'pty:input': {
      const session = sessions.get(msg.sessionId);
      if (session) {
        session.term.write(msg.data ?? '');
      }
      break;
    }

    case 'pty:resize': {
      const session = sessions.get(msg.sessionId);
      if (session && msg.cols > 0 && msg.rows > 0) {
        session.cols = msg.cols;
        session.rows = msg.rows;
        try {
          session.term.resize(msg.cols, msg.rows);
        } catch {
          // PTY may have already exited
        }
      }
      break;
    }

    case 'pty:close': {
      const session = sessions.get(msg.sessionId);
      if (session) {
        try {
          session.term.kill();
        } catch {
          // ignore
        }
        sessions.delete(msg.sessionId);
      }
      break;
    }
  }
}

// ── Connect to API server with reconnect ──

let ws = null;
let reconnectAttempt = 0;
let reconnectTimer = null;

console.log(`[pty-sidecar] starting, connecting to ${REGISTER_URL}`);

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

  try {
    ws = new WebSocket(REGISTER_URL);
  } catch (err) {
    console.error(`[pty-sidecar] failed to create WebSocket:`, err.message ?? err);
    scheduleReconnect();
    return;
  }

  ws.on('open', () => {
    reconnectAttempt = 0;
    console.log('[pty-sidecar] connected, sending registration...');
    sendJson(ws, { type: 'register', name: 'pty', capabilities: ['terminal'] });
  });

  ws.on('message', (data) => {
    try {
      const msg = JSON.parse(typeof data === 'string' ? data : data.toString());

      if (msg.type === 'registered') {
        console.log(`[pty-sidecar] registered with API server (id=${msg.id})`);
        return;
      }

      handleCommand(ws, msg);
    } catch {
      // skip malformed messages
    }
  });

  ws.on('close', () => {
    console.log('[pty-sidecar] disconnected from API server');
    ws = null;
    scheduleReconnect();
  });

  ws.on('error', (err) => {
    if (reconnectAttempt <= 1) {
      console.error(`[pty-sidecar] connection error: ${err.message ?? err}`);
    }
    // onclose will fire after this
  });
}

function scheduleReconnect() {
  if (reconnectTimer) return;
  const delay = RECONNECT_DELAYS[Math.min(reconnectAttempt, RECONNECT_DELAYS.length - 1)];
  reconnectAttempt++;
  reconnectTimer = setTimeout(() => {
    reconnectTimer = null;
    connect();
  }, delay);
}

// Start connecting
connect();
