import type { MultiaddrConnection, Stream } from './index.ts'

/**
 * Create tracked metrics with these options. Loosely based on the
 * interfaces exposed by the prom-client module
 */
export interface MetricOptions {
  /**
   * Optional label for the metric
   */
  label?: string

  /**
   * Optional help for the metric
   */
  help?: string
}

/**
 * A function that returns a tracked metric which may be expensive
 * to calculate so it is only invoked when metrics are being scraped
 */
export type CalculateMetric<T = number> = (() => T) | (() => Promise<T>)

/**
 * Create tracked metrics that are expensive to calculate by passing
 * a function that is only invoked when metrics are being scraped
 */
export interface CalculatedMetricOptions<T = number> extends MetricOptions {
  /**
   * An optional function invoked to calculate the component metric instead of
   * using `.update`, `.increment`, and `.decrement`
   */
  calculate: CalculateMetric<T>
}

/**
 * Call this function to stop the timer returned from the `.timer` method
 * on the metric
 */
export interface StopTimer { (): void }

/**
 * A tracked metric loosely based on the interfaces exposed by the
 * prom-client module
 */
export interface Metric {
  /**
   * Update the stored metric to the passed value
   */
  update(value: number): void

  /**
   * Increment the metric by the passed value or 1
   */
  increment(value?: number): void

  /**
   * Decrement the metric by the passed value or 1
   */
  decrement(value?: number): void

  /**
   * Reset this metric to its default value
   */
  reset(): void

  /**
   * Start a timed metric, call the returned function to
   * stop the timer
   */
  timer(): StopTimer
}

/**
 * A group of related metrics loosely based on the interfaces exposed by the
 * prom-client module
 */
export interface MetricGroup<T extends string = any> {
  /**
   * Update the stored metric group to the passed value
   */
  update(values: Partial<Record<T, number>>): void

  /**
   * Increment the metric group keys by the passed number or
   * `true` to increment by 1
   */
  increment(values: Partial<Record<T, number | true>>): void

  /**
   * Decrement the metric group keys by the passed number or
   * `true` to decrement by 1
   */
  decrement(values: Partial<Record<T, number | true>>): void

  /**
   * Reset the passed key in this metric group to its default value
   * or all keys if no key is passed
   */
  reset(): void

  /**
   * Start a timed metric for the named key in the group, call
   * the returned function to stop the timer
   */
  timer(key: string): StopTimer
}

/**
 * A tracked counter loosely based on the Counter interface exposed
 * by the prom-client module - counters are metrics that only go up
 */
export interface Counter {
  /**
   * Increment the metric by the passed value or 1
   */
  increment(value?: number): void

  /**
   * Reset this metric to its default value
   */
  reset(): void
}

/**
 * A group of tracked counters loosely based on the Counter interface
 * exposed by the prom-client module - counters are metrics that only
 * go up
 */
export interface CounterGroup<T extends string = any> {
  /**
   * Increment the metric group keys by the passed number or
   * any non-numeric value to increment by 1
   */
  increment(values: Partial<Record<T, number | true>>): void

  /**
   * Reset the passed key in this metric group to its default value
   * or all keys if no key is passed
   */
  reset(): void
}

export interface HistogramOptions extends MetricOptions {
  /**
   * Buckets for the histogram
   */
  buckets?: number[]
}

/**
 * Create tracked metrics that are expensive to calculate by passing
 * a function that is only invoked when metrics are being scraped
 */
export interface CalculatedHistogramOptions<T = number> extends HistogramOptions {
  /**
   * An optional function invoked to calculate the component metric instead of
   * using `.observe`
   */
  calculate: CalculateMetric<T>
}

export interface Histogram {
  /**
   * Observe the passed value
   */
  observe(value: number): void

  /**
   * Reset this histogram to its default value
   */
  reset(): void

  /**
   * Start a timed metric, call the returned function to
   * stop the timer
   */
  timer(): StopTimer
}

export interface HistogramGroup<T extends string = any> {
  /**
   * Observe the passed value for the named key in the group
   */
  observe(values: Partial<Record<T, number>>): void

  /**
   * Reset the passed key in this histogram group to its default value
   * or all keys if no key is passed
   */
  reset(): void

  /**
   * Start a timed metric for the named key in the group, call
   * the returned function to stop the timer
   */
  timer(key: string): StopTimer
}

export interface SummaryOptions extends MetricOptions {
  /**
   * Percentiles for the summary
   */
  percentiles?: number[]

  /**
   * Configure how old a bucket can be before it is reset for sliding window
   */
  maxAgeSeconds?: number

  /**
   * Configure how many buckets in the sliding window
   */
  ageBuckets?: number

  /**
   * Remove entries without any new values in the last `maxAgeSeconds`
   */
  pruneAgedBuckets?: boolean

  /**
   * Control compression of data in t-digest
   */
  compressCount?: number
}

/**
 * Create tracked metrics that are expensive to calculate by passing
 * a function that is only invoked when metrics are being scraped
 */
export interface CalculatedSummaryOptions<T = number> extends SummaryOptions {
  /**
   * An optional function invoked to calculate the component metric instead of
   * using `.observe`
   */
  calculate: CalculateMetric<T>
}

/**
 * A tracked summary loosely based on the Summary interface exposed
 * by the prom-client module
 */
export interface Summary {
  /**
   * Observe the passed value
   */
  observe(value: number): void

  /**
   * Reset this summary to its default value
   */
  reset(): void

  /**
   * Start a timed metric, call the returned function to
   * stop the timer
   */
  timer(): StopTimer
}

/**
 * A group of tracked summaries loosely based on the Summary interface
 * exposed by the prom-client module
 */
export interface SummaryGroup<T extends string = any> {
  /**
   * Observe the passed value for the named key in the group
   */
  observe(values: Partial<Record<T, number>>): void

  /**
   * Reset the passed key in this summary group to its default value
   * or all keys if no key is passed
   */
  reset(): void

  /**
   * Start a timed metric for the named key in the group, call
   * the returned function to stop the timer
   */
  timer(key: string): StopTimer
}

/**
 * The libp2p metrics tracking object. This interface is only concerned
 * with the collection of metrics, please see the individual implementations
 * for how to extract metrics for viewing.
 *
 * @example How to register a simple metric
 *
 * ```typescript
 * import { Metrics, Metric } from '@libp2p/interface/metrics'
 *
 * interface MyServiceComponents {
 *   metrics: Metrics
 * }
 *
 * class MyService {
 *   private readonly myMetric: Metric
 *
 *   constructor (components: MyServiceComponents) {
 *     this.myMetric = components.metrics.registerMetric({
 *       name: 'my_metric',
 *       label: 'my_label',
 *       help: 'my help text'
 *     })
 *   }
 *
 *   // later
 *   doSomething () {
 *     this.myMetric.update(1)
 *   }
 * }
 * ```
 *
 * @example How to register a dynamically calculated metric
 *
 * A metric that is expensive to calculate can be created by passing a `calculate` function that will only be invoked when metrics are being scraped:
 *
 * ```typescript
 * import { Metrics, Metric } from '@libp2p/interface/metrics'
 *
 * interface MyServiceComponents {
 *   metrics: Metrics
 * }
 *
 * class MyService {
 *   private readonly myMetric: Metric
 *
 *   constructor (components: MyServiceComponents) {
 *     this.myMetric = components.metrics.registerMetric({
 *       name: 'my_metric',
 *       label: 'my_label',
 *       help: 'my help text',
 *       calculate: async () => {
 *         // do something expensive
 *         return 1
 *       }
 *     })
 *   }
 * }
 * ```
 *
 * @example How to register a group of metrics
 *
 * If several metrics should be grouped together (e.g. for graphing purposes) `registerMetricGroup` can be used instead:
 *
 * ```typescript
 * import { Metrics, MetricGroup } from '@libp2p/interface/metrics'
 *
 * interface MyServiceComponents {
 *   metrics: Metrics
 * }
 *
 * class MyService {
 *   private readonly myMetricGroup: MetricGroup
 *
 *   constructor (components: MyServiceComponents) {
 *     this.myMetricGroup = components.metrics.registerMetricGroup({
 *       name: 'my_metric_group',
 *       label: 'my_label',
 *       help: 'my help text'
 *     })
 *   }
 *
 *   // later
 *   doSomething () {
 *     this.myMetricGroup.increment({ my_label: 'my_value' })
 *   }
 * }
 * ```
 *
 * There are specific metric groups for tracking libp2p connections and streams:
 *
 * @example How to track multiaddr connections
 *
 * This is something only libp2p transports need to do.
 *
 * ```typescript
 * import { Metrics } from '@libp2p/interface/metrics'
 *
 * interface MyServiceComponents {
 *   metrics: Metrics
 * }
 *
 * class MyService {
 *   private readonly metrics: Metrics
 *
 *   constructor (components: MyServiceComponents) {
 *     this.metrics = components.metrics
 *   }
 *
 *   // later
 *   doSomething () {
 *     const connection = {} // create a connection
 *     this.metrics.trackMultiaddrConnection(connection)
 *   }
 * }
 * ```
 *
 * @example How to track protocol streams
 *
 * This is something only libp2p connections need to do.
 *
 * ```typescript
 * import { Metrics } from '@libp2p/interface/metrics'
 *
 * interface MyServiceComponents {
 *   metrics: Metrics
 * }
 *
 * class MyService {
 *   private readonly metrics: Metrics
 *
 *   constructor (components: MyServiceComponents) {
 *     this.metrics = components.metrics
 *   }
 *
 *   // later
 *   doSomething () {
 *     const stream = {} // create a stream
 *     this.metrics.trackProtocolStream(stream)
 *   }
 * }
 * ```
 */
export interface Metrics {
  /**
   * Track a newly opened multiaddr connection
   */
  trackMultiaddrConnection(maConn: MultiaddrConnection): void

  /**
   * Track a newly opened protocol stream
   */
  trackProtocolStream(stream: Stream): void

  /**
   * Register an arbitrary metric. Call this to set help/labels for metrics
   * and update/increment/decrement/etc them by calling methods on the returned
   * metric object
   */
  registerMetric: ((name: string, options?: MetricOptions) => Metric) & ((name: string, options: CalculatedMetricOptions) => void)

  /**
   * Register a a group of related metrics. Call this to set help/labels for
   * groups of related metrics that will be updated with by calling `.update`,
   * `.increment` and/or `.decrement` methods on the returned metric group object
   */
  registerMetricGroup: ((name: string, options?: MetricOptions) => MetricGroup) & ((name: string, options: CalculatedMetricOptions<Record<string, number>>) => void)

  /**
   * Register an arbitrary counter. Call this to set help/labels for counters
   * and increment them by calling methods on the returned counter object
   */
  registerCounter: ((name: string, options?: MetricOptions) => Counter) & ((name: string, options: CalculatedMetricOptions) => void)

  /**
   * Register a a group of related counters. Call this to set help/labels for
   * groups of related counters that will be updated with by calling the `.increment`
   * method on the returned counter group object
   */
  registerCounterGroup: ((name: string, options?: MetricOptions) => CounterGroup) & ((name: string, options: CalculatedMetricOptions<Record<string, number>>) => void)

  /**
   * Register an arbitrary histogram. Call this to set help/labels for histograms
   * and observe them by calling methods on the returned histogram object
   */
  registerHistogram: ((name: string, options?: HistogramOptions) => Histogram) & ((name: string, options: CalculatedHistogramOptions) => void)

  /**
   * Register a a group of related histograms. Call this to set help/labels for
   * groups of related histograms that will be updated with by calling the `.observe`
   * method on the returned histogram group object
   */
  registerHistogramGroup: ((name: string, options?: HistogramOptions) => HistogramGroup) & ((name: string, options: CalculatedHistogramOptions<Record<string, number>>) => void)

  /**
   * Register an arbitrary summary. Call this to set help/labels for summaries
   * and observe them by calling methods on the returned summary object
   */
  registerSummary: ((name: string, options?: SummaryOptions) => Summary) & ((name: string, options: CalculatedSummaryOptions) => void)

  /**
   * Register a a group of related summaries. Call this to set help/labels for
   * groups of related summaries that will be updated with by calling the `.observe`
   * method on the returned summary group object
   */
  registerSummaryGroup: ((name: string, options?: SummaryOptions) => SummaryGroup) & ((name: string, options: CalculatedSummaryOptions<Record<string, number>>) => void)

  /**
   * Wrap a function for tracing purposes.
   *
   * All functions wrapped like this should accept a final optional options arg.
   *
   * In order to pass an execution context along to create a multi-layered
   * trace, the index of the options arg must be specified.
   */
  traceFunction <F extends (...args: any[]) => AsyncIterator<any>> (name: string, fn: F, options?: TraceGeneratorFunctionOptions<Parameters<F>, ReturnType<F>, YieldType<ReturnType<F>>>): F
  traceFunction <F extends (...args: any[]) => Iterator<any>> (name: string, fn: F, options?: TraceGeneratorFunctionOptions<Parameters<F>, ReturnType<F>, YieldType<ReturnType<F>>>): F
  traceFunction <F extends (...args: any[]) => any = (...args: any[]) => any> (name: string, fn: F, options?: TraceFunctionOptions<Parameters<F>, ReturnType<F>>): F

  /**
   * Creates a tracing context that can be used to trace a method call
   */
  createTrace(): any
}

/**
 * Infer the yielded type of an (async)iterable
 */
export type YieldType<T extends AsyncIterator<any> | Iterator<any>> = T extends AsyncIterator<infer Y> ? Y : T extends Iterator<infer Y, any, any> ? Y : never

export type TraceAttributes = Record<string, number | string | boolean | number[] | string[] | boolean[]>

export interface TraceFunctionOptions<A, B> {
  /**
   * To construct a trace that spans multiple method invocations, it's necessary
   * to pass the trace context onwards as part of the options object.
   *
   * Specify the index of the options object in the args array here.
   *
   * @default 0
   */
  optionsIndex?: number

  /**
   * Set attributes on the trace by modifying the passed attributes object.
   */
  getAttributesFromArgs?(args: A, attributes: TraceAttributes): TraceAttributes

  /**
   * Set attributes on the trace by modifying the passed attributes object. The
   * object will have previously been passed to `appendAttributesFromArgs`
   * and/or `appendAttributesFromYieldedValue` (if defined)
   */
  getAttributesFromReturnValue?(value: B, attributes: TraceAttributes): TraceAttributes
}

export interface TraceGeneratorFunctionOptions<A, B, C = any> extends TraceFunctionOptions<A, B> {
  /**
   * Set attributes on the trace by modifying the passed attributes object. The
   * object will have previously been passed to `appendAttributesFromArgs` (if
   * defined)
   */
  getAttributesFromYieldedValue? (value: C, attributes: TraceAttributes, index: number): TraceAttributes
}
