namespace QuickSetup {
    private const string INSTALLED_DRIVERS_DIR = "/usr/share/quick-setup/drivers";
    private const string DEV_DRIVERS_DIR = "data/drivers";
    private const string INSTALLED_DRIVER_HELPER_PATH = "/usr/bin/quick-setup-driver-helper";
    private const string DEV_DRIVER_HELPER_PATH = "scripts/quick-setup-driver-helper";

    public enum DriverPhase {
        PRE_WIFI,
        POST_WIFI
    }

    public string driver_phase_to_string(DriverPhase phase) {
        switch (phase) {
            case DriverPhase.PRE_WIFI:
                return "pre_wifi";
            case DriverPhase.POST_WIFI:
                return "post_wifi";
            default:
                return "pre_wifi";
        }
    }

    public class DriverDefinition : Object {
        public string id { get; set; }
        public string name { get; set; }
        public DriverPhase phase { get; set; }
        public bool requires_reboot { get; set; }
    }

    public class DriverResult : Object {
        public string driver_id { get; set; }
        public string name { get; set; }
        public string status { get; set; }
        public string message { get; set; }
        public string details { get; set; }
        public bool requires_reboot { get; set; }

        public bool is_failure() {
            return this.status == "failed";
        }

        public bool is_success() {
            return this.status == "success";
        }

        public bool is_not_needed() {
            return this.status == "not_needed";
        }
    }

    public class DriverPhaseResult : Object {
        public DriverPhase phase { get; construct set; }
        public List<DriverResult> results;

        public DriverPhaseResult(DriverPhase phase) {
            Object(phase: phase);
            this.results = new List<DriverResult>();
        }

        public bool has_failures() {
            foreach (var result in this.results) {
                if (result.is_failure()) {
                    return true;
                }
            }

            return false;
        }

        public bool has_successes() {
            foreach (var result in this.results) {
                if (result.is_success()) {
                    return true;
                }
            }

            return false;
        }

        public bool requires_reboot() {
            foreach (var result in this.results) {
                if (result.is_success() && result.requires_reboot) {
                    return true;
                }
            }

            return false;
        }

        public string summarize_successes() {
            string[] names = {};

            foreach (var result in this.results) {
                if (result.is_success()) {
                    names += result.name;
                }
            }

            if (names.length == 0) {
                return "No additional drivers were needed.";
            }

            return "Installed " + string.joinv(", ", names) + ".";
        }

        public string summarize_failures() {
            string[] lines = {};

            foreach (var result in this.results) {
                if (!result.is_failure()) {
                    continue;
                }

                string line = result.name;
                if (result.message != null && result.message.strip() != "") {
                    line += ": " + result.message;
                }
                if (result.details != null && result.details.strip() != "") {
                    line += "\n\n" + result.details.strip();
                }
                lines += line;
            }

            return string.joinv("\n", lines);
        }
    }

    public class DriverManager : Object {
        public signal void progress(string driver_name, string stage);
        private string drivers_dir;
        private string helper_path;
        private bool helper_is_installed;
        private List<DriverDefinition> definitions;

        public DriverManager() {
            this.drivers_dir = resolve_drivers_dir();
            this.helper_is_installed = FileUtils.test(INSTALLED_DRIVER_HELPER_PATH, FileTest.EXISTS);
            this.helper_path = this.helper_is_installed ? INSTALLED_DRIVER_HELPER_PATH : DEV_DRIVER_HELPER_PATH;
            this.definitions = load_driver_definitions(this.drivers_dir);
        }

        public bool has_phase_drivers(DriverPhase phase) {
            foreach (var definition in this.definitions) {
                if (definition.phase == phase) {
                    return true;
                }
            }

            return false;
        }

        public async DriverPhaseResult run_phase(DriverPhase phase) {
            var phase_result = new DriverPhaseResult(phase);

            if (!this.has_phase_drivers(phase)) {
                return phase_result;
            }

            if (!FileUtils.test(this.helper_path, FileTest.EXISTS)) {
                phase_result.results.append(create_internal_failure(
                    "Driver Helper",
                    "The driver helper is missing.",
                    this.helper_path
                ));
                return phase_result;
            }

            string stdout_text = "";

            try {
                string[] argv = build_helper_argv(phase);
                var launcher = new SubprocessLauncher(SubprocessFlags.STDOUT_PIPE);

                // Dev mode: the helper hardcodes the system drivers dir so we
                // tell it where the uninstalled manifests live via an env var
                // that the helper only honors when not running as root.
                if (!this.helper_is_installed) {
                    string abs_drivers_dir = File.new_for_path(this.drivers_dir).get_path();
                    if (abs_drivers_dir != null) {
                        launcher.setenv("QUICK_SETUP_DEV_DRIVERS_DIR", abs_drivers_dir, true);
                    }
                }

                var subprocess = launcher.spawnv(argv);
                var stdout_stream = new DataInputStream(subprocess.get_stdout_pipe());
                string? line;
                while ((line = yield stdout_stream.read_line_async()) != null) {
                    if (handle_output_line(line, phase_result)) {
                        stdout_text = line;
                    }
                }
                yield subprocess.wait_async();

                if (!subprocess.get_successful()) {
                    phase_result.results.append(create_internal_failure(
                        "Driver Support",
                        "Driver installation failed to start.",
                        stdout_text.strip()
                    ));
                    return phase_result;
                }
            } catch (Error e) {
                phase_result.results.append(create_internal_failure(
                    "Driver Support",
                    "Quick Setup could not launch the driver helper.",
                    e.message
                ));
                return phase_result;
            }

            if (phase_result.results.length() == 0) {
                phase_result.results.append(create_internal_failure(
                    "Driver Support",
                    "The driver helper returned no results.",
                    stdout_text.strip()
                ));
            }

            return phase_result;
        }

        private bool handle_output_line(string line, DriverPhaseResult phase_result) {
            try {
                var parser = new Json.Parser();
                parser.load_from_data(line);
                var object = parser.get_root().get_object();
                if (object == null) {
                    return false;
                }

                string event_name = get_string_member(object, "event", "result");
                if (event_name == "progress") {
                    this.progress(
                        get_string_member(object, "name", "Driver"),
                        get_string_member(object, "stage", "checking")
                    );
                    return false;
                }

                parse_phase_output(line, phase_result);
                return true;
            } catch (Error e) {
                warning("Ignoring malformed driver helper event: %s", e.message);
                return false;
            }
        }

        // Reboot is intentionally NOT routed through the driver helper. The
        // helper's polkit action is passwordless for unprivileged users; we do
        // not want that same path to grant "reboot whenever" rights. Instead
        // ask logind directly over D-Bus, which already guards Reboot with its
        // own polkit action (org.freedesktop.login1.reboot) that only allows
        // active local sessions.
        public async bool request_reboot(out string details) {
            details = "";

            try {
                var proxy = yield new DBusProxy.for_bus(
                    BusType.SYSTEM,
                    DBusProxyFlags.NONE,
                    null,
                    "org.freedesktop.login1",
                    "/org/freedesktop/login1",
                    "org.freedesktop.login1.Manager",
                    null
                );

                yield proxy.call(
                    "Reboot",
                    new Variant("(b)", false),
                    DBusCallFlags.NONE,
                    -1,
                    null
                );
                return true;
            } catch (Error e) {
                details = e.message;
                return false;
            }
        }

        private string[] build_helper_argv(DriverPhase phase) {
            string phase_name = driver_phase_to_string(phase);
            bool should_use_pkexec = this.helper_is_installed
                && Environment.get_variable("QUICK_SETUP_DRIVER_HELPER_NO_PKEXEC") != "1"
                && FileUtils.test("/usr/bin/pkexec", FileTest.EXISTS);

            if (should_use_pkexec) {
                // pkexec resolves the policy via the program path, so invoke
                // the helper by its absolute installed path (never /usr/bin/python3,
                // which would require a far broader polkit rule).
                return {
                    "/usr/bin/pkexec",
                    INSTALLED_DRIVER_HELPER_PATH,
                    "--phase",
                    phase_name
                };
            }

            return {
                "/usr/bin/python3",
                this.helper_path,
                "--phase",
                phase_name
            };
        }

        private void parse_phase_output(string stdout_text, DriverPhaseResult phase_result) {
            try {
                var parser = new Json.Parser();
                parser.load_from_data(stdout_text);

                var root_object = parser.get_root().get_object();
                if (root_object == null || !root_object.has_member("results")) {
                    phase_result.results.append(create_internal_failure(
                        "Driver Support",
                        "The driver helper returned malformed output.",
                        stdout_text.strip()
                    ));
                    return;
                }

                var results_array = root_object.get_array_member("results");
                for (uint i = 0; i < results_array.get_length(); i++) {
                    var result_obj = results_array.get_object_element(i);
                    if (result_obj == null) {
                        continue;
                    }

                    var result = new DriverResult();
                    result.driver_id = get_string_member(result_obj, "driver_id", "");
                    result.name = get_string_member(result_obj, "name", result.driver_id);
                    result.status = get_string_member(result_obj, "status", "failed");
                    result.message = get_string_member(result_obj, "message", "");
                    result.details = get_string_member(result_obj, "details", "");
                    result.requires_reboot = get_bool_member(result_obj, "requires_reboot", false);
                    phase_result.results.append(result);
                }
            } catch (Error e) {
                phase_result.results.append(create_internal_failure(
                    "Driver Support",
                    "Quick Setup could not parse the driver helper output.",
                    e.message
                ));
            }
        }

        private DriverResult create_internal_failure(string name, string message, string details) {
            var result = new DriverResult();
            result.driver_id = "driver-helper";
            result.name = name;
            result.status = "failed";
            result.message = message;
            result.details = details;
            result.requires_reboot = false;
            return result;
        }

        private static string resolve_drivers_dir() {
            if (FileUtils.test(INSTALLED_DRIVERS_DIR, FileTest.IS_DIR)) {
                return INSTALLED_DRIVERS_DIR;
            }

            return DEV_DRIVERS_DIR;
        }

        private static List<DriverDefinition> load_driver_definitions(string directory_path) {
            var definitions = new List<DriverDefinition>();

            if (!FileUtils.test(directory_path, FileTest.IS_DIR)) {
                return definitions;
            }

            try {
                var dir = Dir.open(directory_path, 0);
                string? entry_name;

                while ((entry_name = dir.read_name()) != null) {
                    if (!entry_name.has_suffix(".json")) {
                        continue;
                    }

                    string file_path = Path.build_filename(directory_path, entry_name);
                    var definition = load_driver_definition(file_path);
                    if (definition != null) {
                        definitions.append(definition);
                    }
                }
            } catch (Error e) {
                warning("Failed to read driver directory %s: %s", directory_path, e.message);
            }

            return definitions;
        }

        private static DriverDefinition? load_driver_definition(string file_path) {
            try {
                var parser = new Json.Parser();
                parser.load_from_file(file_path);

                var root_object = parser.get_root().get_object();
                if (root_object == null) {
                    return null;
                }

                if (!get_bool_member(root_object, "enabled", true)) {
                    return null;
                }

                string id = get_string_member(root_object, "id", "");
                string name = get_string_member(root_object, "name", id);
                string phase_name = get_string_member(root_object, "phase", "pre_wifi");

                if (id == "" || name == "") {
                    warning("Skipping invalid driver manifest %s", file_path);
                    return null;
                }

                var definition = new DriverDefinition();
                definition.id = id;
                definition.name = name;
                definition.phase = parse_phase(phase_name);
                definition.requires_reboot = get_bool_member(root_object, "requires_reboot", false);
                return definition;
            } catch (Error e) {
                warning("Failed to load driver manifest %s: %s", file_path, e.message);
                return null;
            }
        }

        private static DriverPhase parse_phase(string phase_name) {
            if (phase_name == "post_wifi") {
                return DriverPhase.POST_WIFI;
            }

            return DriverPhase.PRE_WIFI;
        }

        private static string get_string_member(Json.Object object, string member_name, string fallback) {
            if (!object.has_member(member_name)) {
                return fallback;
            }

            return object.get_string_member(member_name);
        }

        private static bool get_bool_member(Json.Object object, string member_name, bool fallback) {
            if (!object.has_member(member_name)) {
                return fallback;
            }

            return object.get_boolean_member(member_name);
        }
    }
}
