#!/usr/bin/env node
import { createRequire } from "node:module";
import path from "node:path";
import { pathToFileURL } from "node:url";

const modulesBase = process.env.MCP_MODULES_DIR;
const requireFromModules = createRequire(
  modulesBase ? path.join(modulesBase, "package.json") : import.meta.url,
);

const mcpModule = await import(
  resolveModule("@modelcontextprotocol/sdk/server/mcp.js")
);
const stdioModule = await import(
  resolveModule("@modelcontextprotocol/sdk/server/stdio.js")
);
const zodModule = await import(resolveModule("zod"));

const { McpServer } = mcpModule;
const { StdioServerTransport } = stdioModule;
const { z } = zodModule;

const ketoReadUrl = process.env.KETO_READ_URL ?? "http://127.0.0.1:4466";
const ketoWriteUrl = process.env.KETO_WRITE_URL ?? "http://127.0.0.1:4467";
const readApiToken = process.env.KETO_READ_API_TOKEN;
const writeApiToken = process.env.KETO_WRITE_API_TOKEN;
const timeoutMs = Number.parseInt(
  process.env.KETO_HTTP_TIMEOUT_MS ?? "15000",
  10,
);

class HttpError extends Error {
  constructor(message, status, body, url) {
    super(message);
    this.name = "HttpError";
    this.status = status;
    this.body = body;
    this.url = url;
  }
}

function resolveModule(specifier) {
  const resolvedPath = requireFromModules.resolve(specifier);
  return pathToFileURL(resolvedPath).href;
}

function buildUrl(base, pathValue, query) {
  const url = new URL(pathValue, base);
  if (query) {
    for (const [key, value] of Object.entries(query)) {
      if (value === undefined || value === null || value === "") {
        continue;
      }
      url.searchParams.set(key, String(value));
    }
  }
  return url.toString();
}

function encodeBody(body) {
  if (body === undefined) {
    return { payload: undefined, contentType: undefined };
  }
  if (typeof body === "string") {
    return { payload: body, contentType: undefined };
  }
  return { payload: JSON.stringify(body), contentType: "application/json" };
}

async function requestJson(url, { method = "GET", headers, body } = {}, token) {
  const controller = new AbortController();
  const timeoutId = Number.isFinite(timeoutMs)
    ? setTimeout(() => controller.abort(), timeoutMs)
    : null;

  const { payload, contentType } = encodeBody(body);
  const requestHeaders = {
    accept: "application/json",
    ...headers,
  };
  if (contentType && !requestHeaders["content-type"]) {
    requestHeaders["content-type"] = contentType;
  }
  if (token) {
    requestHeaders.authorization = `Bearer ${token}`;
  }

  try {
    const response = await fetch(url, {
      method,
      headers: requestHeaders,
      body: payload,
      signal: controller.signal,
    });

    const contentTypeHeader = response.headers.get("content-type") ?? "";
    const text = await response.text();
    const data = text
      ? contentTypeHeader.includes("application/json")
        ? safeJsonParse(text)
        : text
      : null;

    if (!response.ok) {
      throw new HttpError(
        `HTTP ${response.status} ${response.statusText}`,
        response.status,
        data,
        url,
      );
    }

    return {
      status: response.status,
      data,
    };
  } finally {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
  }
}

function safeJsonParse(text) {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

function formatToolResult(payload) {
  return {
    content: [
      {
        type: "text",
        text: JSON.stringify(payload, null, 2),
      },
    ],
  };
}

function formatErrorResult(error) {
  if (error instanceof HttpError) {
    return formatToolResult({
      error: {
        message: error.message,
        status: error.status,
        url: error.url,
        body: error.body,
      },
    });
  }

  return formatToolResult({
    error: {
      message: error instanceof Error ? error.message : String(error),
    },
  });
}

const HealthInputSchema = z.object({
  service: z.enum(["read", "write"]).optional(),
  probe: z.enum(["alive", "ready"]).optional(),
});

const RelationTupleQuerySchema = z.object({
  namespace: z.string().min(1).optional(),
  object: z.string().min(1).optional(),
  relation: z.string().min(1).optional(),
  subject_id: z.string().min(1).optional(),
  subject_set_namespace: z.string().min(1).optional(),
  subject_set_object: z.string().min(1).optional(),
  subject_set_relation: z.string().min(1).optional(),
  page_size: z.number().int().positive().max(500).optional(),
  page_token: z.string().min(1).optional(),
});

const RelationTupleBodySchema = z.object({
  body: z.record(z.unknown()),
});

const WriteRelationTuplesSchema = z.object({
  method: z.enum(["PATCH", "PUT"]).optional(),
  body: z.record(z.unknown()),
});

async function main() {
  const server = new McpServer({
    name: "mcp-ory-keto",
    version: "0.1.0",
  });

  server.tool(
    "keto_health",
    "Check Keto health using /health/alive or /health/ready on read/write ports.",
    HealthInputSchema.shape,
    async (input) => {
      const service = input.service ?? "read";
      const probe = input.probe ?? "ready";
      const base = service === "write" ? ketoWriteUrl : ketoReadUrl;
      const url = buildUrl(base, `/health/${probe}`);

      try {
        const result = await requestJson(
          url,
          {},
          service === "write" ? writeApiToken : readApiToken,
        );
        return formatToolResult({
          service,
          probe,
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "keto_list_relation_tuples",
    "List relation tuples using Keto Read API.",
    RelationTupleQuerySchema.shape,
    async (input) => {
      const url = buildUrl(ketoReadUrl, "/relation-tuples", input);
      try {
        const result = await requestJson(url, {}, readApiToken);
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "keto_check_relation_tuple",
    "Check relation tuples using Keto Read API.",
    RelationTupleBodySchema.shape,
    async (input) => {
      const url = buildUrl(ketoReadUrl, "/relation-tuples/check");
      try {
        const result = await requestJson(
          url,
          {
            method: "POST",
            body: input.body,
          },
          readApiToken,
        );
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "keto_expand",
    "Expand relation tuples using Keto Read API.",
    RelationTupleBodySchema.shape,
    async (input) => {
      const url = buildUrl(ketoReadUrl, "/relation-tuples/expand");
      try {
        const result = await requestJson(
          url,
          {
            method: "POST",
            body: input.body,
          },
          readApiToken,
        );
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "keto_write_relation_tuples",
    "Write relation tuples using Keto Write API.",
    WriteRelationTuplesSchema.shape,
    async (input) => {
      const method = input.method ?? "PATCH";
      const url = buildUrl(ketoWriteUrl, "/relation-tuples");
      try {
        const result = await requestJson(
          url,
          {
            method,
            body: input.body,
          },
          writeApiToken,
        );
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "keto_delete_relation_tuples",
    "Delete relation tuples using Keto Write API.",
    RelationTupleQuerySchema.shape,
    async (input) => {
      const url = buildUrl(ketoWriteUrl, "/relation-tuples", input);
      try {
        const result = await requestJson(
          url,
          {
            method: "DELETE",
          },
          writeApiToken,
        );
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  const transport = new StdioServerTransport();
  await server.connect(transport);
}

await main();
