import type { SidecarCommand, SidecarEvent } from '../protocol';
import { createSidecarConnector } from '../connect';
import { handleConfigRoute } from './config';
import { handleOfficerRoute } from './routes';
import { probe } from './rpc';
import { getTransmissionConfig } from './upstream';
import { API_URL } from '../../officer-url.mjs';

// The officer-transmission sidecar. Owns the whole Transmission contract for Officer: the daemon URL and
// credentials, the X-Transmission-Session-Id CSRF handshake, and the translation from Transmission's
// single-endpoint RPC vocabulary into real REST paths. The platform API is a thin auth-gated forwarder
// (src/servers/api/transmission/router.ts) holding no Transmission credentials.
//
// ─────────────────────────────────────────────────────────────────────────────────────────────────
// HTTP CONTRACT — the platform strips its /api/transmission mount prefix before forwarding.
//
//   GET  /_health                     ours. Probes the daemon with a cheap session-get.
//   GET  /_config                     { configured, connection } — where the daemon is, never the password
//   PUT  /_config                     { url, username?, password?, rpcPath? } — validated with a live
//                                     session-get, then stored. A blank password keeps the stored one
//   DEL  /_config                     forget the connection
//   GET  /_officer/session            full session settings + version + rpc-version
//   POST /_officer/session            write a partial settings object (whitelisted), returns it read back
//   GET  /_officer/stats              session-stats: cumulative + current-session counters
//   GET  /_officer/torrents           the list poll — LIST_FIELDS for every torrent, decorated
//   GET  /_officer/torrents/:id       one torrent with files/peers/trackers (DETAIL_FIELDS)
//   POST /_officer/torrents/add       {metainfo|filename, downloadDir?, labels?, paused?, …}
//                                     → {status:'added'|'duplicate', torrent}
//   POST /_officer/torrents/action    {ids, action} — start|start-now|stop|verify|reannounce|queue-*
//   POST /_officer/torrents/set       {ids, …whitelisted torrent-set fields}
//   POST /_officer/torrents/location  {ids, location, move}
//   POST /_officer/torrents/rename    {id, path, name}
//   POST /_officer/torrents/remove    {ids, deleteLocalData}
//   GET  /_officer/free-space?path=   free space at a path
//   GET  /_officer/port-test          is the peer port reachable from outside
//   POST /_officer/blocklist-update   refresh the blocklist, returns the new size
//   anything else                     404
//
// There is deliberately NO transparent /transmission/rpc passthrough. Transmission multiplexes every verb
// through one POST body, reports failures as `{"result": "<error>"}` inside a 200, and demands a CSRF token
// that rotates on every daemon restart. Proxying that raw would push all three into the browser. Every
// quirk is absorbed here — see rpc.ts.
//
// Every route needs `X-Officer-User`, which the platform proxy sets after authenticating the owner. We bind
// loopback only, so its presence is the trust signal — a request without it did not come through the
// platform, and which daemon to talk to is per-owner data.
// ─────────────────────────────────────────────────────────────────────────────────────────────────

/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
  const probeServer = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response('') });
  const p = probeServer.port;
  probeServer.stop(true);
  if (p == null) throw new Error('failed to acquire a free port');
  return p;
}

const port = getFreePort();

const server = Bun.serve({
  port,
  hostname: '127.0.0.1',
  // A base64 .torrent for a large multi-file release travels in the request body of /torrents/add.
  maxRequestBodySize: 256 * 1024 * 1024,
  async fetch(req) {
    const url = new URL(req.url);

    const officerUser = req.headers.get('X-Officer-User');
    const userId = Number(officerUser);
    if (!officerUser || !Number.isInteger(userId) || userId <= 0) {
      return Response.json({ error: 'missing or invalid X-Officer-User' }, { status: 401 });
    }

    if (url.pathname === '/_config' || url.pathname.startsWith('/_config/')) {
      try {
        return await handleConfigRoute(req, userId, url.pathname.slice('/_config'.length));
      } catch (err) {
        console.error(`[transmission] ${req.method} ${url.pathname} failed`, err);
        return Response.json({ error: 'internal error' }, { status: 500 });
      }
    }

    // 503 with `configured: false` is the signal the UI turns into the setup form. Distinguishing it from a
    // configured-but-unreachable daemon is the whole reason the flag is on the response.
    if (url.pathname === '/_health') {
      const cfg = await getTransmissionConfig(userId);
      if (!cfg) return Response.json({ ok: false, configured: false, error: 'not connected' }, { status: 503 });
      const started = Date.now();
      const result = await probe(cfg);
      return Response.json(
        { ...result, configured: true, ms: Date.now() - started },
        { status: result.ok ? 200 : 502 },
      );
    }

    if (url.pathname.startsWith('/_officer/')) {
      try {
        const res = await handleOfficerRoute(userId, req, url);
        if (res) return res;
        return Response.json({ error: 'not found' }, { status: 404 });
      } catch (err) {
        console.error(`[transmission] ${req.method} ${url.pathname} failed`, err);
        return Response.json({ error: 'internal error' }, { status: 500 });
      }
    }

    return Response.json({ error: 'not found' }, { status: 404 });
  },
});

// No upstream in the banner: where the daemon lives is now per-owner state read from the database per
// request, not a constant this process knows at boot.
console.log(`[transmission] listening on 127.0.0.1:${port}`);

type ReplyFn = (msg: SidecarEvent) => void;

function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
  switch (cmd.type) {
    case 'ping':
      reply({ type: 'pong', id: cmd.id });
      break;
    default:
      reply({
        type: 'error',
        id: (cmd as SidecarCommand).id,
        error: `Unknown command type: ${(cmd as Record<string, unknown>).type}`,
      });
  }
}

const connection = createSidecarConnector({
  apiUrl: `${API_URL}/api/sidecar/register`,
  name: 'transmission',
  capabilities: ['transmission'],
  onCommand(cmd, reply) {
    handleCommand(cmd as SidecarCommand, reply as ReplyFn);
  },
  onConnected() {
    connection.send({ type: 'transmission:server', port });
    console.log(`[transmission] reported server port ${port} to API`);
  },
});

function shutdown(signal: string) {
  console.log(`[transmission] ${signal} received, shutting down...`);
  try {
    server.stop(true);
  } catch {
    /* already stopped */
  }
  connection.destroy();
  process.exit(0);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
