import {
  Duration,
  enforce,
} from "vscript";
import { Stats, stats } from "./utils.js";

type Milliseconds = number;
type Label = string;
type Tag = string;

export class Benchmarks {
  private mData: Map<Label, [start_date: Date, duration: Milliseconds][]> = new Map();
  // TODO: log to influxdb
  constructor(private readonly mTags: Record<Tag, string | number>) {}

  // promise should ideally be created inline, otherwise we may significantly underestimate true timings
  async benchmarked<T>(
    label: Label,
    promise: Promise<T>,
    pars?: { skip_if_below?: Duration },
  ): Promise<T> {
    const start_date = new Date();
    const ms0 = start_date.valueOf();
    const result = await promise;
    const ms1 = new Date().valueOf();
    if (ms1 >= ms0 + (pars?.skip_if_below?.ms() ?? 0)) {
      if (!this.mData.has(label)) {
        this.mData.set(label, []);
      }
      this.mData.get(label)!.push([start_date, ms1 - ms0]);
    }
    return result;
  }

  empty(): boolean {
    return this.mData.size === 0;
  }

  to_influxdb_lineprotocol(pars: {
    measurement_name: string;
    precision: "ns" | "ms";
    tags?: Record<string, number | string | boolean>;
  }): string {
    let prefix = pars.measurement_name;
    const as_string = (s: string) => `"${s.replaceAll('"', '\\"')}"`;
    if (pars.tags) {
      for (const tagname in pars.tags) {
        prefix += `,${tagname}=`;
        const tag = pars.tags[tagname];
        if (typeof tag === "string") {
          prefix += as_string(tag);
        } else {
          prefix += `${tag}`;
        }
      }
    }
    let result = "";
    const suffix = pars.precision === "ns" ? "000000" : "";
    for (const [label, data] of this.mData) {
      for (const [start_date, ms] of data) {
        if (result.length !== 0) result += "\n";
        result += `${prefix},label=${as_string(label)} duration=${Math.round(
          ms,
        )}i ${start_date.valueOf()}${suffix}`;
      }
    }
    return result;
  }

  to_string_hum(): string {
    let result = "";
    for (const [label, data] of this.mData) {
      result += `==== ${label} `.padEnd(60, "=") + "\n";
      const maybe_stats = stats(data.map((x) => x[1]));
      if (maybe_stats) {
        result += `min: ${maybe_stats.min} ms\n`;
        result += `q5: ${maybe_stats.q5} ms\n`;
        result += `median: ${maybe_stats.median} ms\n`;
        result += `mean: ${maybe_stats.mean} ms\n`;
        result += `q95: ${maybe_stats.q95} ms\n`;
        result += `max: ${maybe_stats.max} ms\n`;
        result += `stddev: ${maybe_stats.stddev} ms\n`;
      } else {
        result += data
          .map((x) => x[1])
          .sort((a, b) => a - b)
          .map((x) => new Duration(x, "ms").toString("convenient"))
          .join(", ");
      }
      result += "\n";
    }
    return result;
  }

  to_json() {
    const results: Record<Label, { all_data: Milliseconds[]; stats?: Stats<Milliseconds> }> = {};
    for (const [label, all_data] of this.mData) {
      enforce(!Object.prototype.hasOwnProperty.call(results, label));
      const maybe_stats = stats(all_data.map((x) => x[1]));
      results[label] = {
        all_data: all_data.map((x) => x[1]),
        ...(maybe_stats ? { stats: maybe_stats } : {}),
      };
    }
    return {
      tags: this.mTags,
      results,
    };
  }
}
