/**
 * Terminal Commands
 */

import { stat } from "node:fs/promises";
import { basename, isAbsolute } from "node:path";
import {
  encodeTerminalBinaryFrame,
  TERMINAL_BINARY_PROTOCOL_VERSION,
  TERMINAL_INPUT_ACTIVITIES,
  TerminalBinaryFrameType,
  type TerminalInputActivity,
  TerminalInputBase64Args,
  TerminalInputBinaryArgs,
  TerminalSnapshotBinaryResult,
} from "@coder-studio/core";
import { z } from "zod";
import { resolveSafe } from "../fs/file-io.js";
import { registerCommand } from "../ws/dispatch.js";

const TerminalInputActivitySchema = z.enum(TERMINAL_INPUT_ACTIVITIES).optional();

const TerminalInputSchema = z.union([
  z.object({
    terminalId: z.string(),
    bytes: z.string(),
    activity: TerminalInputActivitySchema,
    submittedText: z.string().optional(),
  }),
  z.object({
    terminalId: z.string(),
    transport: z.literal("binary"),
    streamId: z.number().int().nonnegative(),
    size: z.number().int().nonnegative(),
    activity: TerminalInputActivitySchema,
    submittedText: z.string().optional(),
  }),
]);

const DEFAULT_TERMINAL_READ_BYTES = 4096;
const MAX_TERMINAL_READ_BYTES = 65_536;

const pendingTerminalInput = new Map<number, { args: TerminalInputBinaryArgs; payload: Buffer }>();
let nextOutboundBinaryStreamId = 0;

function decodeTerminalInput(args: TerminalInputBase64Args | TerminalInputBinaryArgs): Buffer {
  if ("bytes" in args) {
    return Buffer.from(args.bytes, "base64");
  }

  const pending = pendingTerminalInput.get(args.streamId);
  if (!pending) {
    throw {
      code: "terminal_input_binary_missing",
      message: "Missing binary terminal input payload",
    };
  }
  pendingTerminalInput.delete(args.streamId);
  return pending.payload;
}

function normalizeTerminalInputActivity(
  activity: TerminalInputActivity | undefined
): TerminalInputActivity | undefined {
  return activity;
}

export function registerPendingTerminalInput(args: TerminalInputBinaryArgs, payload: Buffer): void {
  pendingTerminalInput.set(args.streamId, { args, payload });
}

export function clearPendingTerminalInput(streamId: number): void {
  pendingTerminalInput.delete(streamId);
}

function allocateOutboundBinaryStreamId(): number {
  nextOutboundBinaryStreamId = (nextOutboundBinaryStreamId + 1) >>> 0;
  return nextOutboundBinaryStreamId;
}

function sendTerminalBinaryFrame(
  clientId: string | undefined,
  ctx: Parameters<typeof registerCommand>[2] extends (
    args: unknown,
    ctx: infer T,
    clientId?: string
  ) => Promise<unknown>
    ? T
    : never,
  frame: {
    type: (typeof TerminalBinaryFrameType)[keyof typeof TerminalBinaryFrameType];
    meta: number;
    streamId: number;
    payload: Buffer;
  }
): void {
  if (!clientId) {
    return;
  }

  ctx.broadcaster.sendBinaryToClient(
    clientId,
    Buffer.from(
      encodeTerminalBinaryFrame(
        {
          version: TERMINAL_BINARY_PROTOCOL_VERSION,
          type: frame.type,
          flags: 0,
          meta: frame.meta,
          streamId: frame.streamId,
          payloadSize: frame.payload.length,
        },
        frame.payload
      )
    )
  );
}

function resolveShellCommand(): { argv: string[]; title: string } {
  if (process.platform === "win32") {
    const shellPath = process.env.ComSpec || process.env.COMSPEC || "cmd.exe";
    return {
      argv: [shellPath],
      title: basename(shellPath) || shellPath,
    };
  }

  const shellPath = process.env.SHELL || "/bin/bash";
  const shellName = basename(shellPath);
  const argv = shellName === "cmd.exe" ? [shellPath] : [shellPath, "-i"];

  return {
    argv,
    title: shellName || shellPath,
  };
}

// terminal.list
registerCommand(
  "terminal.list",
  z.object({
    workspaceId: z.string(),
  }),
  async (args, ctx) => {
    return ctx.terminalMgr
      .getAll()
      .map((terminal) => terminal.toDTO())
      .filter((terminal) => terminal.workspaceId === args.workspaceId);
  }
);

// terminal.read
registerCommand(
  "terminal.read",
  z.object({
    terminalId: z.string(),
    bytes: z.number().int().positive().max(MAX_TERMINAL_READ_BYTES).optional(),
  }),
  async (args, ctx) => {
    const bytes = args.bytes ?? DEFAULT_TERMINAL_READ_BYTES;
    const tail = ctx.terminalMgr.getRingBufferTail(args.terminalId, bytes);

    return {
      terminalId: args.terminalId,
      bytes,
      text: tail.toString("utf8"),
    };
  }
);

// terminal.create
registerCommand(
  "terminal.create",
  z.object({
    workspaceId: z.string(),
    cols: z.number().int().positive().optional(),
    rows: z.number().int().positive().optional(),
    cwdPath: z.string().optional(),
    themeBackground: z
      .string()
      .regex(/^#[0-9a-fA-F]{3,8}$/)
      .optional(),
  }),
  async (args, ctx) => {
    const workspace = ctx.workspaceMgr.get(args.workspaceId);
    if (!workspace) {
      throw { code: "workspace_not_found", message: `Workspace not found: ${args.workspaceId}` };
    }

    let cwd = workspace.path;
    if (args.cwdPath && args.cwdPath !== ".") {
      if (isAbsolute(args.cwdPath)) {
        throw { code: "invalid_cwd_path", message: "cwdPath must be workspace-relative" };
      }

      let resolvedCwd: string;
      try {
        resolvedCwd = resolveSafe(workspace.path, args.cwdPath);
      } catch (error) {
        if (
          typeof error === "object" &&
          error !== null &&
          "code" in error &&
          error.code === "path_escape"
        ) {
          throw { code: "invalid_cwd_path", message: "cwdPath must be workspace-relative" };
        }
        throw error;
      }

      const cwdStats = await stat(resolvedCwd).catch(() => null);
      if (!cwdStats) {
        throw { code: "cwd_not_found", message: `Directory not found: ${args.cwdPath}` };
      }
      if (!cwdStats.isDirectory()) {
        throw { code: "cwd_not_directory", message: `Not a directory: ${args.cwdPath}` };
      }

      cwd = resolvedCwd;
    }

    const shell = resolveShellCommand();

    // Create shell terminal
    const terminal = ctx.terminalMgr.create({
      workspaceId: args.workspaceId,
      kind: "shell",
      argv: shell.argv,
      title: shell.title,
      cwd,
      cols: args.cols ?? 120,
      rows: args.rows ?? 30,
      themeBackground: args.themeBackground,
    });

    return terminal;
  }
);

// terminal.replay
registerCommand(
  "terminal.replay",
  z.object({
    terminalId: z.string(),
    lastSeq: z.number().int().nonnegative().optional(),
  }),
  async (args, ctx, clientId) => {
    const replay = ctx.terminalMgr.replay(args.terminalId, args.lastSeq ?? 0);

    if (replay.status !== "ok") {
      return replay;
    }

    const streamId = allocateOutboundBinaryStreamId();
    sendTerminalBinaryFrame(clientId, ctx, {
      type: TerminalBinaryFrameType.Replay,
      meta: replay.seq,
      streamId,
      payload: replay.data,
    });

    return {
      status: "ok" as const,
      transport: "binary" as const,
      streamId,
      size: replay.data.length,
      seq: replay.seq,
    };
  }
);

// terminal.snapshot
registerCommand(
  "terminal.snapshot",
  z.object({
    terminalId: z.string(),
  }),
  async (args, ctx, clientId) => {
    const snapshot = await ctx.terminalMgr.snapshot(args.terminalId);

    if (snapshot.status !== "ok") {
      return snapshot;
    }

    const streamId = allocateOutboundBinaryStreamId();
    sendTerminalBinaryFrame(clientId, ctx, {
      type: TerminalBinaryFrameType.Snapshot,
      meta: snapshot.seq,
      streamId,
      payload: snapshot.data,
    });

    return {
      status: "ok" as const,
      transport: "binary" as const,
      streamId,
      size: snapshot.data.length,
      seq: snapshot.seq,
      rows: snapshot.rows,
      cols: snapshot.cols,
      source: "headless" as const,
    } satisfies TerminalSnapshotBinaryResult;
  }
);

// terminal.close
registerCommand(
  "terminal.close",
  z.object({
    terminalId: z.string(),
  }),
  async (args, ctx) => {
    await ctx.terminalMgr.close(args.terminalId);
  }
);

// terminal.input
registerCommand("terminal.input", TerminalInputSchema, async (args, ctx) => {
  const buffer = decodeTerminalInput(args);
  const sessionId = ctx.sessionMgr.findSessionIdByTerminal(args.terminalId);
  if (sessionId) {
    ctx.sessionMgr.sendInput(
      sessionId,
      buffer,
      normalizeTerminalInputActivity(args.activity),
      args.submittedText
    );
    return;
  }

  ctx.terminalMgr.write(args.terminalId, buffer);
});

// terminal.resize
registerCommand(
  "terminal.resize",
  z.object({
    terminalId: z.string(),
    cols: z.number().int().positive(),
    rows: z.number().int().positive(),
  }),
  async (args, ctx) => {
    const sessionId = ctx.sessionMgr.findSessionIdByTerminal(args.terminalId);
    if (sessionId) {
      ctx.sessionMgr.resize(sessionId, args.cols, args.rows);
      return;
    }

    ctx.terminalMgr.resize(args.terminalId, args.cols, args.rows);
  }
);

// terminal.syncThemeBackground
registerCommand(
  "terminal.syncThemeBackground",
  z.object({
    workspaceId: z.string(),
    themeBackground: z
      .string()
      .regex(/^#[0-9a-fA-F]{3,8}$/)
      .optional(),
  }),
  async (args, ctx) => {
    ctx.terminalMgr.syncThemeBackgroundForWorkspace(args.workspaceId, args.themeBackground);
  }
);
