import type * as stream from 'stream';
import type * as http from 'http';
import type { EventEmitter } from 'events';

export const DEFAULT_ADMIN_SERVER_PORT = 45454;

export enum Method {
    GET,
    POST,
    PUT,
    DELETE,
    PATCH,
    HEAD,
    OPTIONS
}

export enum RulePriority {
    FALLBACK = 0,
    DEFAULT = 1
}

export interface Headers {
    // An arbitrary set of headers that are known to
    // only ever appear once (for valid requests).
    host?: string;
    'content-length'?: string;
    'content-type'?: string;
    'user-agent'?: string;
    cookie?: string;
    ':method'?: string;
    ':scheme'?: string;
    ':authority'?: string;
    ':path'?: string;

    // In general there may be 0+ of any header
    [key: string]: undefined | string | string[];
}

export interface Trailers {
    // 0+ of any trailer
    [key: string]: undefined | string | string[];
}

export type RawHeaders = Array<[key: string, value: string]>;
export type RawTrailers = RawHeaders; // Just a convenient alias

// --- Terminology: ---
// Hostname = String of IP or domain name
// Host = String of hostname + optional port (if not default for protocol)
// Destination = hostname + mandatory port as a structured object
// N.b. IPv6 is only [bracketed] in place in URLs/headers, not elsewhere.
export interface Destination {
    hostname: string;
    port: number;
}

export interface Request {
    id: string;
    matchedRuleId?: string;

    protocol: string;
    httpVersion: string;
    method: string;
    url: string;
    path: string;

    remoteIpAddress?: string; // Not set remotely with older servers or in some error cases
    remotePort?: number; // Not set remotely with older servers or in some error cases

    /**
     * The best guess at the target host + port of the request. This uses tunnelling metadata
     * wherever possible, or the headers if not.
     */
    destination: Destination;

    headers: Headers;
    rawHeaders: RawHeaders;

    timingEvents: TimingEvents;
    tags: string[];
}

export interface TlsConnectionEvent {
    remoteIpAddress?: string; // Can be unavailable in some error cases
    remotePort?: number; // Can be unavailable in some error cases
    tags: string[];
    timingEvents: TlsTimingEvents;
    destination?: Destination; // Set for tunnelled requests only
    tlsMetadata: TlsSocketMetadata;
}

export interface TlsSocketMetadata {
    sniHostname?: string;
    clientAlpn?: string[];
    ja3Fingerprint?: string;
    ja4Fingerprint?: string;
}

export interface TlsPassthroughEvent extends RawPassthroughEvent, TlsConnectionEvent {
    // Removes ambiguity of the two parent interface fields
    destination: Destination;
    remoteIpAddress: string;
    remotePort: number;
    timingEvents: TlsTimingEvents;
}

export interface TlsHandshakeFailure extends TlsConnectionEvent {
    failureCause:
        | 'closed'
        | 'reset'
        | 'cert-rejected'
        | 'no-shared-cipher'
        | 'handshake-timeout'
        | 'unknown';
    timingEvents: TlsFailureTimingEvents;
}

export interface RawPassthroughEvent {
    id: string;

    destination: Destination;

    /**
     * The IP address of the remote client that initiated the connection.
     */
    remoteIpAddress: string;
    /**
     * The port of the remote client that initiated the connection.
     */
    remotePort: number;

    tags: string[];
    timingEvents: ConnectionTimingEvents;
}

export interface RawPassthroughDataEvent {
    /**
     * The id of the passthrough tunnel.
     */
    id: string;

    /**
     * The direction of the message, from the downstream perspective (received from the client,
     * or sent back to the client).
     */
    direction: 'sent' | 'received';

    /**
     * The contents of the message as a raw buffer.
     */
    content: Uint8Array;

    /**
     * A high-precision floating-point monotonically increasing timestamp.
     * Comparable and precise, but not related to specific current time.
     *
     * To link this to the current time, compare it to `timingEvents.startTime`.
     */
    eventTimestamp: number;
}

export interface ConnectionTimingEvents {
    /**
     * When the socket initially connected, in MS since the unix
     * epoch.
     */
    startTime: number;

    /**
     * When the socket initially connected, equivalent to startTime.
     *
     * High-precision floating-point monotonically increasing timestamps.
     * Comparable and precise, but not related to specific current time.
     */
    connectTimestamp: number;

    /**
     * When the outer tunnel (e.g. a preceeding CONNECT request/SOCKS
     * connection) was created, if there was one.
     */
    tunnelTimestamp?: number;

    /**
     * When the connection was closed, if it has been closed.
     */
    disconnectTimestamp?: number;
}

export interface TlsTimingEvents extends ConnectionTimingEvents {
    /**
     * When Mockttp's handshake for this connection was completed (if there
     * was one). This is not set for passed through connections.
     */
    handshakeTimestamp?: number;
}

export interface TlsFailureTimingEvents extends TlsTimingEvents {
    /**
     * When the TLS connection failed. This may be due to a failed handshake
     * (in which case `handshakeTimestamp` will be undefined) or due to a
     * subsequent error which means the TLS connection was not usable (like
     * an immediate closure due to an async certificate rejection).
     */
    failureTimestamp: number;
}

// Internal representation of an ongoing HTTP request whilst it's being processed
export interface OngoingRequest extends Request, stream.Readable {
    body: OngoingBody;
    rawTrailers?: RawHeaders;
}

export interface OngoingBody {
    asStream: () => stream.Readable;
    asBuffer: () => Promise<Buffer>;
    asDecodedBuffer: () => Promise<Buffer>;
    asText: () => Promise<string>;
    asJson: () => Promise<object>;
    asFormData: () => Promise<{ [key: string]: string | string[] | undefined }>;
}

export interface CompletedBody {
    /**
     * The raw bytes of the response. If a content encoding was used, this is
     * the raw encoded data.
     */
    buffer: Buffer;

    /**
     * The decoded bytes of the response. If no encoding was used, this is the
     * same as `.buffer`. The response is decoded and returned asynchronously
     * as a Promise.
     */
    getDecodedBuffer(): Promise<Buffer | undefined>;

    /**
     * The contents of the response, decoded and parsed as a UTF-8 string.
     * The response is decoded and returned asynchronously as a Promise.
     */
    getText(): Promise<string | undefined>;

    /**
     * The contents of the response, decoded, parsed as UTF-8 string, and
     * then parsed a JSON. The response is decoded and returned asynchronously
     * as a Promise.
     */
    getJson(): Promise<object | undefined>;

    /**
     * The contents of the response, decoded, and then parsed automatically as
     * either one of the form encoding types (either URL-encoded or multipart),
     * determined automatically from the message content-type header.
     *
     * This method is convenient and offers a single mechanism to parse both
     * formats, but you may want to consider parsing on format explicitly with
     * the `getUrlEncodedFormData()` or `getMultipartFormData()` methods instead.
     *
     * After parsing & decoding, the result is returned asynchronously as a
     * Promise for a key-value(s) object.
     */
    getFormData(): Promise<{ [key: string]: string | string[] | undefined } | undefined>;

    /**
     * The contents of the response, decoded, parsed as UTF-8 string, and then
     * parsed as URL-encoded form data. After parsing & decoding, the result is
     * returned asynchronously as a Promise for a key-value(s) object.
     */
    getUrlEncodedFormData(): Promise<{ [key: string]: string | string[] | undefined } | undefined>;

    /**
     * The contents of the response, decoded, and then parsed as multi-part
     * form data. The response is result is returned asynchronously as a
     * Promise for an array of parts with their names, data and metadata.
     */
    getMultipartFormData(): Promise<Array<{ name?: string, filename?: string, type?: string, data: Buffer }> | undefined>;
}

// Internal & external representation of an initiated (no body yet received) HTTP request.
export type InitiatedRequest = Request;

export interface AbortedRequest extends InitiatedRequest {
    error?: {
        name?: string;
        code?: string;
        message?: string;
        stack?: string;
    };
}

// Internal & external representation of a fully completed HTTP request
export interface CompletedRequest extends Request {
    body: CompletedBody;
    rawTrailers: RawTrailers;
    trailers: Trailers;
}

export interface TimingEvents {
    // Milliseconds since unix epoch
    startTime: number;

    // High-precision floating-point monotonically increasing timestamps.
    // Comparable and precise, but not related to specific current time.
    startTimestamp: number; // When the request was initially received
    bodyReceivedTimestamp?: number; // When the request body was fully received
    headersSentTimestamp?: number; // When the response headers were sent
    responseSentTimestamp?: number; // When the response was fully completed

    wsAcceptedTimestamp?: number; // When the websocket was accepted
    wsClosedTimestamp?: number; // When the websocket was closed

    abortedTimestamp?: number; // When the connected was aborted
}

export interface OngoingResponse extends http.ServerResponse {
    id: string;
    getHeaders(): Headers;
    getRawHeaders(): RawHeaders;
    body: OngoingBody;
    getRawTrailers(): RawTrailers;
    timingEvents: TimingEvents;
    tags: string[];
    sendInformationalResponse(status: number, flatHeaders: string[]): void;
}

export interface InitiatedResponse {
    id: string;
    statusCode: number;
    statusMessage: string;
    headers: Headers;
    rawHeaders: RawHeaders;
    timingEvents: TimingEvents;
    tags: string[];
}

/**
 * An HTTP 1xx informational response (e.g. 100 Continue, 102 Processing,
 * 103 Early Hints) sent before the final response. Multiple of these may
 * be sent for a single request before the final response headers arrive.
 */
export interface InformationalResponse extends InitiatedResponse {
    /**
     * A high-precision floating-point monotonically increasing timestamp
     * recording when the informational response was sent.
     *
     * To link this to the current time, compare it to `timingEvents.startTime`.
     */
    eventTimestamp: number;
}

export interface BodyData {
    /**
     * The id of the request or response this data belongs to.
     */
    id: string;

    /**
     * The contents of the chunk as a raw buffer. Note that this may be empty,
     * when the body is finishing, if it wasn't known to be finished at the
     * preceeding chunk of data.
     */
    content: Uint8Array;

    /**
     * Indicates whether the body has completed or whether there's more coming.
     * This will not be set if the body is aborted incomplete - listen for abort
     * events separately to check for this.
     */
    isEnded: boolean;

    /**
     * A high-precision floating-point monotonically increasing timestamp.
     * Comparable and precise, but not related to specific current time.
     *
     * To link this to the current time, compare it to `timingEvents.startTime`.
     */
    eventTimestamp: number;

}

export interface CompletedResponse extends InitiatedResponse {
    body: CompletedBody;
    rawTrailers: RawTrailers;
    trailers: Trailers;
}

export interface WebSocketMessage {
    /**
     * The id of this websocket stream. This will match the id of the request,
     * the initial connection response, and any other WebSocket events for the
     * same connection stream.
     */
    streamId: string;

    /**
     * Whether the message was sent by Mockttp, or received from a Mockttp client.
     */
    direction: 'sent' | 'received';

    /**
     * The contents of the message as a raw buffer. This is already decompressed,
     * if the WebSocket uses compression.
     */
    content: Uint8Array;

    /**
     * Whether this is a string message or a raw binary data message.
     */
    isBinary: boolean;

    /**
     * A high-precision floating-point monotonically increasing timestamp.
     * Comparable and precise, but not related to specific current time.
     *
     * To link this to the current time, compare it to `timingEvents.startTime`.
     */
    eventTimestamp: number;

    timingEvents: TimingEvents;
    tags: string[];
}

export interface WebSocketClose {
    /**
     * The id of this websocket stream. This will match the id of the request,
     * the initial connection response, and any other WebSocket events for the
     * same connection stream.
     */
    streamId: string;

    /**
     * The close code of the shutdown. This is the close code that was received
     * from the remote client (either initiated remotely, or echoing our own sent
     * close frame).
     *
     * This may be undefined only if a close frame was received but did not contain
     * any close code. If no close frame was received before the connection was
     * lost (i.e. the connection was not cleanly closed) this event will not
     * fire at all, and an 'abort' event will fire instead.
     */
    closeCode: number | undefined;

    /**
     * The close reason of the shutdown.
     */
    closeReason: string;

    timingEvents: TimingEvents;
    tags: string[];
}

/**
 * A client error event describes a request (or our best guess at parsing it),
 * that wasn't correctly completed, and the error response it received, or
 * 'aborted' if the connection was disconnected before we could respond.
 */
export interface ClientError {
    errorCode?: string;
    request: {
        id: string;
        timingEvents: TimingEvents;
        tags: string[];

        // All of these are best guess, depending on what's parseable:
        protocol?: string;
        httpVersion: string;
        method?: string;
        url?: string;
        path?: string;
        destination?: Destination;

        headers: Headers;
        rawHeaders: RawHeaders;

        remoteIpAddress?: string;
        remotePort?: number;
    };
    response: CompletedResponse | 'aborted';
}

/**
 * An event fired from an individual rule during request processing.
 */
export interface RuleEvent<T = unknown> {
    requestId: string;
    ruleId: string;
    eventType: string;
    eventData: T;
}

/**
 * A mocked endpoint provides methods to see the current state of
 * a mock rule.
 */
export interface MockedEndpoint {
    id: string;

    /**
     * Get the requests that this endpoint has seen so far.
     *
     * This method returns a promise, which resolves with the requests seen
     * up until now, once all ongoing requests have terminated. The returned
     * lists are immutable, so won't change if more requests arrive in future.
     * Call `getSeenRequests` again later to get an updated list.
     *
     * Requests are included here once the response is completed, even if the request
     * itself failed, the responses failed or exceptions are thrown elsewhere. To
     * watch for errors or detailed response info, look at the various server.on(event)
     * methods.
     */
    getSeenRequests(): Promise<CompletedRequest[]>;

    /**
     * Reports whether this endpoint is still pending: if it either hasn't seen the
     * specified number of requests (if one was specified e.g. with .twice())
     * or if it hasn't seen at least one request, by default.
     *
     * This method returns a promise, which resolves with the result once all
     * ongoing requests have terminated.
     */
    isPending(): Promise<boolean>;
}

export interface MockedEndpointData {
    id: string;
    explanation?: string;
    seenRequests: CompletedRequest[];
    isPending: boolean;
}

export interface Explainable {
    explain(): string;
}

export interface ProxyEnvConfig {
    HTTP_PROXY: string;
    HTTPS_PROXY: string;
}

// A slightly weird one: this is necessary because we export types that inherit from EventEmitter,
// so the docs include EventEmitter's methods, which @link to this type, that's otherwise not
// defined in this module. Reexporting the values avoids warnings for that.
export type defaultMaxListeners = typeof EventEmitter.defaultMaxListeners;