import {uuid_v4} from './utility';

const _FWS = WebSocket;

export class FlitterSocketTransaction {
    connection: any;
    type: string;
    id: string;
    resolved = false;
    socket: any;
    outgoing: any;
    incoming: any;
    connectionId: string;
    sent = false;
    received = false;
    // tslint:disable-next-line:variable-name
    _status = 200;
    // tslint:disable-next-line:variable-name
    _message = '';
    endpoint = '';
    json = '';

    constructor(data, conn) {
        this.connection = conn;
        this.type = data.type;
        this.id = data.transaction_id ? data.transaction_id : conn.uuid();
        this.socket = conn.socket;
        this.outgoing = data.outgoing ? data.outgoing : {};
        this.incoming = data.incoming ? data.incoming : {};
        this.connectionId = conn.id;
    }

    resolve() {
        this.resolved = true;
    }

    status(code: number = null) {
        if ( code ) {
            this._status = code;
            return this;
        }

        return this._status;
    }

    message(msg: string = null) {
        if ( msg ) {
            this._message = msg;
            return this;
        }

        return this._message;
    }

    send(data: any = null) {
        if ( this.type === 'response' ) {
            if ( this.resolved ) { throw new Error(`Transaction can only be sent once per request. (ID: ${this.id})`); }

            const obj = {
                status: this._status,
                transaction_id: this.id,
                type: 'response',
                ... (this._message && {message: this._message}),
                ... (data && {data}),
                ... ((!data && this.outgoing) && {data: this.outgoing})
            };

            this.json = JSON.stringify(obj);
            this.resolve();
        } else if ( this.type === 'request' ) {
            if ( this.sent ) { throw new Error(`Request can only be sent once per Transaction. (ID: ${this.id})`); }

            const obj = {
                endpoint: this.endpoint,
                transaction_id: this.id,
                type: 'request',
                ... (data && {data}),
                ... ((!data && this.outgoing) && {data: this.outgoing})
            };

            this.json = JSON.stringify(obj);
        }

        this.sent = true;
        console.log('Sending message...', this.json);
        return this.socket.send(this.json);
    }
}

export class FlitterSocketClientServerTransaction extends FlitterSocketTransaction {
    // tslint:disable-next-line:variable-name
    _handler: any;

    constructor(data, conn) {
        if ( data.data ) { data.outgoing = data.data; }
        super(data, conn);

        this.type = 'request';
        if ( data.endpoint ) { this.endpoint = data.endpoint; }
    }

    handler(fn) {
        if ( !fn ) { return this._handler; }

        this._handler = fn;
        return this;
    }

    receipt(data) {
        this.received = true;
        return this._handler(this, this.socket, data);
    }

}

export class FlitterSocketServerClientTransaction extends FlitterSocketTransaction {
    constructor(data, conn) {
        if ( data.data ) { data.incoming = data.data; }
        super(data, conn);

        this.type = 'response';
        if ( data.endpoint ) { this.endpoint = data.endpoint; }
    }

    set(key, value = null) {
        if ( value && typeof this.outgoing === 'object' ) { this.outgoing[key] = value; } else { this.outgoing = key; }

        return this;
    }
}

export class FlitterSocketConnection {
    url: string;
    open = false;
    activeTransactions: any = {};
    id: string;
    // tslint:disable-next-line:variable-name
    _controller: any = {};
    closeResolves: any[] = [];
    closeCallbacks: any[] = [];
    openResolves: any[] = [];
    openCallbacks: any[] = [];
    socket: any;

    constructor(url) {
        this.url = url;
        this.open = false;
        this.activeTransactions = {};
        this.id = this.uuid();
        this._controller = {};
        this._create_socket();
        this.closeResolves = [];
        this.closeCallbacks = [];
        this.openResolves = [];
        this.openCallbacks = [];
    }

    on_close(callback = false) {
        if ( !callback ) {
            return new Promise(resolve => {
                this.closeResolves.push(resolve);
            });
        } else {
            this.closeCallbacks.push(callback);
        }
    }

    on_open(callback = false) {
        if ( !callback ) {
            return new Promise(resolve => {
                this.openResolves.push(resolve);
            });
        } else {
            this.openCallbacks.push(callback);
        }
    }

    _create_socket() {
        console.log('Connecting to socket:', this.url);
        this.socket = new _FWS(this.url);
        this.socket.onopen = (e) => {
            this.open = true;
            this.openResolves.forEach(r => r(this));
            this.openCallbacks.forEach(c => c(this));
            this.openResolves = [];
            this.openCallbacks = [];
        };
        this.socket.onmessage = this._on_message.bind(this);
        this.socket.onclose = (e) => {
            this.open = false;
            this.closeResolves.forEach(r => r(this));
            this.closeCallbacks.forEach(c => c(this));
            this.closeResolves = [];
            this.closeCallbacks = [];
        };
    }

    _on_message(event) {
        const data = this.validate_message(event.data);
        if ( !data ) { return; }

        if ( data.type === 'response' ) {
            if ( Object.keys(this.activeTransactions).includes(data.transaction_id) ) {
                const t = this.activeTransactions[data.transaction_id];
                t.receipt(data.data);
                if ( t.resolved ) { delete this.activeTransactions[t.id]; }
            } else { throw new Error('Response: transaction ID not found. It\'s possible that the transaction was already resolved.'); }
        } else if ( data.type === 'request' ) {
            const t = new FlitterSocketServerClientTransaction(data, this);
            this.activeTransactions[t.id] = t;

            this._controller[t.endpoint](t, this.socket);
            if ( t.resolved ) { delete this.activeTransactions[t.id]; }
        }
    }

    request(endpoint, data, handler) {
        const t = new FlitterSocketClientServerTransaction({data, endpoint}, this);
        t.handler(handler);

        this.activeTransactions[t.id] = t;
        t.send();
    }

    async asyncRequest(endpoint, data): Promise<[any, any, any]> {
        return new Promise((res, rej) => {
            try {
                this.request(endpoint, data, (...args: any) => {
                    res(args);
                });
            } catch (e: unknown) {
                rej(e);
            }
        });
    }

    validate_message(msg) {
        let fail = false;
        let validTid = true;
        let error = '';
        let code = 400;

        // check if valid JSON
        if ( !this.is_json(msg) ) {
            fail = true;
            error = 'Incoming message must be valid FSP JSON object.';
            validTid = false;
        }

        let data;
        if ( !fail ) { data = JSON.parse(msg); }

        // check for required fields: transaction_id, type
        if ( !fail && !Object.keys(data).includes('transaction_id') ) {
            fail = true;
            error = 'Incoming message must include universally-unique transaction_id.';
            validTid = false;
        }
        if ( !fail && (!Object.keys(data).includes('type') || !(['request', 'response'].includes(data.type))) ) {
            fail = true;
            error = 'Incoming message must include valid type, which may be one of: request, response.';
        }

        // if request, check for required fields: endpoint
        if ( !fail && data.type === 'request' && !Object.keys(data).includes('endpoint') ) {
            fail = true;
            error = 'Incoming request message must include a valid endpoint.';
        }

        // if request, check if transaction_id is unique
        if ( !fail && data.type === 'request' && Object.keys(this.activeTransactions).includes(data.transaction_id) ) {
            fail = true;
            error = 'Incoming request message must have a universally-unique, NON-EXISTENT transaction_id.';
            validTid = false;
        }

        // if request, check for valid endpoint
        if ( !fail && data.type === 'request' && !(typeof this._controller[data.endpoint] === 'function') ) {
            fail = true;
            error = 'The requested endpoint does not exist or is invalid.';
            code = 404;
        }

        // if response, check if transaction_id exists
        if ( !fail && data.type === 'response' && !Object.keys(this.activeTransactions).includes(data.transaction_id)) {
            fail = true;
            error = 'The specified transaction_id does not exist. It\'s possible that this transaction has already resolved.';
        }

        if ( fail ) {
            const sendData = {
                transaction_id: validTid ? data.transaction_id : 'unknown',
                message: error,
                status: code,
                type: 'response'
            };
            this.send_raw(sendData);
        } else { return data; }
    }

    send_raw(data) {
        this.socket.send(JSON.stringify(data));
    }

    uuid() {
        return uuid_v4();
    }

    controller(set: any = false) {
        if ( !set ) { return this._controller; }
        this._controller = set;
        return this;
    }

    is_json(str) {
        try {
            JSON.parse(str);
            return true;
        } catch (e) {
            return false;
        }
    }
}
