import type { BrokerBroadcastGuarantee, BrokerBroadcastNoGuarantee } from "../message-broker/broadcast.js";
import type {
    BrokerContract,
    BrokerContractDeliveryFulfilled,
    InferBrokerContract,
} from "../message-broker/contract.js";
import type { BrokerPointToPointGuarantee, BrokerPointToPointNoGuarantee } from "../message-broker/point-to-point.js";
import type { BrokerRequestReply } from "../message-broker/request.js";
import type { Contract, ContractVersion, InferContract } from "../webserver/contract.js";
import type { IValidationError } from "../webserver/http-response.js";
import type { EHttpStatusCodeClientError, EHttpStatusCodeServerError } from "../webserver/http-status-code.js";
import type { IRouteLocationInterface } from "../webserver/route.js";

const ClientHandlingErrorStatusCode = -1;
const DefaultErrorStatusCode = "default";

enum ClientHandlingErrorReason {
    Abort = -1,
    Timeout = -2,
    Network = -3,
    ParseError = -4,
    RetryLimitReached = -5,
}

type ExtractContractsFromMapFnContract = Contract<any, any, any, any>;
type ExtractContractsFromMapFn = (...args: any[]) => ExtractContractsFromMapFnContract;

/**
 * From routes, get all types which are of type `Contract`
 */
type ExtractContractsFromMap<Routes> = {
    [K in keyof Routes as Routes[K] extends ExtractContractsFromMapFn
        ? K
        : Routes[K] extends ExtractContractsFromMapFnContract
          ? K
          : never]: Routes[K] extends ExtractContractsFromMapFn
        ? InferContract<Routes[K]>
        : Routes[K] extends ExtractContractsFromMapFnContract
          ? Routes[K]
          : never;
};

/**
 * From routes, get all types which are of type `BrokerContract`
 */
type ExtractBrokerContractsFromMap<Routes> = {
    [K in keyof Routes as Routes[K] extends (...args: any[]) => BrokerContract<any, any>
        ? K
        : never]: Routes[K] extends (...args: any[]) => BrokerContract<any, any>
        ? InferBrokerContract<Routes[K]>
        : never;
};

// Helpers
type Values<T> = T[keyof T];
type ContractRecord = Record<any, Contract<IRouteLocationInterface>>;
type BrokerContractRecord = Record<any, BrokerContract<any, any>>;
type StripSlash<S> = S extends `/${infer R}` ? R : S;
type SlashesToDots<S extends string | unknown> = S extends `${infer Head}/${infer Tail}`
    ? `${Head}.${SlashesToDots<Tail>}`
    : S;
type ToCamelCase<S extends string | unknown> = S extends `${infer Head}-${infer Tail}`
    ? `${Lowercase<Head>}${Capitalize<ToCamelCase<Tail>>}`
    : Lowercase<S & string>;

// Type to convert a dotted key to a nested object type
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type PathToObject<Path extends string, V> = Path extends `${infer H}.${infer R}`
    ? { [K in H]: PathToObject<R, V> }
    : { [K in Path]: V };
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
type DotRecordToNested<R extends Record<string, any>> = Prettify<
    UnionToIntersection<{ [K in Extract<keyof R, string>]: PathToObject<K, R[K]> }[Extract<keyof R, string>]>
>;

// All path keys (without the leading "/", slashes to dots and camel case)
type Paths<T extends ContractRecord> = StripSlash<Values<T> extends { route: { path: infer P } } ? P : never>;

// All available versions
type ContractVersions<T extends ContractRecord> = Values<T> extends { versions: infer V } ? V : never;

// For a given path P, what lowercased methods exist there?
type MethodsForPath<T extends ContractRecord, P extends Paths<T>> = Lowercase<
    (Extract<Values<T>, { route: { path: `/${P & string}` } }> extends {
        route: { method: infer M };
    }
        ? M
        : never) &
        string
>;

// For (path P, method M), what is the specific contract?
type ContractFor<T extends ContractRecord, P extends Paths<T>, M extends MethodsForPath<T, P>> = Extract<
    Values<T>,
    { route: { path: `/${P & string}`; method: Uppercase<M> } }
>;

type Domains<T extends BrokerContractRecord> = Values<T> extends { domain: infer S } ? S : never;
type SubjectsForDomain<T extends BrokerContractRecord, Domain extends Domains<T>> =
    Extract<Values<T>, { domain: Domain }> extends {
        domain: Domain;
        subject: infer S;
    }
        ? S
        : never;

// For a given subject, what is the specific contract?
type BrokerContractFor<
    T extends BrokerContractRecord,
    Domain extends Domains<T>,
    Subject extends SubjectsForDomain<T, Domain>,
> = Extract<Values<T>, { domain: Domain; subject: Subject }>;

// Convert a contract to a closure which accepts the request, params and headers as parameters
// and returns the response.
type ContractToClosure<
    ThisContract extends Contract<any, any, any, any>,
    ClientOptions extends Record<string, any>,
    ClientResponse extends Record<string, any> = Record<string, unknown>,
    Request = ThisContract["types"]["request"],
    Params = ThisContract["types"]["params"],
    Headers = ThisContract["types"]["headers"],
    // Request should only be required if the contract has a request body
    RequestOptionalOrRequired = Request extends { [key: string]: any }
        ? {
              request: Request;
          }
        : { request?: Request },
    // Parameters should only be required if the contract has one required parameter
    ParamsOptionalOrRequired = Params extends { [key: string]: any }
        ? {
              params: Params;
          }
        : { params?: Params },
    Responses = {
        [StatusCode in keyof ThisContract["types"]["response"]]: ThisContract["types"]["response"][StatusCode] extends {
            response: infer R;
        }
            ? {
                  status: StatusCode;
                  response: R;
              } & ClientResponse
            : never;
    },
> = (
    payload: {
        // Headers are always optional
        headers?: Headers;
    } & ParamsOptionalOrRequired &
        RequestOptionalOrRequired &
        ClientOptions,
) => Promise<
    // When using `ContractFromOpenApiPath` we will have a `default` response, so we need to handle it differently
    | (Responses extends { default: { response: infer DefaultResponse } }
          ? {
                status: typeof DefaultErrorStatusCode;
                code: number;
                response: DefaultResponse;
            } & ClientResponse
          : // Default error response for entities which could not be parsed
            | ContractDefaultResponseIfNotGiven<
                      Responses,
                      EHttpStatusCodeClientError.UnprocessableEntity,
                      IValidationError[],
                      ClientResponse
                  >
                // Default error response when the server throws an unexpected error
                | ContractDefaultResponseIfNotGiven<
                      Responses,
                      EHttpStatusCodeServerError.InternalServerError,
                      IValidationError[],
                      ClientResponse
                  >)
    | ({
          status: typeof ClientHandlingErrorStatusCode;
          response: never;
          reason: ClientHandlingErrorReason;
          error: Error;
      } & ClientResponse)
    | Values<Omit<Responses, "default">>
>;

/**
 * Dead Letter Queue message data structure.
 * This is attached to messages when subscribing to the DLQ.
 *
 */
type BrokerSubscribeMessageDlqData = {
    /**
     * The original subject where the message was published before it failed.
     */
    readonly originalSubject: string;
    /**
     * Error information from when the message processing failed.
     * This is a serialized representation of the error, not an Error instance.
     * See type documentation for why we use a plain object instead of Error.
     */
    readonly error: {
        /**
         * Error message (equivalent to Error.message).
         */
        readonly message: string;
        /**
         * Error stack trace (equivalent to Error.stack).
         * May be an empty string if the original error had no stack.
         */
        readonly stack: string;
        /**
         * Error name/type (equivalent to Error.name, e.g., "Error", "TypeError", "ReferenceError").
         */
        readonly name: string;
    };
    /**
     * ISO timestamp when the message was sent to the DLQ.
     */
    readonly timestamp: string;
};

type BrokerSubscribeOptionsWithPurpose<C extends BrokerContract<any, any>> = C["types"]["retention"] extends "workqueue"
    ? unknown
    : {
          /**
           * The purpose of a subscription.
           * It explains the reason for the subscription and enables distinct processing paths.
           * For queue groups, different purposes create separate queue groups, allowing multiple use cases to receive all messages independently.
           * Best practice is to name it in present tense, e.g. "create-order", "add-order-to-queue", "process-order", "send-email", etc.
           */
          purpose: string;
      };

type BrokerSubscribeOptions<
    Contract extends BrokerContract<any, any>,
    Delivery extends BrokerContractDeliveryFulfilled = Contract["delivery"],
    MessageParameter = Contract["message"] &
        (Contract["types"]["ttlMsSeenKey"] extends number ? { ttlMsSeenKey: number } : unknown),
> =
    // Common parameters
    Pick<Contract["types"]["subject"], "params"> & // Pick the correct options based on the delivery type
        (Delivery extends BrokerPointToPointNoGuarantee | BrokerBroadcastNoGuarantee | BrokerBroadcastGuarantee
            ? {
                  listener: (message: MessageParameter) => Promise<void>;
              } & BrokerSubscribeOptionsWithPurpose<Contract>
            : Delivery extends BrokerPointToPointGuarantee
              ?
                    | ({
                          listener: (message: MessageParameter) => Promise<void>;
                      } & BrokerSubscribeOptionsWithPurpose<Contract>)
                    | ({
                          listenToDeadLetterQueue: (
                              message: MessageParameter & { dlq: BrokerSubscribeMessageDlqData },
                          ) => Promise<void>;
                      } & BrokerSubscribeOptionsWithPurpose<Contract>)
              : Delivery extends BrokerRequestReply
                ? {
                      listener: (message: MessageParameter) => Promise<Contract["types"]["response"]>;
                  }
                : never);

type BrokerPublishOptions<
    Contract extends BrokerContract<any, any>,
    Delivery extends BrokerContractDeliveryFulfilled = Contract["delivery"],
> =
    // Common parameters
    Pick<Contract["types"]["subject"], "params"> & // Pick the correct options based on the delivery type
        (Delivery extends
            | BrokerBroadcastNoGuarantee
            | BrokerBroadcastGuarantee
            | BrokerPointToPointNoGuarantee
            | BrokerPointToPointGuarantee
            | BrokerRequestReply
            ? Contract["message"]
            : never);

// Publish and subscribe are the same, but we use different names to make it more clear about the request+reply pattern
interface BrokerContractToInterface<
    Contracts extends BrokerContractRecord,
    SubscribeContracts extends BrokerContractRecord = {
        [P in keyof Contracts as Contracts[P]["delivery"] extends BrokerRequestReply ? never : P]: Contracts[P];
    },
    RequestContracts extends BrokerContractRecord = {
        [P in keyof Contracts as Contracts[P]["delivery"] extends BrokerRequestReply ? P : never]: Contracts[P];
    },
> {
    subscribe<K extends keyof SubscribeContracts, Contract extends SubscribeContracts[K]>(
        subject: K,
        opts: BrokerSubscribeOptions<Contract>,
    ): Promise<{ unsubscribe: () => Promise<void> }>;
    reply<K extends keyof RequestContracts, Contract extends RequestContracts[K]>(
        subject: K,
        opts: BrokerSubscribeOptions<Contract>,
    ): Promise<{ unsubscribe: () => Promise<void> }>;
    publish<K extends keyof SubscribeContracts, Contract extends SubscribeContracts[K]>(
        subject: K,
        opts: BrokerPublishOptions<Contract>,
    ): Promise<void>;
    request<K extends keyof RequestContracts, Contract extends RequestContracts[K]>(
        subject: K,
        opts: BrokerPublishOptions<Contract>,
    ): Promise<{
        data: Contract["delivery"] extends BrokerRequestReply ? Contract["types"]["response"] : never;
    }>;
}

type ContractDefaultResponseIfNotGiven<
    Responses extends Record<any, any>,
    StatusCode,
    Response,
    BesideResponse,
> = Responses[StatusCode] extends {
    response: any;
}
    ? never
    : {
          status: StatusCode;
          response: Response;
      } & BesideResponse;

type ElementType<T> = T extends ContractVersion[] ? T[number] : never;
type ContractVersionPicker<Routes> = ElementType<ContractVersions<ExtractContractsFromMap<Routes>>>;

type ContractsRPCClient<
    Routes,
    Version extends ContractVersion,
    ClientOptions extends Record<string, any> = Record<string, unknown>,
    ClientResponse extends Record<string, any> = Record<string, unknown>,
    Contracts extends ContractRecord = ExtractContractsFromMap<Routes>,
    /**
     * First, we have a flat record of all contracts, like this:
     *
     * ```
     * {
     *     "example": { get: () => Promise, post: () => Promise },
     *     "user": { delete: () => Promise },
     *     "support/response-message": { get: () => Promise, post: () => Promise, delete: () => Promise },
     *     "support/response-messages": { get: () => Promise },
     * }
     * ```
     */
    ContractsFlat = {
        [Path in Paths<Contracts> & string]: {
            [M in MethodsForPath<Contracts, Path>]: Version extends ContractFor<Contracts, Path, M>["versions"][number]
                ? ContractToClosure<ContractFor<Contracts, Path, M>, ClientOptions, ClientResponse>
                : never;
        };
    },
    /**
     * Then, we convert it to a nested record, like this:
     *
     * ```
     * {
     *     example: { get: () => Promise, post: () => Promise },
     *     user: { delete: () => Promise },
     *     support: {
     *         responseMessage: {
     *             get: () => Promise,
     *             post: () => Promise,
     *             delete: () => Promise,
     *         },
     *         responseMessages: { get: () => Promise },
     *     },
     * }
     * ```
     */
    ContractsNested = DotRecordToNested<{
        [Path in keyof ContractsFlat as ContractsFlat[Path] extends { [key: string]: any }
            ? ToCamelCase<SlashesToDots<Path>>
            : never]: ContractsFlat[Path];
    }>,
> = ContractsNested;

type BrokerContractsRPCClient<
    Events,
    Contracts extends BrokerContractRecord = ExtractBrokerContractsFromMap<Events>,
    /**
     * For a message broker, we have a flat record of all contract domains, like this:
     *
     * ```
     * {
     *     "website": { publish: () => Promise, subscribe: () => Promise, request: () => Promise, reply: () => Promise },
     *     "website": { ... },
     *     "website": { ... },
     *     "orders": { ... },
     * }
     * ```
     */
    ContractsFlat = {
        [Domain in Domains<Contracts> & string]: BrokerContractToInterface<{
            [Subject in SubjectsForDomain<Contracts, Domain> &
                string as `${Subject}.${BrokerContractFor<Contracts, Domain, Subject>["majorVersion"]}`]: BrokerContractFor<
                Contracts,
                Domain,
                Subject
            >;
        }>;
    },
> = ContractsFlat;

export type {
    ExtractContractsFromMap,
    ExtractBrokerContractsFromMap,
    ContractsRPCClient,
    BrokerContractsRPCClient,
    ContractToClosure,
    BrokerSubscribeOptions,
    BrokerPublishOptions,
    BrokerContractToInterface,
    BrokerSubscribeMessageDlqData,
    ContractVersionPicker,
    ContractVersions,
};
export { ClientHandlingErrorStatusCode, ClientHandlingErrorReason, DefaultErrorStatusCode };
