import {
    ContractReference,
    HttpMethods,
    KnownTransports,
    ProxyContractConfig,
    ProxyMethodConfig,
    StubContractConfig
} from "../types";
import { ContextDomain } from "./ContextDomain";
import { IEndpointContext } from "./IEndpointContext";
import { RpcError } from "errors";
import { ITransportProvider } from "./ITransportProvider";
import { IInterceptorProvider } from "./IInterceptorProvider";
import { ErrorMessage, IMessage, MethodCallMessage, MethodCallResultMessage } from "./IMessage";

import "reflect-metadata";
import { ITransport } from "./ITransport";
import { IDisposable } from "./IDisposable";


interface TypeMemberContext {

    propertyKey: string | symbol;

    descriptor: TypedPropertyDescriptor<any>;

    config: ProxyMethodConfig;

}

interface TypeContext {

    constructor: Function;

    members: TypeMemberContext[];

    config?: StubContractConfig;

    isFrozen: boolean;
    isContractConfigured: boolean;

}

interface CallContext {
    typeContext: TypeContext;
    memberContext: TypeMemberContext;

    transport: ITransport;

    endpoint: string;
    target: any;
    fn: Function;

    transportConfig: StubTransportConfig;

    transportProvider: ITransportProvider;
    interceptorProvider: IInterceptorProvider;
}

export interface StubInMemoryTransportConfig {
    type: 'in-memory';
}

export interface StubHttpTransportConfig {

    type: 'http';

    defaultHttpMethod: HttpMethods;

}

export interface StubElectronTransportConfig {

    type: 'electron';

}

export interface StubPipeTransportConfig {

    type: 'pipe';

}

export interface StubMessageBusTransportConfig {

    type: 'message-bus';

}

export interface StubCustomTransportConfig {

    type: 'custom';

    driver: string;

}

export type StubTransportConfig =
    StubInMemoryTransportConfig |
    StubHttpTransportConfig |
    StubElectronTransportConfig |
    StubPipeTransportConfig |
    StubMessageBusTransportConfig |
    StubCustomTransportConfig
;

export interface StubConfig {

    transport: StubTransportConfig;

    timeoutInMilliseconds?: number;

}

export interface StubContext extends IEndpointContext, IDisposable {
    addStub<T>(
        contract: ContractReference<T>,
        config?: StubConfig
    ): IDisposable;


}

export interface StubContextMutator extends StubContext {
    defineMember(
        config: ProxyMethodConfig,
        target: Object | Function,
        propertyKey: string | symbol,
        descriptor: TypedPropertyDescriptor<any>
    ): TypedPropertyDescriptor<any>;

    defineContract<TFunction extends Function>(
        config: StubContractConfig,
        constructor: TFunction
    ): void | TFunction;
}

export class StubContextImpl implements StubContextMutator {

    private isHandlingReceivedMessages: boolean = false;
    private endpoints: Record<string, CallContext>;

    constructor(
        private contextDomain: string,
        private transportProvider: ITransportProvider,
        private interceptorProvider: IInterceptorProvider
    ) {
        this.endpoints = {};

        this.onMessageReceived = this.onMessageReceived.bind(this);
    }

    dispose(): Promise<void> {
        throw new Error("Method not implemented.");
    }

    addStub<T>(
        contract: ContractReference<T>,
        config?: StubConfig
    ): IDisposable {
        if (!config) {
            throw new RpcError('Configuration is required.');
        }

        if (typeof config.transport === 'undefined') {
            throw new RpcError('A transport must be configured.');
        }

        const name = typeof contract === 'string'
            ? contract
            : contract.name;

        const metaData = Reflect.getOwnMetadata(name, this) as TypeContext;

        if (!metaData.isContractConfigured) {
            throw new RpcError('Contract was not found.');
        }

        const transport = this.transportProvider.getTransport(config.transport.type);

        const contractConfig = metaData.config!;
        const target = (new (metaData.constructor as any)) as T;

        const path = contractConfig.path;

        const endpoints: Record<string, CallContext> = {};
        for (const member of metaData.members) {
            const memberName = member.config.name ?? member.propertyKey.toString();
            const endpoint = `${path}/${memberName}`;

            if (this.endpoints[endpoint]) {
                throw new RpcError(`Duplicate endpoint '${endpoint}' found`);
            }

            const callContext: CallContext = {
                memberContext: member,
                typeContext: metaData,

                transport,
                endpoint,
                target,
                fn: undefined!,

                transportConfig: config.transport,

                interceptorProvider: this.interceptorProvider,
                transportProvider: this.transportProvider
            };

            callContext.fn = Stub_CallAsyncMethod.bind(callContext);

            endpoints[endpoint] = callContext;
        }

        for (const key in endpoints) {
            if (Object.prototype.hasOwnProperty.call(endpoints, key)) {
                const element = endpoints[key];
                this.endpoints[key] = element;
            }
        }

        metaData.isFrozen = true;

        if (!this.isHandlingReceivedMessages) {
            transport.subscribe(this.onMessageReceived);
            this.isHandlingReceivedMessages = true;
        }

        return {
            dispose: async () => {
                
            }
        };
    }

    private async onMessageReceived(msg: IMessage): Promise<IMessage> {
        switch (msg.type) {
            case 'call':
                const callMessage = msg as MethodCallMessage;

                const endpointCtx = this.findEndpoint(callMessage.endpoint);

                if (!endpointCtx) {
                    return {

                        type: 'error',
                        errorCode: 404,
                        errorMessage: 'Endpoint was not found'

                    } as ErrorMessage;
                } else {
                    
                    let result: any;
                    let isError = false;
                    let errorMessage;

                    try {
                        result = await endpointCtx.fn(...callMessage.args);
                    } catch (e: any) {
                        isError = true;
                        errorMessage = e?.toString();
                    }

                    const responseMessage: IMessage = isError
                        ? {
                            type: "error",
                            errorCode: 500,
                            errorMessage
                        } as ErrorMessage
                        : {
                            type: "result",
                            value: result
                        } as MethodCallResultMessage;

                    return responseMessage;
                }
            default: return {
                type: 'error',
                errorCode: 400,
                errorMessage: 'Unknown message type'
            } as ErrorMessage
        }
    }

    public defineMember(
        config: ProxyMethodConfig,
        target: Object | Function,
        propertyKey: string | symbol,
        descriptor: TypedPropertyDescriptor<any>
    ): TypedPropertyDescriptor<any> {

        const ctx = this.getOwnTypeContext(target);

        if (ctx.isFrozen) {
            throw new RpcError('The new prototype has already been applied to the original prototype.');
        }

        ctx.members.push({
            propertyKey,
            descriptor,
            config
        });

        return descriptor;
    }

    public defineContract<TFunction extends Function>(
        config: StubContractConfig,
        constructor: TFunction
    ): void | TFunction {

        const name = constructor.name;

        let ctx = Reflect.getOwnMetadata(name, this) as TypeContext;

        if (!!ctx) {
            ctx.config = config;
            ctx.isContractConfigured = true;
            ctx.constructor = constructor;
        } else {
            Reflect.defineMetadata(name, {
                isFrozen: false,
                isContractConfigured: true,
                config,
                constructor,
                members: []
            } as TypeContext, this);
        }

        return constructor;
    }

    private getOwnTypeContext(
        target: Object | Function
    ): TypeContext {
        const prototype = target.constructor ?? Object.getPrototypeOf(target);
        const name = prototype.name;

        let ctx = Reflect.getOwnMetadata(name, this) as TypeContext;

        if (typeof ctx === 'undefined') {
            ctx = {
                isFrozen: false,
                isContractConfigured: false,
                members: []
            };

            Reflect.defineMetadata(name, ctx, this);
        }

        return ctx;
    }

    private findEndpoint(path: string): CallContext {
        const endpoint = this.endpoints[path] as CallContext;

        return endpoint;
    }
}

export const StubContext = new ContextDomain<StubContext>(StubContextImpl);

async function Stub_CallAsyncMethod(this: CallContext, ...args: any[]): Promise<any> {
    const targetFunction = this.target[this.memberContext.propertyKey].bind(this.target);

    const result = await targetFunction(...args);

    return result;
}
