export type MetricLabels = Record<string, string | number | boolean>;

export interface MetricPoint {
  name: string;
  type: "counter" | "gauge" | "histogram";
  value: number;
  count?: number;
  sum?: number;
  min?: number;
  max?: number;
  labels: MetricLabels;
  timestamp: number;
}

function labelKey(labels: MetricLabels): string {
  return Object.entries(labels)
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([key, value]) => `${key}=${String(value)}`)
    .join(",");
}

interface StoredMetric extends MetricPoint {
  key: string;
}

export class MetricsRegistry {
  private points = new Map<string, StoredMetric>();
  constructor(private readonly clock: () => number = Date.now) {}

  increment(name: string, value = 1, labels: MetricLabels = {}): void {
    const key = `counter:${name}:${labelKey(labels)}`;
    const current = this.points.get(key);
    this.points.set(key, {
      key,
      name,
      type: "counter",
      value: (current?.value ?? 0) + value,
      labels: { ...labels },
      timestamp: this.clock(),
    });
  }

  gauge(name: string, value: number, labels: MetricLabels = {}): void {
    const key = `gauge:${name}:${labelKey(labels)}`;
    this.points.set(key, {
      key,
      name,
      type: "gauge",
      value,
      labels: { ...labels },
      timestamp: this.clock(),
    });
  }

  observe(name: string, value: number, labels: MetricLabels = {}): void {
    const key = `histogram:${name}:${labelKey(labels)}`;
    const current = this.points.get(key);
    const count = (current?.count ?? 0) + 1;
    const sum = (current?.sum ?? 0) + value;
    this.points.set(key, {
      key,
      name,
      type: "histogram",
      value: sum / count,
      count,
      sum,
      min: current?.min === undefined ? value : Math.min(current.min, value),
      max: current?.max === undefined ? value : Math.max(current.max, value),
      labels: { ...labels },
      timestamp: this.clock(),
    });
  }

  snapshot(): MetricPoint[] {
    return [...this.points.values()].map(({ key: _key, ...point }) => ({ ...point }));
  }

  reset(): void {
    this.points.clear();
  }
}
