import './servers/bootstrap';
import type { ServerWebSocket } from 'bun';
import { serve } from 'bun';
import { honoServer } from './servers/hono';
import { verify } from './servers/jwt';
import { isTokenBlacklisted } from 'officerdb';
import { terminalWebsocket } from './servers/api/terminal/websocket';
import { chatWebsocket } from './servers/api/chat/websocket';
import { taskRunnerWebsocket } from './servers/api/tasks/task-executor';
import { pipelineWebsocket } from './servers/api/tasks/pipeline-executor';
import { cliampWebsocket } from './servers/api/cliamp/websocket';
import { cliampAudioWebsocket } from './servers/api/cliamp/audio-ws';
import { desktopWebsocket } from './servers/api/desktop/websocket';
import { vaultWebsocket, upgradeVaultWs } from './servers/api/vault/websocket';
import { findEntryByProxyId, touchEntry } from './servers/api/dev-server/router';
import officerWeb from './apps/officer-web/index.gen.html';
import { startBrowserRelay } from './servers/api/browser/relay';
import { registerSidecar, unregisterSidecar, handleSidecarMessage } from './servers/sidecar-registry';
import './servers/api/chat/opencode/sidecar-server'; // subscribe to the opencode sidecar's port report
import { broadcastEmailNew } from './servers/api/email/email';
import type { SidecarRegistration } from './servers/sidecar/registration-protocol';
import { toShellUsername } from './servers/data-path';

const { PORT = '5000' } = process.env;

// Build static file routes from public/
const publicRoutes: Record<string, (req: Request) => Response> = {};
for await (const file of new Bun.Glob('**').scan({ cwd: './public' })) {
  const bunFile = Bun.file(`./public/${file}`);
  publicRoutes[`/${file}`] = () => new Response(bunFile);
}

type WSData = {
  userId: number;
  email: string;
  username: string;
  provider:
    | 'terminal'
    | 'chat'
    | 'task-runner'
    | 'pipeline'
    | 'dev-server'
    | 'cliamp'
    | 'cliamp-audio'
    | 'desktop'
    | 'vault'
    | 'sidecar';
  sessionId?: string;
  cwd?: string;
  command?: string;
  cols?: number;
  rows?: number;
  files?: string;
  devServerPort?: number;
  devServerSlug?: string;
  wsProxyPath?: string;
  wsToken?: string;
};

// Sidecar registration WebSocket handler
const sidecarConnections = new Map<ServerWebSocket<WSData>, string>(); // ws → sidecar ID

const sidecarWebsocket = {
  open(_ws: ServerWebSocket<WSData>) {
    // Wait for registration message
  },
  message(ws: ServerWebSocket<WSData>, raw: string | Buffer) {
    try {
      const data = typeof raw === 'string' ? raw : raw.toString();
      const msg = JSON.parse(data);

      if (msg.type === 'register') {
        const id = registerSidecar(ws, msg as SidecarRegistration);
        sidecarConnections.set(ws, id);
        ws.send(JSON.stringify({ type: 'registered', id }));
        return;
      }

      // Handle queue commands from sidecars (e.g., email sidecar enqueuing jobs)
      if (typeof msg.type === 'string' && msg.type.startsWith('queue:') && msg.id) {
        handleSidecarQueueCommand(ws, msg);
        return;
      }

      // Email IDLE watcher reporting new mail → push to that user's /email SSE clients.
      if (msg.type === 'email:new' && typeof msg.userEmail === 'string') {
        broadcastEmailNew(msg.userEmail);
        return;
      }

      const id = sidecarConnections.get(ws);
      if (id) {
        handleSidecarMessage(id, msg);
      }
    } catch {
      // skip malformed messages
    }
  },
  close(ws: ServerWebSocket<WSData>) {
    const id = sidecarConnections.get(ws);
    if (id) {
      unregisterSidecar(id);
      sidecarConnections.delete(ws);
    }
  },
  drain() {},
};

// Handle queue commands from sidecars (e.g., email sidecar enqueuing jobs)
async function handleSidecarQueueCommand(ws: ServerWebSocket<WSData>, msg: Record<string, unknown>) {
  const id = msg.id as string;
  try {
    switch (msg.type) {
      case 'queue:enqueue': {
        const job = await queueEnqueue(msg.params as import('./servers/queue/types').EnqueueParams);
        ws.send(JSON.stringify({ type: 'queue:enqueued', id, job }));
        break;
      }
      case 'queue:cancel': {
        const job = await queueCancel(msg.jobId as string);
        ws.send(JSON.stringify({ type: 'queue:cancelled', id, job }));
        break;
      }
      case 'queue:list': {
        const jobs = await queueList();
        ws.send(JSON.stringify({ type: 'queue:list', id, jobs }));
        break;
      }
      case 'queue:get': {
        const job = await queueGet(msg.jobId as string);
        ws.send(JSON.stringify({ type: 'queue:get', id, job }));
        break;
      }
      default:
        ws.send(JSON.stringify({ type: 'queue:error', id, error: `Unknown queue command: ${msg.type}` }));
    }
  } catch (err) {
    ws.send(JSON.stringify({ type: 'queue:error', id, error: err instanceof Error ? err.message : String(err) }));
  }
}

const handlers: Record<string, any> = {
  terminal: terminalWebsocket,
  chat: chatWebsocket,
  'task-runner': taskRunnerWebsocket,
  pipeline: pipelineWebsocket,
  cliamp: cliampWebsocket,
  'cliamp-audio': cliampAudioWebsocket,
  desktop: desktopWebsocket,
  vault: vaultWebsocket,
  sidecar: sidecarWebsocket,
};

// Dev-server WebSocket proxy: bridges client WS ↔ upstream dev server WS (for HMR etc.)
type UpstreamState = { ws: WebSocket; queue: (string | Buffer)[]; ready: boolean };

// Bun hands WS frames over as `string | Buffer`, but the DOM WebSocket.send signature won't accept a
// Buffer<ArrayBufferLike> (it can't rule out a SharedArrayBuffer backing). A Buffer is a Uint8Array
// at runtime, so this forwards as-is rather than paying for a copy on every proxied frame.
const asWsPayload = (raw: string | Buffer): string | Uint8Array<ArrayBuffer> =>
  typeof raw === 'string' ? raw : (raw as Uint8Array<ArrayBuffer>);
const devServerUpstreams = new Map<ServerWebSocket<WSData>, UpstreamState>();

const devServerWebsocket = {
  async open(ws: ServerWebSocket<WSData>) {
    const { devServerPort, wsProxyPath, wsToken } = ws.data;

    // Validate JWT (deferred from upgrade which must be synchronous in Bun)
    if (!wsToken) {
      ws.close(4001, 'Unauthorized');
      return;
    }
    try {
      const payload = await verify(wsToken);
      if (!payload) {
        ws.close(4001, 'Unauthorized');
        return;
      }
      if (payload.jti && (await isTokenBlacklisted(payload.jti))) {
        ws.close(4001, 'Unauthorized');
        return;
      }
    } catch {
      ws.close(4001, 'Unauthorized');
      return;
    }

    const upstream = new WebSocket(`ws://localhost:${devServerPort}${wsProxyPath}`);
    const state: UpstreamState = { ws: upstream, queue: [], ready: false };
    devServerUpstreams.set(ws, state);

    upstream.addEventListener('open', () => {
      state.ready = true;
      for (const msg of state.queue) upstream.send(asWsPayload(msg));
      state.queue.length = 0;
    });

    upstream.addEventListener('message', (event) => {
      ws.send(event.data as string | ArrayBuffer);
    });

    upstream.addEventListener('close', () => {
      devServerUpstreams.delete(ws);
      ws.close();
    });

    upstream.addEventListener('error', () => {
      devServerUpstreams.delete(ws);
      ws.close();
    });
  },
  message(ws: ServerWebSocket<WSData>, raw: string | Buffer) {
    const state = devServerUpstreams.get(ws);
    if (!state) return;
    if (state.ready) {
      state.ws.send(asWsPayload(raw));
    } else {
      state.queue.push(raw);
    }
  },
  close(ws: ServerWebSocket<WSData>) {
    const state = devServerUpstreams.get(ws);
    if (state) {
      state.ws.close();
      devServerUpstreams.delete(ws);
    }
  },
};
handlers['dev-server'] = devServerWebsocket;

async function upgradeWs(
  req: Request,
  server: any,
  provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'cliamp' | 'cliamp-audio' | 'desktop',
) {
  const token = new URL(req.url).searchParams.get('token');
  if (!token) return new Response('Unauthorized', { status: 401 });

  try {
    const user = await verify(token);
    if (!user) return new Response('Unauthorized', { status: 401 });

    if (user.jti) {
      if (await isTokenBlacklisted(user.jti)) return new Response('Unauthorized', { status: 401 });
    }

    const url = new URL(req.url);
    const sessionId = url.searchParams.get('sessionId') ?? undefined;
    const cwd = url.searchParams.get('cwd') ?? undefined;
    const command = url.searchParams.get('command') ?? undefined;
    const cols = url.searchParams.get('cols') ? Number(url.searchParams.get('cols')) : undefined;
    const rows = url.searchParams.get('rows') ? Number(url.searchParams.get('rows')) : undefined;
    const files = url.searchParams.get('files') ?? undefined;
    const ok = server.upgrade(req, {
      data: {
        userId: user.id,
        email: user.email,
        username: toShellUsername(user.username ?? '', user.email),
        provider,
        sessionId,
        cwd,
        command,
        cols,
        rows,
        files,
      },
    });
    if (!ok) return new Response('Upgrade failed', { status: 500 });
  } catch {
    return new Response('Unauthorized', { status: 401 });
  }
}

function upgradeDevServerWs(req: Request, server: any) {
  const url = new URL(req.url);
  const match = url.pathname.match(/^\/api\/dev-server-proxy\/([^/]+)(\/.*)?$/);
  if (!match) return new Response('Not found', { status: 404 });

  const proxyId = match[1]!;
  const entry = findEntryByProxyId(proxyId);
  if (!entry) return new Response('No dev server running', { status: 404 });

  const wsToken = url.searchParams.get('token');
  if (!wsToken) return new Response('Unauthorized', { status: 401 });

  touchEntry(entry);

  const wsProxyPath = match[2] || '/';
  const ok = server.upgrade(req, {
    data: {
      userId: 0,
      email: '',
      provider: 'dev-server' as const,
      devServerPort: entry.port,
      devServerSlug: proxyId,
      wsProxyPath,
      wsToken,
    },
  });
  if (!ok) return new Response('Upgrade failed', { status: 500 });
}

const server = serve({
  port: Number(PORT),
  idleTimeout: 60,
  maxRequestBodySize: 1024 * 1024 * 1024 * 50, // 50 GB
  routes: {
    ...publicRoutes,
    '/novnc/*': (req) => {
      const file = Bun.file(`public${new URL(req.url).pathname}`);
      return new Response(file);
    },
    '/vendor/*': (req) => {
      const file = Bun.file(`public${new URL(req.url).pathname}`);
      return new Response(file);
    },
    '/api/dev-server-proxy/*': (req, server) => {
      if (req.headers.get('upgrade') === 'websocket') return upgradeDevServerWs(req, server);
      return honoServer.fetch(req, server);
    },
    // Vaultwarden notifications hub: upgrade the WebSocket here (proxied to upstream by vaultWebsocket);
    // everything else on this path (SignalR long-poll negotiate/poll) falls through to the HTTP proxy.
    '/api/vault/notifications/*': (req, server) => {
      if (req.headers.get('upgrade') === 'websocket') return upgradeVaultWs(req, server);
      return honoServer.fetch(req, server);
    },
    '/api/sidecar/register': (req: Request, server: any) => {
      const ok = server.upgrade(req, {
        data: { provider: 'sidecar', userId: 0, email: '', username: '' },
      });
      if (!ok) return new Response('Upgrade failed', { status: 500 });
    },
    '/api/tasks/run/ws': (req, server) => upgradeWs(req, server, 'task-runner'),
    '/api/tasks/pipeline/ws': (req, server) => upgradeWs(req, server, 'pipeline'),
    '/api/terminal/ws': (req, server) => upgradeWs(req, server, 'terminal'),
    '/api/chat/ws': (req, server) => upgradeWs(req, server, 'chat'),
    '/api/cliamp/ws': (req, server) => upgradeWs(req, server, 'cliamp'),
    '/api/cliamp/audio/ws': (req, server) => upgradeWs(req, server, 'cliamp-audio'),
    '/api/desktop/ws': (req, server) => upgradeWs(req, server, 'desktop'),
    '/': officerWeb,
    '/*': officerWeb,
    '/api': honoServer.fetch,
    '/api/*': honoServer.fetch,
  },

  websocket: {
    open(ws) {
      const { provider } = (ws as unknown as ServerWebSocket<WSData>).data;
      handlers[provider]!.open(ws as any);
    },
    message(ws, raw) {
      const { provider } = (ws as unknown as ServerWebSocket<WSData>).data;
      handlers[provider]!.message(ws as any, raw);
    },
    close(ws) {
      const { provider } = (ws as unknown as ServerWebSocket<WSData>).data;
      handlers[provider]!.close(ws as any);
    },
    drain() {},
  },

  development: process.env.NODE_ENV !== 'production' && {
    hmr: true,
  },
});

console.log(`🚀 Server running at ${server.url}`);

const BROWSER_RELAY_PORT = Number(process.env.BROWSER_RELAY_PORT ?? '18792');
try {
  await startBrowserRelay(BROWSER_RELAY_PORT);
  console.log(`[browser-relay] listening on port ${BROWSER_RELAY_PORT}`);
} catch (err) {
  console.error('[browser-relay] failed to start:', err instanceof Error ? err.message : err);
}

// Initialize queue engine in API server process
import {
  initQueue,
  enqueueJob as queueEnqueue,
  cancelJob as queueCancel,
  listAllJobs as queueList,
  readJob as queueGet,
} from './servers/queue/init';
initQueue().catch((err) => console.error('[queue] failed to initialize:', err));

// Mark any orphaned pipeline jobs from previous server run
import { cleanupOnStartup } from './servers/api/tasks/pipeline-job-manager';
cleanupOnStartup().catch((err) => console.error('[pipeline-jobs] startup cleanup failed:', err));

// Ensure PulseAudio is running with virtual sink for cliamp audio streaming
(async () => {
  const pulseaudio = Bun.which('pulseaudio');
  const pactl = Bun.which('pactl');
  if (!pulseaudio || !pactl) {
    console.log('[cliamp] pulseaudio not installed, skipping audio setup');
    return;
  }

  // Start PulseAudio daemon if not running
  const check = Bun.spawnSync({ cmd: [pulseaudio, '--check'], stdout: 'ignore', stderr: 'ignore' });
  if (check.exitCode !== 0) {
    const start = Bun.spawnSync({ cmd: [pulseaudio, '--start', '-D'], stdout: 'ignore', stderr: 'ignore' });
    if (start.exitCode !== 0) {
      console.error('[cliamp] failed to start pulseaudio');
      return;
    }
    console.log('[cliamp] pulseaudio started');
  } else {
    console.log('[cliamp] pulseaudio already running');
  }

  // Load null sink if not already loaded
  const sinks = Bun.spawnSync({ cmd: [pactl, 'list', 'short', 'sinks'], stdout: 'pipe', stderr: 'ignore' });
  const sinkList = sinks.stdout.toString();
  if (!sinkList.includes('virtual_out')) {
    const load = Bun.spawnSync({
      cmd: [
        pactl,
        'load-module',
        'module-null-sink',
        'sink_name=virtual_out',
        'sink_properties=device.description=Virtual_Output',
      ],
      stdout: 'pipe',
      stderr: 'pipe',
    });
    if (load.exitCode !== 0) {
      console.error('[cliamp] failed to load null sink:', load.stderr.toString().trim());
    } else {
      console.log('[cliamp] virtual_out null sink loaded');
    }
  } else {
    console.log('[cliamp] virtual_out sink already exists');
  }
})();

// Pi check/install is handled by bootstrap.ts (imported above)
