#include "http_server.hpp"

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <stdexcept>

#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>

using json = nlohmann::json;
namespace fs = std::filesystem;

HttpServer::HttpServer(Application &application, std::string host, int port, std::string web_root)
    : application_(application), host_(std::move(host)), port_(port), web_root_(std::move(web_root))
{
    server_.new_task_queue = []
    { return new httplib::ThreadPool(8); };
    registerRoutes();
}

void HttpServer::sendJson(httplib::Response &response, const json &body, int status)
{
    response.status = status;
    response.set_content(body.dump(), "application/json; charset=utf-8");
    response.set_header("Cache-Control", "no-store");
}

json HttpServer::parseJson(const httplib::Request &request)
{
    if (request.body.empty())
        return json::object();
    return json::parse(request.body);
}

std::size_t HttpServer::cameraIndex(const httplib::Request &request)
{
    if (request.matches.size() < 2)
        throw std::invalid_argument("Kameraindex saknas");
    const int index = std::stoi(request.matches[1].str());
    if (index < 0 || index >= static_cast<int>(CameraManager::CameraCount))
        throw std::out_of_range("Ogiltigt kameraindex");
    return static_cast<std::size_t>(index);
}

void HttpServer::registerRoutes()
{
    server_.set_default_headers({
        {"X-Content-Type-Options", "nosniff"},
        {"X-Frame-Options", "SAMEORIGIN"},
        {"Referrer-Policy", "same-origin"},
        {"Content-Security-Policy", "default-src 'self'; img-src 'self' data: blob:; style-src 'self'; script-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'"},
    });
    server_.set_payload_max_length(1024 * 1024);
    server_.set_read_timeout(10, 0);
    server_.set_write_timeout(30, 0);

    server_.set_exception_handler([](const auto &, auto &response, std::exception_ptr exception)
                                  {
        try
        {
            if (exception)
                std::rethrow_exception(exception);
        }
        catch (const std::exception &error)
        {
            sendJson(response, {{"ok", false}, {"message", error.what()}}, 400);
        } });

    server_.Get("/api/health", [&](const httplib::Request &, httplib::Response &response)
                { sendJson(response, {{"status", "ok"}, {"service", "OpenDartboard x86"}, {"platform", "linux-x86_64"}}); });

    server_.Get("/api/state", [&](const httplib::Request &, httplib::Response &response)
                { sendJson(response, application_.state()); });

    server_.Get("/api/cameras/available", [&](const httplib::Request &, httplib::Response &response)
                { sendJson(response, application_.availableCameraSources()); });

    server_.Put("/api/cameras", [&](const httplib::Request &request, httplib::Response &response)
                {
        const json body = parseJson(request);
        if (!body.contains("sources") || !body["sources"].is_array() || body["sources"].size() != CameraManager::CameraCount)
        {
            sendJson(response, {{"ok", false}, {"message", "Exakt tre kamerakällor krävs"}}, 400);
            return;
        }
        std::array<std::string, CameraManager::CameraCount> sources;
        for (std::size_t index = 0; index < CameraManager::CameraCount; ++index)
            sources[index] = body["sources"][index].get<std::string>();
        const json result = application_.configureCameras(sources);
        sendJson(response, result, result.value("ok", false) ? 200 : 409); });

    server_.Put(R"(/api/cameras/(\d+)/view)", [&](const httplib::Request &request, httplib::Response &response)
                {
        const json body = parseJson(request);
        const CameraManager::View view{
            body.value("zoom", 1.0),
            body.value("pan_x", 0.0),
            body.value("pan_y", 0.0),
        };
        const json result = application_.updateCameraView(cameraIndex(request), view);
        sendJson(response, result, result.value("ok", false) ? 200 : 409); });

    server_.Get(R"(/api/cameras/(\d+)/snapshot\.jpg)", [&](const httplib::Request &request, httplib::Response &response)
                {
        const bool overlay = !request.has_param("overlay") || request.get_param_value("overlay") != "0";
        cv::Mat image = application_.cameraSnapshot(cameraIndex(request), overlay);
        if (image.empty())
        {
            image = cv::Mat::zeros(720, 1280, CV_8UC3);
            cv::putText(image, "Ingen kamerabild", cv::Point(420, 370), cv::FONT_HERSHEY_SIMPLEX, 1.6, cv::Scalar(190, 190, 190), 3);
        }

        int requested_width = 640;
        if (request.has_param("width"))
            requested_width = std::clamp(std::stoi(request.get_param_value("width")), 320, 1280);
        if (image.cols != requested_width)
        {
            const int requested_height = std::max(1, image.rows * requested_width / image.cols);
            cv::resize(image, image, cv::Size(requested_width, requested_height), 0.0, 0.0, cv::INTER_AREA);
        }

        std::vector<unsigned char> encoded;
        cv::imencode(".jpg", image, encoded, {cv::IMWRITE_JPEG_QUALITY, 78});
        response.set_content(reinterpret_cast<const char *>(encoded.data()), encoded.size(), "image/jpeg");
        response.set_header("Cache-Control", "no-store, max-age=0"); });

    server_.Post("/api/calibration/start", [&](const httplib::Request &, httplib::Response &response)
                 {
        const json result = application_.startCalibration();
        sendJson(response, result, result.value("ok", false) ? 202 : 409); });

    server_.Post("/api/engine/start", [&](const httplib::Request &, httplib::Response &response)
                 {
        const json result = application_.startDetection();
        sendJson(response, result, result.value("ok", false) ? 200 : 409); });

    server_.Post("/api/engine/stop", [&](const httplib::Request &, httplib::Response &response)
                 { sendJson(response, application_.stopDetection()); });

    server_.Post("/api/game/start", [&](const httplib::Request &request, httplib::Response &response)
                 {
        const json body = parseJson(request);
        if (!body.contains("players") || !body["players"].is_array())
        {
            sendJson(response, {{"accepted", false}, {"message", "En lista med spelare krävs"}}, 400);
            return;
        }
        const json result = application_.startGame(body["players"].get<std::vector<std::string>>());
        sendJson(response, result, result.value("accepted", false) ? 200 : 409); });

    server_.Post("/api/game/dart", [&](const httplib::Request &request, httplib::Response &response)
                 {
        const json body = parseJson(request);
        const json result = application_.submitDart(body.value("score", ""), body.value("source", "manual"));
        sendJson(response, result, result.value("accepted", false) ? 200 : 409); });

    server_.Post("/api/game/undo", [&](const httplib::Request &, httplib::Response &response)
                 {
        const json result = application_.undoDart();
        sendJson(response, result, result.value("accepted", false) ? 200 : 409); });

    server_.Post("/api/game/board-clear", [&](const httplib::Request &, httplib::Response &response)
                 { sendJson(response, application_.markBoardClear()); });

    server_.Post("/api/game/reset", [&](const httplib::Request &, httplib::Response &response)
                 { sendJson(response, application_.resetGame()); });

    if (!fs::is_directory(web_root_))
        throw std::runtime_error("Webbkatalogen finns inte: " + web_root_);
    if (!server_.set_mount_point("/", web_root_))
        throw std::runtime_error("Kunde inte publicera webbkatalogen: " + web_root_);
}

bool HttpServer::run()
{
    running_ = true;
    const bool result = server_.listen(host_, port_);
    running_ = false;
    return result;
}

void HttpServer::stop()
{
    if (running_)
        server_.stop();
}
