import { getMeter } from "@internal/tracing";
import { $replica } from "~/db.server";
import { runStore } from "~/v3/runStore.server";
import { env } from "~/env.server";
import { singleton } from "~/utils/singleton";
import { getCachedLimit } from "../platform.v3.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { ClickHouseRunListResolver } from "./clickHouseRunListResolver.server";
import { EnvChangeRouter, type EnvChangeSource } from "./envChangeRouter.server";
import { NativeRealtimeClient } from "./nativeRealtimeClient.server";
import { RealtimeConcurrencyLimiter } from "./realtimeConcurrencyLimiter.server";
import { getRunChangeNotifier } from "./runChangeNotifierInstance.server";
import { RedisReplayCursorStore } from "./replayCursorStore.server";
import { createPostgresReplicaLagSource, ReplicaLagEstimator } from "./replicaLagEstimator.server";
import { RunHydrator } from "./runReader.server";

// Process-singleton wiring for the native realtime client; only constructed when a
// request actually routes to it, so a disabled webapp never instantiates it.
function initializeNativeRealtimeClient(): NativeRealtimeClient {
  const meter = getMeter("realtime-native");

  const wakeups = meter.createCounter("realtime_native.wakeups", {
    description:
      "Live realtime wakeups by reason. A rising 'timeout' share suggests a write site is missing its publishChangeRecord delegate.",
  });

  const runSetResolves = meter.createCounter("realtime_native.runset_resolves", {
    description:
      "Multi-run (tag-list/batch) resolve+hydrate outcomes. 'hit'/'coalesced' vs 'miss' shows how effectively concurrent same-filter feeds share a single ClickHouse + Postgres query.",
  });

  const runSetQueryMs = meter.createHistogram("realtime_native.runset_query_ms", {
    description: "Latency of the multi-run resolve (ClickHouse) and hydrate (Postgres) stages.",
    unit: "ms",
  });

  const livePollPaths = meter.createCounter("realtime_native.live_polls", {
    description:
      "How live polls resolved. 'fast-hydrate' = router wake with rows hydrated by id (no ClickHouse); 'full-resolve' = backstop; 'cold-resolve' = fresh env subscription probed once.",
  });

  const routerHydrates = meter.createCounter("realtime_native.router_hydrated_runs", {
    description:
      "Runs hydrated by the EnvChangeRouter's batch-hydrate (one query per column set per wake, shared across all feeds matching the same run).",
  });

  const resolveAdmissionWaits = meter.createCounter("realtime_native.resolve_admission_waits", {
    description:
      "Fresh ClickHouse resolves that had to queue for an admission permit. A rising count means a distinct-filter reconnect stampede is being throttled (the gate is doing its job).",
  });

  const replays = meter.createCounter("realtime_native.replays", {
    description:
      "Buffered change records replayed to a newly-armed feed (inter-poll gap recovery). 'delivered' = rows reached the feed; 'empty' = candidates hydrated but none survived the filter/diff.",
  });

  const replayEvictions = meter.createCounter("realtime_native.replay_evictions", {
    description:
      "Replay-buffer evictions. 'window' expiry is normal; 'cap' means an env churns more runs inside the window than the buffer holds (replay guarantee degrading — retune the knobs).",
  });

  const deliveryLagMs = meter.createHistogram("realtime_native.delivery_lag_ms", {
    description:
      "Live emissions: now minus the newest emitted row's updatedAt (PG clock vs app clock, so approximate). The end-to-end delivery SLI — a p99 near the backstop hold means wakes are being missed.",
    unit: "ms",
  });

  const emittedRows = meter.createHistogram("realtime_native.emitted_rows", {
    description:
      "Rows per live emission. Deltas should be small; a fat tail means working-set/offset-floor fallbacks are re-emitting full sets.",
    unit: "rows",
  });

  const backstops = meter.createCounter("realtime_native.backstops", {
    description:
      "Backstop full resolves by outcome. 'empty' is normal idle behavior; sustained 'delivered' means the notify/replay path missed changes — alert on it.",
  });

  const concurrencyRejections = meter.createCounter("realtime_native.concurrency_rejections", {
    description: "Polls rejected (429) by the per-env concurrency limiter.",
  });

  const replayCursorOps = meter.createCounter("realtime_native.replay_cursor_ops", {
    description:
      "Shared replay-cursor store operations by outcome. Errors degrade hops to cold resolves (watch live_polls{path='cold-resolve'} rise with them), never failed polls.",
  });

  const staleHydrates = meter.createCounter("realtime_native.stale_hydrates", {
    description:
      "Wake hydrates the read-your-writes tripwire caught reading behind the publish. 'recovered' = a retry delivered the fresh row; sustained 'gave_up' means replica lag is outrunning the retry budget.",
  });

  const limiter = new RealtimeConcurrencyLimiter({
    keyPrefix: "tr:realtime:native:concurrency",
    redis: {
      port: env.RATE_LIMIT_REDIS_PORT,
      host: env.RATE_LIMIT_REDIS_HOST,
      username: env.RATE_LIMIT_REDIS_USERNAME,
      password: env.RATE_LIMIT_REDIS_PASSWORD,
      tlsDisabled: env.RATE_LIMIT_REDIS_TLS_DISABLED === "true",
      clusterMode: env.RATE_LIMIT_REDIS_CLUSTER_MODE_ENABLED === "1",
    },
  });

  // Fleet-shared replay cursors (one timestamp per connection) on the same Redis as the
  // change channel, so a load-balancer hop reads the connection's true inter-poll gap.
  const replayCursorStore =
    env.REALTIME_BACKEND_NATIVE_SHARED_REPLAY_CURSORS === "1"
      ? new RedisReplayCursorStore({
          redis: {
            host: env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_HOST,
            port: env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_PORT,
            username: env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_USERNAME,
            password: env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_PASSWORD,
            tlsDisabled: env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_TLS_DISABLED === "true",
            clusterMode: env.REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_CLUSTER_MODE_ENABLED === "1",
          },
          ttlMs: env.REALTIME_BACKEND_NATIVE_WORKING_SET_TTL_MS,
          onResult: (op, ok) => replayCursorOps.add(1, { op, result: ok ? "ok" : "error" }),
        })
      : undefined;

  // One RunHydrator shared by the router and the client, so its single-flight + short-TTL cache covers both.
  const runReader = new RunHydrator({
    replica: $replica,
    runStore,
    cacheTtlMs: env.REALTIME_BACKEND_NATIVE_RUN_CACHE_TTL_MS,
    maxCacheEntries: env.REALTIME_BACKEND_NATIVE_RUN_CACHE_MAX_ENTRIES,
  });

  // Read-your-writes gate: the estimator samples replica lag (reader-side only, paused
  // when idle) and the router delays wake hydrates by it, anchored to each record's
  // updatedAtMs — so a publish racing the replica's apply is waited out, not read stale.
  const lagEstimator =
    env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_GATE_ENABLED === "1"
      ? new ReplicaLagEstimator({
          source: createPostgresReplicaLagSource($replica),
          sampleIntervalMs: env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_SAMPLE_INTERVAL_MS,
          idleAfterMs: env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_IDLE_AFTER_MS,
          windowMs: env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_WINDOW_MS,
          defaultLagMs: env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_DEFAULT_MS,
          observedFloorTtlMs: env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_OBSERVED_FLOOR_TTL_MS,
        })
      : undefined;

  // The notifier wrapped so router activity keeps the lag sampler warm.
  const notifier = getRunChangeNotifier();
  const source: EnvChangeSource = lagEstimator
    ? {
        subscribeToEnv(environmentId, onBatch) {
          lagEstimator.touch();
          return notifier.subscribeToEnv(environmentId, (records) => {
            lagEstimator.touch();
            onBatch(records);
          });
        },
      }
    : notifier;

  const router = new EnvChangeRouter({
    source,
    hydrator: runReader,
    onHydrate: (runCount) => routerHydrates.add(runCount),
    replayWindowMs: env.REALTIME_BACKEND_NATIVE_REPLAY_WINDOW_MS,
    replayMaxRunsPerEnv: env.REALTIME_BACKEND_NATIVE_REPLAY_MAX_RUNS,
    unsubscribeLingerMs: env.REALTIME_BACKEND_NATIVE_UNSUBSCRIBE_LINGER_MS,
    onReplay: (result) => replays.add(1, { result }),
    onReplayEviction: (reason) => replayEvictions.add(1, { reason }),
    replicaLag: lagEstimator
      ? {
          getLagMs: () => lagEstimator.getLagMs(),
          noteObservedLagMs: (lagMs) => lagEstimator.noteObservedLagMs(lagMs),
          marginMs: env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_MARGIN_MS,
          maxDelayMs: env.REALTIME_BACKEND_NATIVE_REPLICA_LAG_MAX_DELAY_MS,
          staleRetries: env.REALTIME_BACKEND_NATIVE_STALE_HYDRATE_RETRIES,
          onStaleHydrate: (outcome, runCount) => staleHydrates.add(runCount, { outcome }),
        }
      : undefined,
  });

  const client = new NativeRealtimeClient({
    runReader,
    runListResolver: new ClickHouseRunListResolver({
      getClickhouse: (organizationId) =>
        clickhouseFactory.getClickhouseForOrganization(organizationId, "realtime"),
      prisma: $replica,
    }),
    router,
    limiter,
    cachedLimitProvider: {
      async getCachedLimit(organizationId, defaultValue) {
        const result = await getCachedLimit(
          organizationId,
          "realtimeConcurrentConnections",
          defaultValue
        );
        return result.val;
      },
    },
    defaultConcurrencyLimit: env.REALTIME_BACKEND_NATIVE_DEFAULT_CONCURRENCY_LIMIT,
    livePollTimeoutMs: env.REALTIME_BACKEND_NATIVE_LIVE_POLL_TIMEOUT_MS,
    livePollJitterRatio: env.REALTIME_BACKEND_NATIVE_LIVE_POLL_JITTER_RATIO,
    maximumCreatedAtFilterAgeMs: env.REALTIME_MAXIMUM_CREATED_AT_FILTER_AGE_IN_MS,
    maxListResults: env.REALTIME_BACKEND_NATIVE_MAX_LIST_RESULTS,
    runSetResolveCacheTtlMs: env.REALTIME_BACKEND_NATIVE_RUNSET_CACHE_TTL_MS,
    runSetResolveCacheMaxEntries: env.REALTIME_BACKEND_NATIVE_RUNSET_CACHE_MAX_ENTRIES,
    listCacheMaxEntries: env.REALTIME_BACKEND_NATIVE_WORKING_SET_MAX_ENTRIES,
    workingSetCacheTtlMs: env.REALTIME_BACKEND_NATIVE_WORKING_SET_TTL_MS,
    runSetCreatedAtBucketMs: env.REALTIME_BACKEND_NATIVE_RUNSET_CREATED_AT_BUCKET_MS,
    holdOnEmpty: env.REALTIME_BACKEND_NATIVE_HOLD_ON_EMPTY === "1",
    resolveAdmissionLimit: env.REALTIME_BACKEND_NATIVE_RESOLVE_ADMISSION_LIMIT,
    replayCursorStore,
    onWakeup: (reason) => wakeups.add(1, { reason }),
    onLivePollPath: (path) => livePollPaths.add(1, { path }),
    onRunSetResolve: (result) => runSetResolves.add(1, { result }),
    onRunSetQuery: (stage, ms) => runSetQueryMs.record(ms, { stage }),
    onResolveAdmissionWait: () => resolveAdmissionWaits.add(1),
    onEmit: (path, lagMs, rowCount) => {
      deliveryLagMs.record(Math.max(lagMs, 0), { path });
      emittedRows.record(rowCount);
    },
    onBackstopResult: (result) => backstops.add(1, { result }),
    onConcurrencyRejected: () => concurrencyRejections.add(1),
  });

  meter
    .createObservableGauge("realtime_native.working_set_size", {
      description:
        "Entries in the per-handle working-set cache (one per active multi-run feed session).",
    })
    .addCallback((result) => result.observe(client.workingSetCacheSize));

  meter
    .createObservableGauge("realtime_native.resolve_admission_in_use", {
      description:
        "Fresh ClickHouse resolves currently holding an admission permit (live concurrency against the gate's limit).",
    })
    .addCallback((result) => result.observe(client.resolveAdmissionInUse));

  meter
    .createObservableGauge("realtime_native.held_feeds", {
      description: "Long-polls currently held, by feed kind — the system's capacity unit.",
    })
    .addCallback((result) => {
      const counts = router.heldFeedCounts;
      result.observe(counts.run, { kind: "run" });
      result.observe(counts.tag, { kind: "tag" });
      result.observe(counts.batch, { kind: "batch" });
    });

  meter
    .createObservableGauge("realtime_native.active_envs", {
      description:
        "Environments currently routed on this instance (held feeds + lingering subscriptions).",
    })
    .addCallback((result) => result.observe(router.activeEnvCount));

  if (lagEstimator) {
    meter
      .createObservableGauge("realtime_native.replica_lag_estimate_ms", {
        description:
          "The read-your-writes gate's current replica-lag estimate (max sample in the window). Wake hydrates are delayed by roughly this much past each change's commit.",
      })
      .addCallback((result) => result.observe(lagEstimator.getLagMs()));
  }

  return client;
}

export function getNativeRealtimeClient(): NativeRealtimeClient {
  return singleton("nativeRealtimeClient", initializeNativeRealtimeClient);
}
