"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = __importDefault(require("axios"));
const models_1 = require("../models");
const types_1 = require("../types");
const rxjs_1 = require("rxjs");
const debug = require('debug')('connection');
const isTest = process.env.NODE_ENV === 'test';
const queryString = require('query-string');
class ConnectionService {
    constructor(state, detectionService, walletNodeFactory, membernodeFactory) {
        this.state = state;
        this.detectionService = detectionService;
        this.walletNodeFactory = walletNodeFactory;
        this.membernodeFactory = membernodeFactory;
        this.block = new rxjs_1.Subject();
        this.events = new rxjs_1.Subject();
        this.broadcast = new rxjs_1.Subject();
    }
    get events$() {
        return this.events.asObservable();
    }
    get block$() {
        return this.block.asObservable();
    }
    get broadcast$() {
        return this.broadcast.asObservable();
    }
    initSockets() {
        return __awaiter(this, void 0, void 0, function* () {
            this.walletSocket = yield this.walletNodeFactory.create(this.state);
            yield this.walletSocket.connect();
            this.memberSocket = yield this.membernodeFactory.create(this.state);
            yield this.memberSocket.connect();
            this.state.socketsInitialised = true;
            debug('both sockets up and running!');
            rxjs_1.merge(this.walletSocket.events$, this.memberSocket.events$).subscribe(event => this.events.next(event));
            this.memberSocket.broadcast$.subscribe(bc => this.broadcast.next(bc));
            this.walletSocket.on('block', e => this.block.next(e));
        });
    }
    detect(endpoint) {
        return __awaiter(this, void 0, void 0, function* () {
            this.state.detectResults = yield this.detectionService.detect(endpoint);
            this.state.detected = true;
            debug('Got autodetect results %s', this.state.detectResults);
        });
    }
    /**
     * Perform a request to the API
     *
     * @static
     * @returns {Promise<any>}
     * @memberof ConnectionService
     */
    send(request, { path, apiType, method }) {
        return __awaiter(this, void 0, void 0, function* () {
            /* Return promise. */
            if (!isTest && !this.state.authed && path !== 'utility/login') {
                throw new Error('You need to login before making a request');
            }
            if (!isTest && !this.state.detected) {
                yield this.detect(this.state.endpoint);
            }
            this.events.next({ type: 'apiRequest', payload: request, schema: { method, path } });
            return new Promise((resolve, reject) => {
                /* Check that this method exists. */
                if (!types_1.HttpMethod[method.toUpperCase()]) {
                    return reject('Unsupported method');
                }
                const node = this.state.detectResults[apiType === types_1.ApiType.MN ? 'mnRest' : 'wnRest'];
                /* Construct the URL. */
                const urlFragments = [
                    `${node.scheme}://`,
                    this.state.endpoint,
                    `:${node.port}`,
                    this.generatePath(path, apiType),
                    ...((method.toUpperCase() === 'GET' && Object.keys(request).length) ? [`?${queryString.stringify(request)}`] : []),
                ];
                const wrappedResolve = resolve => (payload) => {
                    this.events.next({ type: 'apiResponse', payload });
                    resolve(payload);
                };
                /* Make the request. */
                debug(`[request]: ${urlFragments.join('')} ${JSON.stringify(request)}`);
                const opts = {
                    timeout: 1000,
                    headers: this.getHeaders(path, apiType),
                    maxRedirects: 0,
                };
                const args = [
                    urlFragments.join(''),
                    (method.toUpperCase() === 'GET' ? opts : request),
                    ...(method.toUpperCase() === 'POST' ? [opts] : []),
                ];
                axios_1.default[method.toLowerCase()](...args)
                    .then((response) => {
                    this.onData(response, wrappedResolve(resolve), reject);
                })
                    .catch((error) => {
                    this.onError(error, reject);
                });
            });
        });
    }
    /**
     * On data returned from request, try to parse to JSON and resolve
     *
     * @static
     * @returns {void}
     * @memberof ConnectionService
     */
    onData(data, resolve, reject) {
        let json;
        try {
            // Fail if not 2xx.
            if (data.status > 299) {
                reject({
                    Status: 'ERROR',
                    Message: 'API Request failed.',
                    data: {
                        statusCode: data.status,
                        statusMessage: data.statusText,
                    },
                });
            }
        }
        catch (e) {
            return reject({
                Status: 'ERROR',
                Message: 'Internal error failed to connect',
            });
        }
        // Fail if no JSON in body, or erroneous JSON in body.
        try {
            const body = JSON.stringify(data.data);
            json = JSON.parse(body.toString());
            if (json.error) {
                return reject(json.error);
            }
            resolve(json);
        }
        catch (e) {
            return reject({
                Status: 'ERROR',
                Message: 'Failed to parse JSON.',
                data: {
                    error: e,
                    response: data.data,
                },
            });
        }
    }
    onError(error, reject) {
        try {
            if (!error.response) {
                return reject({
                    Status: 'ERROR',
                    Message: 'Failed to connect to provided hostname',
                    data: {
                        error: error.code,
                        response: error.hostname,
                    },
                });
            }
            // Fail if not 2xx.
            if (error.response.status > 299) {
                reject({
                    Status: 'ERROR',
                    Message: 'API Request failed.',
                    data: {
                        statusCode: error.response.status,
                        statusMessage: error.response.statusText,
                    },
                });
            }
        }
        catch (e) {
            reject({
                Status: 'ERROR',
                Message: 'Internal error failed to connect',
            });
        }
    }
    /**
     * Generate the relevant API path
     *
     * @static
     * @returns {string}
     * @memberof ConnectionService
     */
    generatePath(path, apiType) {
        if (apiType === types_1.ApiType.MN) {
            return `${this.state.opts.memberNodePath}${path}`;
        }
        else if (apiType === types_1.ApiType.WN) {
            return `${this.state.opts.walletNodePath}${path}`;
        }
    }
    /**
     * Generate the applicable headers for the request
     *
     * @static
     * @returns {any}
     * @memberof ConnectionService
     */
    getHeaders(path, apiType) {
        let headers = {
            'Content-Type': 'application/json; charset=utf-8',
        };
        if (path !== models_1.apiConfig.token.path && this.state.token) {
            if (apiType === types_1.ApiType.MN) {
                headers = {
                    'Content-Type': 'application/json; charset=utf-8',
                    Authorization: this.state.token.toString(),
                };
            }
            else if (types_1.ApiType.WN) {
                headers = {
                    'Content-Type': 'application/json; charset=utf-8',
                    Authorization: `Bearer ${this.state.token.toString()}`,
                };
            }
        }
        return headers;
    }
}
exports.ConnectionService = ConnectionService;
