/*
 This file is part of GNU Taler
 (C) 2026 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * Type and schema definitions for performance stats from the wallet to
 * clients of the wallet.
 */

/**
 * Imports.
 */
import { assertUnreachable } from "./index.js";
import { ObservabilityEvent, ObservabilityEventType } from "./notifications.js";

export enum PerformanceStatType {
  HttpFetch = "http-fetch",
  DbQuery = "db-query",
  Crypto = "crypto",
  WalletRequest = "wallet-request",
  WalletTask = "wallet-task",
}

export type PerformanceStat =
  | {
      type: PerformanceStatType.HttpFetch;
      url: string;
      avgDurationMs: number;
      maxDurationMs: number;
      minDurationMs: number;
      totalDurationMs: number;
      count: number;
    }
  | {
      type: PerformanceStatType.DbQuery;
      name: string;
      location: string;
      avgDurationMs: number;
      maxDurationMs: number;
      minDurationMs: number;
      totalDurationMs: number;
      count: number;
    }
  | {
      type: PerformanceStatType.Crypto;
      operation: string;
      avgDurationMs: number;
      maxDurationMs: number;
      minDurationMs: number;
      totalDurationMs: number;
      count: number;
    }
  | {
      type: PerformanceStatType.WalletRequest;
      operation: string;
      avgDurationMs: number;
      maxDurationMs: number;
      minDurationMs: number;
      totalDurationMs: number;
      count: number;
    }
  | {
      type: PerformanceStatType.WalletTask;
      taskId: string;
      avgDurationMs: number;
      maxDurationMs: number;
      minDurationMs: number;
      totalDurationMs: number;
      count: number;
    };

export namespace PerformanceStat {
  export function fromNotification(
    evt: ObservabilityEvent & { durationMs: number },
  ): PerformanceStat | undefined {
    if (
      (evt.type === ObservabilityEventType.HttpFetchFinishSuccess ||
        evt.type === ObservabilityEventType.HttpFetchFinishError
      ) && !evt.longPolling
    ) {
      return {
        type: PerformanceStatType.HttpFetch,
        url: evt.url,
        avgDurationMs: evt.durationMs,
        maxDurationMs: evt.durationMs,
        minDurationMs: evt.durationMs,
        totalDurationMs: evt.durationMs,
        count: 1,
      };
    } else if (
      evt.type === ObservabilityEventType.DbQueryFinishSuccess ||
      evt.type === ObservabilityEventType.DbQueryFinishError
    ) {
      return {
        type: PerformanceStatType.DbQuery,
        name: evt.name,
        location: evt.location,
        avgDurationMs: evt.durationMs,
        maxDurationMs: evt.durationMs,
        minDurationMs: evt.durationMs,
        totalDurationMs: evt.durationMs,
        count: 1,
      };
    } else if (
      evt.type === ObservabilityEventType.CryptoFinishSuccess ||
      evt.type === ObservabilityEventType.CryptoFinishError
    ) {
      return {
        type: PerformanceStatType.Crypto,
        operation: evt.operation,
        avgDurationMs: evt.durationMs,
        maxDurationMs: evt.durationMs,
        minDurationMs: evt.durationMs,
        totalDurationMs: evt.durationMs,
        count: 1,
      };
    } else if (
      evt.type === ObservabilityEventType.RequestFinishSuccess ||
      evt.type === ObservabilityEventType.RequestFinishError
    ) {
      return {
        type: PerformanceStatType.WalletRequest,
        operation: evt.operation,
        avgDurationMs: evt.durationMs,
        maxDurationMs: evt.durationMs,
        minDurationMs: evt.durationMs,
        totalDurationMs: evt.durationMs,
        count: 1,
      };
    } else if (evt.type === ObservabilityEventType.ShepherdTaskResult) {
      return {
        type: PerformanceStatType.WalletTask,
        taskId: evt.taskId,
        avgDurationMs: evt.durationMs,
        maxDurationMs: evt.durationMs,
        minDurationMs: evt.durationMs,
        totalDurationMs: evt.durationMs,
        count: 1,
      };
    }

    return undefined;
  }

  export function equals(a: PerformanceStat, b: PerformanceStat): boolean {
    if (a.type !== b.type) return false;
    if (a.type === PerformanceStatType.HttpFetch) {
      return a.url === b["url" as keyof typeof b];
    } else if (a.type === PerformanceStatType.DbQuery) {
      return (
        a.name === b["name" as keyof typeof b] &&
        a.location === b["location" as keyof typeof b]
      );
    } else if (a.type === PerformanceStatType.Crypto) {
      return a.operation === b["operation" as keyof typeof b];
    } else if (a.type === PerformanceStatType.WalletRequest) {
      return a.operation === b["operation" as keyof typeof b];
    } else if (a.type === PerformanceStatType.WalletTask) {
      return a.taskId === b["taskId" as keyof typeof b];
    } else {
      assertUnreachable(a);
    }
  }
}

/**
 * Max size of each performance table.
 */
const MAX_PERFORMANCE_TABLE_SIZE = 500;

export type PerformanceTable = {
  [key in PerformanceStatType]?: PerformanceStat[];
};

export namespace PerformanceTable {
  export function insertEvent(tab: PerformanceTable, evt: ObservabilityEvent) {
    if ("durationMs" in evt && typeof evt.durationMs === "number") {
      const stat = PerformanceStat.fromNotification(evt);
      if (!stat) return;
      insertOrIncrement(tab, stat);
      sort(tab);
      rotate(tab, stat.type);
    }
  }

  /**
   * Extract the N largest stats of each table.
   */
  export function limit(tab: PerformanceTable, n?: number): PerformanceTable {
    if (n === undefined || n === Number.MAX_VALUE) {
      return tab;
    }
    const limited: PerformanceTable = {};
    for (const k of Object.keys(tab)) {
      const key = k as keyof typeof tab;
      limited[key] = tab[key]!!.slice(0, n);
    }
    return limited;
  }

  /**
   * Insert event to performance table.
   *
   * If matching event is found, increment durationMs in place.
   */
  function insertOrIncrement(tab: PerformanceTable, stat: PerformanceStat) {
    if (!tab[stat.type]) {
        tab[stat.type] = [];
        tab[stat.type]?.push(stat);
        return;
    }

    const index = tab[stat.type]!!.findIndex(
      (el) => PerformanceStat.equals(el, stat),
    );
    if (index === -1) {
      tab[stat.type]?.push(stat);
    } else {
      const existing = tab[stat.type]!![index];
      existing.avgDurationMs = Math.floor((existing.avgDurationMs + stat.totalDurationMs) / 2);
      existing.maxDurationMs = Math.max(existing.maxDurationMs, stat.maxDurationMs);
      existing.minDurationMs = Math.min(existing.minDurationMs, stat.minDurationMs);
      existing.totalDurationMs = existing.totalDurationMs + stat.totalDurationMs;
      existing.count += 1;
      tab[stat.type]!![index] = existing;
    }
  }

  /**
   * Sort all performance tables in place.
   */
  function sort(tab: PerformanceTable) {
    for (const k of Object.keys(tab)) {
      const key = k as keyof typeof tab
      tab[key]!!.sort((a, b) => b.avgDurationMs - a.avgDurationMs);
    }
  }

  /**
   * Keep performance table under size limit.
   */
  function rotate(tab: PerformanceTable, type: PerformanceStatType) {
    if (tab[type]!!.length > MAX_PERFORMANCE_TABLE_SIZE) {
      tab[type]?.splice(0, 1);
    }
  }
}
