// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "Setup.hpp"

#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <locale>

#include <everest/run_application/run_application.hpp>

#include <fmt/core.h>

using namespace everest::run_application;

namespace module {

// set WifiConfigureClass to the class to use for configuring WiFi
typedef WpaCliSetup WifiConfigureClass;

void to_json(json& j, const NetworkDeviceInfo& k) {
    j = json::object({{"interface", k.interface},
                      {"wireless", k.wireless},
                      {"blocked", k.blocked},
                      {"rfkill_id", k.rfkill_id},
                      {"ipv4", k.ipv4},
                      {"ipv6", k.ipv6},
                      {"mac", k.mac},
                      {"link_type", k.link_type}});
}

void to_json(json& j, const WifiCredentials& k) {
    j = json::object({{"interface", k.interface}, {"ssid", k.ssid}, {"psk", k.psk}, {"hidden", k.hidden}});
}

void from_json(const json& j, WifiCredentials& k) {
    k.interface = j.at("interface");
    k.ssid = j.at("ssid");
    k.psk = j.at("psk");
    k.hidden = false;
    // optional item
    auto it = j.find("hidden");
    if ((it != j.end() && *it)) {
        k.hidden = true;
    }
}

void to_json(json& j, const InterfaceAndNetworkId& k) {
    j = json::object({{"interface", k.interface}, {"network_id", k.network_id}});
}
void from_json(const json& j, InterfaceAndNetworkId& k) {
    k.interface = j.at("interface");
    k.network_id = j.at("network_id");
}

void to_json(json& j, const SupportedSetupFeatures& k) {
    j = json::object(
        {{"setup_wifi", k.setup_wifi}, {"localization", k.localization}, {"setup_simulation", k.setup_simulation}});
}

void to_json(json& j, const ApplicationInfo& k) {
    j = json::object({{"initialized", k.initialized},
                      {"mode", k.mode},
                      {"default_language", k.default_language},
                      {"current_language", k.current_language},
                      {"release_metadata_file", k.release_metadata_file}});
}

//------------------------------------------------------------------------------
// JSON conversion for WifiConfigureClass types
static void to_json(json& j, const WifiConfigureClass::WifiNetworkStatus& k) {
    j = json::object({{"interface", k.interface},
                      {"network_id", k.network_id},
                      {"ssid", k.ssid},
                      {"connected", k.connected},
                      {"signal_level", k.signal_level}});
}

static void to_json(json& j, const WifiConfigureClass::WifiScan& k) {
    auto flags_array = json::array();
    flags_array = k.flags;
    j = json::object({{"bssid", k.bssid},
                      {"ssid", k.ssid},
                      {"frequency", k.frequency},
                      {"signal_level", k.signal_level},
                      {"flags", flags_array}});
}

//------------------------------------------------------------------------------
void Setup::init() {
    // Set default locale "C" when no locale is set at all
    try {
        std::locale loc("");
    } catch (const std::runtime_error& e) {
        setenv("LC_ALL", "C", 1);
    }
}

void Setup::ready() {
    this->discover_network_thread = std::thread([this]() {
        while (true) {
            if ((this->config.setup_wifi) && (wifi_scan_enabled)) {
                this->discover_network();
            }
            this->publish_hostname();
            std::this_thread::sleep_for(std::chrono::seconds(5));
        }
    });

    this->publish_application_info_thread = std::thread([this]() {
        while (true) {
            this->publish_supported_features();
            this->publish_application_info();
            this->publish_ap_state();
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });

    std::string set_mode_cmd = this->cmd_base + "set_mode";
    this->mqtt.subscribe(set_mode_cmd, [this](const std::string& data) { this->set_mode(data); });

    std::string set_initialized_cmd = this->cmd_base + "set_initialized";
    this->mqtt.subscribe(set_initialized_cmd, [this](const std::string& data) { this->set_initialized(true); });

    std::string reset_initialized_cmd = this->cmd_base + "reset_initialized";
    this->mqtt.subscribe(reset_initialized_cmd, [this](const std::string& data) { this->set_initialized(false); });

    std::string change_default_language_cmd = this->cmd_base + "change_default_language";
    this->mqtt.subscribe(change_default_language_cmd,
                         [this](const std::string& data) { this->set_default_language(data); });

    std::string change_current_language_cmd = this->cmd_base + "change_current_language";
    this->mqtt.subscribe(change_current_language_cmd,
                         [this](const std::string& data) { this->set_current_language(data); });

    std::string get_application_info_cmd = this->cmd_base + "get_application_info";
    this->mqtt.subscribe(get_application_info_cmd,
                         [this](const std::string& data) { this->publish_application_info(); });

    if (this->config.setup_wifi) {
        std::string rfkill_unblock_cmd = this->cmd_base + "rfkill_unblock";
        this->mqtt.subscribe(rfkill_unblock_cmd, [this](const std::string& data) { this->rfkill_unblock(data); });

        std::string rfkill_block_cmd = this->cmd_base + "rfkill_block";
        this->mqtt.subscribe(rfkill_block_cmd, [this](const std::string& data) { this->rfkill_block(data); });

        std::string list_configured_networks_cmd = this->cmd_base + "list_configured_networks";
        this->mqtt.subscribe(list_configured_networks_cmd,
                             [this](const std::string& data) { this->publish_configured_networks(); });

        std::string add_network_cmd = this->cmd_base + "add_network";
        this->mqtt.subscribe(add_network_cmd, [this](const std::string& data) {
            WifiCredentials wifi_credentials = json::parse(data);
            WifiConfigureClass wifi;
            this->add_and_enable_network(wifi_credentials.interface, wifi_credentials.ssid, wifi_credentials.psk,
                                         wifi_credentials.hidden);
            wifi.save_config(wifi_credentials.interface);
            this->publish_configured_networks();
        });

        std::string enable_network_cmd = this->cmd_base + "enable_network";
        this->mqtt.subscribe(enable_network_cmd, [this](const std::string& data) {
            InterfaceAndNetworkId wifi_details = json::parse(data);
            WifiConfigureClass wifi;
            wifi.enable_network(wifi_details.interface, wifi_details.network_id);
            wifi.save_config(wifi_details.interface);
            this->publish_configured_networks();
        });

        std::string disable_network_cmd = this->cmd_base + "disable_network";
        this->mqtt.subscribe(disable_network_cmd, [this](const std::string& data) {
            InterfaceAndNetworkId wifi_details = json::parse(data);
            WifiConfigureClass wifi;
            wifi.disable_network(wifi_details.interface, wifi_details.network_id);
            wifi.save_config(wifi_details.interface);
            this->publish_configured_networks();
        });

        std::string select_network_cmd = this->cmd_base + "select_network";
        this->mqtt.subscribe(select_network_cmd, [this](const std::string& data) {
            InterfaceAndNetworkId wifi_details = json::parse(data);
            WifiConfigureClass wifi;
            wifi.select_network(wifi_details.interface, wifi_details.network_id);
            wifi.save_config(wifi_details.interface);
            this->publish_configured_networks();
        });

        std::string remove_network_cmd = this->cmd_base + "remove_network";
        this->mqtt.subscribe(remove_network_cmd, [this](const std::string& data) {
            InterfaceAndNetworkId wifi_details = json::parse(data);
            WifiConfigureClass wifi;
            wifi.remove_network(wifi_details.interface, wifi_details.network_id);
            wifi.save_config(wifi_details.interface);
            this->publish_configured_networks();
        });

        std::string scan_wifi_cmd = this->cmd_base + "scan_wifi";
        this->mqtt.subscribe(scan_wifi_cmd, [this](const std::string& data) { this->discover_network(); });

        std::string enable_wifi_scanning_cmd = this->cmd_base + "enable_wifi_scanning";
        this->mqtt.subscribe(enable_wifi_scanning_cmd,
                             [this](const std::string& data) { this->wifi_scan_enabled = true; });

        std::string disable_wifi_scanning_cmd = this->cmd_base + "disable_wifi_scanning";
        this->mqtt.subscribe(disable_wifi_scanning_cmd,
                             [this](const std::string& data) { this->wifi_scan_enabled = false; });

        std::string remove_all_networks_cmd = this->cmd_base + "remove_all_networks";
        this->mqtt.subscribe(remove_all_networks_cmd, [this](const std::string& data) {
            this->remove_all_networks();
            this->publish_configured_networks();
        });

        std::string reboot_cmd = this->cmd_base + "reboot";
        this->mqtt.subscribe(reboot_cmd, [this](const std::string& data) { this->reboot(); });

        std::string check_online_status_cmd = this->cmd_base + "check_online_status";
        this->mqtt.subscribe(check_online_status_cmd, [this](const std::string& data) { this->check_online_status(); });

        std::string enable_ap_cmd = this->cmd_base + "enable_ap";
        this->mqtt.subscribe(enable_ap_cmd, [this](const std::string& data) { enable_ap(); });

        std::string disable_ap_cmd = this->cmd_base + "disable_ap";
        this->mqtt.subscribe(disable_ap_cmd, [this](const std::string& data) { disable_ap(); });
    }
}

void Setup::publish_supported_features() {
    SupportedSetupFeatures supported_setup_features;
    supported_setup_features.setup_wifi = this->config.setup_wifi;
    supported_setup_features.localization = this->config.localization;
    supported_setup_features.setup_simulation = this->config.setup_simulation;

    std::string supported_setup_features_var = this->var_base + "supported_setup_features";

    json supported_setup_features_json = supported_setup_features;

    this->mqtt.publish(supported_setup_features_var, supported_setup_features_json.dump());
}

void Setup::publish_application_info() {
    ApplicationInfo application_info;
    application_info.initialized = this->get_initialized();
    application_info.mode = this->get_mode();
    application_info.default_language = this->get_default_language();
    application_info.current_language = this->get_current_language();
    application_info.release_metadata_file = this->info.paths.etc / this->config.release_metadata_file;

    std::string application_info_var = this->var_base + "application_info";

    json application_info_json = application_info;

    this->mqtt.publish(application_info_var, application_info_json.dump());
}

void Setup::publish_hostname() {
    std::string hostname_var = this->var_base + "hostname";

    this->mqtt.publish(hostname_var, this->get_hostname());
}

void Setup::publish_ap_state() {
    std::string ap_state_var = this->var_base + "ap_state";

    auto hostapd_enabled_output = run_application("systemctl", {"is-active", "--quiet", "hostapd"});
    if (hostapd_enabled_output.exit_code == 0) {
        this->ap_state = "enabled";
    } else {
        this->ap_state = "disabled";
    }

    this->mqtt.publish(ap_state_var, this->ap_state);
}

void Setup::set_default_language(std::string language) {
    this->r_store->call_store("everest_localization_default_language", language);
}

std::string Setup::get_default_language() {
    auto language = this->r_store->call_load("everest_localization_default_language");
    if (!std::holds_alternative<std::string>(language)) {
        return "unknown";
    }
    return std::get<std::string>(language);
}

void Setup::set_current_language(const std::string& language) {
    this->current_language = language;
}

std::string Setup::get_current_language() {
    if (this->current_language.empty()) {
        this->current_language = this->get_default_language();
    }

    return this->current_language;
}

void Setup::set_mode(std::string mode) {
    this->r_store->call_store("everest_mode", mode);
}

std::string Setup::get_mode() {
    auto mode = this->r_store->call_load("everest_mode");
    if (!std::holds_alternative<std::string>(mode)) {
        return "unknown";
    }
    return std::get<std::string>(mode);
}

void Setup::set_initialized(bool initialized) {
    this->r_store->call_store("everest_initialized", initialized);
}

bool Setup::get_initialized() {
    if (this->config.initialized_by_default) {
        return true;
    }
    auto initialized = this->r_store->call_load("everest_initialized");
    if (!std::holds_alternative<bool>(initialized)) {
        return false;
    }
    return std::get<bool>(initialized);
}

void Setup::discover_network() {
    std::vector<NetworkDeviceInfo> device_info = this->get_network_devices();

    this->populate_rfkill_status(device_info);
    this->populate_ip_addresses(device_info);

    std::string network_device_info_var = this->var_base + "network_device_info";
    json device_info_json = json::array();
    device_info_json = device_info;
    this->mqtt.publish(network_device_info_var, device_info_json.dump());

    auto wifi_info = this->scan_wifi(device_info);

    std::string wifi_info_var = this->var_base + "wifi_info";
    json wifi_info_json = json::array();
    wifi_info_json = wifi_info;
    this->mqtt.publish(wifi_info_var, wifi_info_json.dump());

    this->publish_configured_networks();
}

std::string Setup::read_type_file(const fs::path& type_path) {
    if (!fs::exists(type_path)) {
        return "";
    }

    std::ifstream ifs(type_path.c_str());
    std::string type_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
    // trim newlines
    type_file.erase(std::remove(type_file.begin(), type_file.end(), '\n'), type_file.end());

    return type_file;
}

std::vector<NetworkDeviceInfo> Setup::get_network_devices() {
    auto sys_net_path = fs::path("/sys/class/net");
    auto sys_virtual_net_path = fs::path("/sys/devices/virtual/net");

    std::vector<NetworkDeviceInfo> device_info;

    for (auto&& net_it : fs::directory_iterator(sys_net_path)) {
        auto net_path = net_it.path();
        auto type_path = net_path / "type";
        if (!fs::exists(type_path)) {
            continue;
        }

        std::string type_file = this->read_type_file(type_path);

        auto interface = net_path.filename();
        auto virtual_interface = sys_virtual_net_path / interface;

        // check if type is ethernet:
        if (type_file == "1") {
            if (fs::exists(virtual_interface)) {
                continue;
            }

            auto device = NetworkDeviceInfo();
            device.interface = interface.string();
            device.link_type = "ether";

            // check if its wireless or not:
            auto wireless_path = net_path / "wireless";
            if (fs::exists(wireless_path)) {
                device.wireless = true;
                auto phy80211_path = net_path / "phy80211";
                for (auto&& rfkill_it : fs::directory_iterator(phy80211_path)) {
                    auto phy_file_path = rfkill_it.path().filename().string();
                    std::string rfkill = "rfkill";
                    if (phy_file_path.find(rfkill) == 0) {
                        phy_file_path.erase(0, rfkill.size());
                        device.rfkill_id = phy_file_path;
                        break;
                    }
                }
            }

            device_info.push_back(device);
        } else if (type_file == "65534") {
            if (!fs::exists(virtual_interface)) {
                continue;
            }

            auto virtual_type_path = virtual_interface / "type";

            if (!fs::exists(virtual_type_path)) {
                continue;
            }

            std::string virtual_type_file = this->read_type_file(virtual_type_path);
            if (virtual_type_file == type_file) {
                // assume it's a vpn, but check ip link
                auto ip_output = run_application("ip", {"--json", "-details", "link", "show", interface});
                if (ip_output.exit_code != 0) {
                    continue;
                }
                const auto ip_json = json::parse(ip_output.output);
                if (ip_json.size() < 1) {
                    continue;
                }
                const auto& entry = ip_json.at(0);
                if (entry.contains("linkinfo") and entry.at("linkinfo").contains("info_kind")) {
                    auto device = NetworkDeviceInfo();
                    device.interface = interface.string();
                    device.link_type = entry.at("linkinfo").at("info_kind");
                    device_info.push_back(device);
                }
            }
        }
    }

    return device_info;
}

void Setup::populate_rfkill_status(std::vector<NetworkDeviceInfo>& device_info) {
    auto rfkill_output = run_application("rfkill", {"--json"});
    if (rfkill_output.exit_code != 0) {
        return;
    }
    auto rfkill_json = json::parse(rfkill_output.output);
    for (auto rfkill_object : rfkill_json.items()) {
        for (auto rfkill_device : rfkill_object.value()) {
            for (auto& device : device_info) {
                int device_id = rfkill_device.at("id");
                if (!device.rfkill_id.empty() && std::to_string(device_id) == device.rfkill_id) {
                    if (rfkill_device.at("soft") == "blocked") {
                        device.blocked = true;
                    }
                    break;
                }
            }
        }
    }
}

bool Setup::rfkill_unblock(std::string rfkill_id) {
    auto network_devices = this->get_network_devices();
    this->populate_rfkill_status(network_devices);
    bool found = false;
    for (auto device : network_devices) {
        if (device.rfkill_id == rfkill_id) {
            if (!device.blocked) {
                return true;
            }
            found = true;
            break;
        }
    }

    if (!found) {
        return false;
    }

    auto rfkill_output = run_application("rfkill", {"unblock", rfkill_id});
    if (rfkill_output.exit_code != 0) {
        return false;
    }

    return true;
}

bool Setup::rfkill_block(std::string rfkill_id) {
    auto network_devices = this->get_network_devices();
    this->populate_rfkill_status(network_devices);
    bool found = false;
    for (auto device : network_devices) {
        if (device.rfkill_id == rfkill_id) {
            if (device.blocked) {
                return true;
            }
            found = true;
            break;
        }
    }

    if (!found) {
        return false;
    }

    auto rfkill_output = run_application("rfkill", {"block", rfkill_id});
    if (rfkill_output.exit_code != 0) {
        return false;
    }

    return true;
}

void Setup::publish_configured_networks() {
    auto network_devices = this->get_network_devices();

    WpaCliSetup::WifiNetworkStatusList all_wifi_networks;

    for (auto device : network_devices) {
        if (!device.wireless) {
            continue;
        }
        WifiConfigureClass wifi;
        auto network_list = wifi.list_networks_status(device.interface);
        for (auto& i : network_list) {
            all_wifi_networks.push_back(std::move(i));
        }
    }

    std::string network_list_var = this->var_base + "configured_networks";
    json configured_networks_json = json::array();
    configured_networks_json = all_wifi_networks;
    this->mqtt.publish(network_list_var, configured_networks_json.dump());
}

bool Setup::add_and_enable_network(const std::string& interface, const std::string& ssid, const std::string& psk,
                                   bool hidden) {
    WifiConfigureClass wifi;

    std::string net_if = interface;
    if (net_if.empty()) {
        EVLOG_warning << "Attempting to add a network without an interface, attempting to use the first one";
        auto network_devices = this->get_network_devices();
        for (auto device : network_devices) {
            if (device.wireless) {
                net_if = device.interface;
                break;
            }
        }
    }

    auto network_id = wifi.add_network(net_if);
    bool bResult = network_id != -1;
    bResult = bResult && wifi.set_network(net_if, network_id, ssid, psk, hidden);
    bResult = bResult && wifi.enable_network(net_if, network_id);
    return bResult;
}

bool Setup::remove_all_networks() {
    auto network_devices = this->get_network_devices();
    std::uint32_t remove_fail = 0;

    for (auto device : network_devices) {
        if (!device.wireless) {
            continue;
        }

        WifiConfigureClass wifi;
        auto networks = wifi.list_networks(device.interface);

        for (auto network : networks) {
            if (!wifi.remove_network(device.interface, network.network_id)) {
                remove_fail++;
            }
        }

        wifi.save_config(device.interface);
    }

    return remove_fail == 0;
}

bool Setup::reboot() {
    bool success = true;
    auto reboot_output = run_application("systemctl", {"reboot"});
    if (reboot_output.exit_code != 0) {
        success = false;
    }
    return success;
}

bool Setup::is_online() {
    bool success = true;
    auto reboot_output = run_application("ping", {"-c", "1", this->config.online_check_host});
    if (reboot_output.exit_code != 0) {
        success = false;
    }
    return success;
}

void Setup::check_online_status() {
    std::string online_status_var = this->var_base + "online_status";

    if (this->is_online()) {
        this->mqtt.publish(online_status_var, "online");
    } else {
        this->mqtt.publish(online_status_var, "offline");
    }
}

void Setup::enable_ap() {
    auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "disconnect"});
    if (wpa_cli_output.exit_code != 0) {
        EVLOG_error << "Could not disconnect from wireless LAN";
    }
    auto start_hostapd_output = run_application("systemctl", {"start", "hostapd"});
    if (start_hostapd_output.exit_code != 0) {
        EVLOG_error << "Could not start hostapd";
    }
    auto start_dnsmasq_output = run_application("systemctl", {"start", "dnsmasq"});
    if (start_dnsmasq_output.exit_code != 0) {
        EVLOG_error << "Could not start dnsmasq";
    }
    auto add_static_ip_output =
        run_application("ip", {"addr", "add", this->config.ap_ipv4, "dev", this->config.ap_interface});
    if (add_static_ip_output.exit_code != 0) {
        EVLOG_error << "Could not add static ip to interface " << this->config.ap_interface;
    }
}

void Setup::disable_ap() {
    auto del_static_ip_output =
        run_application("ip", {"addr", "del", this->config.ap_ipv4, "dev", this->config.ap_interface});
    if (del_static_ip_output.exit_code != 0) {
        EVLOG_error << "Could not del static ip " << this->config.ap_ipv4 << " from interface "
                    << this->config.ap_interface;
    }
    auto stop_dnsmasq_output = run_application("systemctl", {"stop", "dnsmasq"});
    if (stop_dnsmasq_output.exit_code != 0) {
        EVLOG_error << "Could not stop dnsmasq";
    }
    auto stop_hostapd_output = run_application("systemctl", {"stop", "hostapd"});
    if (stop_hostapd_output.exit_code != 0) {
        EVLOG_error << "Could not stop hostapd";
    }

    auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "reconnect"});
    if (wpa_cli_output.exit_code != 0) {
        EVLOG_error << "Could not reconnect to wireless LAN";
    }
}

static void add_addr_infos_to_device(const json& addr_infos, NetworkDeviceInfo& device) {
    for (const auto& addr_info : addr_infos) {
        if (addr_info.at("family") == "inet") {
            device.ipv4.push_back(addr_info.at("local"));
        } else if (addr_info.at("family") == "inet6") {
            device.ipv6.push_back(addr_info.at("local"));
        }
    }
}

void Setup::populate_ip_addresses(std::vector<NetworkDeviceInfo>& device_info) {
    auto ip_output = run_application("ip", {"--json", "address", "show"});
    if (ip_output.exit_code != 0) {
        return;
    }
    const auto ip_json = json::parse(ip_output.output);
    for (const auto& ip_object : ip_json) {
        const std::string ifname = ip_object.at("ifname");
        auto device = std::find_if(device_info.begin(), device_info.end(),
                                   [&ifname](NetworkDeviceInfo& device) { return device.interface == ifname; });
        if (device == device_info.end()) {
            continue;
        }
        if (ip_object.contains("address")) {
            device->mac = ip_object.at("address");
        }
        add_addr_infos_to_device(ip_object.at("addr_info"), *device);
    }
}

WifiConfigureClass::WifiScanList Setup::scan_wifi(const std::vector<NetworkDeviceInfo>& device_info) {
    WifiConfigureClass::WifiScanList wifi_info;
    WifiConfigureClass wifi;

    for (auto device : device_info) {
        if (!device.wireless) {
            continue;
        }

        auto dev_list = wifi.scan_wifi(device.interface);
        wifi_info.insert(wifi_info.end(), dev_list.begin(), dev_list.end());
    }

    return wifi_info;
}

std::string Setup::get_hostname() {
    auto hostname_output = run_application("hostname", {});
    if (hostname_output.exit_code == 0 && hostname_output.split_output.size() > 0) {
        return hostname_output.split_output.at(0);
    }
    return "";
}

} // namespace module
