/**
 * Terminal-specific dashboard server wiring.
 * This keeps terminal HTTP routes, WebSocket upgrades, startup health checks,
 * and shutdown handling out of the main dashboard HTTP server.
 */
import type {
  IncomingMessage,
  Server as HttpServer,
  ServerResponse,
} from "node:http";
import type { Duplex } from "node:stream";
import type { WebSocketServer } from "ws";
import {
  decodeTerminalCreateBody,
  decodeTerminalUploadBody,
} from "./decoders.js";
import type { Runner } from "./types.js";
import type { TerminalManager } from "./terminal.js";
import { MAX_SESSIONS } from "./terminal.js";
import {
  buildAttachmentNote,
  persistUploads,
  TERMINAL_UPLOAD_MAX_BODY_BYTES,
  TERMINAL_UPLOAD_MAX_FILES,
  uploadDirForSession,
} from "./terminal-uploads.js";
import {
  recordEvidenceEvent,
  type EvidenceEventKind,
  type EvidencePayload,
} from "../evidence/envelope.js";
import { redactEvidenceText } from "../evidence/redaction.js";
import type { TerminalTraceEvent } from "./terminal.js";

type JsonResponder = (
  res: ServerResponse,
  status: number,
  body: unknown,
) => void;

/** Request-body limits used by terminal create and upload endpoints. */
interface BodyReadOptions {
  maxBytes?: number;
  tooLargeMessage?: string;
}

type BodyReader = (
  req: IncomingMessage,
  options?: BodyReadOptions,
) => Promise<string>;

/** Dependencies injected by the dashboard server so terminal handlers stay route-local and testable. */
interface DashboardTerminalDependencies {
  absDefault: string;
  validRunners: ReadonlySet<string>;
  defaultRunner: Runner;
  jsonResponse: JsonResponder;
  readBody: BodyReader;
  idleTimeoutMinutes?: number;
}

/** Validated terminal launch request after path, runner, and prompt decoding. */
interface DecodedTerminalCreate {
  prompt: string;
  projectPath: string;
  targetPath: string;
  runner: Runner;
}

/**
 * Build the terminal-only dashboard handlers for one server instance.
 *
 * @param deps - server-local dependencies and limits shared by terminal routes
 * @returns terminal route handlers plus the shutdown hook for active sessions
 */
export function createDashboardTerminalHandlers(
  deps: DashboardTerminalDependencies,
): {
  handleTerminalCreateRequest: (
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ) => Promise<boolean>;
  handleTerminalListRequest: (
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ) => Promise<boolean>;
  handleTerminalDeleteRequest: (
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ) => Promise<boolean>;
  handleTerminalUploadRequest: (
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ) => Promise<boolean>;
  handleHealthRequest: (
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ) => Promise<boolean>;
  handleTerminalSessionsRequest: (
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ) => Promise<boolean>;
  handleTerminalUpgrade: (
    req: IncomingMessage,
    socket: Duplex,
    head: Buffer,
    server: HttpServer,
  ) => boolean;
  logStartupNotice: () => void;
  close: () => Promise<void>;
} {
  const { absDefault, validRunners, defaultRunner, jsonResponse, readBody } =
    deps;
  let managerPromise: Promise<TerminalManager> | null = null;
  let wssPromise: Promise<WebSocketServer> | null = null;
  let closePromise: Promise<void> | null = null;

  function recordTerminalEvent(
    projectPath: string,
    eventKind: EvidenceEventKind,
    payload?: EvidencePayload,
  ): void {
    recordEvidenceEvent({
      producer: "dashboard-session-trace",
      actor: "server",
      eventType: eventKind,
      projectRoot: projectPath,
      payload,
    });
  }

  /** Record terminal input trace events without forcing callers to know whether tracing is enabled. */
  function recordTerminalTraceInput(event: TerminalTraceEvent): void {
    recordTerminalEvent(event.projectPath, event.eventKind, {
      session_id: event.sessionId,
      runner: event.runner,
      cwd: event.cwd,
      target_path: event.targetPath,
      bytes: event.bytes,
      input: redactEvidenceText("terminal input", event.input),
    });
  }

  async function createTerminalSession(
    manager: TerminalManager,
    decoded: DecodedTerminalCreate,
  ) {
    const { prompt, projectPath, targetPath, runner } = decoded;
    const result = await manager.create(
      prompt,
      projectPath || absDefault,
      runner,
      { targetPath: targetPath || projectPath || absDefault },
    );
    const session = manager.get(result.id);
    return {
      result,
      session,
      resolvedTargetPath:
        session?.targetPath || targetPath || projectPath || absDefault,
    };
  }

  function recordTerminalLaunchEvents(
    decoded: DecodedTerminalCreate,
    sessionId: string,
    session: ReturnType<TerminalManager["get"]>,
    resolvedTargetPath: string,
  ): void {
    const { prompt, projectPath, runner } = decoded;
    recordTerminalEvent(resolvedTargetPath, "terminal.create", {
      session_id: sessionId,
      runner,
      cwd: session?.cwd || projectPath || absDefault,
      target_path: resolvedTargetPath,
    });
    if (prompt.trim().length > 0) {
      recordTerminalEvent(resolvedTargetPath, "prompt.launch", {
        session_id: sessionId,
        runner,
        prompt: redactEvidenceText("terminal launch prompt", prompt),
      });
    }
  }

  /** Lazy-load the terminal manager the first time a terminal route is used. */
  async function getManager(): Promise<TerminalManager> {
    if (!managerPromise) {
      managerPromise = import("./terminal.js").then(
        ({ TerminalManager: TM }) =>
          new TM(deps.idleTimeoutMinutes, recordTerminalTraceInput),
      );
    }
    return managerPromise;
  }

  /** Lazy-load the WebSocket server that bridges browser terminals to PTY sessions. */
  async function getWSS(): Promise<WebSocketServer> {
    if (!wssPromise) {
      wssPromise = import("ws").then(
        ({ WebSocketServer: WSS }) => new WSS({ noServer: true }),
      );
    }
    return wssPromise;
  }

  /** Map terminal-launch failures to the client-facing HTTP status codes we expose. */
  function terminalCreateStatus(message: string): number {
    return message.includes("Local path validation failed") ||
      message.includes("Maximum") ||
      message.includes("not found") ||
      message.includes("not available") ||
      message.includes("not a directory") ||
      message.includes("does not exist") ||
      message.includes("too large")
      ? 400
      : 500;
  }

  /** Emit the terminal-unavailable startup warning once; swallows health probe failures after logging. */
  function logStartupNotice(): void {
    void getManager()
      .then((manager) => manager.health())
      .then((health) => {
        if (!health.nodePtyAvailable) {
          console.log(
            "Note: Terminal feature unavailable (node-pty failed to load)",
          );
          console.log("  Fix: npm rebuild node-pty (requires C++ build tools)");
          console.log("  pnpm: pnpm approve-builds");
          console.log(
            "  See: https://github.com/blundergoat/goat-flow#troubleshooting",
          );
        }
      })
      .catch(() => {
        console.log(
          "Note: Terminal feature unavailable (node-pty failed to load)",
        );
        console.log("  Fix: npm rebuild node-pty (requires C++ build tools)");
        console.log("  pnpm: pnpm approve-builds");
        console.log(
          "  See: https://github.com/blundergoat/goat-flow#troubleshooting",
        );
      });
  }

  /** Start a terminal session for the requested runner and workspace. */
  async function handleTerminalCreateRequest(
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ): Promise<boolean> {
    if (url.pathname !== "/api/terminal/create" || req.method !== "POST")
      return false;

    try {
      const manager = await getManager();
      const decoded = decodeTerminalCreateBody(await readBody(req), {
        validRunners,
        defaultRunner,
      });
      if (!decoded.ok) {
        jsonResponse(res, 400, { error: decoded.error, path: decoded.path });
        return true;
      }
      const { result, session, resolvedTargetPath } =
        await createTerminalSession(manager, decoded.value);
      recordTerminalLaunchEvents(
        decoded.value,
        result.id,
        session,
        resolvedTargetPath,
      );
      jsonResponse(res, 200, result);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      jsonResponse(res, terminalCreateStatus(message), { error: message });
    }
    return true;
  }

  /** Return the set of currently live terminal sessions. */
  async function handleTerminalListRequest(
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ): Promise<boolean> {
    if (url.pathname !== "/api/terminal/list" || req.method !== "GET")
      return false;

    try {
      const manager = await getManager();
      jsonResponse(res, 200, manager.list());
    } catch (err) {
      jsonResponse(res, 500, {
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return true;
  }

  /** Kill one terminal session and report whether it existed. */
  async function handleTerminalDeleteRequest(
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ): Promise<boolean> {
    if (!url.pathname.startsWith("/api/terminal/") || req.method !== "DELETE")
      return false;

    const id = url.pathname.slice("/api/terminal/".length);
    try {
      const manager = await getManager();
      const session = manager.get(id);
      const killed = manager.kill(id);
      if (killed && session) {
        recordTerminalEvent(
          session.targetPath || session.projectPath,
          "terminal.delete",
          {
            session_id: id,
            runner: session.runner,
            status: session.status,
          },
        );
        jsonResponse(res, 200, { ok: true });
      } else if (killed) {
        jsonResponse(res, 200, { ok: true });
      } else {
        jsonResponse(res, 404, { error: "Session not found" });
      }
    } catch (err) {
      jsonResponse(res, 500, {
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return true;
  }

  /** Read the raw upload body up to TERMINAL_UPLOAD_MAX_BODY_BYTES.
   *  Separate from the dashboard's 64KB readBody so other endpoints stay
   *  capped tightly while uploads can carry several MiB of base64 payload. */
  function readUploadBody(req: IncomingMessage): Promise<string> {
    return new Promise((resolveBody, rejectBody) => {
      const chunks: Buffer[] = [];
      let size = 0;
      let tooLarge = false;
      req.on("data", (chunk: Buffer) => {
        size += chunk.length;
        if (tooLarge) {
          return;
        }
        if (size > TERMINAL_UPLOAD_MAX_BODY_BYTES) {
          tooLarge = true;
          chunks.length = 0;
          return;
        }
        chunks.push(chunk);
      });
      req.on("end", () => {
        if (tooLarge) {
          rejectBody(new Error("Upload body too large"));
          return;
        }
        resolveBody(Buffer.concat(chunks).toString("utf-8"));
      });
      req.on("error", rejectBody);
    });
  }

  /** Accept dragged image files for the active terminal session. */
  // eslint-disable-next-line complexity -- intentional ingress validation; each branch maps to one rejection class.
  async function handleTerminalUploadRequest(
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ): Promise<boolean> {
    const match = url.pathname.match(
      /^\/api\/terminal\/([^/]+)\/upload-image$/u,
    );
    if (!match || req.method !== "POST") return false;

    const sessionId = match[1] ?? "";
    if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) {
      jsonResponse(res, 400, { error: "Invalid session id" });
      return true;
    }

    let body: string;
    try {
      body = await readUploadBody(req);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      jsonResponse(res, 413, { error: message });
      return true;
    }

    const decoded = decodeTerminalUploadBody(body, {
      maxFiles: TERMINAL_UPLOAD_MAX_FILES,
    });
    if (!decoded.ok) {
      jsonResponse(res, 400, { error: decoded.error, path: decoded.path });
      return true;
    }

    try {
      const manager = await getManager();
      const session = manager.get(sessionId);
      if (!session) {
        jsonResponse(res, 404, { error: "Session not found" });
        return true;
      }
      if (session.status !== "active") {
        jsonResponse(res, 409, {
          error: `Session is ${session.status}; uploads require an active session`,
        });
        return true;
      }
      if (!session.targetPath) {
        jsonResponse(res, 409, {
          error: "Session has no target path; cannot resolve upload directory",
        });
        return true;
      }

      let uploadDir: ReturnType<typeof uploadDirForSession>;
      try {
        uploadDir = uploadDirForSession(session.targetPath, sessionId);
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        jsonResponse(res, 400, { error: message });
        return true;
      }

      const result = persistUploads(uploadDir, decoded.value.files);
      const note = buildAttachmentNote(result.accepted);
      recordTerminalEvent(session.targetPath, "terminal.upload", {
        session_id: sessionId,
        runner: session.runner,
        accepted_count: result.accepted.length,
        rejected_count: result.rejected.length,
        bytes: result.accepted.reduce((total, file) => total + file.bytes, 0),
      });
      jsonResponse(res, 200, {
        accepted: result.accepted.map((file) => ({
          originalName: file.originalName,
          savedName: file.savedName,
          savedRelPath: file.savedRelPath,
          bytes: file.bytes,
        })),
        rejected: result.rejected,
        note,
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      jsonResponse(res, 500, { error: message });
    }
    return true;
  }

  /** Return terminal-backend health details for dashboard diagnostics. */
  async function handleHealthRequest(
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ): Promise<boolean> {
    if (url.pathname !== "/api/health" || req.method !== "GET") return false;

    try {
      const manager = await getManager();
      jsonResponse(res, 200, await manager.health());
    } catch (err) {
      jsonResponse(res, 500, {
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return true;
  }

  /** Return enriched terminal session info with age and idle duration. */
  async function handleTerminalSessionsRequest(
    req: IncomingMessage,
    url: URL,
    res: ServerResponse,
  ): Promise<boolean> {
    if (url.pathname !== "/api/terminal/sessions" || req.method !== "GET")
      return false;

    try {
      const manager = await getManager();
      const sessions = manager.list();
      const now = Date.now();
      const enriched = sessions.map((session) => ({
        ...session,
        age: Math.floor((now - new Date(session.createdAt).getTime()) / 1000),
        idleDuration: Math.floor((now - session.lastInputAt) / 1000),
      }));
      jsonResponse(res, 200, {
        sessions: enriched,
        maxSessions: MAX_SESSIONS,
        activeCount: sessions.length,
      });
    } catch (err) {
      jsonResponse(res, 500, {
        error: err instanceof Error ? err.message : String(err),
      });
    }
    return true;
  }

  /** Handle terminal WebSocket upgrades and reject bad origins. */
  function handleTerminalUpgrade(
    req: IncomingMessage,
    socket: Duplex,
    head: Buffer,
    server: HttpServer,
  ): boolean {
    const url = new URL(req.url ?? "/", "http://127.0.0.1");
    if (!url.pathname.startsWith("/ws/terminal/")) return false;

    const origin = req.headers.origin;
    const addr = server.address();
    if (origin && addr && typeof addr !== "string") {
      const expected = `http://127.0.0.1:${addr.port}`;
      if (origin !== expected && origin !== `http://localhost:${addr.port}`) {
        socket.destroy();
        return true;
      }
    }

    const sessionId = url.pathname.slice("/ws/terminal/".length);
    void (async () => {
      try {
        const wss = await getWSS();
        const manager = await getManager();
        wss.handleUpgrade(req, socket, head, (ws) => {
          manager.attachWebSocket(sessionId, ws);
        });
      } catch {
        socket.destroy();
      }
    })();
    return true;
  }

  /** Close terminal resources with one shared promise because shutdown can be triggered from tests and signals. */
  async function close(): Promise<void> {
    if (closePromise) return closePromise;
    closePromise = (async () => {
      if (managerPromise) {
        const manager = await managerPromise;
        manager.shutdown();
      }
      if (wssPromise) {
        const wss = await wssPromise;
        await new Promise<void>((resolve) => {
          wss.close(() => {
            resolve();
          });
        });
      }
    })();
    return closePromise;
  }

  return {
    handleTerminalCreateRequest,
    handleTerminalListRequest,
    handleTerminalDeleteRequest,
    handleTerminalUploadRequest,
    handleHealthRequest,
    handleTerminalSessionsRequest,
    handleTerminalUpgrade,
    logStartupNotice,
    close,
  };
}
