/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

(function() {
    var angular = require('angular');
    var cockpit = require('cockpit');

    require('./kube-client');

    function debug() {
        if (window.debugging == "all" || window.debugging == "kube")
            console.debug.apply(console, arguments);
    }

    function updateMessage(response) {
        if (!response)
            return;

        var obj;
        try {
            obj = JSON.parse(response.data);
        } catch (e) {
            // Some kubernetes versions message up json reponses
            if (response.data && response.headers &&
                response.headers["Content-Type"] === "text/plain") {
                obj = { message: response.data };
            } else {
                return;
            }
        }

        if (obj && obj.message)
            response.message = response.statusText = obj.message;
    }

    /*
     * Currently we assume that the certificates in the kube config
     * file are:
     *
     * base64(PEM(data))
     *
     * Since our http-stream2 expects PEM certificates (although they're
     * nasty, they're better than all the alternatives) so we strip out
     * the outer base64 layer.
     */

    function parseCertOption(object, option) {
        var blob = object[option + "-data"];
        if (blob !== undefined)
            return { data: window.atob(blob) };

        var file = object[option];
        if (file !== undefined)
            return { file: file };

        return undefined;
    }

    function basicToken(user, pass) {
        return window.btoa(window.unescape(encodeURIComponent(user + ":" + pass)));
    }

    angular.module("kubeClient.cockpit", [
        "kubeClient",
    ])

            .factory("CockpitKubeWatch", [
                "$q",
                "KUBE_SCHEMA",
                "cockpitKubeDiscover",
                function($q, KUBE_SCHEMA, cockpitKubeDiscover) {
                    return function CockpitKubeWatch(path, callback) {
                        debug("creating watch:", path);

                        /* Used to track the last resource for restarting query */
                        var lastResource;

                        /* Whether close has been called */
                        var stopping = false;

                        /* The current HTTP request */
                        var channel = null;

                        /* The http options */
                        var http = null;

                        /* Waiting to do the next http request */
                        var wait = null;

                        /*
                 * Loading logic.
                 *
                 * For performance, we only use watches here. So we have
                 * to guess when loading is finished and when updates begin.
                 * There are several heuristics:
                 *
                 *  1) Receiving a MODIFY or DELETE means loading has finished.
                 *  2) A timeout after last ADDED
                 *  3) Error or connection closed.
                 *
                 * Remember that a watch object can restart its request for a number
                 * of reasons, and so the loading/loaded state may go back and forth.
                 *
                 * When transitioning from a loading to a loaded state, we have to:
                 *  a) Notify the caller if not already done
                 *  b) See if any objects present before load need to be removed.
                 */

                        var defer = $q.defer();
                        var promise = defer.promise;
                        var loaded = false;
                        var objects = { };
                        var previous;
                        var loading;

                        function loadBegin(full) {
                            if (full) {
                                previous = objects;
                                objects = { };
                            } else {
                                previous = null;
                            }
                            loadPoke(true);
                        }

                        function loadPoke(force) {
                            if (force || loading !== undefined) {
                                window.clearTimeout(loading);
                                loading = window.setTimeout(loadFinish, 100);
                            }
                        }

                        function loadFinish(ex) {
                            if (loaded)
                                return;
                            loaded = true;

                            var key, prev;
                            var frames = [];
                            prev = previous;
                            previous = null;
                            for (key in prev) {
                                if (!(key in objects))
                                    frames.push({ type: "DELETED", object: prev[key] });
                            }

                            /* Simulated delete frames */
                            if (frames.length)
                                callback(frames);

                            /* Tell callback to flush */
                            callback([]);

                            if (ex)
                                defer.reject(ex);
                            else
                                defer.resolve();
                        }

                        /*
                 * Each change is sent as an individual line from Kubernetes
                 * but they may not arrive exactly that way, so we buffer
                 * and split lines again.
                 */

                        var buffer;
                        function handleWatch(data) {
                            if (buffer)
                                data = buffer + data;

                            var lines = data.split("\n");
                            var i;
                            var length = lines.length - 1;

                            /* Last line is incomplete save for later */
                            buffer = lines[length];

                            var frames = [];

                            /* Process all the others */
                            var frame, object;
                            for (i = 0; i < length; i++) {
                                try {
                                    frame = JSON.parse(lines[i]);
                                } catch (ex) {
                                    console.warn(lines[i], ex);
                                    channel.close();
                                    continue;
                                }

                                object = frame.object;
                                if (!object) {
                                    console.warn("invalid watch without object", frame);
                                    continue;
                                }

                                /* The watch failed, likely due to invalid resourceVersion */
                                if (frame.type == "ERROR") {
                                    if (lastResource) {
                                        lastResource = null;
                                        startWatch();
                                    }
                                    continue;
                                }

                                var meta = object.metadata;
                                if (!meta || (!meta.uid && object.kind !== "Project") || !object.kind) {
                                    console.warn("invalid kube object: ", object);
                                    continue;
                                }

                                lastResource = meta.resourceVersion;

                                /* We track objects here so we can restart watches */
                                var uid = meta.uid;
                                if (frame.type == "DELETED")
                                    delete objects[uid];
                                else
                                    objects[uid] = object;

                                debug(frame.type, object.kind, meta.uid);
                                frames.push(frame);
                            }

                            callback(frames);
                            loadPoke();
                        }

                        function startWatch() {
                            if (channel)
                                return;

                            var full = true;
                            var uri = path + "?watch=true";

                            /*
                     * If we have a last resource we can guarantee that we don't miss
                     * any objects or changes to objects. If we don't have one, then we
                     * have to list everything again. Still watch at the same time though.
                     */
                            if (lastResource) {
                                uri += "&resourceVersion=" + encodeURIComponent(lastResource);
                                full = false;
                            }

                            /*
                     * As a precaution, watch must take at least 1 second
                     * to complete. Otherwise we could be in a tight loop here.
                     * eg: if the API of Kubernetes changes unpredictably.
                     */
                            var blocked = false;
                            window.setTimeout(function() {
                                blocked = true;
                            }, 1000);

                            var opts = angular.extend({ }, http, {
                                path: uri,
                                method: "GET"
                            });

                            channel = cockpit.channel(opts);

                            var response = { };
                            var failed = false;
                            channel.addEventListener("control", function(ev, options) {
                                if (options.command == "response") {
                                    response = options;
                                    if (response.status > 299)
                                        failed = true;
                                    loadBegin(full);
                                }
                            });

                            channel.addEventListener("message", function(ev, payload) {
                                if (failed)
                                    response.data = (response.data || "") + payload;
                                else
                                    handleWatch(payload);
                            });

                            channel.addEventListener("close", function(ev, options) {
                                channel = null;
                                loading = false;
                                if (stopping)
                                    return;

                                var msg = "watching " + path + " failed: ";
                                var problem = options.problem;
                                var status = response.status;

                                if (problem) {
                                    msg += problem;
                                    if (problem == "disconnected")
                                        debug(msg);
                                    else
                                        console.warn(msg);
                                    response.problem = problem;
                                    response.status = 999;
                                    loadFinish(response);
                                } else if (failed) {
                                    updateMessage(response);
                                    msg += (response.message || response.reason || status);
                                    if (status === 404 || status === 410 || status === 403) {
                                        debug(msg);
                                        loadFinish(response);
                                        return; /* don't try watch again if we get a 404/410/403 */
                                    } else {
                                        console.warn(msg);
                                    }
                                    loadFinish(response);
                                } else if (!blocked) {
                                    console.warn("watching kube " + path + " didn't block");
                                } else {
                                    startWatch();
                                    return;
                                }

                                startWatchLater();
                            });

                            /* No http request body */
                            channel.control({ command: "done" });
                        }

                        function startWatchLater() {
                            if (!wait) {
                                wait = window.setTimeout(function() {
                                    wait = null;
                                    startWatch();
                                }, 5000);
                            }
                        }

                        $q.when(cockpitKubeDiscover(), function(options) {
                            http = options;
                            startWatch();
                        }, function(resp) {
                            loadFinish(resp);
                        });

                        promise.cancel = function cancel(ex) {
                            stopping = true;
                            var problem;
                            if (channel) {
                                if (ex)
                                    problem = ex.problem;
                                channel.close(problem || "disconnected");
                                channel = null;
                            }
                            window.clearTimeout(wait);
                            wait = null;
                            http = null;
                            loadFinish(ex);
                        };

                        return promise;
                    };
                }
            ])

    /*
     * A WebSocket factory for the kubernetes-container-terminal
     * that uses the kubectl command with a fake websocket
     */
            .factory("CockpitKubectlWebSocket", [
                function() {
                    function decode(href, options) {
                        var pos = href.indexOf('?');
                        var first = href;
                        var path;
                        if (pos === -1)
                            first = href;
                        else
                            first = href.substr(0, pos);

                        path = first.split('/').map(decodeURIComponent);

                        if (pos !== -1) {
                            angular.forEach(href.substring(pos + 1).split("&"), function(opt) {
                                var last;
                                var parts = opt.split('=');
                                var name = decodeURIComponent(parts[0]);
                                var value = decodeURIComponent(parts[1]);
                                if (options.hasOwnProperty(name)) {
                                    last = options[name];
                                    if (!angular.isArray(last))
                                        last = options[name] = [ last ];
                                    last.push(value);
                                } else {
                                    options[name] = value;
                                }
                            });
                        }

                        return path;
                    }

                    function parser(url) {
                        var options = { };
                        var path = decode(url, options);

                        var command = [ ];
                        var args = [ ];
                        var namespace = "default";
                        var container = null;
                        var cmd = "log";
                        var pod = "";

                        var i, len;
                        for (i = 0, len = path.length; i < len; i++) {
                            if (path[i] === "namespaces") {
                                namespace = path[++i];
                            } else if (path[i] === "pods") {
                                pod = path[++i];
                                if (path[i + 1] == "exec")
                                    cmd = "exec";
                                else if (path[i + 1] == "log")
                                    cmd = "logs";
                            }
                        }

                        for (i in options) {
                            if (i == "container") {
                                container = options[i];
                            } else if (i == "command") {
                                if (angular.isArray(options[i]))
                                    command = options[i];
                                else
                                    command.push(options[i]);
                            } else if (i == "stdin" || i == "tty" || i == "follow") {
                                args.push("--" + i);
                            }
                        }

                        var ret = [ "kubectl", cmd, "--namespace=" + namespace ];
                        if (container)
                            ret.push("--container=" + container);
                        ret.push.apply(ret, args);
                        ret.push(pod, "--");
                        ret.push.apply(ret, command);
                        return ret;
                    }

                    return function KubeFakeWebSocket(url, protocols) {
                        var cmd = parser(url);
                        var base64 = false;

                        /* A fake WebSocket */
                        var channel;
                        var state = 0; /* CONNECTING */
                        var ws = { };
                        cockpit.event_target(ws);

                        function open() {
                            channel = cockpit.channel({
                                payload: "stream",
                                spawn: cmd,
                                pty: true
                            });

                            channel.addEventListener("close", function(ev, options) {
                                channel = null;

                                state = 3;
                                var cev = new Event("close", { bubbles: false, cancelable: false });
                                ws.dispatchEvent(cev);
                            });

                            channel.addEventListener("message", function(ev, data) {
                                if (base64)
                                    data = "1" + window.btoa(data);
                                var mev = new window.MessageEvent('message', { data: data });
                                ws.dispatchEvent(mev);
                            });

                            state = 1;
                            var oev = new Event("open", { bubbles: false, cancelable: false });
                            ws.dispatchEvent(oev);
                        }

                        function fail() {
                            var ev = new Event("close", { bubbles: false, cancelable: false });
                            ws.dispatchEvent(ev);
                        }

                        function close(code, reason) {
                            if (channel)
                                channel.close(reason);
                        }

                        function send(data) {
                            if (base64) {
                                /*
                         * HACK: container-terminal sends Width/Height commands but
                         * many of the kubernetes implementations don't yet support
                         * that. So filter them out here for now.
                         */
                                if (data[0] === "4")
                                    return;
                                data = window.atob(data.slice(1));
                            }

                            if (channel)
                                channel.send(data);
                        }

                        var valid = true;
                        if (protocols) {
                            if (angular.isArray(protocols))
                                valid = base64 = protocols.indexOf("base64.channel.k8s.io") !== -1;
                            else
                                valid = base64 = protocols === "base64.channel.k8s.io";
                        }

                        /* A fake WebSocket */
                        Object.defineProperties(ws, {
                            binaryType: { value: "arraybuffer" },
                            bufferedAmount: { value: 0 },
                            extensions: { value: "" },
                            protocol: { value: base64 ? "base64.channel.k8s.io" : "" },
                            readyState: { get: function() { return state } },
                            url: { value: url },
                            close: { value: close },
                            send: { value: send },
                        });

                        if (valid) {
                            window.setTimeout(open);
                        } else {
                            console.warn("Unsupported kubernetes container WebSocket subprotocol: " + protocols);
                            window.setTimeout(fail);
                        }

                        return ws;
                    };
                }
            ])

            .factory("CockpitKubeSocket", [
                "$q",
                "$injector",
                function($q, $injector) {
                    return function CockpitKubeSocket(url, config) {
                        var base64 = false;
                        var connect;
                        var state = 0; /* CONNECTING */
                        var ws = { };
                        var channel;

                        var protocols = [];
                        if (config && config.protocols) {
                            protocols = config.protocols;
                            if (!angular.isArray(protocols))
                                protocols = [ String(config.protocols) ];
                        }

                        /*
                 * If we're called with fully formed options, then don't do
                 * connect discovery stuff. Otherwise ask our connect service
                 * for connection info, and do discovery.
                 */
                        if (config && config.port)
                            connect = config;
                        else
                            connect = $injector.get('cockpitKubeDiscover')();

                        function close(code, reason) {
                            if (channel)
                                channel.close(reason);
                        }

                        function send(data) {
                            /*
                     * HACK: container-terminal sends Width/Height commands but
                     * many of the kubernetes implementations don't yet support
                     * that. So filter them out here for now.
                     */
                            if (base64 && data[0] === "4")
                                return;

                            if (channel)
                                channel.send(data);
                        }

                        /* A fake WebSocket */
                        Object.defineProperties(ws, {
                            binaryType: { value: "arraybuffer" },
                            bufferedAmount: { value: 0 },
                            extensions: { value: "" },
                            protocol: { value: protocols[0] },
                            readyState: { get: function() { return state } },
                            url: { value: url },
                            close: { value: close },
                            send: { value: send },
                        });

                        base64 = protocols.indexOf("base64.channel.k8s.io") !== -1;

                        $q.when(connect, function connected(options) {
                            cockpit.event_target(ws);

                            channel = cockpit.channel(angular.extend({ }, options, {
                                payload: "websocket-stream1",
                                path: url,
                                protocols: protocols.length > 0 ? protocols : undefined,
                            }));

                            channel.addEventListener("close", function(ev, options) {
                                channel = null;

                                state = 3;
                                var cev = new Event("close", { bubbles: false, cancelable: false });
                                ws.dispatchEvent(cev);
                            });

                            channel.addEventListener("message", function(ev, data) {
                                var mev = new window.MessageEvent('message', { data: data });
                                ws.dispatchEvent(mev);
                            });

                            state = 1;
                            var oev = new Event("open", { bubbles: false, cancelable: false });
                            ws.dispatchEvent(oev);
                        });

                        return ws;
                    };
                }
            ])

            .factory("CockpitKubeRequest", [
                "$q",
                "$injector",
                function($q, $injector) {
                    var CONTENT_TYPE = "Content-Type";
                    var JSON_TYPE = "application/json";
                    return function CockpitKubeRequest(method, path, body, config) {
                        var defer = $q.defer();
                        var promise = defer.promise;
                        var connect, channel;

                        var heads = { };
                        if (body && typeof body == "object") {
                            body = JSON.stringify(body);
                            heads[CONTENT_TYPE] = JSON_TYPE;
                        }

                        if (!config)
                            config = { };

                        /*
                 * If we're called with fully formed options, then don't do
                 * connect discovery stuff. Otherwise ask our connect service
                 * for connection info, and do discovery.
                 */
                        if (config && config.port)
                            connect = config;
                        else
                            connect = $injector.get('cockpitKubeDiscover')();

                        $q.when(connect, function connected(options) {
                            var opts = angular.extend({ }, config, options, {
                                path: path,
                                method: method,
                                payload: "http-stream2"
                            });

                            opts.headers = angular.extend(heads, config.headers || { }, options.headers || { });

                            debug("request:", method, path, opts);
                            channel = cockpit.channel(opts);

                            var response = { };
                            channel.addEventListener("control", function(ev, options) {
                                if (options.command == "response") {
                                    response = options;
                                    response.statusText = response.reason;
                                }
                            });

                            channel.addEventListener("message", function(ev, payload) {
                                response.data = (response.data || "") + payload;
                            });

                            channel.addEventListener("close", function(ev, options) {
                                channel = null;

                                response.options = options;
                                if (options.problem) {
                                    response.problem = response.statusText = options.problem;
                                    response.status = 999;
                                }

                                var headers = response.headers || { };
                                var content_type = headers[CONTENT_TYPE] || headers[CONTENT_TYPE.toLowerCase()] || "";
                                if (content_type.lastIndexOf(JSON_TYPE, 0) === 0) {
                                    try {
                                        response.data = JSON.parse(response.data);
                                    } catch (ex) {
                                        /* it's not JSON, just leave as text */
                                    }
                                }

                                if (response.status > 299) {
                                    updateMessage(response);
                                    defer.reject(response);
                                } else {
                                    defer.resolve(response);
                                }
                            });

                            if (body)
                                channel.send(body);
                            channel.control({ command: "done" });

                            /* Failed to connect */
                        }, function failed(response) {
                            defer.reject(response);
                        });

                        /* Helpful function on the promise */
                        promise.cancel = function cancel() {
                            if (connect.cancel)
                                connect.cancel();
                            if (channel)
                                channel.close("cancelled");
                        };

                        return promise;
                    };
                }
            ])

            .factory("cockpitRunCommand", [
                '$q',
                function($q) {
                    return function runCommand(args) {
                        var defer = $q.defer();
                        var promise = defer.promise;
                        var channel = cockpit.channel({
                            "payload": "stream",
                            "spawn": args,
                            "err": "message"
                        });

                        var result = "";
                        channel.addEventListener("message", function(ev, payload) {
                            result += payload;
                        });
                        channel.addEventListener("close", function(ev, options) {
                            channel = null;
                            if (options.problem)
                                defer.reject(options);
                            else
                                defer.resolve(result);
                        });

                        promise.cancel = function cancel(options) {
                            if (channel)
                                channel.close(options || "cancelled");
                        };

                        promise.send = function send(data) {
                            if (channel)
                                channel.send(data);
                        };

                        return promise;
                    };
                }
            ])

            .factory("cockpitKubectlConfig", [
                '$q',
                'cockpitRunCommand',
                function($q, runCommand) {
                    function generateKubeOptions(cluster, user) {
                        var parser, token, provider;
                        var options = { port: 8080, headers: { } };

                        if (cluster && cluster.server) {
                            parser = document.createElement('a');
                            parser.href = cluster.server;
                            if (parser.hostname)
                                options.address = parser.hostname;
                            if (parser.port)
                                options.port = parseInt(parser.port, 10);
                            if (parser.protocol == 'https:') {
                                if (!parser.port || parser.port === "0")
                                    options.port = parser.href == cluster.server ? 6443 : 443;

                                options.tls = { };
                                options.tls.authority = parseCertOption(cluster, "certificate-authority");
                                options.tls.validate = !cluster["insecure-skip-tls-verify"];
                            }
                        }

                        if (user) {
                            token = user.token;
                            provider = user["auth-provider"] || {};
                            if (provider.config) {
                                if (provider.config['access-token'])
                                    token = provider.config['access-token'];
                                else if (provider.config['token'])
                                    token = provider.config['token'];
                            }

                            if (token)
                                options.headers["Authorization"] = "Bearer " + token;
                            if (user.username)
                                options.headers["Authorization"] = "Basic " + basicToken(user.username, user.password || "");
                            if (options.tls) {
                                options.tls.certificate = parseCertOption(user, "client-certificate");
                                options.tls.key = parseCertOption(user, "client-key");
                            }
                        }

                        return options;
                    }

                    function parseKubeConfig(data, contextName) {
                        var config, options;

                        try {
                            config = JSON.parse(data);
                        } catch (ex) {
                            console.warn("received invalid kubectl config", ex);
                            return null;
                        }

                        if (!contextName)
                            contextName = config["current-context"];
                        var contexts = config["contexts"] || [];

                        /* Find the cluster info */
                        var userName, clusterName;
                        contexts.forEach(function(info) {
                            if (info.name === contextName) {
                                var context = info.context || { };
                                userName = context.user;
                                clusterName = context.cluster;
                            }
                        });

                        /* Find the user info */
                        var user;
                        var users = config["users"] || [];
                        users.forEach(function(info) {
                            if (info.name === userName)
                                user = info.user;
                        });

                        /* Find the cluster info */
                        var cluster;
                        var clusters = config["clusters"] || [];
                        clusters.forEach(function(info) {
                            if (info.name == clusterName)
                                cluster = info.cluster;
                        });

                        options = generateKubeOptions(cluster, user);
                        debug("parsed kube config", options);
                        return options;
                    }

                    function read() {
                        var cmd = ["kubectl", "config", "view", "--output=json", "--raw"];

                        /* Call kubectl minified config view. That only outputs
                 * the objects that would be used by a connection */
                        return runCommand(["kubectl", "config", "view", "--minify", "--output=json"])
                                .then(function (data) {
                                    var p;
                                    var auth_provider;
                                    var user;

                                    /* If the default data has a 'auth-provider'
                         * section then call kubectl version to try to
                         * get it to fill in any token data.
                         */
                                    try {
                                        user = JSON.parse(data)["users"][0];
                                        if (user && user['user'])
                                            auth_provider = user['user']['auth-provider'];
                                    } catch (ex) {
                                        console.warn("received invalid kubectl config", ex);
                                    }

                                    if (auth_provider) {
                                        p = runCommand(["kubectl", "version"])
                                                .then(function () {
                                                    return runCommand(cmd);
                                                }, function () {
                                                    return runCommand(cmd);
                                                });
                                    } else {
                                        p = runCommand(cmd);
                                    }

                                    return p;
                                });
                    }

                    return {
                        read: read,
                        parseKubeConfig: parseKubeConfig,
                        generateKubeOptions: generateKubeOptions,
                    };
                }
            ])

            .factory("cockpitKubeDiscover", [
                "$q",
                "CockpitKubeRequest",
                "cockpitKubectlConfig",
                "cockpitConnectionInfo",
                "cockpitBrowserStorage",
                function($q, CockpitKubeRequest, cockpitKubectlConfig, info, browser) {
                    var defer = null;

                    return function cockpitKubeDiscover(force) {
                        if (!force && defer)
                            return defer.promise;

                        var last, req, kubectl, loginOptions;
                        var loginData = browser.localStorage.getItem('login-data', true);
                        defer = $q.defer();

                        var schemes = [
                            { port: 8080 },
                            { port: 8443, tls: { } },
                            { port: 6443, tls: { } },
                        ];

                        function step(options, kubeConfig) {
                            if (!options)
                                options = schemes.shift();

                            /* No further ports to try? */
                            if (!options) {
                                last.statusText = cockpit.gettext("Couldn't find running API server");
                                last.problem = "not-found";
                                defer.reject(last);
                                return;
                            }

                            /* If options is a function call it, the function is
                     * responsible to call step again when ready */
                            if (typeof options === "function") {
                                options();
                                return;
                            }

                            options.payload = "http-stream2";
                            debug("trying kube at:", options);
                            req = new CockpitKubeRequest("GET", "/api", "", options);
                            req.then(function(response) {
                                req = null;
                                var resp = response.data;
                                if (resp && resp.versions) {
                                    debug("discovered kube api", resp);
                                    if (kubeConfig) {
                                        info.kubeConfig = kubeConfig;
                                        if (kubectl)
                                            info.type = "kubectl";
                                        else
                                            info.type = "sessionData";
                                    } else {
                                        info.type = "open";
                                    }

                                    defer.resolve(options);
                                } else {
                                    debug("not an api endpoint:", options);
                                    last = response;
                                    kubectl = null;
                                    step();
                                }
                            })
                                    .catch(function(response) {
                                        req = null;
                                        kubectl = null;
                                        last = response;

                                        if (response.problem === "not-found") {
                                            debug("api endpoint not found on:", options);
                                            step();
                                        } else {
                                            debug("connecting to kube failed:", response);
                                            defer.reject(response);
                                        }
                                    });
                        }

                        function kubectlStep() {
                            kubectl = cockpitKubectlConfig.read()
                                    .then(function(data) {
                                        var options = cockpitKubectlConfig.parseKubeConfig(data);
                                        step(options, options ? JSON.parse(data) : null);
                                    })
                                    .catch(function(options) {
                                        console.warn("kubectl failed: " + (options.message || options.problem));
                                        step();
                                    });
                        }

                        if (force && typeof force === 'object') {
                            schemes = [ force ];
                            step();
                        } else if (force === "kubectl") {
                            schemes = [ kubectlStep ];
                            step();
                        } else {
                            schemes.unshift(kubectlStep);
                            if (loginData)
                                loginOptions = cockpitKubectlConfig.parseKubeConfig(loginData);
                            step(loginOptions, loginOptions ? JSON.parse(loginData) : null);
                        }

                        defer.promise.cancel = function cancel() {
                            if (kubectl && kubectl.cancel)
                                kubectl.cancel("cancelled");

                            if (req)
                                req.close("cancelled");
                        };
                        return defer.promise;
                    };
                }
            ])

            .factory("CockpitEnvironment", [
                "$q",
                function($q) {
                    var defer = $q.defer();
                    var settings = null;
                    return function cockpitKubeSettings() {
                        if (settings !== null)
                            return defer.promise;
                        var channel = cockpit.channel({ payload: "dbus-json3", bus: "internal" });
                        channel.addEventListener("close", function(ev, options) {
                            if (options.problem) {
                                console.warn("couldn't retrieve environment:", options.problem);
                                defer.reject(options);
                            } else {
                                defer.resolve(settings);
                            }
                        });
                        channel.addEventListener("message", function(ev, data) {
                            var result = JSON.parse(data);
                            if (result.reply) {
                                settings = result.reply[0][0].Variables.v;
                                channel.close(null);
                            } else if (result.error) {
                                console.warn("error retrieving environment:", result.error);
                                channel.close("internal-error");
                            }
                        });
                        channel.send(JSON.stringify({
                            id: "cookie",
                            call: [ "/environment", "org.freedesktop.DBus.Properties", "GetAll",
                                [ "cockpit.Environment" ] ]
                        }));
                        return defer.promise;
                    };
                }
            ])

            .factory("cockpitKubeDiscoverSettings", [
                "$q",
                "CockpitKubeRequest",
                "cockpitKubeDiscover",
                "CockpitEnvironment",
                'kubeLoader',
                'cockpitConnectionInfo',
                function($q, CockpitKubeRequest, cockpitKubeDiscover,
                    CockpitEnvironment, loader, info) {
                    var promise = null;
                    return function kubeDiscoverSettings(force) {
                        if (!force && promise)
                            return promise;

                        var settings = {
                            registry: {
                                host: "registry",
                                host_explicit: false
                            },
                            flavor: "kubernetes",
                            isAdmin: false,
                            currentUser: null,
                            canChangeConnection: false
                        };

                        var env_p = CockpitEnvironment()
                                .then(function(result) {
                                    var regHost = result["REGISTRY_HOST"];
                                    if (regHost) {
                                        settings.registry.host = regHost;
                                        settings.registry.host_explicit = true;
                                    }
                                    var openshifthost = result["OPENSHIFT_OAUTH_PROVIDER_URL"];
                                    if (openshifthost) {
                                        settings.registry.openshifthost = openshifthost.replace(/^http(s?):\/\//i, "");
                                        settings.registry.openshifthost_explicit = true;
                                    }
                                }, function(ex) {});

                        var discover_p = cockpitKubeDiscover(force)
                                .then(function(options) {
                                    var req = new CockpitKubeRequest("GET", "/oapi/v1/users/~", "", options)
                                            .then(function(response) {
                                                settings.flavor = "openshift";
                                                if (response)
                                                    settings.currentUser = response.data;
                                            }, function () {
                                                settings.flavor = "kubernetes";
                                            });

                                    var watch = loader.watch("namespaces")
                                            .then(function () {
                                                settings.isAdmin = true;
                                            }, function () {
                                                settings.isAdmin = false;
                                            });

                                    var authorization;
                                    /* See if we have a bearer token to use */
                                    if (options.headers) {
                                        authorization = (options.headers['Authorization'] || "").trim();
                                        if (authorization.toLowerCase().indexOf("bearer ") === 0)
                                            settings.registry.password = authorization.substr(7).trim();
                                    }
                                    return $q.all([watch, req]);
                                });

                        promise = $q.all([discover_p, env_p])
                                .then(function() {
                                    settings.canChangeConnection = info.type == "kubectl";
                                    return settings;
                                });

                        return promise;
                    };
                }
            ])

            .factory("cockpitBrowserStorage", [
                "$window",
                function($window) {
                    var base = $window;
                    if (cockpit && cockpit.sessionStorage && cockpit.localStorage)
                        base = cockpit;

                    /* Some browsers fail localStorage access due to corruption, preventing Cockpit login */
                    var localStorage;
                    try {
                        localStorage = base.localStorage;
                    } catch (ex) {
                        localStorage = base.sessionStorage;
                        console.warn(String(ex));
                    }

                    return {
                        sessionStorage: base.sessionStorage,
                        localStorage: localStorage
                    };
                }
            ])

            .factory('cockpitConnectionInfo', function () {
                return {
                    type: null,
                    kubeConfig: null,
                };
            })

            .factory('cockpitContainerWebSocket', [
                'CockpitKubeSocket',
                'CockpitKubectlWebSocket',
                'cockpitConnectionInfo',
                function (socket, kubectlSocket, info) {
                    return function(url, protocols) {
                        /* config retrieved from kubectl? */
                        if (info.type == "kubectl")
                            return kubectlSocket(url, protocols);
                        else
                            return socket(url, { protocols: protocols });
                    };
                }
            ])

            .factory('CockpitFormat', function() {
                return {
                    formatBytes: cockpit.format_bytes,
                    formatBitsPerSec: cockpit.format_bits_per_sec,
                    format: cockpit.format
                };
            })

            .factory('CockpitMetrics', function() {
                return {
                    grid: cockpit.grid,
                    series: cockpit.series,
                };
            })

            .factory('CockpitTranslate', function() {
                return {
                    gettext: cockpit.gettext,
                    ngettext: cockpit.ngettext,
                };
            });
}());
