#include "inc/httplib.h"
#include "inc/zmqhelper.hpp"
#include "inc/json.hpp"
#include "inc/fs.h"
#include "spdlog/spdlog.h"
#include "fmt/format.h"
#include "database.h"
#include <thread>
#include <future>
#include <regex>
#include <cstdio>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <array>

std::string exec(const char* cmd) {
    std::array<char, 128> buffer;
    std::string result;
    std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);
    if (!pipe) {
        // throw std::runtime_error("popen() failed!");
        return result;
    }
    while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
        result += buffer.data();
    }
    return result;
}

using namespace std;
using namespace nlohmann;
using namespace httplib;


class WifiMgr {
    private:
    json info;
    string devSn;
    string baseDir = "web/main/dist";
    Server srv;
    promise<int> p;
    thread monitor;
    json wifiData;
    int mode, lastMode; // 0:no; network 1: ap; 2: ste
    mutex mutMode;
    int mode1Cnt = 0;
    const string apdCfgPath = "/etc/apd.conf";
    const string wpaCfgPath = "/etc/wpa_supplicant/wpa_supplicant-wlan1.conf";

    void scanWifi(){
        lock_guard<mutex> lk(mutMode);

        /// get sn
        LVDB::getSn(this->info);
        devSn = this->info["sn"];

        /// get wifi mac & IP
        auto mac = exec("ifconfig wlan1|grep ether|awk '{print $2}'");
        auto ip = exec("ifconfig wlan1|grep -v inet6|grep inet|awk '{print $2}'");
        wifiData["wifi"]["ip"] = ip;
        wifiData["wifi"]["mac"] = mac;
        spdlog::info("evwifi {} ip: {}, mac: {}", this->devSn, ip, mac);
        
        /// get connected wifi ssid
        if(!ip.empty() && ip != "192.168.0.1"){
            auto ssid = exec("grep ssid /etc/wpa_supplicant/wpa_supplicant-wlan1.conf 2> /dev/null|awk '{print substr($1, 6)}'");
            if(ssid.size() >=3) {
                ssid = ssid.substr(1, ssid.size() - 2);
                wifiData["wifi"]["ssid"] = ssid;
                spdlog::info("evwifi {} ssid: {}", this->devSn, ssid);
            }else{
                if(wifiData["wifi"].count("ssid") != 0) {
                    wifiData["wifi"].erase("ssid");
                }
            }

            auto password = exec("grep psk /etc/wpa_supplicant/wpa_supplicant-wlan1.conf 2> /dev/null|awk '{print substr($1, 5)}'");
            if(password.size() >=3) {
                password = password.substr(1, password.size() - 2);
                wifiData["wifi"]["password"] = password;
                spdlog::info("evwifi {} password: {}", this->devSn, password);
            }else{
                if(wifiData["wifi"].count("password") != 0) {
                    wifiData["wifi"].erase("password");
                }
            }
        }

        // scan wifi
        string res = exec("iwlist wlan1 scan|grep ESSID|awk {'print $1'}");
        wifiData["wifi"]["ssids"].clear();
        httplib::detail::split(&res[0], &res[res.size()], '\n', [&](const char *b, const char *e) {
            string ssid;
            ssid.assign(b,e);
            wifiData["wifi"]["ssids"].push_back(ssid);
        });
    }

    json enableMode(int mode){
        lock_guard<mutex> lk(mutMode);
        json ret;
        ret["code"] = 0;
        ret["msg"] = "ok";
        this->mode1Cnt = 0;

        if( mode == 1) {
            // ap
            this->mode = 1;
            spdlog::info("evwifi {} prepare to enter AP mode", devSn);
            // exec("systemctl dsiable wpa_supplicant@wlan1 ")
            string apdContent = fmt::format("interface=wlan1\ndriver=nl80211\nssid=EVB-{}\nhw_mode=g\n"
            "channel=6\nmacaddr_acl=0\nignore_broadcast_ssid=0\nwpa=0\n", this->info["sn"].get<string>());
            ofstream fileApd(apdCfgPath, ios::out|ios::trunc);
            if(fileApd.is_open()){
                fileApd << apdContent;
                fileApd.close();
                // start hostapd
                auto t = thread([this](){
                    system("pkill hostapd;systemctl stop wpa_supplicant@wlan1;ifconfig wlan1 down;"
                    "ifconfig wlan1 up;ifconfig wlan1 192.168.0.1;hostapd /etc/apd.conf -B");
                    // TODO: check result
                });
                t.detach();
            }else{
                ret["code"] = 1;
                string msg = fmt::format("failed to write ap config file to {}", apdCfgPath);
                spdlog::error("evwifi {} {}", devSn,msg);
                ret["msg"] = msg;
            }
        }else if(mode == 2) {
            // station mode
            this->mode = 2;
            spdlog::info("evwifi {} prepare to enter Station mode", devSn);
            if( wifiData["wifi"].count("ssid") == 0 ||  wifiData["wifi"]["ssid"].size() == 0 ||
             wifiData["wifi"].count("password") == 0 ||  wifiData["wifi"]["password"].size() == 0) {
                 string msg = fmt::format("no valid ssid/password provided");
                 spdlog::error("evwifi {} {}", devSn, msg);
                 ret["msg"] = msg;
                 ret["code"] = 3;
             }
             else{
                string wpaContent = fmt::format("ctrl_interface=/run/wpa_supplicant\nupdate_config=1\nap_scan=1\n"
                "network={{\nssid=\"{}\"\npsk=\"{}\"\n}}\n", this->wifiData["wifi"]["ssid"].get<string>(), this->wifiData["wifi"]["password"].get<string>());
                ofstream wpaFile(wpaCfgPath, ios::out|ios::trunc);
                if(wpaFile.is_open()){
                    wpaFile << wpaContent;
                    wpaFile.close();
                    // TODO: verify
                    auto t = thread([this](){
                        // delay for rest return (ifdown caused no networking available)
                        this_thread::sleep_for(chrono::seconds(1));
                        system("pkill hostapd; pkill dhclient;systemctl enable wpa_supplicant@wlan1;systemctl restart wpa_supplicant@wlan1;"
                        "/sbin/ifdown -a --read-environment; /sbin/ifup -a --read-environment");
                        // check status
                        auto s = exec("ifconfig wlan1|grep -v inet6|grep inet");
                        if(s.empty()) {
                            spdlog::error("evwifi {} failed to connect to wifi {} with password {}. initiazing AP mode", this->devSn, 
                            this->wifiData["wifi"]["ssid"].get<string>(), this->wifiData["wifi"]["password"].get<string>());
                            this->mode = 0;
                        }else{
                            system("systemctl restart evdaemon");
                            spdlog::info("evwifi {} successfully connected to wifi {}", this->devSn, this->wifiData["wifi"]["ssid"].get<string>());
                        }
                    });
                    t.detach();
                }else{
                    string msg = fmt::format("failed write wpa config to {}", wpaCfgPath);
                    ret["code"] = 2;
                    ret["msg"] = msg;
                    spdlog::error("evwifi {} {}", devSn, msg);
                }
             }
        }

        return ret;
    }

    public:
    WifiMgr(){
        LVDB::getSn(this->info);
        devSn = this->info["sn"];
        mode = 0;
        wifiData["info"] = this->info;
        wifiData["wifi"] = json();
        wifiData["wifi"]["ssids"] = json();
        //wifiData["wifi"]["ssid"] = string;
        //wifiData["wifi"]["password"] = string;

        monitor = thread([this](){            
            while(1){
                /// background wifi scanning
                if(this->mode != 2) {
                    this->scanWifi();
                }
                // TODO: flash light
                string ip, ssid, password;

                if(this->wifiData["wifi"].count("ip") != 0) {
                    ip = this->wifiData["wifi"]["ip"];
                }
                if(this->wifiData["wifi"].count("ssid") != 0) {
                    ssid = this->wifiData["wifi"]["ssid"];
                }

                if(this->wifiData["wifi"].count("password") != 0) {
                    password = this->wifiData["wifi"]["password"];
                }

                {
                    // auto s = exec("ifconfig wlan1|grep -v inet6|grep inet|awk '{print $2}'");
                    
                    if(ip.empty()) {
                        // switch to ap automatically
                        if(this->mode == 0) {
                            spdlog::info("evwifi {} detects no wifi connection, enabling AP mode", this->devSn);
                            this->enableMode(1);
                        }else if(this->mode == 1) {
                            // maybe give it a try to switch into mode2 is not a bad idea
                            this->mode1Cnt++;
                            if(!ssid.empty() && !password.empty() && mode1Cnt % 600){
                                spdlog::info("evwifi {} give it a try to mode2, since configuration exists.", this->devSn);
                                this->enableMode(2);
                            }
                        }
                    }else{
                        // having wifi ip
                        if(ip == "192.168.0.1"){
                            this->mode = 1;
                        }else if(!ssid.empty() && !password.empty()){
                            this->mode = 2;
                        }else{
                            spdlog::info("evwifi {} invalid state(having wifi IP but no config), switch to AP mode", this->devSn);
                            this->enableMode(1);
                        }
                    }
                }
                
                this_thread::sleep_for(chrono::seconds(60));
            }
        });

        monitor.detach();

        //Headers headers = {{'Access-Control-Allow-Origin', '*'}, {'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'}};
        srv.set_base_dir(this->baseDir.c_str());

        srv.Get("/wifi", [this](const Request& req, Response& res) {
            this->mode1Cnt = 0;
            string mode = req.get_param_value("mode");
            json ret;
            ret["code"] = 0;
            ret["msg"] = "ok";
            string scan = req.get_param_value("scan");
            if(!scan.empty()){
                if(scan == "true"){
                    this->scanWifi();
                    ret["wifiData"] = this->wifiData;
                }else{
                    ret["wifiData"] = this->wifiData;
                }
            }

            if(scan.empty() && !mode.empty()){
                try{
                    auto i = stoi(mode);
                    if(i == 2) {
                        lock_guard<mutex> lk(mutMode);
                        string ssid = req.get_param_value("ssid");
                        string password = req.get_param_value("password");
                        if(ssid.empty()||password.empty()){
                            string msg = fmt::format("no valid ssid/password provided");
                            spdlog::error("evwifi {} {}", this->devSn, msg);
                            ret["msg"] = msg;
                            ret["code"] = 3;
                        }else{
                            this->mode = 2;
                            this->wifiData["wifi"]["ssid"] = ssid;
                            this->wifiData["wifi"]["password"] = password;
                        }
                    }

                    if(ret["code"] == 0) {
                        ret = this->enableMode(i);
                    }
                    
                }catch(exception &e){
                    string msg = fmt::format("exception in convert mode {} to int:{}", mode, e.what());
                    ret["code"] = -1;
                    ret["msg"] = msg;
                    spdlog::error("evwifi {} {}", devSn, msg);
                }
            }

            res.set_header("Access-Control-Allow-Origin", "*");
            res.set_header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
            
            res.set_content(ret.dump(), "text/json");
        });

        srv.listen("0.0.0.0", 80);
    }
};

int main(){
    WifiMgr mgr;
}