/* eslint-disable functional/no-throw-statements */
/**
 * HTTP loopback helper for MCP tool handlers.
 *
 * Why HTTP loopback (not direct function calls): see
 * `openspec/changes/add-mcp-server/design.md` decision D2. All MCP tools
 * delegate to existing `/api/*` endpoints so any future cross-cutting
 * middleware (auth, rate-limit, audit, metrics) added to `/api` is
 * automatically inherited by MCP without duplicate plumbing.
 *
 * NOTE: TanStack Start / Nitro may expose an in-process route invocation
 * API (e.g. Nitro's `$fetch` helper) that would let us skip the real HTTP
 * round-trip while preserving the routing contract. As of this writing
 * that investigation is open (tasks.md 2.3). If/when adopted, the public
 * surface of this module — `callApi(path, init?)` returning a `Response`
 * — should stay identical so callers don't change.
 */

import { getCurrentPort } from "../lib/serverPort";

const DEFAULT_TIMEOUT_MS = 30_000;

export type CallApiOptions = RequestInit & {
  /** Override default 30s timeout. */
  timeoutMs?: number;
};

export class LoopbackTimeoutError extends Error {
  constructor(
    public readonly path: string,
    public readonly timeoutMs: number,
  ) {
    super(`Loopback call to ${path} timed out after ${timeoutMs}ms`);
    this.name = "LoopbackTimeoutError";
  }
}

/**
 * Make an HTTP request to one of the inspector's own /api/* endpoints
 * via 127.0.0.1 loopback. The returned Response is a standard Web
 * Response; callers decide whether to call .json(), .text(), .arrayBuffer().
 *
 * `path` MUST start with `/` (e.g. `/api/logs`).
 */
export async function callApi(path: string, options: CallApiOptions = {}): Promise<Response> {
  if (!path.startsWith("/")) {
    throw new Error(`callApi: path must start with '/', got: ${path}`);
  }
  const port = getCurrentPort();
  const url = `http://127.0.0.1:${port}${path}`;

  const { timeoutMs = DEFAULT_TIMEOUT_MS, signal: userSignal, ...rest } = options;

  const controller = new AbortController();
  const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);

  // If caller passed their own signal, chain it.
  if (userSignal !== undefined && userSignal !== null) {
    if (userSignal.aborted) {
      controller.abort();
    } else {
      userSignal.addEventListener("abort", () => controller.abort(), { once: true });
    }
  }

  try {
    return await fetch(url, { ...rest, signal: controller.signal });
  } catch (err) {
    if (err instanceof Error && err.name === "AbortError") {
      throw new LoopbackTimeoutError(path, timeoutMs);
    }
    throw err;
  } finally {
    clearTimeout(timeoutHandle);
  }
}
