import type { McpdClient } from './http/mcpd-client.js';
import { DISCOVERY_TIMEOUT_MS } from './http/mcpd-client.js';
import type { McpRouter } from './router.js';
import { McpdUpstream } from './upstream/mcpd.js';

interface McpdServer {
  id: string;
  name: string;
  description?: string;
  transport: string;
  status?: string;
}

/**
 * Discovers MCP servers from mcpd and registers them as upstreams in the router.
 * Called periodically or on demand to keep the router in sync with mcpd.
 */
export async function refreshUpstreams(router: McpRouter, mcpdClient: McpdClient): Promise<string[]> {
  const servers = await mcpdClient.get<McpdServer[]>('/api/v1/servers');
  return syncUpstreams(router, mcpdClient, servers);
}

/**
 * Discovers MCP servers scoped to a project and registers them as upstreams.
 * Uses the project-servers endpoint that returns only servers linked to the project.
 *
 * @param authToken - Optional bearer token forwarded to mcpd for RBAC checks.
 */
export async function refreshProjectUpstreams(
  router: McpRouter,
  mcpdClient: McpdClient,
  projectName: string,
  authToken?: string,
): Promise<string[]> {
  const path = `/api/v1/projects/${encodeURIComponent(projectName)}/servers`;

  let servers: McpdServer[];
  if (authToken) {
    // Forward the client's auth token to mcpd so RBAC applies
    const result = await mcpdClient.forward('GET', path, '', undefined, authToken);
    if (result.status >= 400) {
      throw new Error(`Failed to fetch project servers: ${result.status}`);
    }
    servers = result.body as McpdServer[];
  } else {
    servers = await mcpdClient.get<McpdServer[]>(path);
  }

  // Downstream upstream-proxy calls go through `mcpdClient` too. In HTTP-mode
  // mcplocal the pod has no credentials of its own, so the default token on
  // `mcpdClient` is an empty string — every /api/v1/mcp/proxy call would 401.
  // Bind a per-request client with the caller's bearer so each McpdUpstream
  // forwards the same identity that passed project discovery.
  const upstreamClient = authToken ? mcpdClient.withToken(authToken) : mcpdClient;
  return syncUpstreams(router, upstreamClient, servers);
}

/**
 * Fetch a project's LLM config (llmProvider, llmModel) from mcpd.
 *
 * Phase 4 redefines `llmProvider` semantically: it names a centralized `Llm`
 * resource (see `mcpctl get llms`) — NOT a local provider. Consumers should
 * resolve it through mcpd's inference proxy when reachable. The field remains
 * a free-form string on the wire for backward compatibility; local overrides
 * in `~/.mcpctl/config.json` still take priority, and unknown names fall
 * through to the registry default.
 */
/**
 * Two-namespace `favourite/` + `all/` tool presentation config (per-project).
 * `tools` are canonical `server/tool` pins shown as favourites, in order.
 */
export interface FavouriteIndexConfig {
  enabled?: boolean;
  tools?: string[];
  maxFavourites?: number;
}

export interface ProjectLlmConfig {
  /** Name of an `Llm` resource on mcpd, or 'none' to disable LLM features. */
  llmProvider?: string;
  llmModel?: string;
  proxyModel?: string;
  gated?: boolean;
  serverOverrides?: Record<string, { proxyModel?: string }>;
  favouriteIndex?: FavouriteIndexConfig;
}

/**
 * Resolve a project's `llmProvider` against mcpd's Llm registry. Returns:
 *   - 'registered'  — an Llm with this name exists
 *   - 'disabled'    — value is 'none'
 *   - 'unregistered'— no Llm matches (consumer should fall back to registry default)
 *   - 'unreachable' — mcpd couldn't be queried
 */
export type LlmReferenceStatus = 'registered' | 'disabled' | 'unregistered' | 'unreachable';

export async function resolveProjectLlmReference(
  mcpdClient: McpdClient,
  llmProvider: string | undefined,
): Promise<LlmReferenceStatus> {
  if (llmProvider === undefined || llmProvider === '') return 'unregistered';
  if (llmProvider === 'none') return 'disabled';
  try {
    await mcpdClient.get(`/api/v1/llms/${encodeURIComponent(llmProvider)}`);
    return 'registered';
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    if (msg.includes('404') || msg.toLowerCase().includes('not found')) return 'unregistered';
    return 'unreachable';
  }
}

export async function fetchProjectLlmConfig(
  mcpdClient: McpdClient,
  projectName: string,
): Promise<ProjectLlmConfig> {
  try {
    const project = await mcpdClient.get<{
      llmProvider?: string;
      llmModel?: string;
      proxyModel?: string;
      gated?: boolean;
      serverOverrides?: Record<string, { proxyModel?: string }>;
      favouriteIndex?: FavouriteIndexConfig;
    }>(`/api/v1/projects/${encodeURIComponent(projectName)}`);
    const config: ProjectLlmConfig = {};
    if (project.llmProvider) config.llmProvider = project.llmProvider;
    if (project.llmModel) config.llmModel = project.llmModel;
    // proxyModel is the primary field; gated is derived from it.
    // Backward compat: if proxyModel is empty, infer from gated boolean.
    if (project.proxyModel) {
      config.proxyModel = project.proxyModel;
    } else if (project.gated === false) {
      config.proxyModel = 'content-pipeline';
    } else {
      config.proxyModel = 'default';
    }
    config.gated = config.proxyModel === 'default' || config.proxyModel === 'gate';
    if (project.serverOverrides) config.serverOverrides = project.serverOverrides;
    if (project.favouriteIndex) config.favouriteIndex = project.favouriteIndex;
    return config;
  } catch {
    return {};
  }
}

/** Shared sync logic: reconcile a router's upstreams with a server list. */
function syncUpstreams(router: McpRouter, mcpdClient: McpdClient, servers: McpdServer[]): string[] {
  const registered: string[] = [];

  // Discovery-class calls (`*\/list`) go through a short-timeout client so a single
  // unreachable upstream cannot stall session init for the full tool-call window.
  const discoveryClient = mcpdClient.withTimeout(DISCOVERY_TIMEOUT_MS);

  // Remove stale upstreams
  const currentNames = new Set(router.getUpstreamNames());
  const serverNames = new Set(servers.map((s) => s.name));
  for (const name of currentNames) {
    if (!serverNames.has(name)) {
      router.removeUpstream(name);
    }
  }

  // Add/update upstreams for each server
  for (const server of servers) {
    if (!currentNames.has(server.name)) {
      const upstream = new McpdUpstream(server.id, server.name, mcpdClient, server.description, discoveryClient);
      router.addUpstream(upstream);
    }
    registered.push(server.name);
  }

  return registered;
}
