// Pull in Windows headers before httplib so WinSock2 is set up correctly.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>

// httplib is a single-header library; the full definition is only needed here.
#include <httplib.h>

#include "Web/WebRadarServer.h"

#include <chrono>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <system_error>

#include <spdlog/spdlog.h>

// -------------------------------------------------------------------------
// Constructor / Destructor
// -------------------------------------------------------------------------

WebRadarServer::WebRadarServer(WebRadarConfig config)
    : m_config(std::move(config))
{}

WebRadarServer::~WebRadarServer() {
    Stop();
}

// -------------------------------------------------------------------------
// Start / Stop
// -------------------------------------------------------------------------

void WebRadarServer::Start() {
    // Pre-select the first map that has a PNG in maps/ so the browser
    // can show tiles immediately, before the game session connects.
    if (!m_currentMap) {
        int mapCount = 0;
        const MapInfo* allMaps = MapRegistry::AllMaps(mapCount);
        for (int i = 0; i < mapCount; ++i) {
            if (MapRegistry::HasMapImage(allMaps[i])) {
                m_currentMap = &allMaps[i];
                spdlog::info("Web radar: pre-selected map '{}' from maps/ directory",
                             allMaps[i].id);
                break;
            }
        }
        if (!m_currentMap) {
            spdlog::warn("Web radar: no map PNG found in maps/ — tiles will return 404 until "
                         "a PNG named after a map ID is placed there "
                         "(e.g. maps/chernarusplus.png, maps/sakhal.png).");
        }
    }

    // Build the initial bootstrap JSON now so /api/bootstrap returns valid JSON
    // immediately when the browser loads, before any game session connects.
    // Without this, m_bootstrapJson is "" and response.json() throws in the JS,
    // leaving the page with a black background and no map tiles.
    {
        RuntimeUpdate empty{};
        m_bootstrapJson      = m_snapshotSvc.BuildBootstrapJson(empty, m_currentMap, m_config.port);
        m_lastBootstrapMapId = m_currentMap ? m_currentMap->id : "";
    }

    m_server = std::make_unique<httplib::Server>();
    m_server->new_task_queue = [] {
        return new httplib::ThreadPool(16);
    };

    SetupRoutes();

    m_thread = std::thread([this] {
        spdlog::info("Web radar listening on {}:{}",
                     m_config.bindAddress, m_config.port);
        if (!m_server->listen(m_config.bindAddress, m_config.port)) {
            spdlog::error("Web radar failed to bind {}:{}",
                          m_config.bindAddress, m_config.port);
        }
    });
}

void WebRadarServer::Stop() {
    m_stopping.store(true);
    m_cv.notify_all();
    if (m_server) m_server->stop();
    if (m_thread.joinable()) m_thread.join();
}

// -------------------------------------------------------------------------
// PushSnapshot
// -------------------------------------------------------------------------

void WebRadarServer::PushSnapshot(const RuntimeUpdate& update) {
    int64_t now = NowMs();

    // Always update bullet tracks — they need sub-100ms freshness for
    // trajectory rendering, and the update is cheap (no JSON involved).
    m_bullets.Update(update.bullets, now);

    if (update.serverMapName.has_value()) {
        const MapInfo* resolved = MapRegistry::Resolve(*update.serverMapName);
        if (resolved) {
            if (m_currentMap != resolved)
                spdlog::info("Web radar: map changed to '{}'", resolved->id);
            m_currentMap = resolved;
        } else {
            spdlog::warn("Web radar: unrecognised server map name '{}' — keeping current map",
                         *update.serverMapName);
        }
    }

    // Rate-limit JSON serialization.  At 60 Hz with large entity counts
    // (modded server, built bases) BuildStateJson can take several ms and
    // produce hundreds of KB per call.  10 Hz is more than enough for a map.
    if (now < m_nextSnapshotMs) return;
    m_nextSnapshotMs = now + m_config.snapshotIntervalMs;

    std::string jsonStr = m_snapshotSvc.BuildStateJson(
        update, m_bullets.GetSnapshot(), m_currentMap, now);

    {
        std::lock_guard<std::mutex> lk(m_cvMutex);

        // Bootstrap only changes when the active map changes — don't rebuild
        // it every tick.
        std::string mapId = m_currentMap ? m_currentMap->id : "";
        if (mapId != m_lastBootstrapMapId) {
            m_bootstrapJson      = m_snapshotSvc.BuildBootstrapJson(
                update, m_currentMap, m_config.port);
            m_lastBootstrapMapId = std::move(mapId);
        }

        m_latestJson    = std::move(jsonStr);
        m_latestPayload = "event: state\ndata: " + m_latestJson + "\n\n";
        ++m_broadcastSeq;
    }
    m_cv.notify_all();
}

// -------------------------------------------------------------------------
// SetupRoutes
// -------------------------------------------------------------------------

// Attach CORS headers that allow the UI to be served from a different origin
// (e.g. Caddy on :8080 or a remote static host) while the API runs here.
static void AddCors(httplib::Response& res) {
    res.set_header("Access-Control-Allow-Origin",  "*");
    res.set_header("Access-Control-Allow-Methods", "GET, OPTIONS");
    res.set_header("Access-Control-Allow-Headers", "Content-Type");
}

void WebRadarServer::SetupRoutes() {
    // ---- Static files (fallback — normally served by Caddy or another static host) ---

    m_server->Get("/", [this](const httplib::Request& /*req*/,
                               httplib::Response& res) {
        ServeFile(res, "index.html", "text/html; charset=utf-8");
    });

    m_server->Get("/app.js", [this](const httplib::Request& /*req*/,
                                     httplib::Response& res) {
        ServeFile(res, "app.js", "application/javascript; charset=utf-8");
    });

    m_server->Get("/style.css", [this](const httplib::Request& /*req*/,
                                        httplib::Response& res) {
        ServeFile(res, "style.css", "text/css; charset=utf-8");
    });

    m_server->Get("/favicon.ico", [this](const httplib::Request& /*req*/,
                                          httplib::Response& res) {
        ServeFile(res, "favicon.ico", "image/x-icon");
    });

    // ---- CORS preflight -------------------------------------------------
    // Browsers send an OPTIONS preflight for cross-origin requests.

    m_server->Options(".*",
        [](const httplib::Request& /*req*/, httplib::Response& res) {
            AddCors(res);
            res.status = 204;
        });

    // ---- Bootstrap ------------------------------------------------------

    m_server->Get("/api/bootstrap",
        [this](const httplib::Request& req, httplib::Response& res) {
            AddCors(res);
            if (!Authorise(req.remote_addr,
                           req.get_param_value("password"))) {
                res.status = 401;
                return;
            }
            std::lock_guard<std::mutex> lk(m_cvMutex);
            res.set_content(m_bootstrapJson, "application/json");
        });

    // ---- Polling state --------------------------------------------------

    m_server->Get("/api/state",
        [this](const httplib::Request& req, httplib::Response& res) {
            AddCors(res);
            if (!Authorise(req.remote_addr,
                           req.get_param_value("password"))) {
                res.status = 401;
                return;
            }
            std::lock_guard<std::mutex> lk(m_cvMutex);
            res.set_content(m_latestJson, "application/json");
        });

    // ---- SSE stream -----------------------------------------------------

    m_server->Get("/events",
        [this](const httplib::Request& req, httplib::Response& res) {
            AddCors(res);
            if (!Authorise(req.remote_addr,
                           req.get_param_value("password"))) {
                res.status = 401;
                return;
            }

            res.set_header("Cache-Control", "no-cache");
            res.set_header("X-Accel-Buffering", "no");

            auto lastSeq = std::make_shared<int64_t>(-1);

            res.set_chunked_content_provider(
                "text/event-stream",
                [this, lastSeq](size_t /*offset*/,
                                httplib::DataSink& sink) -> bool {
                    if (m_stopping.load()) return false;

                    std::string payload;
                    {
                        std::unique_lock<std::mutex> lk(m_cvMutex);
                        m_cv.wait_for(lk, std::chrono::seconds(10),
                            [this, &lastSeq] {
                                return m_broadcastSeq != *lastSeq
                                    || m_stopping.load();
                            });

                        if (m_stopping.load()) return false;

                        if (m_broadcastSeq == *lastSeq) {
                            // Keepalive comment to prevent proxy timeouts.
                            payload = ": keepalive\n\n";
                        } else {
                            payload  = m_latestPayload;
                            *lastSeq = m_broadcastSeq;
                        }
                    }

                    return sink.write(payload.c_str(), payload.size());
                }
            );
        });

    // ---- Map tile -------------------------------------------------------

    m_server->Get("/tile",
        [this](const httplib::Request& req, httplib::Response& res) {
            AddCors(res);
            if (!Authorise(req.remote_addr,
                           req.get_param_value("password"))) {
                res.status = 401;
                return;
            }

            // Prefer the explicit mapId param (sent by app.js); fall back to
            // the currently active map resolved from the game session.
            const MapInfo* tileMap = nullptr;
            std::string mapIdParam = req.get_param_value("mapId");
            if (!mapIdParam.empty())
                tileMap = MapRegistry::Resolve(mapIdParam);
            if (!tileMap)
                tileMap = m_currentMap;

            if (!tileMap) {
                res.status = 404;
                return;
            }

            int tx = 0, ty = 0;
            try {
                tx = std::stoi(req.get_param_value("x"));
                ty = std::stoi(req.get_param_value("y"));
            } catch (...) {
                res.status = 400;
                return;
            }

            int code = 200;
            auto bytes = m_tiles.GetTile(*tileMap, tx, ty, code);
            res.status = code;
            if (code == 200) {
                // Cache tiles aggressively — they're static PNG slices that only
                // change when the map PNG is replaced (different URL via mapId param).
                // Cloudflare tunnel and browsers will both benefit from this.
                res.set_header("Cache-Control", "public, max-age=86400");
                res.set_header("Vary", "Accept-Encoding");
                res.set_content(
                    std::string(bytes.begin(), bytes.end()),
                    "image/png");
            }
        });

    // ---- Full map image -------------------------------------------------

    m_server->Get("/map-image",
        [this](const httplib::Request& req, httplib::Response& res) {
            AddCors(res);
            if (!Authorise(req.remote_addr,
                           req.get_param_value("password"))) {
                res.status = 401;
                return;
            }

            const MapInfo* imgMap = nullptr;
            std::string mapIdParam = req.get_param_value("mapId");
            if (!mapIdParam.empty())
                imgMap = MapRegistry::Resolve(mapIdParam);
            if (!imgMap)
                imgMap = m_currentMap;

            if (!imgMap) {
                res.status = 404;
                return;
            }

            wchar_t mapPathBuf[MAX_PATH] = {};
            GetModuleFileNameW(nullptr, mapPathBuf, MAX_PATH);
            std::string path = (std::filesystem::path(mapPathBuf).parent_path()
                                / "maps" / (imgMap->id + ".png")).string();
            std::ifstream f(path, std::ios::binary);
            if (!f) {
                res.status = 404;
                return;
            }

            std::string content(
                (std::istreambuf_iterator<char>(f)),
                std::istreambuf_iterator<char>());
            res.set_content(std::move(content), "image/png");
        });

    // ---- Debug ----------------------------------------------------------
    // No auth — purely diagnostic.  Open /api/debug in a browser to see
    // what the server sees: exe dir, maps/ path, which PNGs exist, and what
    // the bootstrap JSON currently contains.

    m_server->Get("/api/debug",
        [this](const httplib::Request& /*req*/, httplib::Response& res) {
            AddCors(res);
            namespace fs = std::filesystem;

            wchar_t exeBuf[MAX_PATH] = {};
            GetModuleFileNameW(nullptr, exeBuf, MAX_PATH);
            fs::path exeDir  = fs::path(exeBuf).parent_path();
            fs::path mapsDir = exeDir / "maps";

            // Escape a string for inline JSON (handles backslashes in Windows paths).
            auto jsStr = [](const std::string& s) {
                std::string r;
                r.reserve(s.size() + 4);
                for (char c : s) {
                    if      (c == '\\') r += "\\\\";
                    else if (c == '"')  r += "\\\"";
                    else                r += c;
                }
                return r;
            };

            std::ostringstream j;
            j << "{\n";
            j << "  \"exeDir\": \""  << jsStr(exeDir.string())  << "\",\n";
            j << "  \"mapsDir\": \"" << jsStr(mapsDir.string()) << "\",\n";
            j << "  \"mapsDirExists\": " << (fs::exists(mapsDir) ? "true" : "false") << ",\n";
            j << "  \"currentMapId\": \"" << (m_currentMap ? m_currentMap->id : "") << "\",\n";

            // PNGs actually present in maps/.
            j << "  \"pngsFound\": [";
            {
                std::error_code ec;
                bool first = true;
                for (const auto& e : fs::directory_iterator(mapsDir, ec)) {
                    if (!ec && e.path().extension() == ".png") {
                        if (!first) j << ", ";
                        j << "\"" << jsStr(e.path().filename().string()) << "\"";
                        first = false;
                    }
                }
            }
            j << "],\n";

            // All known map IDs with whether their PNG was found.
            j << "  \"knownMaps\": [";
            {
                int mapCount = 0;
                const MapInfo* allMaps = MapRegistry::AllMaps(mapCount);
                for (int i = 0; i < mapCount; ++i) {
                    if (i > 0) j << ", ";
                    j << "{\"id\": \"" << allMaps[i].id << "\", \"hasImage\": "
                      << (MapRegistry::HasMapImage(allMaps[i]) ? "true" : "false") << "}";
                }
            }
            j << "]\n}";

            res.set_content(j.str(), "application/json");
        });
}

// -------------------------------------------------------------------------
// ServeFile
// -------------------------------------------------------------------------

void WebRadarServer::ServeFile(httplib::Response& res,
                               const std::string& filename,
                               const std::string& contentType)
{
    std::string path = WebrootPath(filename);
    std::ifstream f(path, std::ios::binary);
    if (!f) {
        res.status = 404;
        return;
    }

    std::string content(
        (std::istreambuf_iterator<char>(f)),
        std::istreambuf_iterator<char>());
    res.set_content(std::move(content), contentType);
}

// -------------------------------------------------------------------------
// Authorise
// -------------------------------------------------------------------------

bool WebRadarServer::Authorise(const std::string& addr,
                               const std::string& pwd) const
{
    // Always allow loopback connections.
    if (addr == "127.0.0.1" || addr == "::1" || addr == "localhost")
        return true;

    // If no password configured, allow everyone.
    if (m_config.password.empty())
        return true;

    return pwd == m_config.password;
}

// -------------------------------------------------------------------------
// WebrootPath
// -------------------------------------------------------------------------

std::string WebRadarServer::WebrootPath(const std::string& file) const {
    wchar_t buf[MAX_PATH] = {};
    GetModuleFileNameW(nullptr, buf, MAX_PATH);

    // Convert wide string to narrow.
    std::wstring wpath(buf);
    // Strip the filename component to get the exe directory.
    auto lastSlash = wpath.find_last_of(L"\\/");
    if (lastSlash != std::wstring::npos)
        wpath = wpath.substr(0, lastSlash + 1);

    // Convert to std::string (ASCII/UTF-8 — path characters only).
    std::string exeDir(wpath.begin(), wpath.end());
    return exeDir + "webroot\\" + file;
}

// -------------------------------------------------------------------------
// NowMs
// -------------------------------------------------------------------------

int64_t WebRadarServer::NowMs() {
    using namespace std::chrono;
    return duration_cast<milliseconds>(
        system_clock::now().time_since_epoch()).count();
}
