/**************************************************************************************************
 *
 * ADOBE SYSTEMS INCORPORATED
 * Copyright 2015 Adobe Systems Incorporated
 * All Rights Reserved.
 *
 * NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the
 * terms of the Adobe license agreement accompanying it.  If you have received this file from a
 * source other than Adobe, then your use, modification, or distribution of it requires the prior
 * written permission of Adobe.
 *
 **************************************************************************************************/

module.exports.init = function () {
    'use strict';
    const exports = {};
    const child_process = require('child_process');
    const http = require('http');
    const https = require('https');
    const URL = require('url');
    const fs = require('fs');
    const utils = require('./utils');
    const proxyResolverMac = require ('../build/Release/ProxyResolverMac.node');
    let responseCache = {};

    let handleProxyViaURL;

    function parseProxy(proxy, type) {
        const p = utils.stripNonExistant({
            protocol: type,
            hostname: proxy.kCFProxyHostNameKey,
            port: proxy.kCFProxyPortNumberKey,
            username: proxy.kCFProxyUsernameKey,
            password: proxy.kCFProxyPasswordKey,
            config: proxy.kCFProxyAutoConfigurationURLKey,
            raw: proxy
        });
        if (p.username) {
            p.auth = p.username + ':' + p.password;
        }
        return p;
    }

    function parseProxyResponse(url, proxies, callback) {
        let data;
        try {
            data = JSON.parse(proxies);
        } catch (e) {
            return callback('Unable to parse native response: ' + proxies);
        }
        if (!Array.isArray(data)) {
            return callback('Native response not an array: ' + proxies);
        }
        if (data.length === 0) {
            return callback(null, [ {
                protocol: 'none'
            } ]);
        }

        const responseCollector = new utils.ResponseCollector(data.length, callback);
        data.forEach(function (proxy, i) {
            if (!proxy) {
                responseCollector.collect(i, 'No Proxy Response');
                return;
            }
            switch (proxy.kCFProxyTypeKey) {
            case 'kCFProxyTypeNone':
                responseCollector.collect(i, null, {
                    protocol: 'none',
                    raw: proxy
                });
                break;
            case 'kCFProxyTypeAutoConfigurationURL':
                //Handle Proxy via URL
                (function (i) {
                    handleProxyViaURL(url, proxy.kCFProxyAutoConfigurationURLKey, function (errors, responses) {
                        responseCollector.collect(i, null, {
                            protocol: 'script',
                            errors,
                            responses,
                            raw: proxy
                        });
                    });
                }(i));
                break;
            case 'kCFProxyTypeHTTP':
                responseCollector.collect(i, null, parseProxy(proxy, 'http'));
                break;
            case 'kCFProxyTypeHTTPS':
                responseCollector.collect(i, null, parseProxy(proxy, 'https'));
                break;
            case 'kCFProxyTypeSOCKS':
                responseCollector.collect(i, null, parseProxy(proxy, 'socks'));
                break;
            case 'kCFProxyTypeFTP':
                responseCollector.collect(i, null, parseProxy(proxy, 'ftp'));
                break;
            case 'kCFProxyTypeAutoConfigurationJavaScript':
                (function (i) {
                    proxyResolverMac.ProxiesForURLUsingScriptData({ input:url, scriptData:proxy.kCFProxyAutoConfigurationJavaScriptKey }, function (proxies) {
                        parseProxyResponse(url, proxies, function (errors, responses) {
                            responseCollector.collect(i, null, {
                                protocol: 'script',
                                errors,
                                responses,
                                raw: proxy
                            });
                        });
                    });
                }(i));
                break;
            default: //Can be kCFProxyAutoConfigurationHTTPResponseKey or something introduced new
                responseCollector.collect(i, null, parseProxy(proxy, 'unknown'));
                break;
            }
        });

    }

    function clearResponseCache() {
        responseCache = {};
    }

    handleProxyViaURL = function (url, scriptURL, callback) {
        if (responseCache[scriptURL]) {
            proxyResolverMac.ProxiesForURLUsingScriptData({ input:url, scriptData: responseCache[scriptURL] }, function (proxies) {
                parseProxyResponse(url, proxies, callback);
            });
            return;
        }

        const scriptURLDetail = URL.parse(scriptURL);
        let method;
        if (scriptURLDetail.protocol === 'http:') {
            method = http;
        } else if (scriptURLDetail.protocol === 'https:') {
            method = https;
        } else {
            // Not an external URL to download
            if (scriptURLDetail.protocol === 'file:') {
                fs.readFile(scriptURLDetail.path, 'utf8', function (err, data) {
                    if (err) {
                        callback('Could not read pac file ' + scriptURL);
                    } else {
                        responseCache[scriptURL] = data;
                        setTimeout(clearResponseCache, exports.cacheInterval);
                        proxyResolverMac.ProxiesForURLUsingScriptData({ input:url, scriptData: data }, function (proxies) {
                            parseProxyResponse(url, proxies, callback);
                        });
                    }
                });
            } else {
                utils.log(utils.logLevel.WARNING, 'Protocol not supported :' + scriptURLDetail.protocol + ', Full URL: ' + scriptURL);

                proxyResolverMac.ProxiesForURLUsingScriptURL({ input:url, scriptUrl: scriptURL }, function (proxies) {
                    parseProxyResponse(url, proxies, callback);
                });
            }
            return;
        }

        //Fetch the config
        const request = method.request({
            host: scriptURLDetail.hostname,
            port: scriptURLDetail.port,
            path: scriptURLDetail.path,
            method: 'GET'
        }, function (response) {
            let body = '';
            response.on('data', function (d) {
                body += d;
            });
            response.on('end', function () {
                let code = 4;
                try {
                    code = Math.floor(parseInt((response.statusCode), 10) / 100);
                } catch (e) {
                    utils.log(utils.logLevel.WARNING, 'Error fetching PAC file from url ' + scriptURL + ' with message ' + e);
                    proxyResolverMac.ProxiesForURLUsingScriptData({ input:url, scriptData: body }, function (proxies) {
                        parseProxyResponse(url, proxies, callback);
                    });
                    return;
                }
                if (code !== 2) {
                    proxyResolverMac.ProxiesForURLUsingScriptURL({ input:url, scriptUrl: scriptURL }, function (proxies) {
                        parseProxyResponse(url, proxies, callback);
                    });
                } else {
                    responseCache[scriptURL] = body;
                    setTimeout(clearResponseCache, exports.cacheInterval);
                    proxyResolverMac.ProxiesForURLUsingScriptData({ input:url, scriptData: body }, function (proxies) {
                        parseProxyResponse(url, proxies, callback);
                    });
                }
            });
        });
        let handled = false;
        request.on('error', function () {
            if (handled) {
                return;
            }
            handled = true;

            proxyResolverMac.ProxiesForURLUsingScriptURL({ input:url, scriptUrl: scriptURL }, function (proxies) {
                parseProxyResponse(url, proxies, callback);
            });

        });
        request.setTimeout(exports.socketTimeout, function () {
            if (handled) {
                return;
            }
            handled = true;
            utils.log(utils.logLevel.WARNING, 'Socket Timed out');
            request.abort();

            proxyResolverMac.ProxiesForURLUsingScriptURL({ input:url, scriptUrl: scriptURL }, function (proxies) {
                parseProxyResponse(url, proxies, callback);
            });
        });
        request.end();
    };

    //Try to find the best way out. There might be errors. Just ignore them
    //We are returning the debug data anyways.
    function parseOptimistic(data, d) {

        if (!d) {
            d = [];
        }
        if (!data) {
            data = [];
        }
        if (!Array.isArray(d)) {
            utils.log(utils.logLevel.WARNING, 'Unexpected object to parse ' + d);
            d = [];
        }
        if (!Array.isArray(data)) {
            utils.log(utils.logLevel.WARNING, 'Unexpected object to parse ' + data);
            return [];
        }
        data.forEach(function (currentData) {
            if (!currentData) { return; }
            if (currentData.protocol === 'script') {
                d = parseOptimistic(currentData.responses, d);
            } else if (currentData.protocol === 'none') {
                d.push({});
            } else if (currentData.protocol !== 'unknown') {
                d.push(currentData);
            }
        });
        return d;
    }

    function isPacFilePresent(callback) {
        proxyResolverMac.ProxiesForURL({ input: 'http://127.0.0.1' }, function (proxies) {
            let data;
            try {
                data = JSON.parse(proxies);
            } catch (e) {
                return callback(false);
            }
            if (!Array.isArray(data) || data.length < 1) {
                return callback(false);
            }
            if (data[0].kCFProxyTypeKey === 'kCFProxyTypeAutoConfigurationURL' || data[0].kCFProxyTypeKey === 'kCFProxyTypeAutoConfigurationJavaScript') {
                return callback(true);
            }
            return callback(false);
        });
    }

    exports.getLocalhostPacProxyStatus = function (callback) {
        isPacFilePresent(function (isFilePresent) {
            if (!isFilePresent) {
                callback('noPacFile');
            } else {
                exports.resolve('http://127.0.0.1', function (err, data) {
                    if (err) {
                        return callback('pacFileNoProxy');
                    }
                    if (!Array.isArray(data) || data.length < 1 || !data[0].hostname) {
                        return callback('pacFileNoProxy');
                    }
                    return callback('proxied');
                });
            }
        });
    };


    exports.resolve = function (url, callback) {

        proxyResolverMac.ProxiesForURL({ input:url }, function (proxies) {
            parseProxyResponse(url, proxies, function (errors, data) {
                if (!Array.isArray(data)) {
                    callback(errors);
                    return;
                }
                callback(null, parseOptimistic(data), data);
            });
        });

    };


    exports.getRootCAUnsafe = function () {
        let certs = proxyResolverMac.SystemRootCA();
        let returnArray = [];
        try {
            let nativeCerts = JSON.parse(certs);
            if (Array.isArray(nativeCerts)) {
                returnArray = nativeCerts;
            } else {
                utils.log(utils.logLevel.ERROR, 'Unknown response from native call Ca cert call ' + nativeCerts);
            }

            proxyResolverMac.FreeSystemRootCAReturnString();
        } catch (e) {
            utils.log(utils.logLevel.ERROR, 'Error parsing native certificate response:' + e + '\nResponse:\n' + certs);
        }
        return returnArray;
    };


    exports.getNegotiateToken = function (spn, username, password, domain, callback) {
        proxyResolverMac.GenerateNegotiateToken({ spn, username, password, domain }, function (data) {
            let d;
            try {
                d = JSON.parse(data);
            } catch (e) {
                callback('Error parsing native data for negotiate token: ' + data);
                return;
            }

            callback(d.error, d.data, d.more);
        });
    };

    exports.isLocalhostProxied = proxyResolverMac.LocalhostProxied;


    exports.getRootCA = function (callback) {

        // To be safe, we fetch the certificates in a separate process
        // execArgv to allow debugging parent process in VSCode
        // https://stackoverflow.com/questions/50885128
        const child = child_process.fork(__dirname + '/osxCerts.js', { execArgv: [] });

        const killChild = function () {
            // Need to force-kill because the bug is that it gets in an infinite loop in native Mac code.
            child.kill('SIGKILL');
        };

        // Make sure the child process gets killed when we exit
        process.on('exit', killChild);

        let timeout;
        const onFinished = function (certs) {
            clearTimeout(timeout);
            killChild();
            process.removeListener('exit', killChild);

            if (certs) {
                callback(null, certs);
            } else {
                utils.log(utils.logLevel.ERROR, 'Timed out getting response from MacOS');
                callback('timeout');
            }
        };

        // Set a timeout of 10s - if it takes longer, we'll kill the process
        timeout = setTimeout(onFinished, 10000);

        // Listen to events from the child process - it will send us the certificates when it has them
        // (and also any messages it wants to log)
        child.on('message', function (message) {
            if (message.certs) {
                utils.log(utils.logLevel.INFO, 'Got ' + message.certs.length + ' certificates');
                onFinished(message.certs);

            } else if (message.log && message.log.level && message.log.message) {
                utils.log(message.log.level, message.log.message);
            }
        });
    };

    exports.getOSLocale = function (callback) {
        proxyResolverMac.GetOSLocale(function (locale) {
            callback(null, locale);
            proxyResolverMac.FreeOSLocaleReturnString();
        });
    };

    exports.setSettingsChangedCallback = proxyResolverMac.StartListeningForProxySettingsChanges;
    exports.unsetSettingsChangedCallback = proxyResolverMac.StopListeningForProxySettingsChanges;

    exports.socketTimeout = 10 * 1000;
    exports.cacheInterval = 60 * 1000; //Most repeated requests should complete within a minute.

    return exports;

};
