import { Elysia } from "elysia";
import { Effect } from "effect";
import { createCortexRuntime } from "./runtime.js";
import { runsRouter } from "./api/runs.js";
import { messagesRouter } from "./api/messages.js";
import { agentsRouter } from "./api/agents.js";
import { toolsRouter } from "./api/tools.js";
import { mcpServersRouter } from "./api/mcp-servers.js";
import { skillsRouter } from "./api/skills.js";
import { modelsRouter } from "./api/models.js";
import { chatRouter } from "./api/chat.js";
import { memoryRouter } from "./api/memory.js";
import { promptRouter } from "./api/prompts.js";
import { templateResolveRouter } from "./api/template-resolve.js";
import { healthRouter } from "./api/health.js";
import { capabilitiesRouter } from "./api/capabilities.js";
import { ChatSessionService } from "./services/chat-session-service.js";
import { handleIngestMessage } from "./ws/ingest.js";
import {
  handleLiveOpen,
  handleLiveClose,
  replayRunEvents,
  type LiveWsData,
} from "./ws/live.js";
import type { ElysiaWS } from "elysia/ws";
import { CortexEventBridge } from "./services/event-bridge.js";
import type { CortexConfig } from "./types.js";
import { defaultCortexConfig } from "./types.js";
import { cortexLog } from "./cortex-log.js";

/** Base URL for startup log and “open browser” (aligns with CORTEX_URL when set). */
function cortexListenDisplayUrl(port: number): string {
  const raw = process.env.CORTEX_URL?.trim();
  if (raw) {
    try {
      const u = new URL(raw);
      return `${u.protocol}//${u.host}`;
    } catch {
      /* ignore invalid CORTEX_URL */
    }
  }
  return `http://127.0.0.1:${port}`;
}

export async function startCortexServer(
  config: Partial<CortexConfig> = {},
): Promise<void> {
  // Merge with defaults so callers can pass partial config (e.g. just { port }).
  // Auto-resolve UI assets path when not provided, so npm consumers get the bundled UI
  // without configuration. From dist/index.js → `../ui/build`. From source server/index.ts →
  // `../ui/build`. Both layouts go up one level to reach the package root.
  const resolvedConfig: CortexConfig = {
    ...defaultCortexConfig,
    ...config,
    staticAssetsPath:
      config.staticAssetsPath ??
      defaultCortexConfig.staticAssetsPath ??
      new URL("../ui/build", import.meta.url).pathname,
  };

  const runtime = createCortexRuntime(resolvedConfig);

  const bridgeSvc = await Effect.runPromise(
    CortexEventBridge.pipe(Effect.provide(runtime.bridgeLayer)),
  );

  // Hydrate gateway agents — starts persistent processes for all active agents
  void runtime.gateway.hydrate();

  const chatSessionService = new ChatSessionService(runtime.rawDb);

  const app = new Elysia()
    .use(healthRouter)
    .use(capabilitiesRouter)
    .use(runsRouter(runtime.storeLayer, runtime.runnerLayer))
    .use(messagesRouter(runtime.rawDb))
    .use(agentsRouter(runtime.rawDb, runtime.gateway))
    .use(toolsRouter(runtime.storeLayer, runtime.rawDb))
    .use(mcpServersRouter(runtime.rawDb))
    .use(skillsRouter(runtime.storeLayer, runtime.rawDb))
    .use(modelsRouter)
    .use(chatRouter(chatSessionService))
    .use(memoryRouter())
    .use(promptRouter(runtime.rawDb))
    .use(templateResolveRouter())
    .ws("/ws/ingest", {
      open() {
        cortexLog("info", "ingest-ws", "ingest client connected");
      },
      close() {
        cortexLog("info", "ingest-ws", "ingest client disconnected");
      },
      message(ws, raw) {
        handleIngestMessage(ws.raw, raw, runtime.ingestLayer);
      },
    })
    .ws("/ws/live/:agentId", {
      upgrade({ params, query }) {
        const p = params as Record<string, string | undefined>;
        const agentId = p.agentId ?? p.agentid;
        return {
          agentId,
          runId: query.runId,
        };
      },
      open(ws) {
        const live = ws as unknown as ElysiaWS<LiveWsData>;
        handleLiveOpen(live, bridgeSvc);
        if (live.data.runId) {
          void replayRunEvents(live, runtime.storeLayer, runtime.bridgeLayer);
        }
      },
      close(ws) {
        handleLiveClose(ws as unknown as ElysiaWS<LiveWsData>, bridgeSvc);
      },
    })
    // Serve static assets from the built UI (JS, CSS, fonts, images, etc.)
    // Must come before the SPA fallback route so asset requests are matched first.
    .get("/_app/*", async ({ params, set }) => {
      if (!resolvedConfig.staticAssetsPath) {
        set.status = 404;
        return "Cortex UI not built. Run: cd apps/cortex/ui && bun run build";
      }
      const assetPath = `${resolvedConfig.staticAssetsPath}/_app/${params["*"]}`;
      const file = Bun.file(assetPath);
      if (!(await file.exists())) {
        set.status = 404;
        return "Asset not found";
      }
      return file;
    })
    .get("/*", ({ set }) => {
      if (resolvedConfig.staticAssetsPath) {
        return Bun.file(`${resolvedConfig.staticAssetsPath}/index.html`);
      }
      set.status = 404;
      return "Cortex UI not built. Run: cd apps/cortex/ui && bun run build";
    });

  const displayUrl = cortexListenDisplayUrl(resolvedConfig.port);

  // Bun’s default idleTimeout (~10s) drops connections that send no response bytes yet — e.g. long MCP
  // `refresh-tools` (docker pull / handshake). Node’s Vite proxy then logs "socket hang up". 0 disables.
  app.listen(
    { port: resolvedConfig.port, idleTimeout: 0 },
    () => {
      if (process.env.CORTEX_SPAWNED_BY_RAX !== "1") {
        console.log(`\n◈ CORTEX running at ${displayUrl}\n`);
        cortexLog(
          "info",
          "server",
          "logging: set CORTEX_LOG=debug for per-event ingest + WS details (default is info)",
        );
      }
    },
  );

  if (resolvedConfig.openBrowser) {
    const { exec } = await import("node:child_process");
    const platform = process.platform;
    const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
    exec(`${cmd} ${displayUrl}`);
  }
}

if (import.meta.main) {
  const staticFromEnv = process.env.CORTEX_STATIC_PATH?.trim();
  startCortexServer({
    ...defaultCortexConfig,
    port: parseInt(process.env.CORTEX_PORT ?? "4321", 10),
    openBrowser: process.env.CORTEX_NO_OPEN !== "1",
    // Honor explicit env override; otherwise startCortexServer auto-resolves to ../ui/build.
    ...(staticFromEnv ? { staticAssetsPath: staticFromEnv } : {}),
  });
}
