import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IntervalHistogram, monitorEventLoopDelay, performance } from 'perf_hooks';

const LATENCY_BUCKETS_MS = [10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000];
const OVERFLOW_ROUTE = '__other_routes__';

type RouteMetric = {
  requests: number;
  clientErrors: number;
  errors: number;
  timeouts: number;
  totalDurationMs: number;
  maxDurationMs: number;
  latencyBuckets: number[];
};

@Injectable()
export class MetricsService implements OnModuleInit, OnModuleDestroy {
  private readonly startedAt = Date.now();
  private readonly routes = new Map<string, RouteMetric>();
  private readonly eventLoopBaseline = performance.eventLoopUtilization();
  private eventLoopDelay?: IntervalHistogram;
  private totalRequests = 0;
  private inFlightRequests = 0;
  private peakInFlightRequests = 0;

  constructor(private readonly configService: ConfigService) {}

  onModuleInit(): void {
    const resolution =
      this.configService.get<number>('metrics.eventLoopResolutionMs', { infer: true }) ?? 20;
    this.eventLoopDelay = monitorEventLoopDelay({ resolution });
    this.eventLoopDelay.enable();
  }

  onModuleDestroy(): void {
    this.eventLoopDelay?.disable();
  }

  requestStarted(): void {
    this.totalRequests += 1;
    this.inFlightRequests += 1;
    this.peakInFlightRequests = Math.max(this.peakInFlightRequests, this.inFlightRequests);
  }

  requestFinished(method: string, route: string, statusCode: number, durationMs: number): void {
    this.inFlightRequests = Math.max(0, this.inFlightRequests - 1);
    this.observe(method, route, statusCode, durationMs);
  }

  observe(method: string, route: string, statusCode: number, durationMs: number): void {
    const key = this.resolveRouteKey(`${method.toUpperCase()} ${route}`);
    const current = this.routes.get(key) ?? this.createRouteMetric();
    current.requests += 1;
    current.clientErrors += statusCode >= 400 && statusCode < 500 ? 1 : 0;
    current.errors += statusCode >= 500 ? 1 : 0;
    current.timeouts += statusCode === 408 ? 1 : 0;
    current.totalDurationMs += durationMs;
    current.maxDurationMs = Math.max(current.maxDurationMs, durationMs);
    const bucketIndex = LATENCY_BUCKETS_MS.findIndex((boundary) => durationMs <= boundary);
    current.latencyBuckets[bucketIndex === -1 ? LATENCY_BUCKETS_MS.length : bucketIndex] += 1;
    this.routes.set(key, current);
  }

  snapshot() {
    const routes = Array.from(this.routes.entries()).map(([route, metric]) => ({
      route,
      requests: metric.requests,
      clientErrors: metric.clientErrors,
      errors: metric.errors,
      timeouts: metric.timeouts,
      errorRate: metric.requests ? metric.errors / metric.requests : 0,
      averageDurationMs: metric.requests
        ? Math.round((metric.totalDurationMs / metric.requests) * 100) / 100
        : 0,
      p95DurationMs: this.estimatePercentile(metric, 0.95),
      maxDurationMs: metric.maxDurationMs,
    }));
    const eventLoopUtilization = performance.eventLoopUtilization(this.eventLoopBaseline);
    const eventLoop = this.eventLoopSnapshot(eventLoopUtilization.utilization);

    return {
      uptimeSeconds: Math.floor((Date.now() - this.startedAt) / 1000),
      process: {
        pid: process.pid,
        nodeVersion: process.version,
        platform: process.platform,
      },
      memory: process.memoryUsage(),
      operational: {
        totalRequests: this.totalRequests,
        inFlightRequests: this.inFlightRequests,
        peakInFlightRequests: this.peakInFlightRequests,
      },
      eventLoop,
      routes: routes.sort((left, right) => right.requests - left.requests),
    };
  }

  private createRouteMetric(): RouteMetric {
    return {
      requests: 0,
      clientErrors: 0,
      errors: 0,
      timeouts: 0,
      totalDurationMs: 0,
      maxDurationMs: 0,
      latencyBuckets: Array.from({ length: LATENCY_BUCKETS_MS.length + 1 }, () => 0),
    };
  }

  private resolveRouteKey(candidate: string): string {
    if (this.routes.has(candidate)) {
      return candidate;
    }
    const maxRoutes = this.configService.get<number>('metrics.maxRoutes', { infer: true }) ?? 500;
    return this.routes.size < maxRoutes ? candidate : OVERFLOW_ROUTE;
  }

  private estimatePercentile(metric: RouteMetric, percentile: number): number {
    if (!metric.requests) {
      return 0;
    }
    const target = Math.ceil(metric.requests * percentile);
    let cumulative = 0;
    for (let index = 0; index < metric.latencyBuckets.length; index += 1) {
      cumulative += metric.latencyBuckets[index];
      if (cumulative >= target) {
        return index < LATENCY_BUCKETS_MS.length
          ? LATENCY_BUCKETS_MS[index]
          : Math.ceil(metric.maxDurationMs);
      }
    }
    return Math.ceil(metric.maxDurationMs);
  }

  private eventLoopSnapshot(utilization: number) {
    const warnThresholdMs =
      this.configService.get<number>('metrics.eventLoopLagWarnMs', { infer: true }) ?? 100;
    const meanMs = this.nanosecondsToMilliseconds(this.eventLoopDelay?.mean);
    const p50Ms = this.nanosecondsToMilliseconds(this.eventLoopDelay?.percentile(50));
    const p95Ms = this.nanosecondsToMilliseconds(this.eventLoopDelay?.percentile(95));
    const p99Ms = this.nanosecondsToMilliseconds(this.eventLoopDelay?.percentile(99));
    const maxMs = this.nanosecondsToMilliseconds(this.eventLoopDelay?.max);

    return {
      healthy: p99Ms <= warnThresholdMs,
      warnThresholdMs,
      utilization: Math.round(utilization * 10_000) / 10_000,
      lagMeanMs: meanMs,
      lagP50Ms: p50Ms,
      lagP95Ms: p95Ms,
      lagP99Ms: p99Ms,
      lagMaxMs: maxMs,
    };
  }

  private nanosecondsToMilliseconds(value: number | undefined): number {
    if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
      return 0;
    }
    return Math.round((value / 1_000_000) * 100) / 100;
  }
}
