(function() {
    "use strict";

    var $ = require("jquery");
    var cockpit = require("cockpit");

    var mod = { };

    var known_hosts_path = "/etc/ssh/ssh_known_hosts";
    /*
     * We share the Machines state between multiple frames. Only
     * one frame has the job of loading the state, usually index.js
     * The Loader code below does all the loading.
     *
     * The data is stored in sessionStorage in a JSON object, like this
     * {
     *    content: name → info dict from bridge's /machines Machines property
     *    overlay: extra data to augment and override on top of content
     * }
     *
     * This uses sessionStorage rather than cockpit.sessionStorage
     * because we don't ever want to write unprefixed keys.
     */

    var key = cockpit.sessionStorage.prefixedKey("v2-machines.json");
    var session_prefix = cockpit.sessionStorage.prefixedKey("v1-session-machine");

    function generate_session_key(host) {
        return session_prefix + "/" + host;
    }

    function Machines() {
        var self = this;

        var flat = null;
        self.ready = false;

        /* parsed machine data */
        var machines = { };

        /* Data shared between Machines() instances */
        var last = {
            content: null,
            overlay: {
                localhost: {
                    visible: true,
                    manifests: cockpit.manifests
                }
            }
        };

        function storage(ev) {
            if (ev.key === key && ev.storageArea === window.sessionStorage)
                refresh(JSON.parse(ev.newValue));
        }

        window.addEventListener("storage", storage);

        window.setTimeout(function() {
            var value = window.sessionStorage.getItem(key);
            if (!self.ready && value)
                refresh(JSON.parse(value));
        });

        var timeout = null;

        function sync(machine, values, overlay) {
            var desired = $.extend({ }, values || { }, overlay || { });
            var prop;
            for (prop in desired) {
                if (machine[prop] !== desired[prop])
                    machine[prop] = desired[prop];
            }
            for (prop in machine) {
                if (machine[prop] !== desired[prop])
                    delete machine[prop];
            }
            return machine;
        }

        function refresh(shared, push) {
            if (!shared)
                return;

            var emit_ready = !self.ready;

            self.ready = true;
            last = shared;
            flat = null;

            if (push && !timeout) {
                timeout = window.setTimeout(function() {
                    timeout = null;
                    window.sessionStorage.setItem(key, JSON.stringify(last));
                }, 10);
            }

            var host, hosts = { };
            var content = shared.content || { };
            var overlay = shared.overlay || { };
            for (host in content)
                hosts[host] = true;
            for (host in overlay)
                hosts[host] = true;

            var events = [];

            var machine, application;
            for (host in hosts) {
                var old_machine = machines[host] || { };
                var old_conns = old_machine.connection_string;

                /* Invert logic for color, always respect what's on disk */
                if (content[host] && content[host].color && overlay[host])
                    delete overlay[host].color;

                machine = sync(old_machine, content[host], overlay[host]);

                /* Fill in defaults */
                machine.key = host;
                if (!machine.address)
                    machine.address = host;

                machine.connection_string = self.generate_connection_string(machine.user,
                                                                            machine.port,
                                                                            machine.address);

                if (!machine.label) {
                    if (host == "localhost" || host == "localhost.localdomain") {
                        application = cockpit.transport.application();
                        if (application.indexOf('cockpit+=') === 0)
                            machine.label = application.replace('cockpit+=', '');
                        else
                            machine.label = window.location.hostname;
                    } else {
                        machine.label = host;
                    }
                }
                if (!machine.avatar)
                    machine.avatar = "../shell/images/server-small.png";

                events.push([host in machines ? "updated" : "added",
                            [machine, host, old_conns]]);
                machines[host] = machine;
            }

            /* Remove any lost hosts */
            for (host in machines) {
                if (!(host in hosts)) {
                    machine = machines[host];
                    delete machines[host];
                    delete overlay[host];
                    events.push(["removed", [machine, host]]);
                }
            }

            /* Fire off all events */
            var i, sel = $(self), len = events.length;
            for (i = 0; i < len; i++)
                sel.triggerHandler(events[i][0], events[i][1]);
            if (emit_ready)
                $(self).triggerHandler("ready");
        }

        function update_session_machine(machine, host, values) {
            /* We don't save the whole machine object */
            var skey = generate_session_key(host);
            var data = $.extend({}, machine, values);
            window.sessionStorage.setItem(skey, JSON.stringify(data));
            self.overlay(host, values);
            return cockpit.when([]);
        }

        function update_saved_machine(host, values) {
            // wrap values in variants for D-Bus call; at least values.port can
            // be int or string, so stringify everything but the "visible" boolean
            var values_variant = {};
            for (var prop in values)
                if (values[prop] !== null) {
                    if (prop == "visible")
                        values_variant[prop] = cockpit.variant('b', values[prop]);
                    else
                        values_variant[prop] = cockpit.variant('s', values[prop].toString());
                }

            // FIXME: investigate re-using the proxy from Loader (runs in different frame/scope)
            var bridge = cockpit.dbus(null, { bus: "internal", "superuser": "try" });
            var mod = bridge.call("/machines", "cockpit.Machines", "Update", [ "99-webui.json", host, values_variant ])
                .done(function() {
                    var prop, over = { };
                    for (prop in values)
                        over[prop] = null;
                    self.overlay(host, over);
                })
                .fail(function(error) {
                    console.error("failed to call cockpit.Machines.Update(): ", error);
                });

            return mod;
        }

        self.add_key = function(host_key) {
            var known_hosts = cockpit.file(known_hosts_path, { superuser: "try" });
            return known_hosts
                .modify(function(data) {
                    if (!data)
                        data = "";

                    return data + "\n" + host_key;
                })
                .always(function() {
                    known_hosts.close();
                });
        };

        self.add = function add(connection_string, color) {
            var values = self.split_connection_string(connection_string);
            var host = values['address'];

            values = $.extend({
                        visible: true,
                        color: color || self.unused_color(),
                    }, values);

            var machine = self.lookup(host);
            if (machine)
                machine.on_disk = true;

            return self.change(values['address'], values);
        };

        self.unused_color = function unused_color() {
            var i, len = mod.colors.length;
            for (i = 0; i < len; i++) {
                if (!color_in_use(mod.colors[i]))
                    return mod.colors[i];
            }
            return "gray";
        };

        function color_in_use(color) {
            var key, machine, norm = mod.colors.parse(color);
            for (key in machines) {
                machine = machines[key];
                if (machine.color && mod.colors.parse(machine.color) == norm)
                    return true;
            }
            return false;
        }

        function merge(item, values) {
            for (var prop in values) {
                if (values[prop] === null)
                    delete item[prop];
                else
                    item[prop] = values[prop];
            }
        }

        self.change = function change(host, values) {
            var mod, hostnamed, call;
            var machine = self.lookup(host);

            if (values.label) {

                var conn_to = host;
                if (machine)
                    conn_to = machine.connection_string;

                if (!machine || machine.label !== values.label) {
                    hostnamed = cockpit.dbus("org.freedesktop.hostname1", { host: conn_to });
                    call = hostnamed.call("/org/freedesktop/hostname1", "org.freedesktop.hostname1",
                                          "SetPrettyHostname", [ values.label, true ])
                        .always(function() {
                            hostnamed.close();
                        })
                        .fail(function(ex) {
                            console.warn("couldn't set pretty host name: " + ex);
                        });
                }
            }

            if (machine && !machine.on_disk)
                mod = update_session_machine(machine, host, values);
            else
                mod = update_saved_machine(host, values);

            if (call)
                return cockpit.all(call, mod);

            return mod;
        };

        self.data = function data(content) {
            var host, changes = {};

            for (host in content) {
                changes[host] = $.extend({ }, last.overlay[host] || { });
                merge(changes[host], { on_disk: true });
            }

            /* It's a full reload, so data not
             * present is no longer from disk
             */
            for (host in machines) {
                if (content && !content[host]) {
                    changes[host] = $.extend({ }, last.overlay[host] || { });
                    merge(changes[host], { on_disk: null });
                }
            }

            refresh({ content: content,
                      overlay: $.extend({ }, last.overlay, changes) }, true);
        };

        self.overlay = function overlay(host, values) {
            var changes = { };
            changes[host] = $.extend({ }, last.overlay[host] || { });
            merge(changes[host], values);
            refresh({
                content: last.content,
                overlay: $.extend({ }, last.overlay, changes)
            }, true);
        };

        Object.defineProperty(self, "list", {
            enumerable: true,
            get: function get() {
                var key;
                if (!flat) {
                    flat = [];
                    for (key in machines) {
                        if (machines[key].visible)
                            flat.push(machines[key]);
                    }
                    flat.sort(function(m1, m2) {
                        return m1.label.localeCompare(m2.label);
                    });
                }
                return flat;
            }
        });

        Object.defineProperty(self, "addresses", {
            enumerable: true,
            get: function get() {
                return Object.keys(machines);
            }
        });

        self.lookup = function lookup(address) {
            var parts = self.split_connection_string(address);
            return machines[parts.address || "localhost"] || null;
        };

        self.generate_connection_string = function (user, port, addr) {
            var address = addr;
            if (user)
                address = user + "@" + address;

            if (port)
                address = address + ":" + port;

            return address;
        };

        self.split_connection_string = function(conn_to) {
            var parts = {};
            var user_spot = -1;
            var port_spot = -1;

            if (conn_to) {
                user_spot = conn_to.lastIndexOf('@');
                port_spot = conn_to.lastIndexOf(':');
            }

            if (user_spot > 0) {
                parts.user = conn_to.substring(0, user_spot);
                conn_to = conn_to.substring(user_spot+1);
                port_spot = conn_to.lastIndexOf(':');
            }

            if (port_spot > -1) {
                var port = parseInt(conn_to.substring(port_spot+1), 10);
                if (!isNaN(port)) {
                    parts.port = port;
                    conn_to = conn_to.substring(0, port_spot);
                }
            }

            parts.address = conn_to;
            return parts;
        };

        self.close = function close() {
            window.removeEventListener("storage", storage);
        };
    }

    function Loader(machines, session_only) {
        var self = this;

        /* Have we loaded from cockpit session */
        var session_loaded = false;

        /* File we are watching */
        var file;

        /* echo channels to each machine */
        var channels = { };

        /* hostnamed proxies to each machine, if hostnamed available */
        var proxies = { };

        /* clients for the bridge D-Bus API */
        var bridge_dbus = { };

        function process_session_key(key, value) {
            var host, values, machine;
            var parts = key.split("/");
            if (parts[0] == session_prefix &&
                parts.length === 2) {
                    host = parts[1];
                    if (value) {
                        values = JSON.parse(value);
                        machine = machines.lookup(host);
                        if (!machine || !machine.on_disk)
                            machines.overlay(host, values);
                        else if (!machine.visible)
                            machines.change(host, { visible: true });
                        self.connect(host);
                    }
            }
        }

        function load_from_session_storage() {
            var i;
            session_loaded = true;
            for (i = 0; i < window.sessionStorage.length; i++) {
                var k = window.sessionStorage.key(i);
                process_session_key(k, window.sessionStorage.getItem(k));
            }
        }

        function process_session_machines(ev) {
            if (ev.storageArea === window.sessionStorage)
                process_session_key(ev.key || "", ev.newValue);
        }
        window.addEventListener("storage", process_session_machines);

        function state(host, value, problem) {
            var values = { state: value, problem: problem };
            if (value == "connected") {
                values.restarting = false;
            } else if (problem) {
                values.manifests = null;
                values.checksum = null;
            }
            machines.overlay(host, values);
        }

        $(machines).on("added", updated);
        $(machines).on("updated", updated);
        $(machines).on("removed", removed);

        function updated(ev, machine, host, old_conns) {
            if (!machine) {
                machine = machines.lookup(host);
                if (!machine)
                    return;
            }

            var props = proxies[host];
            if (!props || !props.valid)
                props = { };

            var overlay = { };

            if (!machine.color)
                overlay.color = machines.unused_color();

            var label = props.PrettyHostname || props.StaticHostname;
            if (label && label !== machine.label)
                overlay.label = label;

            var os = props.OperatingSystemPrettyName;
            if (os && os != machine.os)
                overlay.os = props.OperatingSystemPrettyName;

            if (!$.isEmptyObject(overlay))
                machines.overlay(host, overlay);

            /* Don't automatically reconnect failed machines */
            if (machine.visible) {
                if (old_conns && machine.connection_string != old_conns) {
                    cockpit.kill(old_conns);
                    self.disconnect(host);
                    self.connect(host);
                } else if (!machine.problem) {
                    self.connect(host);
                }
            } else {
                self.disconnect(host);
            }
        }

        function removed(ev, machine, host) {
            self.disconnect(host);
        }

        self.connect = function connect(host) {
            var machine = machines.lookup(host);
            if (!machine)
                return;

            var channel = channels[host];
            if (channel)
                return;

            var options = {
                host: machine.connection_string,
                payload: "echo"
            };

            if (!machine.on_disk && machine.host_key) {
                options['temp-session'] = false; /* Compatibility option */
                options['session'] = 'shared';
                options['host-key'] = machine.host_key;
                options['unknown-host'] = "1";
            }

            channel = cockpit.channel(options);
            channels[host] = channel;

            var local = host === "localhost";

            /* Request is null, and message is true when connected */
            var request = null;
            var open = local;
            var problem = null;

            var url;
            if (!machine.manifests) {
                if (machine.checksum)
                    url = "../../" + machine.checksum + "/manifests.json";
                else
                    url = "../../@" + encodeURI(machine.connection_string) + "/manifests.json";
            }

            function whirl() {
                if (!request && open)
                    state(host, "connected", null);
                else if (!problem)
                    state(host, "connecting", null);
            }

            /* Here we load the machine manifests, and expect them before going to "connected" */
            function request_manifest() {
                request = $.ajax({ url: url, dataType: "json", cache: true})
                    .done(function(manifests) {
                        var overlay = { manifests: manifests };
                        var etag = request.getResponseHeader("ETag");
                        if (etag) /* and remove quotes */
                            overlay.checksum = etag.replace(/^"(.+)"$/, '$1');
                        machines.overlay(host, overlay);
                    })
                    .fail(function(ex) {
                        console.warn("failed to load manifests from " + machine.connection_string + ": " + ex);
                    })
                    .always(function() {
                        request = null;
                        whirl();
                    });
            }

            /* Try to get change notifications via the internal
               /packages D-Bus interface of the bridge.  Not all
               bridges support this API, so we still get the first
               version of the manifests via HTTP in request_manifest.
            */

            function watch_manifests() {
                var dbus = cockpit.dbus(null, { bus: "internal",
                                                host: machine.connection_string
                                              });
                bridge_dbus[host] = dbus;
                dbus.subscribe({ path: "/packages",
                                 interface: "org.freedesktop.DBus.Properties",
                                 member: "PropertiesChanged" },
                               function (path, iface, mamber, args) {
                                   if (args[0] == "cockpit.Packages") {
                                       if (args[1]["Manifests"]) {
                                           var manifests = JSON.parse(args[1]["Manifests"].v);
                                           machines.overlay(host, { manifests: manifests });
                                       }
                                   }
                               });
            }

            function request_hostname() {
                if (!machine.static_hostname) {
                    var proxy = cockpit.dbus("org.freedesktop.hostname1",
                                             { host: machine.connection_string }).proxy();
                    proxies[host] = proxy;
                    proxy.wait(function() {
                        $(proxy).on("changed", function() {
                            updated(null, null, host);
                        });
                        updated(null, null, host);
                    });
                }
            }

            /* Send a message to the server and get back a message once connected */
            if (!local) {
                channel.send("x");

                $(channel)
                    .on("message", function() {
                        open = true;
                        if (url)
                            request_manifest();
                        watch_manifests();
                        request_hostname();
                        whirl();
                    })
                .on("close", function(ev, options) {
                    var m = machines.lookup(host);
                    open = false;
                    // reset to clean state when removing machine (orderly disconnect), otherwise mark as failed
                    if (!options.problem && m && !m.visible)
                        state(host, null, null);
                    else
                        state(host, "failed", options.problem || "disconnected");
                    if (m && m.restarting) {
                        window.setTimeout(function() {
                            self.connect(host);
                        }, 10000);
                    }
                    self.disconnect(host);
                });
            } else {
                if (url)
                    request_manifest();
                watch_manifests();
                request_hostname();
            }

            /* In case already ready, for example when local */
            whirl();
        };

        self.disconnect = function disconnect(host) {
            if (host === "localhost")
                return;

            var channel = channels[host];
            delete channels[host];
            if (channel) {
                channel.close();
                $(channel).off();
            }

            var proxy = proxies[host];
            delete proxies[host];
            if (proxy) {
                proxy.client.close();
                $(proxy).off();
            }

            var dbus = bridge_dbus[host];
            delete bridge_dbus[host];
            if (dbus) {
                dbus.close();
            }
        };

        self.expect_restart = function expect_restart(host) {
            var parts = machines.split_connection_string(host);
            machines.overlay(parts.address, { restarting: true,
                                              problem: null });
        };

        self.close = function close() {
            $(machines).off("added", updated);
            $(machines).off("changed", updated);
            $(machines).off("removed", removed);
            machines = null;

            if (file)
                file.close();
            file = null;

            window.removeEventListener("storage", process_session_machines);
            var hosts = Object.keys(channels);
            hosts.forEach(self.disconnect);
        };

        if (!session_only) {
            var proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Machines", "/machines");
            $(proxy).on("changed", function(data) {
                // unwrap variants from D-Bus call
                var wrapped = proxy.Machines;
                var data_unwrap = {};
                var host_props;
                for (var host in wrapped) {
                    host_props = {};
                    for (var prop in wrapped[host])
                        host_props[prop] = wrapped[host][prop].v;
                    data_unwrap[host] = host_props;
                }

                machines.data(data_unwrap);
                if (!session_loaded)
                    load_from_session_storage();
            });
        } else {
            load_from_session_storage();
            machines.data({});
        }
    }

    mod.instance = function instance(loader) {
        return new Machines();
    };

    mod.loader = function loader(machines, session_only) {
        return new Loader(machines, session_only);
    };

    mod.colors = [
        "#0099d3",
        "#67d300",
        "#d39e00",
        "#d3007c",
        "#00d39f",
        "#00d1d3",
        "#00618a",
        "#4c8a00",
        "#8a6600",
        "#9b005b",
        "#008a55",
        "#008a8a",
        "#00b9ff",
        "#7dff00",
        "#ffbe00",
        "#ff0096",
        "#00ffc0",
        "#00fdff",
        "#023448",
        "#264802",
        "#483602",
        "#590034",
        "#024830",
        "#024848"
    ];

    mod.colors.parse = function parse_color(input) {
        var div = document.createElement('div');
        div.style.color = input;
        var style = window.getComputedStyle(div, null);
        return style.getPropertyValue("color") || div.style.color;
    };

    mod.known_hosts_path = known_hosts_path;

    cockpit.transport.wait(function() {
        var caps = cockpit.transport.options.capabilities || [];
        /* If cockpit-ws is handling ssh, check for each capability. Otherwise
         * the version is new enough that is has them all */
        if ($.inArray("ssh", caps) > -1) {
            mod.allow_connection_string = $.inArray("connection-string", caps) != -1;
            mod.has_auth_results = $.inArray("auth-method-results", caps) != -1;
            known_hosts_path = "/var/lib/cockpit/known_hosts";
            mod.known_hosts_path = known_hosts_path;
            console.debug("Running against legacy ws with ssh, using legacy file", known_hosts_path);
        } else {
            mod.allow_connection_string = true;
            mod.has_auth_results = true;
        }
    });

    module.exports = mod;
}());
