import { GatewayError } from "../trapos-cloud-gateway/protocol.js";
import type { GatewayRegistry } from "../trapos-cloud-gateway/registry.js";

// MCP server surface, migrated from tools/mcp-bridge. It rides on the existing
// gateway request/reply (GatewayRegistry.request) instead of a dedicated CC-link
// socket, and addresses computers by traposId ("<id>:<label>") rather than a numeric
// computerId. Custom JSON-RPC 2.0 (no SDK), matching the old bridge's wire shape.

export type McpDeps = {
  registry: GatewayRegistry;
  networkId: string;
  // Default deadline for exec-lua / run-file / write-file when the caller omits timeoutMs.
  defaultTimeoutMs: number;
  // Hard ceiling applied to any caller-supplied timeoutMs.
  maxTimeoutMs: number;
  // Short deadline for the per-computer ping in probe-computers.
  probeTimeoutMs: number;
};

type JsonRpcRequest = {
  jsonrpc?: unknown;
  id?: unknown;
  method?: unknown;
  params?: unknown;
};

const SERVER_VERSION = "0.3.0";

const TOOLS = [
  {
    name: "probe-computers",
    description: "List connected TrapOS computers and ping each one. Use the returned traposId with the other tools.",
    inputSchema: { type: "object", properties: {}, additionalProperties: false },
  },
  {
    name: "exec-lua",
    description:
      "Execute an inline Lua snippet on a connected TrapOS computer. Use for short probes; use run-file for existing programs.",
    inputSchema: {
      type: "object",
      properties: {
        traposId: { type: "string", description: 'Target traposId ("<id>:<label>", from probe-computers).' },
        code: { type: "string", description: "Inline Lua source code to execute." },
        timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." },
      },
      required: ["traposId", "code"],
      additionalProperties: false,
    },
  },
  {
    name: "run-file",
    description:
      "Run an existing Lua file or program on a connected TrapOS computer and capture its output. Prefer this for commands such as /programs/ccpm.lua.",
    inputSchema: {
      type: "object",
      properties: {
        traposId: { type: "string", description: 'Target traposId ("<id>:<label>", from probe-computers).' },
        path: { type: "string", description: "Absolute Lua file path on the TrapOS computer to run, for example /programs/ccpm.lua." },
        args: { type: "array", items: { type: "string" }, description: "Optional program arguments passed as Lua varargs." },
        timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." },
      },
      required: ["traposId", "path"],
      additionalProperties: false,
    },
  },
  {
    name: "write-file",
    description: "Create or overwrite a file on a connected TrapOS computer with the supplied content.",
    inputSchema: {
      type: "object",
      properties: {
        traposId: { type: "string", description: 'Target traposId ("<id>:<label>", from probe-computers).' },
        path: { type: "string", description: "Target path on the TrapOS computer." },
        content: { type: "string", description: "File content to write. Empty strings are allowed." },
        timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." },
      },
      required: ["traposId", "path", "content"],
      additionalProperties: false,
    },
  },
];

export async function handleMcpRequest(body: unknown, deps: McpDeps): Promise<unknown> {
  if (Array.isArray(body)) {
    return Promise.all(body.map((item) => handleSingle(item as JsonRpcRequest, deps)));
  }
  return handleSingle(body as JsonRpcRequest, deps);
}

async function handleSingle(request: JsonRpcRequest, deps: McpDeps): Promise<unknown> {
  const id = request && "id" in request ? request.id : null;
  if (!request || request.jsonrpc !== "2.0" || typeof request.method !== "string") {
    return jsonRpcError(id, -32600, "Invalid Request");
  }

  if (request.method === "initialize") {
    return jsonRpcResult(id, {
      protocolVersion: "2024-11-05",
      capabilities: { tools: {} },
      serverInfo: { name: "trapos-mcp", version: SERVER_VERSION },
    });
  }

  if (request.method === "notifications/initialized") {
    return null;
  }

  if (request.method === "tools/list") {
    return jsonRpcResult(id, { tools: TOOLS });
  }

  if (request.method === "tools/call") {
    const params = isRecord(request.params) ? request.params : {};

    if (params.name === "probe-computers") {
      return jsonRpcResult(id, { content: [{ type: "text", text: await probeComputers(deps) }] });
    }

    if (params.name === "exec-lua") {
      const args = isRecord(params.arguments) ? params.arguments : {};
      const parsed = parseExecLuaArgs(args, deps);
      if (!parsed.ok) {
        return jsonRpcError(id, -32602, parsed.error);
      }
      const text = await execLua(deps, parsed.traposId, parsed.code, parsed.timeoutMs);
      return jsonRpcResult(id, { content: [{ type: "text", text }] });
    }

    if (params.name === "run-file") {
      const args = isRecord(params.arguments) ? params.arguments : {};
      const parsed = parseRunFileArgs(args, deps);
      if (!parsed.ok) {
        return jsonRpcError(id, -32602, parsed.error);
      }
      const text = await runFile(deps, parsed.traposId, parsed.path, parsed.args, parsed.timeoutMs);
      return jsonRpcResult(id, { content: [{ type: "text", text }] });
    }

    if (params.name === "write-file") {
      const args = isRecord(params.arguments) ? params.arguments : {};
      const parsed = parseWriteFileArgs(args, deps);
      if (!parsed.ok) {
        return jsonRpcError(id, -32602, parsed.error);
      }
      const text = await writeFile(deps, parsed.traposId, parsed.path, parsed.content, parsed.timeoutMs);
      return jsonRpcResult(id, { content: [{ type: "text", text }] });
    }

    return jsonRpcError(id, -32602, "Unknown tool");
  }

  return jsonRpcError(id, -32601, "Method not found");
}

async function probeComputers(deps: McpDeps): Promise<string> {
  const traposIds = deps.registry.list(deps.networkId);
  if (traposIds.length === 0) {
    return "No computers connected.";
  }

  const lines = await Promise.all(
    traposIds.map(async (traposId) => {
      try {
        await deps.registry.request(deps.networkId, traposId, "ping", {}, deps.probeTimeoutMs);
        return `ok from ${traposId}`;
      } catch (err) {
        return `error from ${traposId}: ${errorText(err)}`;
      }
    }),
  );
  return lines.join("\n");
}

async function execLua(deps: McpDeps, traposId: string, code: string, timeoutMs: number): Promise<string> {
  let payload: Record<string, unknown>;
  try {
    payload = await deps.registry.request(deps.networkId, traposId, "exec-lua", { code }, timeoutMs);
  } catch (err) {
    return transportError(traposId, err);
  }

  return formatExecutionResult(traposId, payload);
}

function formatExecutionResult(traposId: string, payload: Record<string, unknown>): string {
  const ok = payload.ok === true;
  const output = typeof payload.output === "string" ? payload.output : "";
  const lines = [`computer: ${traposId}`, `ok: ${ok ? "true" : "false"}`];
  if (ok) {
    lines.push(`returns: ${JSON.stringify(Array.isArray(payload.returns) ? payload.returns : [])}`);
  } else {
    lines.push(`error: ${typeof payload.error === "string" ? payload.error : "unknown error"}`);
  }
  lines.push("output:");
  if (output !== "") {
    lines.push(output.endsWith("\n") ? output.slice(0, -1) : output);
  }
  return lines.join("\n");
}

async function runFile(deps: McpDeps, traposId: string, path: string, args: string[], timeoutMs: number): Promise<string> {
  let payload: Record<string, unknown>;
  try {
    payload = await deps.registry.request(deps.networkId, traposId, "run-file", { path, args }, timeoutMs);
  } catch (err) {
    return transportError(traposId, err);
  }

  return formatExecutionResult(traposId, payload);
}

async function writeFile(
  deps: McpDeps,
  traposId: string,
  path: string,
  content: string,
  timeoutMs: number,
): Promise<string> {
  let payload: Record<string, unknown>;
  try {
    payload = await deps.registry.request(deps.networkId, traposId, "write-file", { path, content }, timeoutMs);
  } catch (err) {
    return transportError(traposId, err);
  }

  const ok = payload.ok === true;
  const lines = [`computer: ${traposId}`, `ok: ${ok ? "true" : "false"}`];
  if (ok) {
    lines.push(`path: ${typeof payload.path === "string" ? payload.path : ""}`);
    lines.push(`bytes: ${typeof payload.bytes === "number" ? payload.bytes : 0}`);
  } else {
    lines.push(`error: ${typeof payload.error === "string" ? payload.error : "unknown error"}`);
  }
  return lines.join("\n");
}

type ExecLuaArgs = { ok: true; traposId: string; code: string; timeoutMs: number } | { ok: false; error: string };

function parseExecLuaArgs(args: Record<string, unknown>, deps: McpDeps): ExecLuaArgs {
  if (typeof args.traposId !== "string" || args.traposId.trim() === "") {
    return { ok: false, error: "traposId must be a non-empty string" };
  }
  if (typeof args.code !== "string" || args.code.trim() === "") {
    return { ok: false, error: "code must be a non-empty string" };
  }
  const timeout = parseTimeout(args.timeoutMs, deps);
  if (!timeout.ok) {
    return { ok: false, error: timeout.error };
  }
  return { ok: true, traposId: args.traposId, code: args.code, timeoutMs: timeout.value };
}

type RunFileArgs = { ok: true; traposId: string; path: string; args: string[]; timeoutMs: number } | { ok: false; error: string };

function parseRunFileArgs(args: Record<string, unknown>, deps: McpDeps): RunFileArgs {
  if (typeof args.traposId !== "string" || args.traposId.trim() === "") {
    return { ok: false, error: "traposId must be a non-empty string" };
  }
  if (typeof args.path !== "string" || args.path.trim() === "") {
    return { ok: false, error: "path must be a non-empty string" };
  }
  if (args.args !== undefined && !isStringArray(args.args)) {
    return { ok: false, error: "args must be an array of strings" };
  }
  const timeout = parseTimeout(args.timeoutMs, deps);
  if (!timeout.ok) {
    return { ok: false, error: timeout.error };
  }
  return { ok: true, traposId: args.traposId, path: args.path, args: args.args ?? [], timeoutMs: timeout.value };
}

function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every((item) => typeof item === "string");
}

type WriteFileArgs =
  | { ok: true; traposId: string; path: string; content: string; timeoutMs: number }
  | { ok: false; error: string };

function parseWriteFileArgs(args: Record<string, unknown>, deps: McpDeps): WriteFileArgs {
  if (typeof args.traposId !== "string" || args.traposId.trim() === "") {
    return { ok: false, error: "traposId must be a non-empty string" };
  }
  if (typeof args.path !== "string" || args.path.trim() === "") {
    return { ok: false, error: "path must be a non-empty string" };
  }
  if (typeof args.content !== "string") {
    return { ok: false, error: "content must be a string" };
  }
  const timeout = parseTimeout(args.timeoutMs, deps);
  if (!timeout.ok) {
    return { ok: false, error: timeout.error };
  }
  return { ok: true, traposId: args.traposId, path: args.path, content: args.content, timeoutMs: timeout.value };
}

function parseTimeout(
  value: unknown,
  deps: McpDeps,
): { ok: true; value: number } | { ok: false; error: string } {
  if (value === undefined) {
    return { ok: true, value: Math.min(deps.defaultTimeoutMs, deps.maxTimeoutMs) };
  }
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
    return { ok: false, error: "timeoutMs must be a positive finite number" };
  }
  return { ok: true, value: Math.min(value, deps.maxTimeoutMs) };
}

// not_connected / timeout / unknown_type / internal — the request never produced a
// computer reply. Rendered in the same shape as an operational failure for symmetry.
function transportError(traposId: string, err: unknown): string {
  return `computer: ${traposId}\nok: false\nerror: ${errorText(err)}`;
}

function errorText(err: unknown): string {
  if (err instanceof GatewayError) {
    return `${err.code}: ${err.message}`;
  }
  return err instanceof Error ? err.message : String(err);
}

function jsonRpcResult(id: unknown, result: unknown): unknown {
  return { jsonrpc: "2.0", id, result };
}

function jsonRpcError(id: unknown, code: number, message: string): unknown {
  return { jsonrpc: "2.0", id, error: { code, message } };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
