import type { FastifyInstance } from 'fastify';
import type { IPty } from 'node-pty';
import { getSessionInfo } from '../db.js';
import {
  sanitizeId,
  tmuxSessionName,
  ensureSession,
  capturePane,
} from '../pty/manager.js';
import { attachPty } from '../pty/pty.js';
import { getUser } from '../auth.js';
import { register, unregister, appendOutput, touchActivity, consumePendingMetadata, get as getRegistry, getLastLines } from '../pty/registry.js';

export function registerWsAttachRoute(
  app: FastifyInstance,
  tmuxConfPath: string,
  idleTimeoutSeconds?: number,
  absoluteTimeoutSeconds?: number,
): void {
  app.get<{
    Params: { sid: string; pid: string };
    Querystring: { cols?: string; rows?: string };
  }>(
    '/ws/term/sessions/:sid/panes/:pid',
    { websocket: true },
    async (socket, req) => {
      const sid = sanitizeId(req.params.sid);
      const pid = sanitizeId(req.params.pid);
      if (!sid || !pid) {
        socket.close(1008, 'bad_id_format');
        return;
      }

      const user = getUser(req);
      req.log.info({ user, sid, pid }, 'ws attach');

      const session = await getSessionInfo(sid);
      if (!session) {
        socket.close(1008, 'unknown_session');
        return;
      }

      const sessionName = tmuxSessionName(pid);
      const cols = parseInt(req.query.cols ?? '', 10) || 80;
      const rows = parseInt(req.query.rows ?? '', 10) || 24;

      // Idempotent — /start typically created the session already, but cover
      // the race where the client opens the WS before /start's response lands
      // (or skips /start entirely). With per-pane tmux sessions there's no
      // cross-pane interference, so creating-on-attach is safe.
      try {
        await ensureSession(
          tmuxConfPath,
          sessionName,
          session.project_path,
          req.log,
          cols,
          rows,
        );
      } catch (err) {
        req.log.error({ err }, 'ensureSession failed in WS handler');
        socket.close(1011, 'tmux_failed');
        return;
      }

      const pendingMeta = consumePendingMetadata(pid);
      const regOpts: {
        timeoutSeconds?: number;
        absoluteTimeoutSeconds?: number;
        description?: string;
        parentAgent?: string;
      } = {};
      if (idleTimeoutSeconds && idleTimeoutSeconds > 0) regOpts.timeoutSeconds = idleTimeoutSeconds;
      if (absoluteTimeoutSeconds && absoluteTimeoutSeconds > 0) regOpts.absoluteTimeoutSeconds = absoluteTimeoutSeconds;
      if (pendingMeta) {
        if (pendingMeta.description) regOpts.description = pendingMeta.description;
        if (pendingMeta.parentAgent) regOpts.parentAgent = pendingMeta.parentAgent;
      }
      const hasRegOpts =
        regOpts.timeoutSeconds !== undefined ||
        regOpts.absoluteTimeoutSeconds !== undefined ||
        regOpts.description !== undefined ||
        regOpts.parentAgent !== undefined;
      register(sid, pid, session.project_path, session.name ?? undefined, hasRegOpts ? regOpts : undefined);

      let handle: IPty;
      try {
        handle = attachPty({
          sessionName,
          projectRoot: session.project_path,
          cols,
          rows,
          tmuxConfPath,
        });
      } catch (err) {
        req.log.error({ err }, 'attachPty failed');
        socket.close(1011, 'pty_spawn_failed');
        return;
      }

      // Frame contract (boolab pattern):
      //   server → client text:    JSON control — `init` on connect, `exit` on PTY death
      //   server → client binary:  raw PTY bytes (first frame after init = capture-pane replay)
      //   client → server binary:  user keystrokes
      //   client → server text:    JSON control — `{type:"resize", cols, rows}`
      //
      // The init frame lets the client term.clear() before paint so a remount
      // doesn't show stale buffer content. The capture-pane replay then
      // paints the current tmux pane state into the fresh xterm.
      try {
        socket.send(JSON.stringify({ type: 'init', cols, rows, tmux_session: sessionName }));
      } catch (err) {
        req.log.warn({ err }, 'init frame send failed');
      }

      try {
        const capture = await capturePane(tmuxConfPath, sessionName);
        if (capture.length > 0) {
          socket.send(Buffer.from(capture, 'utf8'), { binary: true });
        }
      } catch (err) {
        req.log.warn({ err }, 'capture-pane failed');
      }

      const onData = (data: string): void => {
        if (socket.readyState !== socket.OPEN) return;
        try {
          socket.send(Buffer.from(data, 'utf8'), { binary: true });
        } catch (err) {
          req.log.warn({ err }, 'ws send failed');
        }
        // Feed the ring buffer for pattern-based search
        appendOutput(pid, data);
        // Bump activity timestamp for idle-timeout tracking
        touchActivity(pid);
      };
      handle.onData(onData);

      socket.on('message', (rawData: Buffer | string, isBinary?: boolean) => {
        // ws v8 emits Buffer + isBinary boolean; older versions emit string
        // for text frames. Either way: text path tries JSON parse for the
        // resize control; binary path writes to the PTY.
        const isTextFrame = typeof rawData === 'string' || isBinary === false;
        if (isTextFrame) {
          const text = typeof rawData === 'string' ? rawData : rawData.toString('utf8');
          try {
            const parsed = JSON.parse(text) as { type?: string; cols?: number; rows?: number };
            if (parsed.type === 'resize') {
              const newCols = Math.max(1, Math.min(2000, Math.floor(Number(parsed.cols) || 80)));
              const newRows = Math.max(1, Math.min(2000, Math.floor(Number(parsed.rows) || 24)));
              req.log.info({ pid, cols: newCols, rows: newRows }, 'resize');
              try {
                handle.resize(newCols, newRows);
              } catch {
                /* ignore — invalid winsize bubble */
              }
            }
          } catch {
            /* malformed text frame — drop silently */
          }
          return;
        }
        try {
          handle.write((rawData as Buffer).toString('utf8'));
        } catch (err) {
          req.log.warn({ err }, 'pty write failed');
        }
      });

      handle.onExit(({ exitCode }) => {
        const meta = getRegistry(pid);
        const lastLines = getLastLines(pid, 5);
        const frame = {
          type: 'pty_exited' as const,
          session_id: sid,
          pane_id: pid,
          exit_code: exitCode,
          last_lines: lastLines,
          session_title: meta?.title ?? null,
          session_description: meta?.description ?? null,
          parent_agent: meta?.parentAgent ?? null,
          timed_out: meta?.timedOut ?? false,
        };
        try {
          if (socket.readyState === socket.OPEN) {
            socket.send(JSON.stringify(frame));
          }
        } catch {
          /* ignore */
        }
        try {
          socket.close(1000);
        } catch {
          /* ignore */
        }
      });

      socket.on('close', () => {
        unregister(pid);
        try {
          handle.kill();
        } catch {
          /* ignore */
        }
      });
    },
  );
}
