import http from 'node:http';
import { WebSocketServer } from 'ws';
import * as store from './sessions.mjs';

// The sidecar's own listener — the thing that makes this a real sidecar rather than a set of commands the
// platform drives. It binds an ephemeral loopback port and announces it; the platform authenticates the
// browser and relays bytes here without reading them.
//
// The socket protocol is the one the browser already speaks, unchanged: {input,resize} in,
// {output,replay,exit} out. That is deliberate — the frontend did not have to move for the
// transport to.

const wsSend = (socket) => ({
  send: (msg) => {
    if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(msg));
  },
});

const json = (res, status, body) => {
  const payload = JSON.stringify(body);
  res.writeHead(status, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) });
  res.end(payload);
};

export function startServer() {
  const server = http.createServer((req, res) => {
    const url = new URL(req.url ?? '/', 'http://127.0.0.1');

    // Officer-owned routes, reached through the platform's authenticated proxy.
    //
    // `osUser` scopes both to one account's sessions. The platform sends it for a member and omits it for the
    // owner; absent means unscoped. Until this existed these two listed and killed EVERY shell on the box for
    // anyone who could reach them, which was safe only because the terminal was owner-only.
    const scope = url.searchParams.has('osUser') ? url.searchParams.get('osUser') || null : undefined;

    if (url.pathname === '/_officer/sessions' && req.method === 'GET') {
      return json(res, 200, { sessions: store.list(scope) });
    }

    const killMatch = url.pathname.match(/^\/_officer\/sessions\/([^/]+)$/);
    if (killMatch && req.method === 'DELETE') {
      const killed = store.kill(decodeURIComponent(killMatch[1]), scope);
      return json(res, killed ? 200 : 404, { ok: killed });
    }

    json(res, 404, { error: 'not found' });
  });

  const wss = new WebSocketServer({ server, path: '/ws' });

  wss.on('connection', (socket, req) => {
    const url = new URL(req.url ?? '/', 'http://127.0.0.1');
    const sessionId = url.searchParams.get('sessionId');
    if (!sessionId) {
      socket.close(1008, 'sessionId required');
      return;
    }

    const client = wsSend(socket);
    const session = store.attach(sessionId, client, {
      cwd: url.searchParams.get('cwd') ?? undefined,
      cols: Number(url.searchParams.get('cols')) || 0,
      rows: Number(url.searchParams.get('rows')) || 0,
      // Injected by the platform's bridge from the authenticated account, after stripping whatever the
      // browser sent. Absent for the owner, whose shell runs as this process.
      osUser: url.searchParams.get('osUser') || undefined,
      home: url.searchParams.get('home') || undefined,
    });
    if (!session) {
      socket.close(1011, 'failed to start terminal');
      return;
    }

    socket.on('message', (raw) => {
      let msg;
      try {
        msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
      } catch {
        return; // malformed frame
      }
      if (msg.type === 'input') store.write(sessionId, msg.data);
      else if (msg.type === 'resize') store.resize(sessionId, msg.cols, msg.rows);
    });

    // Detach, never kill: the shell outlives the viewer, which is what makes re-attach work at all.
    socket.on('close', () => store.detach(sessionId, client));
    socket.on('error', () => store.detach(sessionId, client));
  });

  return new Promise((resolve) => {
    // Port 0 — the OS picks, and the platform learns it from the registration socket. Loopback only.
    server.listen(0, '127.0.0.1', () => {
      const { port } = server.address();
      console.log(`[pty-sidecar] listening on http://127.0.0.1:${port}`);
      resolve(port);
    });
  });
}
