#!/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 hydraPublicUrl = process.env.HYDRA_PUBLIC_URL ?? "http://127.0.0.1:4444";
const hydraAdminUrl = process.env.HYDRA_ADMIN_URL ?? "http://127.0.0.1:4445";
const adminApiToken = process.env.HYDRA_ADMIN_API_TOKEN;
const publicApiToken = process.env.HYDRA_PUBLIC_API_TOKEN;
const timeoutMs = Number.parseInt(
  process.env.HYDRA_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, path, query) {
  const url = new URL(path, 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();
}

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

  const requestHeaders = {
    accept: "application/json",
    ...headers,
  };
  if (token) {
    requestHeaders.authorization = `Bearer ${token}`;
  }

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

    const contentType = response.headers.get("content-type") ?? "";
    const text = await response.text();
    const data = text
      ? contentType.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(["public", "admin"]).optional(),
  probe: z.enum(["alive", "ready"]).optional(),
});

const ListClientsInputSchema = z.object({
  limit: z.number().int().positive().max(500).optional(),
  offset: z.number().int().min(0).optional(),
  page_size: z.number().int().positive().max(500).optional(),
  page_token: z.string().min(1).optional(),
});

const ClientIdInputSchema = z.object({
  client_id: z.string().min(1),
});

const ClientPayloadInputSchema = z.object({
  client_id: z.string().min(1),
  payload: z.record(z.unknown()),
});

const RegisterClientInputSchema = z.object({
  payload: z.record(z.unknown()),
});

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

  server.tool(
    "hydra_health",
    "Check Hydra health using /health/alive or /health/ready on public/admin ports.",
    HealthInputSchema.shape,
    async (input) => {
      const service = input.service ?? "admin";
      const probe = input.probe ?? "ready";
      const base = service === "public" ? hydraPublicUrl : hydraAdminUrl;
      const url = buildUrl(base, `/health/${probe}`);

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

  server.tool(
    "hydra_list_clients",
    "List OAuth2 clients from Hydra Admin API.",
    ListClientsInputSchema.shape,
    async (input) => {
      const url = buildUrl(hydraAdminUrl, "/clients", {
        limit: input.limit,
        offset: input.offset,
        page_size: input.page_size,
        page_token: input.page_token,
      });

      try {
        const result = await requestJson(url, {}, adminApiToken);
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "hydra_get_client",
    "Get an OAuth2 client by client_id from Hydra Admin API.",
    ClientIdInputSchema.shape,
    async (input) => {
      const url = buildUrl(
        hydraAdminUrl,
        `/clients/${encodeURIComponent(input.client_id)}`,
      );

      try {
        const result = await requestJson(url, {}, adminApiToken);
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "hydra_register_client",
    "Register an OAuth2 client via Hydra public dynamic client registration endpoint.",
    RegisterClientInputSchema.shape,
    async (input) => {
      const url = buildUrl(hydraPublicUrl, "/oauth2/register");

      try {
        const result = await requestJson(
          url,
          {
            method: "POST",
            headers: {
              "content-type": "application/json",
            },
            body: JSON.stringify(input.payload ?? {}),
          },
          publicApiToken,
        );
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "hydra_update_client",
    "Update an OAuth2 client via Hydra Admin API.",
    ClientPayloadInputSchema.shape,
    async (input) => {
      const url = buildUrl(
        hydraAdminUrl,
        `/clients/${encodeURIComponent(input.client_id)}`,
      );

      try {
        const result = await requestJson(
          url,
          {
            method: "PUT",
            headers: {
              "content-type": "application/json",
            },
            body: JSON.stringify(input.payload ?? {}),
          },
          adminApiToken,
        );
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

  server.tool(
    "hydra_delete_client",
    "Delete an OAuth2 client via Hydra Admin API.",
    ClientIdInputSchema.shape,
    async (input) => {
      const url = buildUrl(
        hydraAdminUrl,
        `/clients/${encodeURIComponent(input.client_id)}`,
      );

      try {
        const result = await requestJson(
          url,
          {
            method: "DELETE",
          },
          adminApiToken,
        );
        return formatToolResult({
          status: result.status,
          data: result.data,
        });
      } catch (error) {
        return formatErrorResult(error);
      }
    },
  );

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

main().catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});
