import type { ServerWebSocket } from 'bun';
import type { SidecarCommand, SidecarEvent } from '../protocol';
import { createSidecarConnector } from '../connect';
import { getVaultBase, getVaultWsBase, stripHopByHop, redactPath } from './upstream';
import { API_URL } from '../../officer-url.mjs';

// The officer-vault sidecar. Same philosophy as the other officer-* sidecars: a singleton process that
// registers with the API server and OWNS a contract — here, a transparent reverse-proxy to a self-hosted
// Vaultwarden (Bitwarden server). The platform API is just a thin, origin-gated forwarder to us; ALL
// knowledge of Vaultwarden (its URL, its paths, its notifications WebSocket) lives here, so the vault can
// one day be broken off into its own deployable unit without touching the platform.
//
// HARD RULE: dumb pass-through. The vault is end-to-end encrypted — we NEVER decrypt, parse, rewrite, or
// log request/response bodies. Method, path, query, headers, status and BOTH body streams pass through
// verbatim; nothing is buffered or cached.
//
// ─────────────────────────────────────────────────────────────────────────────────────────────────
// HTTP CONTRACT — this server proxies to the Vaultwarden ROOT (the platform strips its /api/vault mount
// prefix before forwarding). So `/identity/*`, `/api/*`, `/notifications/*`, `/icons/*`, `/events/*` map
// straight through. `/notifications/hub` upgrades to a WebSocket (SignalR); its long-poll fallback rides
// the normal HTTP path. `GET /_health` is ours (probes Vaultwarden's /alive), not part of the contract.
// The server listens on a random loopback port, reported to the API on connect so it can route here.
// ─────────────────────────────────────────────────────────────────────────────────────────────────

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

// ── notifications WebSocket proxy (sidecar ↔ Vaultwarden) ──

type VaultWSData = { vaultWsPath: string; vaultWsProtocol?: string };
type UpstreamState = { ws: WebSocket; queue: (string | Uint8Array<ArrayBuffer>)[]; ready: boolean };
const upstreams = new Map<ServerWebSocket<VaultWSData>, UpstreamState>();

// Bun hands frames over as `string | Buffer`; a Buffer is a Uint8Array at runtime, so forward as-is.
const asPayload = (raw: string | Buffer): string | Uint8Array<ArrayBuffer> =>
  typeof raw === 'string' ? raw : (raw as Uint8Array<ArrayBuffer>);

const wsHandler = {
  open(ws: ServerWebSocket<VaultWSData>) {
    const wsBase = getVaultWsBase();
    if (!wsBase) {
      ws.close(1011, 'Vault upstream not configured');
      return;
    }
    const protocols = ws.data.vaultWsProtocol
      ? ws.data.vaultWsProtocol
          .split(',')
          .map((s) => s.trim())
          .filter(Boolean)
      : undefined;
    const upstream = protocols?.length
      ? new WebSocket(`${wsBase}${ws.data.vaultWsPath}`, protocols)
      : new WebSocket(`${wsBase}${ws.data.vaultWsPath}`);
    upstream.binaryType = 'arraybuffer';

    const state: UpstreamState = { ws: upstream, queue: [], ready: false };
    upstreams.set(ws, state);

    upstream.addEventListener('open', () => {
      state.ready = true;
      for (const m of state.queue) upstream.send(m);
      state.queue.length = 0;
    });
    upstream.addEventListener('message', (ev) => {
      try {
        ws.send(ev.data as string | ArrayBuffer);
      } catch {
        /* client gone */
      }
    });
    upstream.addEventListener('close', (ev) => {
      upstreams.delete(ws);
      try {
        ws.close(ev.code || 1000, ev.reason || '');
      } catch {
        /* already closed */
      }
    });
    upstream.addEventListener('error', () => {
      upstreams.delete(ws);
      try {
        ws.close(1011, 'upstream error');
      } catch {
        /* already closed */
      }
    });
  },
  message(ws: ServerWebSocket<VaultWSData>, raw: string | Buffer) {
    const state = upstreams.get(ws);
    if (!state) return;
    const payload = asPayload(raw);
    if (state.ready) state.ws.send(payload);
    else state.queue.push(payload); // buffer until the upstream socket opens
  },
  close(ws: ServerWebSocket<VaultWSData>) {
    const state = upstreams.get(ws);
    if (state) {
      try {
        state.ws.close();
      } catch {
        /* already closed */
      }
      upstreams.delete(ws);
    }
  },
  drain() {},
};

// ── HTTP + WS server ──

const port = getFreePort();

const server = Bun.serve<VaultWSData>({
  port,
  hostname: '127.0.0.1',
  idleTimeout: 255, // sync + attachments can idle; Bun caps this at 255s
  maxRequestBodySize: 1024 * 1024 * 1024 * 2, // attachments
  async fetch(req, server) {
    const url = new URL(req.url);

    // Reachability probe — ours, never touches vault data.
    if (url.pathname === '/_health') {
      const base = getVaultBase();
      if (!base) return Response.json({ ok: false, error: 'VAULTWARDEN_URL not configured' }, { status: 503 });
      const started = Date.now();
      try {
        const r = await fetch(`${base}/alive`, { method: 'GET', signal: AbortSignal.timeout(5000) });
        return Response.json({ ok: r.ok, upstreamStatus: r.status, ms: Date.now() - started });
      } catch {
        return Response.json({ ok: false, error: 'upstream unreachable', ms: Date.now() - started }, { status: 502 });
      }
    }

    // Notifications hub: upgrade the WebSocket here (piped to Vaultwarden by wsHandler); the SignalR
    // long-poll fallback is not an upgrade, so it falls through to the HTTP proxy below.
    if (url.pathname.startsWith('/notifications/') && req.headers.get('upgrade') === 'websocket') {
      const ok = server.upgrade(req, {
        data: {
          vaultWsPath: url.pathname + url.search,
          vaultWsProtocol: req.headers.get('sec-websocket-protocol') ?? undefined,
        } satisfies VaultWSData,
      });
      return ok ? undefined : new Response('Upgrade failed', { status: 500 });
    }

    // Faithful HTTP pass-through to Vaultwarden.
    const base = getVaultBase();
    if (!base) return new Response('Vault upstream not configured', { status: 503 });

    const target = `${base}${url.pathname}${url.search}`;
    const method = req.method;
    const hasBody = method !== 'GET' && method !== 'HEAD';
    const started = Date.now();

    // Bun/undici require half-duplex to stream a request body straight through (attachments can be large).
    const init: RequestInit & { duplex?: 'half' } = {
      method,
      headers: stripHopByHop(req.headers),
      body: hasBody ? req.body : undefined,
      redirect: 'manual', // a transparent proxy passes 3xx through rather than following them
    };
    if (hasBody) init.duplex = 'half';

    let upstream: Response;
    try {
      upstream = await fetch(target, init);
    } catch {
      console.error(`[vault] ${method} ${redactPath(url.pathname + url.search)} -> upstream unreachable`);
      return new Response('Vault upstream unreachable', { status: 502 });
    }

    // Log at most method / path / status / duration — never bodies, tokens, or Authorization.
    console.log(`[vault] ${method} ${redactPath(url.pathname)} -> ${upstream.status} ${Date.now() - started}ms`);

    return new Response(upstream.body, { status: upstream.status, headers: stripHopByHop(upstream.headers) });
  },
  websocket: wsHandler,
});

console.log(`[vault] reverse-proxy listening on 127.0.0.1:${port} -> ${getVaultBase() ?? '(VAULTWARDEN_URL unset)'}`);

// ── Command handlers ──

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}`,
      });
  }
}

// ── Connect to API server ──

const connection = createSidecarConnector({
  apiUrl: `${API_URL}/api/sidecar/register`,
  name: 'vault',
  capabilities: ['vault'],
  onCommand(cmd, reply) {
    handleCommand(cmd as SidecarCommand, reply as ReplyFn);
  },
  onConnected() {
    // Tell the API where our proxy is listening, so it can forward /api/vault/* here.
    connection.send({ type: 'vault:server', port });
    console.log(`[vault] reported proxy port ${port} to API`);
  },
});

// ── Graceful shutdown ──

function shutdown(signal: string) {
  console.log(`[vault] ${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'));
