import dgram = require("dgram");
import stream = require("stream");

declare module "hot-shots" {
  export type Tags = { [key: string]: string } | string[];
  export interface ClientOptions {
    bufferFlushInterval?: number;
    bufferHolder?: { buffer: string };
    cacheDns?: boolean;
    cacheDnsTtl?: number;
    errorHandler?: (err: Error) => void;
    globalTags?: Tags;
    includeDataDogTags?: boolean;
    globalize?: boolean;
    host?: string;
    isChild?: boolean;
    maxBufferSize?: number;
    mock?: boolean;
    path?: string;
    port?: number;
    prefix?: string;
    protocol?: 'tcp' | 'udp' | 'uds' | 'stream';
    sampleRate?: number;
    socket?: dgram.Socket;
    stream?: stream.Writable;
    suffix?: string;
    telegraf?: boolean;
    useDefaultRoute?: boolean;
    tagPrefix?: string;
    tagSeparator?: string;
    tcpGracefulErrorHandling?: boolean;
    tcpGracefulRestartRateLimit?: number;
    udsGracefulErrorHandling?: boolean;
    udsGracefulRestartRateLimit?: number;
    closingFlushInterval?: number;
    udpSocketOptions?: dgram.SocketOptions;
    udsRetryOptions?: {
      retries?: number;
      retryDelayMs?: number;
      maxRetryDelayMs?: number;
      backoffFactor?: number;
    };
    includeDatadogTelemetry?: boolean;
    telemetryFlushInterval?: number;
  }

  export interface ChildClientOptions {
    globalTags?: Tags;
    prefix?: string;
    suffix?: string;
    errorHandler?: (err: Error) => void;
  }

  export interface CheckOptions {
    date_happened?: Date;
    hostname?: string;
    message?: string;
  }

  export interface DatadogChecks {
    OK: 0;
    WARNING: 1;
    CRITICAL: 2;
    UNKNOWN: 3;
  }

  type unionFromInterfaceValues4<
    T,
    K1 extends keyof T,
    K2 extends keyof T,
    K3 extends keyof T,
    K4 extends keyof T,
    > = T[K1] | T[K2] | T[K3] | T[K4];

  export type DatadogChecksValues = unionFromInterfaceValues4<DatadogChecks, "OK", "WARNING", "CRITICAL", "UNKNOWN">;

  export interface EventOptions {
    aggregation_key?: string;
    alert_type?: "info" | "warning" | "success" | "error";
    date_happened?: Date;
    hostname?: string;
    priority?: "low" | "normal";
    source_type_name?: string;
  }

  export interface MetricOptions {
    sampleRate?: number;
    tags?: Tags;
    /** Timestamp for the metric (DogStatsD only). Can be a Date or Unix timestamp in seconds. */
    timestamp?: Date | number;
  }

  /**
   * Context object passed to timer/asyncTimer/asyncDistTimer wrapped functions.
   * Allows adding tags dynamically during function execution.
   */
  export interface TimerContext {
    /**
     * Add tags to be included when the metric is recorded.
     * Can be called multiple times to accumulate tags.
     * @param tags Tags to add. Can be an object like {key: 'value'} or array like ['key:value']
     */
    addTags(tags: Tags): void;
  }

  export type StatsCb = (error?: Error, bytes?: number) => void;

  export class StatsD {
    constructor(options?: ClientOptions);
    childClient(options?: ChildClientOptions): StatsD;

    increment(stat: string, tags?: Tags): void;
    increment(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    increment(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void;
    increment(stat: string | string[], value: number, callback?: StatsCb): void;
    increment(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void;
    increment(stat: string | string[], value: number, options?: MetricOptions, callback?: StatsCb): void;

    decrement(stat: string): void;
    decrement(stat: string, tags?: Tags): void;
    decrement(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    decrement(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void;
    decrement(stat: string | string[], value: number, callback?: StatsCb): void;
    decrement(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void;
    decrement(stat: string | string[], value: number, options?: MetricOptions, callback?: StatsCb): void;

    timing(stat: string | string[], value: number | Date, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    timing(stat: string | string[], value: number | Date, tags?: Tags, callback?: StatsCb): void;
    timing(stat: string | string[], value: number | Date, callback?: StatsCb): void;
    timing(stat: string | string[], value: number | Date, sampleRate?: number, callback?: StatsCb): void;
    timing(stat: string | string[], value: number | Date, options?: MetricOptions, callback?: StatsCb): void;

    /**
     * Wraps a function to record its execution time. The wrapped function receives a TimerContext
     * as its last argument, which can be used to add tags dynamically during execution.
     */
    timer<P extends any[], R>(func: (...args: [...P, TimerContext]) => R, stat: string | string[], sampleRate?: number, tags?: Tags, callback?: StatsCb): (...args: P) => R;
    timer<P extends any[], R>(func: (...args: [...P, TimerContext]) => R, stat: string | string[], tags?: Tags, callback?: StatsCb): (...args: P) => R;
    timer<P extends any[], R>(func: (...args: [...P, TimerContext]) => R, stat: string | string[], callback?: StatsCb): (...args: P) => R;
    timer<P extends any[], R>(func: (...args: [...P, TimerContext]) => R, stat: string | string[], sampleRate?: number, callback?: StatsCb): (...args: P) => R;
    timer<P extends any[], R>(func: (...args: [...P, TimerContext]) => R, stat: string | string[], options?: MetricOptions, callback?: StatsCb): (...args: P) => R;

    /**
     * Wraps an async function to record its execution time. The wrapped function receives a TimerContext
     * as its last argument, which can be used to add tags dynamically during execution.
     */
    asyncTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], sampleRate?: number, tags?: Tags, callback?: StatsCb): (...args: P) => Promise<R>;
    asyncTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], tags?: Tags, callback?: StatsCb): (...args: P) => Promise<R>;
    asyncTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], callback?: StatsCb): (...args: P) => Promise<R>;
    asyncTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], sampleRate?: number, callback?: StatsCb): (...args: P) => Promise<R>;
    asyncTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], options?: MetricOptions, callback?: StatsCb): (...args: P) => Promise<R>;

    /**
     * Wraps an async function to record its execution time as a distribution. The wrapped function receives
     * a TimerContext as its last argument, which can be used to add tags dynamically during execution.
     */
    asyncDistTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], sampleRate?: number, tags?: Tags, callback?: StatsCb): (...args: P) => Promise<R>;
    asyncDistTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], tags?: Tags, callback?: StatsCb): (...args: P) => Promise<R>;
    asyncDistTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], callback?: StatsCb): (...args: P) => Promise<R>;
    asyncDistTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], sampleRate?: number, callback?: StatsCb): (...args: P) => Promise<R>;
    asyncDistTimer<P extends any[], R>(func: (...args: [...P, TimerContext]) => Promise<R>, stat: string | string[], options?: MetricOptions, callback?: StatsCb): (...args: P) => Promise<R>;

    histogram(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    histogram(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void;
    histogram(stat: string | string[], value: number, callback?: StatsCb): void;
    histogram(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void;
    histogram(stat: string | string[], value: number, options?: MetricOptions, callback?: StatsCb): void;

    distribution(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    distribution(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void;
    distribution(stat: string | string[], value: number, callback?: StatsCb): void;
    distribution(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void;
    distribution(stat: string | string[], value: number, options?: MetricOptions, callback?: StatsCb): void;

    gauge(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    gauge(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void;
    gauge(stat: string | string[], value: number, callback?: StatsCb): void;
    gauge(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void;
    gauge(stat: string | string[], value: number, options?: MetricOptions, callback?: StatsCb): void;

    gaugeDelta(stat: string | string[], value: number, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    gaugeDelta(stat: string | string[], value: number, tags?: Tags, callback?: StatsCb): void;
    gaugeDelta(stat: string | string[], value: number, callback?: StatsCb): void;
    gaugeDelta(stat: string | string[], value: number, sampleRate?: number, callback?: StatsCb): void;
    gaugeDelta(stat: string | string[], value: number, options?: MetricOptions, callback?: StatsCb): void;

    set(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    set(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void;
    set(stat: string | string[], value: number | string, callback?: StatsCb): void;
    set(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void;
    set(stat: string | string[], value: number | string, options?: MetricOptions, callback?: StatsCb): void;

    unique(stat: string | string[], value: number | string, sampleRate?: number, tags?: Tags, callback?: StatsCb): void;
    unique(stat: string | string[], value: number | string, tags?: Tags, callback?: StatsCb): void;
    unique(stat: string | string[], value: number | string, callback?: StatsCb): void;
    unique(stat: string | string[], value: number | string, sampleRate?: number, callback?: StatsCb): void;
    unique(stat: string | string[], value: number | string, options?: MetricOptions, callback?: StatsCb): void;

    close(callback?: (error?: Error) => void): void;

    event(title: string, text?: string, options?: EventOptions, tags?: Tags, callback?: StatsCb): void;
    event(title: string, text?: string, options?: EventOptions, callback?: StatsCb): void;
    check(name: string, status: DatadogChecksValues, options?: CheckOptions, tags?: Tags, callback?: StatsCb): void;

    public CHECKS: DatadogChecks;
    public mockBuffer?: string[];
    public socket: dgram.Socket;
  }
}

declare const StatsDClient: new (options?: ClientOptions) => StatsD;
export default StatsDClient;
