/**
 * API Bootstrap — creates a fully wired Hono app with all dependencies injected.
 *
 * This module assembles the dependency graph and returns a ready-to-use app.
 * Can be used by the real server or by tests with mock dependencies.
 */

import { Hono, type MiddlewareHandler } from "hono";
import type { RuntimeManifest } from "@covel/shared";
import { readRuntimeEnv } from "@covel/shared";
import {
  loadRuntime as loadRuntimeFromDisk,
  loadPluginLlmConfig,
  type PluginRegistry,
  type LoadedRuntime,
  type PluginLlmConfig,
  type PluginRuntimeGateway,
  type PluginRuntimeUtils,
} from "@covel/plugin-loader";
import { createStateManager, type StateManager } from "@covel/state";
import { createEventBus, type EventBus } from "@covel/events";
import type { DataStore, StoreBackend } from "@covel/store";
import type { LLMAdapter } from "@covel/runtime";
import { createToolExecutor, createModelResolver } from "@covel/runtime";
import type { CompactorRunner } from "@covel/context";
import {
  builtinUITools,
  createPluginDataTools,
  createCharacterTools,
  buildSessionCharacterWriteTools,
  createWorldDimensionTools,
  suspendTool,
  runtimeDoneTool,
  type ToolModule,
  type CharacterToolDeps,
} from "@covel/tools";
import { createApprovalPipeline } from "@covel/approval";
import type { PermissionRule } from "@covel/approval";

import {
  createInProcessSessionLock,
  type SessionLock,
} from "../../lib/session-lock.js";
import { sessionRoutes } from "./session.js";
import { turnRoutes } from "./turn.js";
import { pluginRoutes } from "./plugins.js";
import { frameworkRoutes } from "./framework.js";
import { stateRoutes } from "./state.js";
import { eventRoutes } from "./events.js";
import { createHealthRoutes } from "./health.js";
import { worldRoutes } from "./worlds.js";
import { messageRoutes } from "./messages.js";
import { characterRoutes } from "./characters.js";
import { actionRoutes, setMemorySystem } from "./actions.js";
import { subscribeRoutes } from "./subscribe.js";
import { pluginDataRoutes } from "./plugin-data.js";
import { workingMemoryRoutes } from "./working-memory.js";
import { installRoutes } from "./install.js";
import { aiRoutes } from "./ai.js";
import { traceRoutes } from "./traces.js";
import { mediaRoutes } from "./media.js";
import type { MediaStore } from "@covel/store";
import type { MediaStoreBackend, VectorBackend } from "@covel/store";
import { resumeRoutes } from "./resume.js";
import { snapshotRoutes } from "./snapshots.js";
import { lorebookRoutes } from "./lorebook.js";
import { runtimeOutputRoutes } from "./runtime-outputs.js";
import { pluginRpcRoutes } from "./plugin-rpc.js";
import { approvalRoutes, sessionApprovalRoutes } from "./approvals.js";
export { wrapStoreWithPluginDataEvents } from "./bootstrap/plugin-data-store-events.js";
import { createBootstrapCompactorRunner } from "./bootstrap/compactor.js";
import { discoverAndRegisterPlugins } from "./bootstrap/plugin-discovery.js";
import { createBootstrapHookPipeline } from "./bootstrap/plugin-hooks.js";
import { createBootstrapLocalTools } from "./bootstrap/local-tools.js";
import { createBootstrapMemorySystem } from "./bootstrap/memory.js";
import { createBootstrapPluginRpc } from "./bootstrap/plugin-rpc-wiring.js";
import { wrapStoreWithPluginDataEvents } from "./bootstrap/plugin-data-store-events.js";

// ── Bootstrap config ─────────────────────────────────────────────

export interface ApiBootstrapConfig {
  /** Path to plugins directory (e.g., 'plugins/'). Used when `pluginsDirs` is not provided. */
  readonly pluginsDir: string;
  /**
   * Optional ordered list of plugin directories (first wins on collision).
   * When provided, overrides `pluginsDir`. Typical desktop config:
   *   [bundledPluginsDir, userPluginsDir]
   * Bundled plugins take precedence so a user plugin with the same id cannot
   * shadow a core plugin.
   */
  readonly pluginsDirs?: readonly string[];
  /** Ordered world package directories. Later directories override earlier ones for session imports. */
  readonly worldsDirs?: readonly string[];
  /** Covel home directory for user world overrides. */
  readonly covelHome?: string;
  /** LLM adapter (real or mock). */
  readonly llmAdapter: LLMAdapter;
  /**
   * Optional narrow gateway facade exposed to function-runtime handlers
   * via `FunctionHandlerContext.gateway`. The server composition root
   * builds this from `createAiStack().gateway`; tests that don't need
   * LLM access from function runtimes can leave it out.
   */
  readonly pluginGateway?: PluginRuntimeGateway;
  /**
   * Plugin-facing utility surface (SSRF guard + retrying fetch) exposed
   * via `FunctionHandlerContext.utils`. Stateless singleton — typically
   * built from `@covel/ai-provider`'s `validateBaseUrlForPlugin` +
   * `fetchWithRetry` exports in the composition root.
   */
  readonly pluginUtils?: PluginRuntimeUtils;
  /** DataStore for all persistence. */
  readonly store: DataStore;
  /**
   * Active store backend identifier (e.g. `'sqlite'`, `'pg'`, `'memory'`).
   * Used by the health route to report the actual backend in use rather
   * than re-deriving it from environment variables.
   */
  readonly storeBackend: StoreBackend;
  /**
   * Optional embedding-lock helper. When provided, route handlers that
   * begin a turn (start_session, send_message, …) call it to lazily
   * register the session's embedding model in vector_models and lock
   * the session row. No-op when the store has no vector capability or
   * no embed slot is configured.
   */
  readonly ensureEmbeddingLock?: (sessionId: string) => Promise<void>;
  /** Optional pre-created state manager. */
  readonly stateManager?: StateManager;
  /**
   * Preferred slot name for internal memory LLM work.
   *
   * The app composition root computes this from the server slot registry so
   * memory uses the same slot-id contract exposed to runtime bindings and
   * player-facing settings (`memory` → `plugin` → `story` → first text slot).
   */
  readonly preferredMemorySlot?: string;
  /** Optional config provider for injecting world context etc. into runtime execution. */
  readonly getConfigFn?: (
    pluginId: string,
    runtimeId: string,
  ) => Readonly<Record<string, unknown>>;
  /**
   * Optional per-request middleware inserted AFTER the default dependency
   * injection middleware but BEFORE route handlers execute. Intended for
   * request-scoped overrides (e.g. swapping `llmAdapter` based on headers
   * that carry browser-defined custom slots or API keys).
   */
  readonly perRequestMiddleware?: readonly MiddlewareHandler[];
  /**
   * Per-session serializer used by turn-executing routes. When omitted,
   * `bootstrapApi()` installs an in-process `Map`-based lock which is
   * correct for single-node deployments (memory/sqlite, single PG pod).
   * Multi-pod PG deployments MUST pass a distributed implementation —
   * typically `createPgAdvisorySessionLock(sql)` from
   * `../../lib/pg-session-lock.ts` — so mutual exclusion is enforced
   * across processes. See `docs/architecture-audit-followups/F5-*`.
   */
  readonly sessionLock?: SessionLock;
  /**
   * Optional content-addressable media store backing `/api/media/:id`
   * and `ctx.media`. Composition roots that do not generate or serve
   * media (e.g. headless test harnesses) may leave this unset; the
   * route returns 503 in that case. See SPEC §5.1 (b)(g).
   */
  readonly mediaStore?: MediaStore;
  readonly mediaBackend?: MediaStoreBackend;
  readonly vectorBackend?: VectorBackend;
}

export interface ApiBootstrapResult {
  readonly app: Hono;
  readonly registry: PluginRegistry;
  readonly stateManager: StateManager;
  readonly store: DataStore;
  readonly eventBus: EventBus;
  readonly compactorRunner: CompactorRunner;
  /**
   * Refresh the per-session tool override cache for `(create|update)-character`
   * so the next `executeTurn` exposes schema-typed `fields` to the LLM.
   * Idempotent and safe to call when no schema exists yet (it simply clears
   * the cache for that session).
   */
  readonly prepareToolsForSession: (sessionId: string) => Promise<void>;
}

// ── Bootstrap function ───────────────────────────────────────────

/**
 * Create a fully wired API Hono app.
 *
 * 1. Discover and register all plugins
 * 2. Create shared state (session store, state manager, etc.)
 * 3. Inject all dependencies into routes via middleware
 * 4. Mount all route groups
 */
export async function bootstrapApi(
  config: ApiBootstrapConfig,
): Promise<ApiBootstrapResult> {
  // 1. Create shared infrastructure first (eventBus needed by registry)
  const stateManager = config.stateManager ?? createStateManager(config.store);
  const eventBus = createEventBus(config.store);

  // Per-session serializer. The caller (e.g. `app.ts`) may inject a PG
  // advisory-lock implementation for multi-pod safety; otherwise we fall
  // back to the in-process chain lock which is correct for single-process
  // deployments (memory/sqlite, single PG pod). Route handlers read this
  // via `c.get('sessionLock')` and never import a concrete lock module.
  const sessionLock: SessionLock =
    config.sessionLock ?? createInProcessSessionLock();
  console.log(
    `[bootstrap] session lock: ${config.sessionLock ? "external (injected)" : "in-process"}`,
  );

  // Wrap store to automatically emit plugin-data.changed SSE events
  // on every setPluginData / setPluginDataBatch call, regardless of caller.
  const store = wrapStoreWithPluginDataEvents(config.store, eventBus);

  const { registry, discoveryMap, manifestCache } =
    await discoverAndRegisterPlugins({
      pluginsDir: config.pluginsDir,
      pluginsDirs: config.pluginsDirs,
      eventBus,
    });

  // 3. Load plugin-level llm.toml configs for model resolution
  const pluginLlmConfigs = new Map<string, PluginLlmConfig>();
  for (const [pluginId, discovery] of discoveryMap) {
    const llmConfig = await loadPluginLlmConfig(discovery.rootPath);
    if (llmConfig) {
      // Map each runtime name to its plugin's LLM config
      const manifests = manifestCache.get(pluginId);
      if (manifests) {
        for (const parsed of manifests) {
          pluginLlmConfigs.set(parsed.manifest.name, llmConfig);
        }
      }
    }
  }

  const resolveModel = createModelResolver({ pluginLlmConfigs });

  // 4. (sessionScopes was removed 2026-04-12 — see audit Finding 2:
  //     `createSessionScope` had no production caller, the map was always
  //     empty, and PATCH /api/plugins/:id/config always 404'd. Runtime config
  //     now comes from explicit runtime/plugin settings.)

  // 5. loadRuntime resolver (locale-aware: loads PLUGIN.en.md when locale is "en-US")
  const loadRuntimeFn = async (
    manifest: RuntimeManifest,
    locale?: string,
  ): Promise<LoadedRuntime | undefined> => {
    for (const [pluginId, discovery] of discoveryMap) {
      const manifests = manifestCache.get(pluginId);
      if (manifests?.some((m) => m.manifest.name === manifest.name)) {
        return loadRuntimeFromDisk(discovery, manifest.name, locale);
      }
    }
    return undefined;
  };

  // 6. Create ToolExecutor with builtin + plugin local tools + approval
  const builtinToolNames = new Set<string>();
  const localToolNames = new Set<string>();
  const toolMap = new Map<string, ToolModule>();

  for (const t of builtinUITools) {
    toolMap.set(t.name, t);
    builtinToolNames.add(t.name);
  }

  // Register suspend tool. The sentinel becomes a normal tool result returned
  // to the LLM when no suspension handler consumes it.
  toolMap.set(suspendTool.name, suspendTool);
  builtinToolNames.add(suspendTool.name);

  // Register runtime-done tool. Framework contract: agent runtimes call this
  // immediately after completing their business tool calls to exit without
  // burning an extra LLM round-trip on a terminator message. The completion
  // preamble in buildFrameworkPreamble instructs every runtime how to use it.
  toolMap.set(runtimeDoneTool.name, runtimeDoneTool);
  builtinToolNames.add(runtimeDoneTool.name);

  // Register plugin-data tools (store-bound via closure; events emitted by store proxy)
  for (const t of createPluginDataTools(store)) {
    toolMap.set(t.name, t);
    builtinToolNames.add(t.name);
  }

  // Register character management tools (writes characters table + mirrors to plugin-data).
  // The turn-band refactor removed session.phase, so the former phase.changed
  // hook no longer applies — status transitions are driven by turnCount and
  // status updates, not character creation side effects.
  //
  // `findWorldDataPluginId` lets create/update-character locate the schema
  // produced by whichever plugin declares `capabilities: [world-data-provider]`
  // (framework/plugin isolation — same pattern as world-dimension tools).
  // When present, write tools append soft schema warnings to their `_text`
  // output so the LLM can self-correct.
  const characterToolDeps: CharacterToolDeps = {
    findWorldDataPluginId: (sessionId) =>
      registry.findPluginByCapability(sessionId, "world-data-provider"),
  };
  for (const t of createCharacterTools(store, characterToolDeps)) {
    toolMap.set(t.name, t);
    builtinToolNames.add(t.name);
  }

  // ── Per-session tool overrides (Phase 2) ──────────────────────
  //
  // Phase 1 made write-tool `fields` validation soft (warnings in `_text`).
  // Phase 2 lets the LLM see the world-specific schema directly in the tool
  // parameters: `prepareToolsForSession(sessionId)` loads the active
  // CharacterAttributeSchema and rebuilds `create-character`/`update-character`
  // with strongly-typed `fields` Zod, then caches them per session. The
  // `findTool` resolver below checks this cache before falling back to the
  // generic toolMap. Action handlers call `prepareToolsForSession` before
  // every `executeTurn` so the LLM always gets the freshest schema.
  const sessionToolOverrides = new Map<string, Map<string, ToolModule>>();
  const SESSION_OVERRIDABLE_TOOLS = new Set([
    "create-character",
    "update-character",
  ]);

  async function prepareToolsForSession(sessionId: string): Promise<void> {
    if (typeof store.getPluginData !== "function") return;
    const worldPluginId = characterToolDeps.findWorldDataPluginId?.(sessionId);
    if (!worldPluginId) {
      sessionToolOverrides.delete(sessionId);
      return;
    }
    let row: { value: unknown; updatedAt: string } | null = null;
    try {
      row = await store.getPluginData(
        sessionId,
        worldPluginId,
        "schema",
        "character-attributes",
      );
    } catch {
      sessionToolOverrides.delete(sessionId);
      return;
    }
    const value = row?.value;
    if (!value || typeof value !== "object") {
      sessionToolOverrides.delete(sessionId);
      return;
    }
    const schemaShape = value as { attributes?: unknown };
    if (
      !Array.isArray(schemaShape.attributes) ||
      schemaShape.attributes.length === 0
    ) {
      sessionToolOverrides.delete(sessionId);
      return;
    }

    const overrides = new Map<string, ToolModule>();
    for (const t of buildSessionCharacterWriteTools(
      store,
      characterToolDeps,
      value as Parameters<typeof buildSessionCharacterWriteTools>[2],
    )) {
      overrides.set(t.name, t);
    }
    sessionToolOverrides.set(sessionId, overrides);
  }

  // Register world-dimension query tools so agent runtimes can fetch only the
  // fields they need instead of relying on bulk prompt injection.
  for (const t of createWorldDimensionTools(store, {
    findWorldDataPluginId: (sessionId) =>
      registry.findPluginByCapability(sessionId, "world-data-provider"),
  })) {
    toolMap.set(t.name, t);
    builtinToolNames.add(t.name);
  }

  const { activatePluginLocalTools, pluginToolAccess } =
    await createBootstrapLocalTools({
      discoveryMap,
      manifestCache,
      store,
      toolMap,
      localToolNames,
    });

  // Approval: whitelist builtin + known local tools, deny unknown third-party
  const approvalRules: PermissionRule[] = [
    { pattern: "builtin:*", action: "allow" },
    { pattern: "local:*", action: "allow" },
    { pattern: "third-party:*", action: "deny" },
  ];
  const approval = createApprovalPipeline(store, approvalRules);

  const toolExecutor = createToolExecutor({
    findTool: (name, context) => {
      // Per-session override (Phase 2): when the active session has a
      // CharacterAttributeSchema, return the schema-aware variant so both the
      // LLM-facing JSON schema and the Zod validation reflect typed fields.
      if (context && SESSION_OVERRIDABLE_TOOLS.has(name)) {
        const override = sessionToolOverrides.get(context.sessionId)?.get(name);
        if (override) return override;
      }
      // Builtin tools are always accessible
      if (builtinToolNames.has(name)) return toolMap.get(name);
      // Local tools: only accessible if declared by the calling plugin
      if (context) {
        const allowed = pluginToolAccess.get(context.pluginId);
        if (!allowed?.has(name)) return undefined; // Cross-plugin call blocked
      }
      return toolMap.get(name);
    },
    store,
    approval,
    getToolSource: (name) => {
      if (builtinToolNames.has(name)) return "builtin";
      if (localToolNames.has(name)) return "local";
      return "third-party";
    },
  });

  // 6b. Eagerly load runtimes that declare UI specs so /api/ui-specs has data at boot
  for (const [pluginId] of discoveryMap) {
    const manifests = manifestCache.get(pluginId);
    if (!manifests) continue;
    for (const parsed of manifests) {
      if (parsed.manifest.ui) {
        try {
          const loaded = await loadRuntimeFn(parsed.manifest);
          if (loaded) {
            const entry = registry.get(pluginId);
            if (entry) {
              (entry.loadedRuntimes as Map<string, typeof loaded>).set(
                parsed.manifest.name,
                loaded,
              );
            }
          }
        } catch (err) {
          console.warn(
            `[bootstrap] Failed to load UI specs for ${parsed.manifest.name}:`,
            err,
          );
        }
      }
    }
  }

  // 7. getConfigFn — per-request config injection
  //    Actual config pre-loading happens in route handlers (actions.ts, turn.ts)
  //    before calling executeTurn, bridging async store reads to sync getConfig interface.
  const getConfigFn =
    config.getConfigFn ??
    ((
      _pluginId: string,
      _runtimeId: string,
    ): Readonly<Record<string, unknown>> => ({}));
  const getPluginSource = (pluginId: string) => registry.get(pluginId)?.source;

  const { rpcRegistry, rpcExecutor, rpcApprovalGate } =
    createBootstrapPluginRpc({
      discoveryMap,
      manifestCache,
    });

  const hookPipeline = createBootstrapHookPipeline({
    discoveryMap,
    manifestCache,
  });

  const runtimeEnv = readRuntimeEnv();
  const compactorRunner = createBootstrapCompactorRunner({
    manifestCache,
    store,
    llmAdapter: config.llmAdapter,
    contextWindow: runtimeEnv.compactorContextWindow,
  });

  // 8. Create memory system (Letta-style three-tier memory)
  const bootstrapMemory = createBootstrapMemorySystem({
    manifestCache,
    store,
    llmAdapter: config.llmAdapter,
    preferredMemorySlot: config.preferredMemorySlot,
    resolveModel,
  });
  if (bootstrapMemory) {
    for (const t of bootstrapMemory.tools) {
      toolMap.set(t.name, t);
      builtinToolNames.add(t.name);
    }
    setMemorySystem(bootstrapMemory.memorySystem);
  }

  // 9. Create app with dependency injection middleware
  const app = new Hono();

  const isDev = runtimeEnv.nodeEnv !== "production";
  app.onError((err, c) => {
    const message = err instanceof Error ? err.message : String(err);
    console.error(`[api] Route error:`, err);
    return c.json(
      { error: isDev ? message : "Internal server error" },
      { status: 500 },
    );
  });

  app.use("*", async (c, next) => {
    c.set("store", store);
    c.set("stateManager", stateManager);
    c.set("eventBus", eventBus);
    c.set("pluginRegistry", registry);
    c.set("llmAdapter", config.llmAdapter);
    if (config.pluginGateway) {
      c.set("pluginGateway", config.pluginGateway);
    }
    if (config.pluginUtils) {
      c.set("pluginUtils", config.pluginUtils);
    }
    c.set("loadRuntimeFn", loadRuntimeFn);
    c.set("toolExecutor", toolExecutor);
    c.set("getConfigFn", getConfigFn);
    c.set("resolveModel", resolveModel);
    c.set("compactorRunner", compactorRunner);
    c.set("hookPipeline", hookPipeline);
    // memorySystem injected via module-level setter, not Hono context
    c.set("rpcExecutor", rpcExecutor);
    c.set("rpcRegistry", rpcRegistry);
    c.set("rpcApprovalGate", rpcApprovalGate);
    c.set("sessionLock", sessionLock);
    c.set("prepareToolsForSession", prepareToolsForSession);
    c.set("getPluginSource", getPluginSource);
    c.set("activatePluginLocalTools", activatePluginLocalTools);
    if (config.worldsDirs) {
      c.set("worldsDirs", config.worldsDirs);
    }
    if (config.covelHome) {
      c.set("covelHome", config.covelHome);
    }
    if (config.ensureEmbeddingLock) {
      c.set("ensureEmbeddingLock", config.ensureEmbeddingLock);
    }
    if (config.mediaStore) {
      c.set("mediaStore", config.mediaStore);
    }
    c.set("builtinToolNames", [...builtinToolNames].sort());
    await next();
  });

  // Optional request-scoped middleware (e.g. per-request llmAdapter swap
  // driven by X-Provider-Keys / X-Slot-Config headers). Runs AFTER the
  // dependency-injection middleware above so it can override any value
  // that was just set by reading from c.get / c.set.
  if (config.perRequestMiddleware) {
    for (const mw of config.perRequestMiddleware) {
      app.use("*", mw);
    }
  }

  // 9. Mount routes — all under /api/ prefix
  // Session routes: frontend uses /api/sessions (plural) for all session operations
  app.route("/api/sessions", sessionRoutes);
  app.route("/api/sessions", turnRoutes);
  app.route("/api/sessions", stateRoutes);
  app.route("/api/sessions", messageRoutes);
  app.route("/api/sessions", characterRoutes);
  app.route("/api/sessions", pluginDataRoutes);
  app.route("/api/sessions", workingMemoryRoutes);
  app.route("/api/sessions", resumeRoutes); // S4-T4: suspend/resume (resume + suspensions list/delete)
  app.route("/api/sessions", snapshotRoutes); // S4-T2: state snapshots + fork
  app.route("/api/sessions", lorebookRoutes); // S3-T6: session-level lorebook viewer
  app.route("/api/sessions", runtimeOutputRoutes); // PR-1: translation-layer observability
  app.route("/api/sessions", pluginRpcRoutes); // PR-3: plugin RPC channel
  app.route("/api/sessions", sessionApprovalRoutes); // PR-7: per-session approvals listing
  app.route("/api/approvals", approvalRoutes); // PR-7: approval lookup + decision
  app.route("/api/plugins", pluginRoutes);
  app.route("/api/framework", frameworkRoutes);
  app.route("/api/events", eventRoutes);
  app.route("/api/events", subscribeRoutes);
  app.route("/api/worlds", worldRoutes);
  app.route(
    "/api/health",
    createHealthRoutes(store, config.storeBackend, {
      mediaBackend: config.mediaBackend,
      mediaStore: config.mediaStore,
      vectorBackend: config.vectorBackend,
    }),
  );
  app.route("/api/install", installRoutes);
  app.route("/api/ai", aiRoutes);
  app.route("/api/actions", actionRoutes);
  app.route("/api/traces", traceRoutes);
  app.route("/api/media", mediaRoutes); // SPEC §5.1 (g): signed-URL access to MediaStore

  return {
    app,
    registry,
    stateManager,
    store,
    eventBus,
    compactorRunner,
    prepareToolsForSession,
  };
}
