#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <thread>

#include "utils.h"
#include "json/parser.h"
#include "manager.h"
#include "http.h"

// declare of scoped vars and funcs
namespace hm {
    int gServerId = 0;
    std::map<std::string, std::string> _gDefaultHeader = {  // NOLINT
            {"Origin",                       "HM-Tech CPP"},
            {"Server",                       "TinWeb"},
            {"Cross-Origin-Resource-Policy", "*/*"},
            {"Content-Type",                 "application/json"},
            {"Content-Length",               "2"},
    };
    struct Request {
        std::string method, uri, ver = "HTTP/1.1", unknown;
        json::Value json;
        std::map<std::string, std::string> headers, form, args;
    };

    struct Response {
        std::string status, msg, data;
        std::map<std::string, std::string> headers;

        std::string body_data() const;    // NOLINT

        void response(const int &client) const;
    };

    Response Success(const std::string &data);

    Response Fail(const std::string &msg);

    std::string vec2str(const std::vector<std::string> &vec);

    void RequestHandler(const int &client);

    Response httpGetFaceDevices(Request &request, const int &client);

    Response httpGetFaceDeviceUsers(Request &request, const int &client);

    Response httpInsertFaceDeviceUser(Request &request, const int &client);

    Response httpRemoveFaceDeviceUser(Request &request, const int &client);

    Response httpGetCarDevices(Request &request, const int &client);

    Response httpGetCarDeviceWoBList(Request &request, const int &client);

    Response httpInsertCarDeviceWoBList(Request &request, const int &client);

    Response httpRemoveCarDeviceWoBList(Request &request, const int &client);
}

// implement of scoped funcs
namespace hm {
    std::string Response::body_data() const {
        /* 28: {"status":,"msg":"","data":}
         * */
        size_t size = status.size() + msg.size() + data.size() + 28;
        std::string res(size, 0);
        sprintf(
                res.data(), R"({"status":%s,"msg":"%s","data":%s})",
                status.c_str(), msg.c_str(), data.c_str()
        );
        return res;
    }

    void Response::response(const int &client) const {
        std::string res("HTTP/1.1 200 OK\r\n");
        for (auto &it : headers) res.append(it.first + ": " + it.second + "\r\n");
        res.append("\r\n" + body_data());
        send(client, res.c_str(), res.size(), 0);
    }

    Response Success(const std::string &data) {
        Response resp = {.status="true", .msg="success", .data=data, .headers=_gDefaultHeader};
        resp.headers["Content-Length"] = std::to_string(resp.body_data().size());
        return resp;
    }

    Response Fail(const std::string &msg) {
        Response resp = {.status="false", .msg=msg, .data="null", .headers=_gDefaultHeader};
        resp.headers["Content-Length"] = std::to_string(resp.body_data().size());
        return resp;
    }

    std::string vec2str(const std::vector<std::string> &vec) {
        std::string res;
        for (auto &it : vec) res.append(',' + it);
        if (res.empty()) res = "[]";
        else res[0] = '[', res.push_back(']');
        return res;
    }

    void RequestHandler(const int &client) {
        size_t size = 1024, read;
        char charBuf[size];
        std::string strBuf;
        read = recv(client, charBuf, size, 0);
        if (read <= 0) {
            Fail("Unrecognized Request").response(client);
            close(client);
            return;
        }
        strBuf.append(charBuf, read);
        Request request;
        // request line
        size_t pos = strBuf.find(' ');
        request.method = strBuf.substr(0, pos);
        strBuf.erase(0, pos + 1);
        pos = strBuf.find(' ');
        request.uri = strBuf.substr(0, pos);
        if (request.uri.find('?') != std::string::npos) {
            request.uri = request.uri.substr(0, request.uri.find('?'));
            // TODO: parse url args
        }
        strBuf.erase(0, pos + 1);
        pos = strBuf.find("\r\n");
        request.ver = strBuf.substr(0, pos);
        strBuf.erase(0, pos + 2);
        // header
        pos = strBuf.find("\r\n\r\n");
        if (pos == std::string::npos) {
            while (true) {
                read = recv(client, charBuf, size, 0);
                strBuf.append(charBuf, read);
                pos = strBuf.find("\r\n\r\n");
                if (pos != std::string::npos) break;
            }
        }
        std::string header = strBuf.substr(0, pos + 2), key;
        strBuf.erase(0, pos + 4);
        while (true) {
            pos = header.find(':');
            if (pos == std::string::npos) break;
            key = header.substr(0, pos);
            header.erase(0, pos + 2);
            pos = header.find('\r');
            request.headers[key] = header.substr(0, pos);
            header.erase(0, pos + 2);
        }
        // body
        if (request.headers.find("Content-Length") != request.headers.end()) {
            size_t rest = stoi(request.headers["Content-Length"]) - strBuf.size();
            while (rest > 0) {
                read = recv(client, charBuf, size, 0);
                if (read <= 0) {
                    Fail("Error Content Body").response(client);
                    close(client);
                    return;
                }
                rest -= read;
                strBuf.append(charBuf, read);
            }
        }
        // form-data only
        if (request.headers.find("Content-Type") != request.headers.end()) {
            if (startsWith(request.headers["Content-Type"], "multipart/form-data")) {
                std::string bond = request.headers["Content-Type"].substr(30);
                while (true) {
                    if (strBuf.find(bond) == std::string::npos) break;
                    strBuf.erase(0, strBuf.find("name=") + 6);
                    key = strBuf.substr(0, strBuf.find('"'));
                    strBuf.erase(0, strBuf.find("\r\n\r\n") + 4);
                    pos = strBuf.find(bond);
                    request.form[key] = strBuf.substr(0, pos - 4);
                    strBuf.erase(0, pos + bond.size());
                }
                strBuf.clear();
            } else if (request.headers["Content-Type"] == "application/json") {
                try {
                    json::Parser parser;
                    request.json = parser.parse(strBuf);
                    strBuf.clear();
                } catch (std::exception &error) {
                    Fail(error.what()).response(client);
                    close(client);
                    return;
                }
            } else {
                Fail("Unrecognized Content Type").response(client);
                close(client);
                return;
            }
        } else {
            if (!strBuf.empty()) {
                Fail("Content-Type is needed if content is not null").response(client);
                strBuf.clear();
                close(client);
                return;
            }
        }

        Response resp;
        if (request.uri == "/getDevices") resp = httpGetFaceDevices(request, client);
        else if (request.uri == "/getDeviceUsers") resp = httpGetFaceDeviceUsers(request, client);
        else if (request.uri == "/addDeviceUser") resp = httpInsertFaceDeviceUser(request, client);
        else if (request.uri == "/delDeviceUser") resp = httpRemoveFaceDeviceUser(request, client);
        else if (request.uri == "/getCarDevices") resp = httpGetCarDevices(request, client);
        else if (request.uri == "/getCarDeviceWoBList") resp = httpGetCarDeviceWoBList(request, client);
        else if (request.uri == "/addCarDeviceWoBList") resp = httpInsertCarDeviceWoBList(request, client);
        else if (request.uri == "/delCarDeviceWoBList") resp = httpRemoveCarDeviceWoBList(request, client);
        else {
            resp = Fail("API Not Found");
            resp.response(client);
        }
        Log(
                Info, "method: [%s], uri: [%s], status: %s",
                request.method.c_str(), request.uri.c_str(), resp.status.c_str()
        );
        close(client);
    }

    Response httpGetFaceDevices(Request &request, const int &client) {
        Response resp;  // GET/POST
        std::string msg;
        std::vector<std::string> devices;
        if (managerGetFaceDevices(devices, msg)) resp = Success(vec2str(devices));
        else resp = Fail(msg);
        resp.response(client);
        return resp;
    }

    Response httpGetFaceDeviceUsers(Request &request, const int &client) {
        Response resp;  // GET/POST
        if (request.form.find("seq") == request.form.end()) resp = Fail("seq need");
        else {
            std::vector<std::string> users;
            std::string msg;
            if (managerGetFaceDeviceUsers(request.form["seq"], users, msg)) resp = Success(vec2str(users));
            else resp = Fail(msg);
        }
        resp.response(client);
        return resp;
    }

    Response httpInsertFaceDeviceUser(Request &request, const int &client) {
        Response resp;  // POST
        if (request.method == "GET") resp = Fail("POST method only");
        else if (
                request.form.find("seq") == request.form.end() ||
                request.form.find("uid") == request.form.end() ||
                request.form.find("name") == request.form.end() ||
                request.form.find("password") == request.form.end() ||
                request.form.find("timestamp") == request.form.end() ||
                request.form.find("face") == request.form.end()
                ) {
            resp = Fail("seq, uid, name, password, timestamp, face[file] need");
        } else {
            try {
                User user = {
                        .uid = request.form["uid"],
                        .name = request.form["name"],
                        .password = request.form["password"],
                        .face = request.form["face"],
                        .timestamp = std::stol(request.form["timestamp"])
                };
                if (user.face.size() > 200 * 1024) resp = Fail("image is too large");
                else {
                    std::string msg;
                    if (managerInsertFaceDeviceUser(request.form["seq"], user, msg)) {
                        resp = Success("null");
                    } else resp = Fail(msg);
                }
            } catch (...) {
                resp = Fail("param type error");
            }
        }
        resp.response(client);
        return resp;
    }

    Response httpRemoveFaceDeviceUser(Request &request, const int &client) {
        Response resp;  // POST
        if (request.method == "GET") resp = Fail("POST method only");
        else if (
                request.form.find("seq") == request.form.end() ||
                request.form.find("uid") == request.form.end()
                ) {
            resp = Fail("seq, uid, name, year, month, day, face[file] need");
        } else {
            std::string msg;
            if (managerRemoveFaceDeviceUser(request.form["seq"], request.form["uid"], msg)) {
                resp = Success("null");
            } else resp = Fail(msg);
        }
        resp.response(client);
        return resp;
    }

    Response httpGetCarDevices(Request &request, const int &client) {
        Response resp;  // GET/POST
        std::string msg;
        std::vector<std::string> devices;
        if (managerGetCarDevices(devices, msg)) resp = Success(vec2str(devices));
        else resp = Fail(msg);
        resp.response(client);
        return resp;
    }

    Response httpGetCarDeviceWoBList(Request &request, const int &client) {
        Response resp;  // GET/POST
        try {
            auto dict = request.json.value<json::dict_type>();
            if (dict.find("seq") == dict.end() || dict.find("isBlack") == dict.end()) {
                resp = Fail("seq, isBlack need");
            } else {
                std::vector<std::string> plates;
                std::string msg, seq = dict["seq"].value<std::string>();
                if (managerGetCarDeviceWoBList(seq, dict["isBlack"].value<bool>(), plates, msg))
                    resp = Success(vec2str(plates));
                else resp = Fail(msg);
            }
        } catch (std::exception &error) {
            resp = Fail(error.what());
        }
        resp.response(client);
        return resp;
    }

    Response httpInsertCarDeviceWoBList(Request &request, const int &client) {
        Response resp;  // POST
        if (request.method == "GET") resp = Fail("POST method only");
        try {
            auto dict = request.json.value<json::dict_type>();
            if (
                    dict.find("seq") == dict.end() ||
                    dict.find("isBlack") == dict.end() ||
                    dict.find("plates") == dict.end()
                    ) {
                resp = Fail("seq, isBlack, plates need in the first layer");
            } else {
                std::string seq = dict["seq"].value<std::string>(), msg;
                bool isBlack = dict["isBlack"].value<bool>(), errHpn = false;
                auto platesJson = dict["plates"].value<json::list_type>();
                std::vector<Plate> plates(platesJson.size());
                for (int i = 0; i < platesJson.size(); ++i) {
                    auto dr = platesJson[i].value<json::dict_type>();
                    if (
                            dr.find("name") == dr.end() ||
                            dr.find("plate") == dr.end() ||
                            dr.find("timestamp") == dr.end()
                            ) {
                        resp = Fail("name, plate, timestamp need in plates list");
                        errHpn = true;
                        break;
                    } else {
                        plates[i] = {
                                .name = dr["name"].value<std::string>(), .plate = dr["plate"].value<std::string>(),
                                .timestamp = dr["timestamp"].value<json::int_type>()
                        };
                    }
                }
                if (!errHpn) {
                    if (managerInsertCarDeviceWoBList(seq, isBlack, plates, msg)) {
                        resp = Success(msg);
                    } else resp = Fail(msg);
                }
            }
        } catch (std::exception &error) {
            resp = Fail(error.what());
        }
        resp.response(client);
        return resp;
    }

    Response httpRemoveCarDeviceWoBList(Request &request, const int &client) {
        Response resp;  // POST
        if (request.method == "GET") resp = Fail("POST method only");
        try {
            auto dict = request.json.value<json::dict_type>();
            if (
                    dict.find("seq") == dict.end() ||
                    dict.find("isBlack") == dict.end() ||
                    dict.find("cids") == dict.end()
                    ) {
                resp = Fail("seq, isBlack, cids need");
            } else {
                std::string seq = dict["seq"].value<std::string>(), msg;
                bool isBlack = dict["isBlack"].value<bool>();
                auto cidsJson = dict["cids"].value<json::list_type>();
                std::vector<int> cids(cidsJson.size());
                for (int i = 0; i < cidsJson.size(); ++i) cids[i] = (int)cidsJson[i].value<json::int_type>();
                if (managerRemoveCarDeviceWoBList(seq, isBlack, cids, msg)) {
                    resp = Success(msg);
                } else resp = Fail(msg);
            }
        } catch (std::exception &error) {
            resp = Fail(error.what());
        }
        resp.response(client);
        return resp;
    }
}

// implement of export funcs
namespace hm {
    [[noreturn]] void StartHttpServer() {
        gServerId = socket(AF_INET, SOCK_STREAM, 0);
        if (gServerId == -1) {
            LogM(Error, "can't start http server: 1");
            throw "can't start http server: 1";  // NOLINT
        }

        sockaddr_in serverAddress{};
        serverAddress.sin_family = AF_INET;
        serverAddress.sin_addr.s_addr = INADDR_ANY;
        serverAddress.sin_port = htons(gConfServerPort);

        if (bind(gServerId, (struct sockaddr *) &serverAddress, sizeof(serverAddress)) == -1) {
            LogM(Error, "can't listen at port<%d>", gConfServerPort);
            throw "can't listen at port";  // NOLINT
        }

        if (listen(gServerId, 5) == -1) {
            LogM(Error, "can't start http server: 2");
            close(gServerId);
            throw "can't start http server: 2";  // NOLINT
        }
        Log(Info, "server running on: http://0.0.0.0:%d/", gConfServerPort);

        while (true) {
            sockaddr_in addr{};
            socklen_t size = sizeof addr;
            int client = accept(gServerId, (struct sockaddr *) &addr, &size);
            if (client == -1) {
                Log(Error, "got an unacceptable connection");
                continue;
            }
            std::thread thread(RequestHandler, client);
            thread.detach();
        }
    }

    void StopHttpServer() {
        close(gServerId);
        Log(Info, "http server stopped ~");
    }
}
