import { ClickHouse } from "@internal/clickhouse";
import { replicationContainerTest } from "@internal/testcontainers";
import { fork } from "node:child_process";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { setTimeout } from "node:timers/promises";
import { z } from "zod";
import { RunsReplicationService } from "~/services/runsReplicationService.server";
import { TestReplicationClickhouseFactory } from "./utils/testReplicationClickhouseFactory";
import { createInMemoryMetrics, createInMemoryTracing } from "./utils/tracing";

// Extend test timeout for benchmarks
vi.setConfig({ testTimeout: 300_000 }); // 5 minutes

/**
 * Benchmark configuration
 */
const BENCHMARK_CONFIG = {
  // Number of runs to create - adjust this to test different volumes
  // Start with smaller numbers (1000) for quick tests, increase to 10000+ for realistic benchmarks
  NUM_RUNS: parseInt(process.env.BENCHMARK_NUM_RUNS || "5000", 10),

  // Error rate (7% = realistic production load with some failures)
  ERROR_RATE: 0.07,

  // Batch size for producer
  PRODUCER_BATCH_SIZE: 100,

  // Replication service settings
  FLUSH_BATCH_SIZE: 50,
  FLUSH_INTERVAL_MS: 100,
  MAX_FLUSH_CONCURRENCY: 4,

  // How long to wait for replication to complete (in ms)
  REPLICATION_TIMEOUT_MS: 120_000, // 2 minutes
};

interface BenchmarkResult {
  name: string;
  fingerprintingEnabled: boolean;
  producerStats: {
    created: number;
    withErrors: number;
    duration: number;
    throughput: number;
  };
  replicationStats: {
    duration: number;
    throughput: number;
    replicatedRuns: number;
  };
  eluStats: {
    mean: number;
    p50: number;
    p95: number;
    p99: number;
    samples: number[];
  };
  metricsStats: {
    batchesFlushed: number;
    taskRunsInserted: number;
    payloadsInserted: number;
    eventsProcessed: number;
  };
}

/**
 * Measure Event Loop Utilization during benchmark
 */
class ELUMonitor {
  private samples: number[] = [];
  private interval: NodeJS.Timeout | null = null;
  private startELU: { idle: number; active: number } | null = null;

  start(intervalMs: number = 100) {
    this.samples = [];
    this.startELU = performance.eventLoopUtilization();

    this.interval = setInterval(() => {
      const elu = performance.eventLoopUtilization();
      const utilization = elu.utilization * 100; // Convert to percentage
      this.samples.push(utilization);
    }, intervalMs);
  }

  stop(): { mean: number; p50: number; p95: number; p99: number; samples: number[] } {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }

    if (this.samples.length === 0) {
      return { mean: 0, p50: 0, p95: 0, p99: 0, samples: [] };
    }

    const sorted = [...this.samples].sort((a, b) => a - b);
    const mean = sorted.reduce((sum, val) => sum + val, 0) / sorted.length;
    const p50 = sorted[Math.floor(sorted.length * 0.5)];
    const p95 = sorted[Math.floor(sorted.length * 0.95)];
    const p99 = sorted[Math.floor(sorted.length * 0.99)];

    return { mean, p50, p95, p99, samples: sorted };
  }
}

/**
 * Run the producer script in a separate process
 */
async function runProducer(config: {
  postgresUrl: string;
  organizationId: string;
  projectId: string;
  environmentId: string;
  numRuns: number;
  errorRate: number;
  batchSize: number;
}): Promise<{ created: number; withErrors: number; duration: number; throughput: number }> {
  return new Promise((resolve, reject) => {
    const producerPath = path.join(__dirname, "runsReplicationBenchmark.producer.ts");

    // Use tsx to run the TypeScript file directly
    const child = fork(producerPath, [JSON.stringify(config)], {
      stdio: ["ignore", "pipe", "pipe", "ipc"],
      execArgv: ["-r", "tsx/cjs"],
    });

    let output = "";

    child.stdout?.on("data", (data) => {
      const text = data.toString();
      output += text;
      console.log(text.trim());
    });

    child.stderr?.on("data", (data) => {
      console.error(data.toString().trim());
    });

    child.on("message", (message: any) => {
      if (message.type === "complete") {
        resolve(message.stats);
      } else if (message.type === "error") {
        reject(new Error(message.error));
      }
    });

    child.on("error", (error) => {
      reject(error);
    });

    child.on("exit", (code) => {
      if (code !== 0) {
        reject(new Error(`Producer exited with code ${code}`));
      }
    });
  });
}

/**
 * Wait for all runs to be replicated to ClickHouse
 */
async function waitForReplication(
  clickhouse: ClickHouse,
  organizationId: string,
  expectedCount: number,
  timeoutMs: number
): Promise<{ duration: number; replicatedRuns: number }> {
  const startTime = performance.now();
  const deadline = startTime + timeoutMs;

  const queryRuns = clickhouse.reader.query({
    name: "benchmark-count",
    query:
      "SELECT count(*) as count FROM trigger_dev.task_runs_v2 WHERE organization_id = {org_id:String}",
    schema: z.object({ count: z.number() }),
    params: z.object({ org_id: z.string() }),
  });

  while (performance.now() < deadline) {
    const [error, result] = await queryRuns({ org_id: organizationId });

    if (error) {
      throw new Error(`Failed to query ClickHouse: ${error.message}`);
    }

    const count = result?.[0]?.count || 0;

    if (count >= expectedCount) {
      const duration = performance.now() - startTime;
      return { duration, replicatedRuns: count };
    }

    // Wait a bit before checking again
    await setTimeout(500);
  }

  throw new Error(
    `Replication timeout: expected ${expectedCount} runs, but only found ${await getRunCount(
      clickhouse
    )} after ${timeoutMs}ms`
  );
}

async function getRunCount(clickhouse: ClickHouse): Promise<number> {
  const queryRuns = clickhouse.reader.query({
    name: "benchmark-count",
    query: "SELECT count(*) as count FROM trigger_dev.task_runs_v2",
    schema: z.object({ count: z.number() }),
  });

  const [error, result] = await queryRuns({});
  if (error) return 0;
  return result?.[0]?.count || 0;
}

/**
 * Extract metrics from OpenTelemetry metrics
 */
function extractMetrics(metrics: any[]): {
  batchesFlushed: number;
  taskRunsInserted: number;
  payloadsInserted: number;
  eventsProcessed: number;
} {
  function getMetricData(name: string) {
    for (const resourceMetrics of metrics) {
      for (const scopeMetrics of resourceMetrics.scopeMetrics) {
        for (const metric of scopeMetrics.metrics) {
          if (metric.descriptor.name === name) {
            return metric;
          }
        }
      }
    }
    return null;
  }

  function sumCounterValues(metric: any): number {
    if (!metric?.dataPoints) return 0;
    return metric.dataPoints.reduce((sum: number, dp: any) => sum + (dp.value || 0), 0);
  }

  return {
    batchesFlushed: sumCounterValues(getMetricData("runs_replication.batches_flushed")),
    taskRunsInserted: sumCounterValues(getMetricData("runs_replication.task_runs_inserted")),
    payloadsInserted: sumCounterValues(getMetricData("runs_replication.payloads_inserted")),
    eventsProcessed: sumCounterValues(getMetricData("runs_replication.events_processed")),
  };
}

/**
 * Run a single benchmark test
 */
async function runBenchmark(
  name: string,
  fingerprintingEnabled: boolean,
  {
    clickhouseContainer,
    redisOptions,
    postgresContainer,
    prisma,
  }: {
    clickhouseContainer: any;
    redisOptions: any;
    postgresContainer: any;
    prisma: any;
  }
): Promise<BenchmarkResult> {
  console.log(`\n${"=".repeat(80)}`);
  console.log(`BENCHMARK: ${name}`);
  console.log(`Error Fingerprinting: ${fingerprintingEnabled ? "ENABLED" : "DISABLED"}`);
  console.log(
    `Runs: ${BENCHMARK_CONFIG.NUM_RUNS}, Error Rate: ${(BENCHMARK_CONFIG.ERROR_RATE * 100).toFixed(
      1
    )}%`
  );
  console.log(`${"=".repeat(80)}\n`);

  // Setup
  const organization = await prisma.organization.create({
    data: {
      title: `benchmark-${name}`,
      slug: `benchmark-${name}`,
    },
  });

  const project = await prisma.project.create({
    data: {
      name: `benchmark-${name}`,
      slug: `benchmark-${name}`,
      organizationId: organization.id,
      externalRef: `benchmark-${name}`,
    },
  });

  const runtimeEnvironment = await prisma.runtimeEnvironment.create({
    data: {
      slug: `benchmark-${name}`,
      type: "DEVELOPMENT",
      projectId: project.id,
      organizationId: organization.id,
      apiKey: `benchmark-${name}`,
      pkApiKey: `benchmark-${name}`,
      shortcode: `benchmark-${name}`,
    },
  });

  // Setup ClickHouse
  const clickhouse = new ClickHouse({
    url: clickhouseContainer.getConnectionUrl(),
    name: `benchmark-${name}`,
    compression: {
      request: true,
    },
    logLevel: "warn",
  });

  // Setup tracing and metrics
  const { tracer } = createInMemoryTracing();
  const metricsHelper = createInMemoryMetrics();

  // Create and start replication service
  const runsReplicationService = new RunsReplicationService({
    clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
    pgConnectionUrl: postgresContainer.getConnectionUri(),
    serviceName: `benchmark-${name}`,
    slotName: `benchmark_${name.replace(/-/g, "_")}`,
    publicationName: `benchmark_${name.replace(/-/g, "_")}_pub`,
    redisOptions,
    maxFlushConcurrency: BENCHMARK_CONFIG.MAX_FLUSH_CONCURRENCY,
    flushIntervalMs: BENCHMARK_CONFIG.FLUSH_INTERVAL_MS,
    flushBatchSize: BENCHMARK_CONFIG.FLUSH_BATCH_SIZE,
    leaderLockTimeoutMs: 10000,
    leaderLockExtendIntervalMs: 2000,
    ackIntervalSeconds: 10,
    tracer,
    meter: metricsHelper.meter,
    logLevel: "warn",
    disableErrorFingerprinting: !fingerprintingEnabled,
  });

  await runsReplicationService.start();

  // Start ELU monitoring
  const eluMonitor = new ELUMonitor();
  eluMonitor.start(100);

  let producerStats!: BenchmarkResult["producerStats"];
  let replicationResult!: { duration: number; replicatedRuns: number };
  let metricsStats!: BenchmarkResult["metricsStats"];
  let eluStats!: BenchmarkResult["eluStats"];

  try {
    // Run producer in separate process
    console.log("\n[Benchmark] Starting producer...");
    producerStats = await runProducer({
      postgresUrl: postgresContainer.getConnectionUri(),
      organizationId: organization.id,
      projectId: project.id,
      environmentId: runtimeEnvironment.id,
      numRuns: BENCHMARK_CONFIG.NUM_RUNS,
      errorRate: BENCHMARK_CONFIG.ERROR_RATE,
      batchSize: BENCHMARK_CONFIG.PRODUCER_BATCH_SIZE,
    });

    console.log("\n[Benchmark] Waiting for replication to complete...");
    replicationResult = await waitForReplication(
      clickhouse,
      organization.id,
      producerStats.created,
      BENCHMARK_CONFIG.REPLICATION_TIMEOUT_MS
    );

    const metrics = await metricsHelper.getMetrics();
    metricsStats = extractMetrics(metrics);
  } finally {
    eluStats = eluMonitor.stop();
    await runsReplicationService.stop();
    await metricsHelper.shutdown();
  }

  const throughput = (replicationResult.replicatedRuns / replicationResult.duration) * 1000;

  const result: BenchmarkResult = {
    name,
    fingerprintingEnabled,
    producerStats,
    replicationStats: {
      duration: replicationResult.duration,
      throughput,
      replicatedRuns: replicationResult.replicatedRuns,
    },
    eluStats,
    metricsStats,
  };

  // Print results
  console.log(`\n${"=".repeat(80)}`);
  console.log(`RESULTS: ${name}`);
  console.log(`${"=".repeat(80)}`);
  console.log("\nProducer:");
  console.log(`  Created: ${producerStats.created} runs`);
  console.log(
    `  With errors: ${producerStats.withErrors} (${(
      (producerStats.withErrors / producerStats.created) *
      100
    ).toFixed(1)}%)`
  );
  console.log(`  Duration: ${producerStats.duration.toFixed(0)}ms`);
  console.log(`  Throughput: ${producerStats.throughput.toFixed(0)} runs/sec`);
  console.log("\nReplication:");
  console.log(`  Replicated: ${replicationResult.replicatedRuns} runs`);
  console.log(`  Duration: ${replicationResult.duration.toFixed(0)}ms`);
  console.log(`  Throughput: ${throughput.toFixed(0)} runs/sec`);
  console.log("\nEvent Loop Utilization:");
  console.log(`  Mean: ${eluStats.mean.toFixed(2)}%`);
  console.log(`  P50: ${eluStats.p50.toFixed(2)}%`);
  console.log(`  P95: ${eluStats.p95.toFixed(2)}%`);
  console.log(`  P99: ${eluStats.p99.toFixed(2)}%`);
  console.log(`  Samples: ${eluStats.samples.length}`);
  console.log("\nMetrics:");
  console.log(`  Batches flushed: ${metricsStats.batchesFlushed}`);
  console.log(`  Task runs inserted: ${metricsStats.taskRunsInserted}`);
  console.log(`  Payloads inserted: ${metricsStats.payloadsInserted}`);
  console.log(`  Events processed: ${metricsStats.eventsProcessed}`);
  console.log(`${"=".repeat(80)}\n`);

  return result;
}

/**
 * Compare two benchmark results and print delta
 */
function compareBenchmarks(baseline: BenchmarkResult, comparison: BenchmarkResult) {
  console.log(`\n${"=".repeat(80)}`);
  console.log("COMPARISON");
  console.log(
    `Baseline: ${baseline.name} (fingerprinting ${baseline.fingerprintingEnabled ? "ON" : "OFF"})`
  );
  console.log(
    `Comparison: ${comparison.name} (fingerprinting ${
      comparison.fingerprintingEnabled ? "ON" : "OFF"
    })`
  );
  console.log(`${"=".repeat(80)}`);

  const replicationDurationDelta =
    ((comparison.replicationStats.duration - baseline.replicationStats.duration) /
      baseline.replicationStats.duration) *
    100;
  const throughputDelta =
    ((comparison.replicationStats.throughput - baseline.replicationStats.throughput) /
      baseline.replicationStats.throughput) *
    100;
  const eluMeanDelta =
    ((comparison.eluStats.mean - baseline.eluStats.mean) / baseline.eluStats.mean) * 100;
  const eluP99Delta =
    ((comparison.eluStats.p99 - baseline.eluStats.p99) / baseline.eluStats.p99) * 100;

  console.log("\nReplication Duration:");
  console.log(
    `  ${baseline.replicationStats.duration.toFixed(
      0
    )}ms → ${comparison.replicationStats.duration.toFixed(0)}ms (${
      replicationDurationDelta > 0 ? "+" : ""
    }${replicationDurationDelta.toFixed(2)}%)`
  );

  console.log("\nThroughput:");
  console.log(
    `  ${baseline.replicationStats.throughput.toFixed(
      0
    )} → ${comparison.replicationStats.throughput.toFixed(0)} runs/sec (${
      throughputDelta > 0 ? "+" : ""
    }${throughputDelta.toFixed(2)}%)`
  );

  console.log("\nEvent Loop Utilization (Mean):");
  console.log(
    `  ${baseline.eluStats.mean.toFixed(2)}% → ${comparison.eluStats.mean.toFixed(2)}% (${
      eluMeanDelta > 0 ? "+" : ""
    }${eluMeanDelta.toFixed(2)}%)`
  );

  console.log("\nEvent Loop Utilization (P99):");
  console.log(
    `  ${baseline.eluStats.p99.toFixed(2)}% → ${comparison.eluStats.p99.toFixed(2)}% (${
      eluP99Delta > 0 ? "+" : ""
    }${eluP99Delta.toFixed(2)}%)`
  );

  console.log(`\n${"=".repeat(80)}\n`);

  // Return deltas for assertions if needed
  return {
    replicationDurationDelta,
    throughputDelta,
    eluMeanDelta,
    eluP99Delta,
  };
}

describe("RunsReplicationService Benchmark", () => {
  replicationContainerTest.skipIf(process.env.BENCHMARKS_ENABLED !== "1")(
    "should benchmark error fingerprinting performance impact",
    async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
      // Enable replica identity for TaskRun table
      await prisma.$executeRawUnsafe(`ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`);

      console.log("\n" + "=".repeat(80));
      console.log("RUNS REPLICATION SERVICE - ERROR FINGERPRINTING BENCHMARK");
      console.log("=".repeat(80));
      console.log(`Configuration:`);
      console.log(`  Total runs: ${BENCHMARK_CONFIG.NUM_RUNS}`);
      console.log(`  Error rate: ${(BENCHMARK_CONFIG.ERROR_RATE * 100).toFixed(1)}%`);
      console.log(
        `  Expected errors: ~${Math.floor(BENCHMARK_CONFIG.NUM_RUNS * BENCHMARK_CONFIG.ERROR_RATE)}`
      );
      console.log(`  Producer batch size: ${BENCHMARK_CONFIG.PRODUCER_BATCH_SIZE}`);
      console.log(`  Replication batch size: ${BENCHMARK_CONFIG.FLUSH_BATCH_SIZE}`);
      console.log(`  Max flush concurrency: ${BENCHMARK_CONFIG.MAX_FLUSH_CONCURRENCY}`);
      console.log("=".repeat(80) + "\n");

      // Run benchmark WITHOUT error fingerprinting (baseline)
      const baselineResult = await runBenchmark("baseline-no-fingerprinting", false, {
        clickhouseContainer,
        redisOptions,
        postgresContainer,
        prisma,
      });

      // Run benchmark WITH error fingerprinting
      const fingerprintingResult = await runBenchmark("with-fingerprinting", true, {
        clickhouseContainer,
        redisOptions,
        postgresContainer,
        prisma,
      });

      // Compare results
      const deltas = compareBenchmarks(baselineResult, fingerprintingResult);

      // Basic assertions - just to ensure benchmarks completed successfully
      expect(baselineResult.replicationStats.replicatedRuns).toBe(BENCHMARK_CONFIG.NUM_RUNS);
      expect(fingerprintingResult.replicationStats.replicatedRuns).toBe(BENCHMARK_CONFIG.NUM_RUNS);

      // Log final summary
      console.log("BENCHMARK COMPLETE");
      console.log(
        `Fingerprinting impact on replication duration: ${
          deltas.replicationDurationDelta > 0 ? "+" : ""
        }${deltas.replicationDurationDelta.toFixed(2)}%`
      );
      console.log(
        `Fingerprinting impact on throughput: ${
          deltas.throughputDelta > 0 ? "+" : ""
        }${deltas.throughputDelta.toFixed(2)}%`
      );
      console.log(
        `Fingerprinting impact on ELU (mean): ${
          deltas.eluMeanDelta > 0 ? "+" : ""
        }${deltas.eluMeanDelta.toFixed(2)}%`
      );
      console.log(
        `Fingerprinting impact on ELU (P99): ${
          deltas.eluP99Delta > 0 ? "+" : ""
        }${deltas.eluP99Delta.toFixed(2)}%`
      );
    }
  );
});
