// apps/web-next/src/server/bind-production.ts
// SERVER-ONLY: this module imports Payload config and must never be bundled into the browser.
import "reflect-metadata";
import { Container } from "inversify";
import { getPayload } from "payload";
import config from "@repo/core-cms";
import {
  bindNoopInstrumentation,
  bindSentryInstrumentation,
  withCapture,
  withSpan,
  type ITracer,
  type ILogger,
} from "@repo/core-shared/instrumentation";
import {
  InMemoryEventBus,
  PayloadJobsEventBus,
  type IEventBus,
} from "@repo/core-events";
import {
  InMemoryJobQueue,
  PayloadJobQueue,
  type IJobQueue,
} from "@repo/core-shared/jobs";
import {
  InMemoryRealtimeBroadcaster,
  RealtimeHandlerRegistry,
  realtimePingInboundDescriptor,
  type IRealtimeBroadcaster,
  type IRealtimeHandlerRegistry,
} from "@repo/core-realtime";
import { bindProductionBlog } from "@repo/blog/di/bind-production";
import { bindProductionAuth } from "@repo/auth/di/bind-production";
import { bindProductionMarketingPages } from "@repo/marketing-pages/di/bind-production";
import { bindProductionNavigation } from "@repo/navigation/di/bind-production";
import { bindProductionMedia } from "@repo/media/di/bind-production";
import { bindDevSeedBlog } from "@repo/blog/di/bind-dev-seed";
import { bindDevSeedAuth } from "@repo/auth/di/bind-dev-seed";
import { bindDevSeedMarketingPages } from "@repo/marketing-pages/di/bind-dev-seed";
import { bindDevSeedNavigation } from "@repo/navigation/di/bind-dev-seed";
import { bindDevSeedMedia } from "@repo/media/di/bind-dev-seed";

type BindAllDeps = {
  realtime: IRealtimeBroadcaster;
  realtimeRegistry: IRealtimeHandlerRegistry;
};

let bound = false;

// Shared container holds TRACER + LOGGER bindings; per-feature containers
// receive references via parameter passing. This separates the instrumentation
// container (one) from feature containers (per-feature, ADR-008).
const sharedContainer = new Container();

let resolvedTracer: ITracer | null = null;
let resolvedLogger: ILogger | null = null;
let resolvedBus: IEventBus | null = null;
let resolvedQueue: IJobQueue | null = null;

/** Rule 0: pick instrumentation backend from DSN env (orthogonal to repo mode). */
function resolveInstrumentation(): { tracer: ITracer; logger: ILogger } {
  if (resolvedTracer && resolvedLogger) {
    return { tracer: resolvedTracer, logger: resolvedLogger };
  }
  const dsn = process.env.WEB_NEXT_SENTRY_DSN;
  const result = dsn
    ? bindSentryInstrumentation(sharedContainer, { dsn, app: "web-next" })
    : bindNoopInstrumentation(sharedContainer);
  resolvedTracer = result.tracer;
  resolvedLogger = result.logger;
  return result;
}

/**
 * Production-mode event bus + job queue: backed by Payload's job system so
 * events fan out via durable Payload tasks (`PayloadJobsEventBus` enqueues
 * `__events.<publisher>.<event>.<consumer>` per subscribed handler) and ad-hoc
 * jobs go through `PayloadJobQueue.enqueue`. Cached after first resolution.
 */
async function resolveEventsAndJobsProduction(): Promise<{
  bus: IEventBus;
  queue: IJobQueue;
}> {
  if (resolvedBus && resolvedQueue) return { bus: resolvedBus, queue: resolvedQueue };
  const resolvedConfig = await config;
  const payload = await getPayload({ config: resolvedConfig });
  const queue = new PayloadJobQueue(payload);
  const bus = new PayloadJobsEventBus(queue);
  resolvedBus = bus;
  resolvedQueue = queue;
  return { bus, queue };
}

/**
 * Dev-seed mode: in-process bus + queue. Per-feature binders register their
 * job handlers via `queue.register(slug, handler)` and subscribe their event
 * handlers via `bus.subscribe(...)` at bind time, so dev/test exercises the
 * full publish → handler → enqueue path without booting Payload.
 */
function resolveEventsAndJobsDevSeed(): { bus: IEventBus; queue: IJobQueue } {
  if (resolvedBus && resolvedQueue) return { bus: resolvedBus, queue: resolvedQueue };
  const queue = new InMemoryJobQueue();
  const bus = new InMemoryEventBus();
  resolvedBus = bus;
  resolvedQueue = queue;
  return { bus, queue };
}

/**
 * Production path: swap each feature's mock repository binding for the real
 * Payload-backed one. Constructs `new XRepository(config, tracer, logger)` per
 * feature as Phase E feature wiring lands (blog: task 18; remaining: tasks 19–22).
 */
export async function bindAllProduction(deps: BindAllDeps): Promise<void> {
  if (bound) return;
  bound = true;
  const { tracer, logger } = resolveInstrumentation(); // Rule 0
  const { bus, queue } = await resolveEventsAndJobsProduction();
  const resolvedConfig = await config;
  const { realtime, realtimeRegistry } = deps;
  bindProductionAuth(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 19
  bindProductionBlog(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 18
  bindProductionMarketingPages(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 20
  bindProductionNavigation(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 21
  bindProductionMedia(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 22
  maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger);
  bindRealtimeBridge(bus, realtime);
}

/**
 * Dev-seed path: keep each feature's MockXRepository in place but populate it
 * with realistic seed data so the running app shows non-empty UI without
 * Payload booted. Mutually exclusive with `bindAllProduction()`.
 */
export async function bindAllDevSeed(deps: BindAllDeps): Promise<void> {
  if (bound) return;
  bound = true;
  const { tracer, logger } = resolveInstrumentation(); // Rule 0
  const { bus, queue } = resolveEventsAndJobsDevSeed();
  const { realtime, realtimeRegistry } = deps;
  await bindDevSeedAuth(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 19
  await bindDevSeedBlog(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 18
  await bindDevSeedMarketingPages(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 20
  await bindDevSeedNavigation(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 21
  await bindDevSeedMedia(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 22
  maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger);
  bindRealtimeBridge(bus, realtime);
}

/**
 * Boot dispatcher: pick the binder based on the environment.
 *
 * Resolution order (first match wins):
 *
 *   Rule 0 (always): instrumentation (Noop vs Sentry) from WEB_NEXT_SENTRY_DSN
 *                    presence — runs inside both bindAllProduction and
 *                    bindAllDevSeed via resolveInstrumentation().
 *   Rule 1: USE_DEV_SEED === "true"  → dev seed (explicit override)
 *   Rule 2: NODE_ENV === "production" → real Payload via bindAllProduction
 *   Rule 3: otherwise                  → dev seed (developer-friendly default)
 *
 * `deps` is optional; omitting it (page-level fallback, already-bound guard)
 * uses in-memory noop implementations. In the custom-server flow, `server.ts`
 * always provides Socket.IO-backed deps before any page request runs.
 */
export async function bindAll(deps?: Partial<BindAllDeps>): Promise<void> {
  const resolvedDeps: BindAllDeps = {
    realtime: deps?.realtime ?? new InMemoryRealtimeBroadcaster(),
    realtimeRegistry: deps?.realtimeRegistry ?? new RealtimeHandlerRegistry(),
  };
  if (process.env.USE_DEV_SEED === "true") {
    await bindAllDevSeed(resolvedDeps);
    return;
  }
  if (process.env.NODE_ENV === "production") {
    await bindAllProduction(resolvedDeps);
    return;
  }
  await bindAllDevSeed(resolvedDeps);
}

// Wraps the built-in realtime-ping inbound handler in the same span+capture
// sandwich the realtime-handler generator emits (R41–R44), so the
// proof-of-life channel models the convention rather than registering raw.
// Skipped entirely when REALTIME_PING_DISABLED === "true".
function maybeRegisterRealtimePing(
  registry: IRealtimeHandlerRegistry,
  realtime: IRealtimeBroadcaster,
  tracer: ITracer,
  logger: ILogger,
): void {
  if (process.env.REALTIME_PING_DISABLED === "true") return;
  const { descriptor, handler } = realtimePingInboundDescriptor(realtime);
  const wrappedHandler = withSpan(
    tracer,
    { name: "core-realtime.realtimePing", op: "realtime-handler" },
    withCapture(
      logger,
      { feature: "core-realtime", layer: "realtime-handler", name: "core-realtime.realtimePing" },
      handler,
    ),
  );
  registry.register({ descriptor, handler: wrappedHandler });
}

function bindRealtimeBridge(_bus: IEventBus, _broadcaster: IRealtimeBroadcaster): void {
  // v1 ships with an empty allowlist. The dashboard PR adds the first entries here.
  // Example shape (commented out so v1 doesn't try to use it):
  // bus.subscribe(userSignedUpEvent, "realtime-bridge", async (payload) =>
  //   broadcaster.broadcast(adminEventStreamChannel, { kind: "user.signed-up", payload }),
  // );
}

/** Test-only resets — not exported via package. Used by bind-production.test.ts. */
export function __resetBindStateForTests(): void {
  bound = false;
  resolvedTracer = null;
  resolvedLogger = null;
  resolvedBus = null;
  resolvedQueue = null;
}

/** Test-only accessor for resolved instrumentation. */
export function __getInstrumentationForTests(): {
  tracer: ITracer | null;
  logger: ILogger | null;
} {
  return { tracer: resolvedTracer, logger: resolvedLogger };
}
