import {
    ContractReference,
    HttpMethods,
    KnownTransports,
    ProxyContractConfig,
    ProxyMethodConfig
} 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?: ProxyContractConfig;

    isFrozen: boolean;
    isContractConfigured: boolean;

}

interface CallContext {
    typeContext: TypeContext;
    memberContext: TypeMemberContext;

    transport: ITransport;

    endpoint: string;

    transportConfig: ProxyTransportConfig;

    transportProvider: ITransportProvider;
    interceptorProvider: IInterceptorProvider;
}

export interface ProxyInMemoryTransportConfig {
    type: 'in-memory';
}

export interface ProxyHttpTransportConfig {

    type: 'http';

    defaultHttpMethod: HttpMethods;

}

export interface ProxyElectronTransportConfig {

    type: 'electron';

}

export interface ProxyPipeTransportConfig {

    type: 'pipe';

}

export interface ProxyMessageBusTransportConfig {

    type: 'message-bus';

}

export interface ProxyCustomTransportConfig {

    type: 'custom';

    driver: string;

}

export type ProxyTransportConfig =
    ProxyInMemoryTransportConfig |
    ProxyHttpTransportConfig |
    ProxyElectronTransportConfig |
    ProxyPipeTransportConfig |
    ProxyMessageBusTransportConfig |
    ProxyCustomTransportConfig
;

export interface ProxyChannelConfig {

    transport: ProxyTransportConfig;

    timeoutInMilliseconds?: number;

}

export interface ProxyContext extends IEndpointContext, IDisposable {
    createChannel<T>(
        contract: ContractReference<T>,
        config: ProxyChannelConfig
    ): T;
}

export interface ProxyContextMutator extends ProxyContext {
    defineMember(
        config: ProxyMethodConfig,
        target: Object | Function,
        propertyKey: string | symbol,
        descriptor: TypedPropertyDescriptor<any>
    ): TypedPropertyDescriptor<any>;

    defineContract<TFunction extends Function>(
        config: ProxyContractConfig,
        constructor: TFunction
    ): void | TFunction
}

class ProxyContextImpl implements ProxyContextMutator {

    constructor(
        private contextDomain: string,
        private transportProvider: ITransportProvider,
        private interceptorProvider: IInterceptorProvider
    ) {

    }

    dispose(): Promise<void> {
        return Promise.resolve();
    }

    createChannel<T>(
        contract: ContractReference<T>,
        config: ProxyChannelConfig
    ): T {
        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;

        for (const member of metaData.members) {
            const memberName = member.config.name ?? member.propertyKey.toString();
            const endpoint = `${path}/${memberName}`;

            const callContext: CallContext = {
                memberContext: member,
                typeContext: metaData,

                transport,
                endpoint,

                transportConfig: config.transport,

                interceptorProvider: this.interceptorProvider,
                transportProvider: this.transportProvider
            };

            (target as Record<string | symbol, any>)[member.propertyKey] = Proxy_CallAsyncMethod.bind(callContext);
        }

        metaData.isFrozen = true;

        return target;
    }


    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: ProxyContractConfig,
        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;
    }
}

async function Proxy_TransformResponse(ctx: CallContext, res: IMessage): Promise<any> {
    switch (res.type) {
        case 'error':
            const errorMessage = res as ErrorMessage;
            throw new RpcError(errorMessage.errorMessage);
        case 'result':
            const resultMessage = res as MethodCallResultMessage;
            return resultMessage.value;
        default:
            throw new RpcError('Unknown response found');
    }
}

async function Proxy_TransformRequest(ctx: CallContext, req: IMessage): Promise<IMessage> {
    // this.interceptorProvider.getInterceptor('').
    return req;
}

async function Proxy_CallAsyncMethod(this: CallContext, ...args: any[]): Promise<any> {
    const msg: MethodCallMessage = {

        type: 'call',
        endpoint: this.endpoint,
        args

    };

    const request = await Proxy_TransformRequest(this, msg);

    const response = await this.transport.sendAsync(request);

    return await Proxy_TransformResponse(this, response);
}

export const ProxyContext = new ContextDomain<ProxyContext>(ProxyContextImpl);
