import { AWSError } from 'aws-sdk';
import Aws from 'aws-sdk/clients/all';
import { NextToken } from 'aws-sdk/clients/cloudformation';
import { CredentialsOptions } from 'aws-sdk/lib/credentials';
import { PromiseResult } from 'aws-sdk/lib/request';
import { Service, ServiceConfigurationOptions } from 'aws-sdk/lib/service';
import { EventEmitter } from 'events';
import { builder, IBuilder } from '@org-formation/tombok';
import { Exclude, Expose } from 'class-transformer';

import {
    BaseDto,
    BaseResourceHandlerRequest,
    BaseModel,
    Constructor,
    Dict,
    HandlerErrorCode,
    OperationStatus,
    OverloadedArguments,
    ServiceProperties,
} from './interface';

type ClientMap = typeof Aws;
export type ClientName = keyof ClientMap;
export type Client = InstanceType<ClientMap[ClientName]>;

export type Result<T> = T extends (...args: any) => infer R ? R : any;
export type Input<T> = T extends (...args: infer P) => any ? P : never;
export type ServiceOptions<S extends Service = Service> = ConstructorParameters<
    Constructor<S>
>[0];
export type ServiceOperation<
    S extends Service = Service,
    C extends Constructor<S> = Constructor<S>,
    O extends ServiceProperties<S, C> = ServiceProperties<S, C>,
    E extends Error = AWSError
> = InstanceType<C>[O] & {
    promise(): Promise<PromiseResult<any, E>>;
};
export type InferredResult<
    S extends Service = Service,
    C extends Constructor<S> = Constructor<S>,
    O extends ServiceProperties<S, C> = ServiceProperties<S, C>,
    E extends Error = AWSError,
    N extends ServiceOperation<S, C, O, E> = ServiceOperation<S, C, O, E>
> = Input<Input<Result<Result<N>['promise']>['then']>[0]>[0];

type AwsTaskSignature = <
    S extends Service = Service,
    C extends Constructor<S> = Constructor<S>,
    O extends ServiceProperties<S, C> = ServiceProperties<S, C>,
    E extends Error = AWSError,
    N extends ServiceOperation<S, C, O, E> = ServiceOperation<S, C, O, E>
>(
    params: any
) => Promise<InferredResult<S, C, O, E, N>>;

/**
 * Promise final result Type from a AWS Service Function
 *
 * @param S Type of the AWS Service
 * @param C Type of the constructor function of the AWS Service
 * @param O Names of the operations (method) within the service
 * @param E Type of the error thrown by the service function
 * @param N Type of the service function inferred by the given operation name
 */
export type ExtendedClient<S extends Service = Service> = S & {
    serviceIdentifier?: string;
    makeRequestPromise?: <
        C extends Constructor<S> = Constructor<S>,
        O extends ServiceProperties<S, C> = ServiceProperties<S, C>,
        E extends Error = AWSError,
        N extends ServiceOperation<S, C, O, E> = ServiceOperation<S, C, O, E>
    >(
        operation: O,
        input?: OverloadedArguments<N>,
        headers?: Record<string, string>
    ) => Promise<InferredResult<S, C, O, E, N>>;
};
export interface AwsTaskWorkerPool extends EventEmitter {
    runAwsTask: AwsTaskSignature;
    shutdown: (doDestroy?: boolean) => Promise<boolean>;
    completed?: number;
    duration?: number;
}
export interface Session {
    client: <S extends Service>(
        service: ClientName | S | Constructor<S>,
        options?: ServiceConfigurationOptions
    ) => ExtendedClient<S>;
}

export class SessionProxy implements Session {
    constructor(private options: ServiceConfigurationOptions) {}

    private extendAwsClient<
        S extends Service = Service,
        C extends Constructor<S> = Constructor<S>,
        O extends ServiceProperties<S, C> = ServiceProperties<S, C>,
        E extends Error = AWSError,
        N extends ServiceOperation<S, C, O, E> = ServiceOperation<S, C, O, E>
    >(
        service: S,
        options?: ServiceConfigurationOptions,
        workerPool?: AwsTaskWorkerPool
    ): ExtendedClient<S> {
        const client: ExtendedClient<S> = new Proxy(service, {
            get(obj: ExtendedClient<S>, prop: string) {
                if ('makeRequestPromise' === prop) {
                    // Extend AWS client with promisified make request method
                    // that can be used with worker pool
                    return async (
                        operation: O,
                        input?: OverloadedArguments<N>,
                        headers?: Record<string, string>
                    ): Promise<InferredResult<S, C, O, E, N>> => {
                        if (workerPool && workerPool.runAwsTask) {
                            try {
                                const result = await workerPool.runAwsTask<
                                    S,
                                    C,
                                    O,
                                    E,
                                    N
                                >({
                                    name: obj.serviceIdentifier,
                                    options,
                                    operation,
                                    input,
                                    headers,
                                });
                                return result;
                            } catch (err) {
                                console.log(err);
                            }
                        }
                        const request = obj.makeRequest(operation as string, input);
                        const headerEntries = Object.entries(headers || {});
                        if (headerEntries.length) {
                            request.on('build', () => {
                                for (const [key, value] of headerEntries) {
                                    request.httpRequest.headers[key] = value;
                                }
                            });
                        }
                        return await request.promise();
                    };
                }
                return obj[prop];
            },
        });
        if (client.config && client.config.update) {
            client.config.update(options);
        }
        return client;
    }

    public client<S extends Service = Service>(
        service: ClientName | S | Constructor<S>,
        options?: ServiceConfigurationOptions,
        workerPool?: AwsTaskWorkerPool
    ): ExtendedClient<S> {
        const updatedConfig = { ...this.options, ...options };
        let ctor: Constructor<S>;
        let client: ExtendedClient<S>;
        if (typeof service === 'string') {
            // Kept for backward compatibility
            const clients: { [K in ClientName]: ClientMap[K] } = Aws;
            ctor = (clients[service] as unknown) as Constructor<S>;
        } else if (typeof service === 'function') {
            ctor = service as Constructor<S>;
        } else {
            client = this.extendAwsClient(service, updatedConfig, workerPool);
        }
        if (!client) {
            client = this.extendAwsClient(
                new ctor(updatedConfig),
                updatedConfig,
                workerPool
            );
        }
        return client;
    }

    get configuration(): ServiceConfigurationOptions {
        return this.options;
    }

    public static getSession(
        credentials?: CredentialsOptions,
        region?: string
    ): SessionProxy | null {
        if (!credentials) {
            return null;
        }
        return new SessionProxy({
            credentials,
            region,
        });
    }
}

@builder
export class ProgressEvent<
    ResourceT extends BaseModel = BaseModel,
    CallbackT = Dict
> extends BaseDto {
    /**
     * The status indicates whether the handler has reached a terminal state or is
     * still computing and requires more time to complete
     */
    @Expose() status: OperationStatus;

    /**
     * If OperationStatus is FAILED or IN_PROGRESS, an error code should be provided
     */
    @Expose() errorCode?: HandlerErrorCode;

    /**
     * The handler can (and should) specify a contextual information message which
     * can be shown to callers to indicate the nature of a progress transition or
     * callback delay; for example a message indicating "propagating to edge"
     */
    @Expose() message = '';

    /**
     * The callback context is an arbitrary datum which the handler can return in an
     * IN_PROGRESS event to allow the passing through of additional state or
     * metadata between subsequent retries; for example to pass through a Resource
     * identifier which can be used to continue polling for stabilization
     */
    @Expose() callbackContext?: CallbackT;

    /**
     * A callback will be scheduled with an initial delay of no less than the number
     * of seconds specified in the progress event.
     */
    @Expose() callbackDelaySeconds = 0;

    /**
     * The output resource instance populated by a READ for synchronous results and
     * by CREATE/UPDATE/DELETE for final response validation/confirmation
     */
    @Expose() resourceModel?: ResourceT;

    /**
     * The output resource instances populated by a LIST for synchronous results
     */
    @Expose() resourceModels?: Array<ResourceT>;

    /**
     * The token used to request additional pages of resources for a LIST operation
     */
    @Expose() nextToken?: NextToken;

    constructor(partial?: Partial<ProgressEvent>) {
        super();
        if (partial) {
            Object.assign(this, partial);
        }
    }

    // TODO: remove workaround when decorator mutation implemented: https://github.com/microsoft/TypeScript/issues/4881
    @Exclude()
    public static builder<T extends ProgressEvent>(template?: Partial<T>): IBuilder<T> {
        /* istanbul ignore next */
        return null;
    }

    /**
     * Convenience method for constructing FAILED response
     */
    @Exclude()
    public static failed<T extends ProgressEvent>(
        errorCode: HandlerErrorCode,
        message: string
    ): T {
        const event = ProgressEvent.builder<T>()
            .status(OperationStatus.Failed)
            .errorCode(errorCode)
            .message(message)
            .build();
        return event;
    }

    /**
     * Convenience method for constructing IN_PROGRESS response
     */
    @Exclude()
    public static progress<T extends ProgressEvent>(model?: any, ctx?: any): T {
        const progress = ProgressEvent.builder<T>().status(OperationStatus.InProgress);
        if (ctx) {
            progress.callbackContext(ctx);
        }
        if (model) {
            progress.resourceModel(model);
        }
        const event = progress.build();
        return event;
    }

    /**
     * Convenience method for constructing a SUCCESS response
     */
    @Exclude()
    public static success<T extends ProgressEvent>(model?: any, ctx?: any): T {
        const event = ProgressEvent.progress<T>(model, ctx);
        event.status = OperationStatus.Success;
        return event;
    }
}

/**
 * This interface describes the request object for the provisioning request
 * passed to the implementor. It is transformed from an instance of
 * HandlerRequest by the LambdaWrapper to only items of concern
 *
 * @param <T> Type of resource model being provisioned
 */
export class ResourceHandlerRequest<
    T extends BaseModel
> extends BaseResourceHandlerRequest<T> {}