import type { ServerWebSocket } from 'bun';
import { join } from 'node:path';
import { sendPtyCommand, sendPtyCommandAsync, on, isTerminalConnected } from '@@/sidecar-registry';
import type { PtyInitConfig } from '../../sidecar/protocol';

type WSData = {
  userId: number;
  email: string;
  username: string;
  sessionId?: string;
  cwd?: string;
  cols?: number;
  rows?: number;
};

type BridgeSession = {
  client: ServerWebSocket<WSData>;
  sessionId: string;
  unsubOutput: (() => void) | null;
  unsubExit: (() => void) | null;
};

const sessions = new Map<ServerWebSocket<WSData>, BridgeSession>();

let idCounter = 0;
function nextId(): string {
  return `pty_${Date.now()}_${++idCounter}`;
}

const sendOutput = (ws: ServerWebSocket<WSData>, data: string) => {
  try {
    ws.send(JSON.stringify({ type: 'output', data }));
  } catch {
    // ws already closed
  }
};

const resolveCwd = (home: string, cwd?: string) => {
  if (!cwd || cwd === '~') return home;
  if (cwd.startsWith('~/')) return join(home, cwd.slice(2));
  if (cwd.startsWith('/')) return cwd;
  return home;
};

export const terminalWebsocket = {
  async open(ws: ServerWebSocket<WSData>) {
    const { email, username } = ws.data;

    console.log(`[terminal] open: email=${email} username=${username}`);

    if (!isTerminalConnected()) {
      sendOutput(ws, '\r\n[Terminal error] PTY sidecar is not connected\r\n');
      return;
    }

    const sessionId = ws.data.sessionId ?? `host-${ws.data.userId}`;

    // The server owner is the only account, so the terminal is always a plain host shell.
    const config: PtyInitConfig = {
      sessionId,
      host: true,
      shell: { command: process.env.SHELL ?? '/bin/zsh', args: ['-i'] },
      cwd: resolveCwd(process.env.HOME!, ws.data.cwd),
      homeDir: process.env.HOME!,
      userLabel: email,
      cols: ws.data.cols,
      rows: ws.data.rows,
    };

    // Subscribe to events for this session
    const unsubOutput = on('pty:output', (msg) => {
      if (msg.type === 'pty:output' && msg.sessionId === sessionId) {
        try {
          ws.send(JSON.stringify({ type: 'output', data: msg.data }));
        } catch {
          // ws already closed
        }
      }
    });

    const unsubExit = on('pty:exit', (msg) => {
      if (msg.type === 'pty:exit' && msg.sessionId === sessionId) {
        try {
          ws.send(JSON.stringify({ type: 'exit' }));
        } catch {
          // ws already closed
        }
      }
    });

    const session: BridgeSession = { client: ws, sessionId, unsubOutput, unsubExit };
    sessions.set(ws, session);

    // Send init command to PTY sidecar
    try {
      await sendPtyCommandAsync({ type: 'pty:init', id: nextId(), sessionId, config });
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to initialize terminal';
      sendOutput(ws, `\r\n[Terminal error] ${message}\r\n`);
      unsubOutput();
      unsubExit();
      sessions.delete(ws);
    }
  },

  message(ws: ServerWebSocket<WSData>, raw: string | Buffer) {
    const session = sessions.get(ws);
    if (!session) return;

    try {
      const payload = typeof raw === 'string' ? raw : raw.toString();
      const msg = JSON.parse(payload);

      switch (msg.type) {
        case 'input':
          sendPtyCommand({ type: 'pty:input', id: nextId(), sessionId: session.sessionId, data: msg.data ?? '' });
          break;
        case 'resize':
          if (msg.cols > 0 && msg.rows > 0) {
            sendPtyCommand({
              type: 'pty:resize',
              id: nextId(),
              sessionId: session.sessionId,
              cols: msg.cols,
              rows: msg.rows,
            });
          }
          break;
        case 'cwd':
          if (msg.path) {
            sendPtyCommand({
              type: 'pty:input',
              id: nextId(),
              sessionId: session.sessionId,
              data: `cd ${JSON.stringify(msg.path)}\r`,
            });
          }
          break;
      }
    } catch {
      // ignore malformed messages
    }
  },

  close(ws: ServerWebSocket<WSData>) {
    const session = sessions.get(ws);
    if (session) {
      session.unsubOutput?.();
      session.unsubExit?.();
      // Don't kill PTY — it can be reattached
      sessions.delete(ws);
    }
  },

  drain() {},
};

export const broadcastPanelRefresh = (email: string) => {
  const msg = JSON.stringify({ type: 'panel-refresh' });
  for (const [ws] of sessions) {
    if (ws.data.email === email) {
      try {
        ws.send(msg);
      } catch {
        /* ignore */
      }
    }
  }
};
