#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import type { FastifyInstance } from 'fastify';
import type { ProxyConfig, UpstreamConfig } from './types.js';
import { McpRouter } from './router.js';
import { StdioProxyServer } from './server.js';
import { StdioUpstream } from './upstream/stdio.js';
import { HttpUpstream } from './upstream/http.js';
import { createHttpServer } from './http/server.js';
import { loadHttpConfig, loadLlmProviders, loadLocalAgents, loadPublisherSuffix, applyPublisherSuffix } from './http/config.js';
import type { HttpConfig, LlmProviderFileEntry, AgentFileEntry } from './http/config.js';
import { createProvidersFromConfig } from './llm-config.js';
import { createSecretStore } from '@mcpctl/shared';
import type { ProviderRegistry } from './providers/registry.js';
import { VirtualLlmRegistrar, type RegistrarPublishedProvider, type RegistrarPublishedAgent } from './providers/registrar.js';
import { startWatchers, stopWatchers, reloadStages } from './proxymodel/watcher.js';
import { existsSync, readFileSync as readFileSyncNs } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

interface ParsedArgs {
  configPath: string | undefined;
  upstreams: string[];
  noHttp: boolean;
}

function parseArgs(argv: string[]): ParsedArgs {
  let configPath: string | undefined;
  const upstreams: string[] = [];
  let noHttp = false;

  for (let i = 2; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === '--config' && i + 1 < argv.length) {
      configPath = argv[++i];
    } else if (arg?.startsWith('--config=')) {
      configPath = arg.slice('--config='.length);
    } else if (arg === '--upstream' && i + 1 < argv.length) {
      upstreams.push(argv[++i]!);
    } else if (arg?.startsWith('--upstream=')) {
      upstreams.push(arg.slice('--upstream='.length));
    } else if (arg === '--no-http') {
      noHttp = true;
    }
  }
  return { configPath, upstreams, noHttp };
}

function loadConfig(configPath: string): ProxyConfig {
  const raw = readFileSync(configPath, 'utf-8');
  return JSON.parse(raw) as ProxyConfig;
}

function createUpstream(config: UpstreamConfig) {
  if (config.transport === 'stdio') {
    return new StdioUpstream(config);
  }
  return new HttpUpstream(config);
}

export interface MainResult {
  router: McpRouter;
  server: StdioProxyServer;
  httpServer: FastifyInstance | undefined;
  httpConfig: HttpConfig;
  providerRegistry: ProviderRegistry;
}

export async function main(argv: string[] = process.argv): Promise<MainResult> {
  const args = parseArgs(argv);
  const httpConfig = loadHttpConfig();

  // Load LLM providers from user config + secret store
  const llmEntries = loadLlmProviders();
  const secretStore = await createSecretStore();
  const providerRegistry = await createProvidersFromConfig(llmEntries, secretStore);
  if (providerRegistry.hasTierConfig()) {
    const fast = providerRegistry.getTierProviders('fast');
    const heavy = providerRegistry.getTierProviders('heavy');
    process.stderr.write(`LLM providers: fast=[${fast.join(',')}] heavy=[${heavy.join(',')}]\n`);
  } else {
    const activeLlm = providerRegistry.getActive();
    if (activeLlm) {
      process.stderr.write(`LLM provider: ${activeLlm.name}\n`);
    }
  }

  let upstreamConfigs: UpstreamConfig[] = [];

  if (args.configPath) {
    const config = loadConfig(args.configPath);
    upstreamConfigs = config.upstreams;
  }

  // --upstream flags: "name:command arg1 arg2" for STDIO or "name:http://url" for HTTP
  for (const spec of args.upstreams) {
    const colonIdx = spec.indexOf(':');
    if (colonIdx === -1) continue;
    const name = spec.slice(0, colonIdx);
    const rest = spec.slice(colonIdx + 1);

    if (rest.startsWith('http://') || rest.startsWith('https://')) {
      upstreamConfigs.push({
        serverId: name,
        name,
        transport: 'streamable-http',
        url: rest,
      });
    } else {
      const parts = rest.split(' ').filter(Boolean);
      const command = parts[0];
      if (!command) continue;
      const config: UpstreamConfig = {
        serverId: name,
        name,
        transport: 'stdio',
        command,
        args: parts.slice(1),
      };
      upstreamConfigs.push(config);
    }
  }

  const router = new McpRouter();

  for (const config of upstreamConfigs) {
    const upstream = createUpstream(config);
    if (upstream instanceof StdioUpstream) {
      await upstream.start();
    }
    router.addUpstream(upstream);
  }

  // Start stdio proxy server
  const server = new StdioProxyServer(router);
  server.start();
  process.stderr.write(`mcpctl-proxy started with ${upstreamConfigs.length} upstream(s)\n`);

  // Start HTTP server unless disabled
  let httpServer: FastifyInstance | undefined;
  if (!args.noHttp) {
    httpServer = await createHttpServer(httpConfig, { router, providerRegistry });
    await httpServer.listen({ port: httpConfig.httpPort, host: httpConfig.httpHost });
    process.stderr.write(`mcpctl-proxy HTTP server listening on ${httpConfig.httpHost}:${httpConfig.httpPort}\n`);
  }

  // Load custom stages and start file watchers for hot-reload
  await reloadStages();
  startWatchers();

  // Virtual-LLM registrar: publish opted-in providers (`publish: true`)
  // into mcpd's Llm registry. Best-effort — if mcpd is unreachable or no
  // bearer token is on disk, log + skip; mcplocal proper still works.
  const localAgents = loadLocalAgents();
  const registrar = await maybeStartVirtualLlmRegistrar(providerRegistry, llmEntries, localAgents);

  // Graceful shutdown
  let shuttingDown = false;
  const shutdown = async () => {
    if (shuttingDown) return;
    shuttingDown = true;

    stopWatchers();
    registrar?.stop();
    providerRegistry.disposeAll();
    server.stop();
    if (httpServer) {
      await httpServer.close();
    }
    await router.closeAll();
    process.exit(0);
  };

  process.on('SIGTERM', () => void shutdown());
  process.on('SIGINT', () => void shutdown());

  return { router, server, httpServer, httpConfig, providerRegistry };
}

// Run when executed directly
const isMain =
  process.argv[1]?.endsWith('main.js') ||
  process.argv[1]?.endsWith('main.ts') ||
  process.argv[1]?.endsWith('mcpctl-local');
if (isMain) {
  main().catch((err) => {
    process.stderr.write(`Fatal: ${err}\n`);
    process.exit(1);
  });
}

/**
 * Start the virtual-LLM registrar if any local provider has `publish: true`
 * AND we have a bearer token on disk. Returns the registrar instance for
 * lifecycle teardown, or null when nothing was started. All failure paths
 * log a warning and resolve null — the registrar is best-effort, not a
 * boot blocker.
 */
async function maybeStartVirtualLlmRegistrar(
  providerRegistry: ProviderRegistry,
  llmEntries: LlmProviderFileEntry[],
  localAgents: AgentFileEntry[] = [],
): Promise<VirtualLlmRegistrar | null> {
  const opted = llmEntries.filter((e) => e.publish === true);
  if (opted.length === 0 && localAgents.length === 0) return null;

  // v6: per-publisher namespacing. Each user's mcplocal can append a
  // suffix to all published names so two publishers sharing a config
  // template (`vllm-local-qwen3`) don't collide on mcpd's cluster-wide
  // unique-name constraint. Empty suffix = today's behavior.
  const publisherSuffix = loadPublisherSuffix();
  // Map of local-provider-name → wire-side publish name. Used twice:
  // once when building RegistrarPublishedProvider entries, again when
  // rewriting agent.llm references so the agent rows get pinned to
  // the suffixed wire name (otherwise they'd 404 on register).
  const publishNameByLocal = new Map<string, string>();

  const published: RegistrarPublishedProvider[] = [];
  for (const entry of opted) {
    const provider = providerRegistry.get(entry.name);
    if (provider === undefined) {
      process.stderr.write(`virtual-llm registrar: provider '${entry.name}' opted-in but not registered locally; skipping\n`);
      continue;
    }
    const wireName = applyPublisherSuffix(entry.name, publisherSuffix);
    publishNameByLocal.set(entry.name, wireName);
    const item: RegistrarPublishedProvider = {
      provider,
      type: entry.type,
      model: entry.model ?? entry.name,
    };
    if (entry.tier !== undefined) item.tier = entry.tier;
    if (entry.wake !== undefined) item.wake = entry.wake;
    if (entry.poolName !== undefined) item.poolName = entry.poolName;
    if (wireName !== provider.name) item.publishName = wireName;
    // v7: pass visibility through; registrar already defaults to
    // 'private' when omitted, and the per-provider override flows
    // straight through to the register payload.
    if (entry.visibility !== undefined) item.visibility = entry.visibility;
    published.push(item);
  }
  // v3: forward locally-declared agents alongside the providers. We
  // only forward agents whose `llm` field points at a name we're
  // actually publishing (or pre-declared). Stale entries are dropped
  // with a warning rather than failing the whole registration.
  // v6: agent's `llm` field also gets the publisher suffix applied
  // (only when it matches a locally-published provider). When it
  // doesn't match, the agent is presumed to be pinning a public Llm
  // by name and the suffix is NOT applied.
  const publishedAgents: RegistrarPublishedAgent[] = [];
  for (const a of localAgents) {
    // Pin to suffixed wire name when the agent's llm is one of ours;
    // pass through otherwise (publisher means it for a public Llm).
    const llmWireName = publishNameByLocal.get(a.llm) ?? a.llm;
    const agentWireName = applyPublisherSuffix(a.name, publisherSuffix);
    const item: RegistrarPublishedAgent = { name: agentWireName, llmName: llmWireName };
    if (a.description !== undefined) item.description = a.description;
    if (a.systemPrompt !== undefined) item.systemPrompt = a.systemPrompt;
    if (a.project !== undefined) item.project = a.project;
    if (a.defaultParams !== undefined) item.defaultParams = a.defaultParams;
    if (a.extras !== undefined) item.extras = a.extras;
    if (a.visibility !== undefined) item.visibility = a.visibility;
    publishedAgents.push(item);
  }

  if (published.length === 0 && publishedAgents.length === 0) return null;

  // Resolve mcpd URL + bearer. Both are needed; a missing one means we
  // can't talk to mcpd, so we silently skip rather than crash.
  const credsPath = join(homedir(), '.mcpctl', 'credentials');
  if (!existsSync(credsPath)) {
    process.stderr.write(`virtual-llm registrar: ~/.mcpctl/credentials missing — skipping (run \`mcpctl auth login\` to publish virtual LLMs)\n`);
    return null;
  }
  let mcpdUrl: string;
  let token: string;
  try {
    const parsed = JSON.parse(readFileSyncNs(credsPath, 'utf-8')) as { mcpdUrl?: string; token?: string };
    if (typeof parsed.mcpdUrl !== 'string' || typeof parsed.token !== 'string') {
      process.stderr.write(`virtual-llm registrar: credentials missing mcpdUrl/token — skipping\n`);
      return null;
    }
    mcpdUrl = parsed.mcpdUrl;
    token = parsed.token;
  } catch (err) {
    process.stderr.write(`virtual-llm registrar: failed to read credentials: ${(err as Error).message}\n`);
    return null;
  }

  const registrar = new VirtualLlmRegistrar({
    mcpdUrl,
    token,
    publishedProviders: published,
    ...(publishedAgents.length > 0 ? { publishedAgents } : {}),
    sessionFilePath: join(homedir(), '.mcpctl', 'provider-session'),
    log: {
      info: (msg) => process.stderr.write(`${msg}\n`),
      warn: (msg) => process.stderr.write(`${msg}\n`),
      error: (msg) => process.stderr.write(`${msg}\n`),
    },
  });
  try {
    await registrar.start();
  } catch (err) {
    process.stderr.write(`virtual-llm registrar: start failed: ${(err as Error).message}\n`);
    return null;
  }
  return registrar;
}
