#include "Web/WebSnapshotService.h"

#include <cmath>
#include <format>
#include <string>

#include <nlohmann/json.hpp>

using json = nlohmann::json;

// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------

static constexpr float kPi = 3.14159265358979323846f;

/// Convert a world address to a hex-string id (e.g. "1A2B3C4D").
static std::string AddrId(uint64_t address) {
    return std::format("{:X}", address);
}

/// Translate world-space (x, z) to map-image space.
/// If map is null, returns the raw coordinates.
static std::pair<float, float> WorldToMap(const MapInfo* map,
                                          float worldX, float worldZ)
{
    float ox = worldX, oy = worldZ;
    if (map)
        MapRegistry::Translate(*map, worldX, worldZ, ox, oy);
    return { ox, oy };
}

// -------------------------------------------------------------------------
// Entity JSON builders
// -------------------------------------------------------------------------

static json PlayerEntityJson(const DayZPlayerEntry& p, const MapInfo* map) {
    json obj;
    obj["id"]   = AddrId(p.address);
    obj["kind"] = "player";

    // Label: prefer nickname, fall back to typeName.
    obj["label"] = p.nickname.empty() ? p.typeName : p.nickname;

    if (p.position.has_value()) {
        auto [mx, my] = WorldToMap(map, p.position->x, p.position->z);
        obj["x"] = mx;
        obj["y"] = my;
    } else {
        obj["x"] = 0.0f;
        obj["y"] = 0.0f;
    }

    // headingDegrees is already in degrees from the reader.
    obj["rotation"]    = p.headingDegrees.value_or(0.0f);
    obj["distance"]    = -1;
    obj["networkId"]   = p.networkId;
    obj["steamId"]     = "";
    obj["dead"]        = p.isDead;
    obj["handItem"]    = p.itemInHands;
    obj["wornItems"]   = p.wornItems;
    obj["visibleOnMap"] = true;
    return obj;
}

static json PlayerListEntryJson(const DayZPlayerEntry& p) {
    json obj;
    obj["networkId"]   = p.networkId;
    obj["label"]       = p.nickname.empty() ? p.typeName : p.nickname;
    obj["steamId"]     = "";
    obj["visibleOnMap"] = true;
    obj["dead"]        = p.isDead;
    obj["handItem"]    = p.itemInHands;
    obj["wornItems"]   = p.wornItems;
    obj["distance"]    = -1;
    return obj;
}

static json ZombieEntityJson(const DayZZombieEntry& z, const MapInfo* map) {
    json obj;
    obj["id"]   = AddrId(z.address);
    obj["kind"] = "zombie";
    obj["label"] = z.entityName.empty() ? z.typeName : z.entityName;

    if (z.position.has_value()) {
        auto [mx, my] = WorldToMap(map, z.position->x, z.position->z);
        obj["x"] = mx;
        obj["y"] = my;
    } else {
        obj["x"] = 0.0f;
        obj["y"] = 0.0f;
    }

    obj["rotation"]     = z.headingDegrees.value_or(0.0f);
    obj["distance"]     = -1;
    obj["visibleOnMap"] = true;
    return obj;
}

static json AnimalEntityJson(const DayZAnimalEntry& a, const MapInfo* map) {
    json obj;
    obj["id"]   = AddrId(a.address);
    obj["kind"] = "animal";
    obj["label"] = a.entityName.empty() ? a.typeName : a.entityName;

    if (a.position.has_value()) {
        auto [mx, my] = WorldToMap(map, a.position->x, a.position->z);
        obj["x"] = mx;
        obj["y"] = my;
    } else {
        obj["x"] = 0.0f;
        obj["y"] = 0.0f;
    }

    obj["rotation"]     = a.headingDegrees.value_or(0.0f);
    obj["distance"]     = -1;
    obj["visibleOnMap"] = true;
    return obj;
}

static json VehicleEntityJson(const DayZCarAndBoatEntry& v, const MapInfo* map) {
    json obj;
    obj["id"]   = AddrId(v.address);
    obj["kind"] = "vehicle";
    obj["label"] = v.entityName.empty() ? v.typeName : v.entityName;

    if (v.position.has_value()) {
        auto [mx, my] = WorldToMap(map, v.position->x, v.position->z);
        obj["x"] = mx;
        obj["y"] = my;
    } else {
        obj["x"] = 0.0f;
        obj["y"] = 0.0f;
    }

    obj["rotation"]     = v.headingDegrees.value_or(0.0f);
    obj["distance"]     = -1;
    obj["visibleOnMap"] = true;
    return obj;
}

static json OtherEntityJson(const DayZOtherEntityEntry& e, const MapInfo* map) {
    json obj;
    obj["id"]   = AddrId(e.address);
    obj["kind"] = "other-entity";
    obj["label"] = e.entityName.empty() ? e.typeName : e.entityName;

    if (e.position.has_value()) {
        auto [mx, my] = WorldToMap(map, e.position->x, e.position->z);
        obj["x"] = mx;
        obj["y"] = my;
    } else {
        obj["x"] = 0.0f;
        obj["y"] = 0.0f;
    }

    obj["rotation"]     = e.headingDegrees.value_or(0.0f);
    obj["distance"]     = -1;
    obj["visibleOnMap"] = true;
    return obj;
}

static json LootEntityJson(const DayZItemListEntry& item, const MapInfo* map) {
    json obj;
    obj["id"]   = AddrId(item.address);
    obj["kind"] = "loot";
    obj["label"] = item.cleanName.empty() ? item.entityName : item.cleanName;

    if (item.position.has_value()) {
        auto [mx, my] = WorldToMap(map, item.position->x, item.position->z);
        obj["x"] = mx;
        obj["y"] = my;
    } else {
        obj["x"] = 0.0f;
        obj["y"] = 0.0f;
    }

    obj["rotation"]     = 0.0f;
    obj["distance"]     = -1;
    obj["visibleOnMap"] = true;
    obj["lootCategory"] = item.filterKey;  // filterKey = lowercase composite from item_filters.json
    return obj;
}

// -------------------------------------------------------------------------
// Bullet JSON builder
// -------------------------------------------------------------------------

static json BulletJson(const BulletTrack& t, const MapInfo* map) {
    json obj;
    obj["id"]    = AddrId(t.address);
    obj["label"] = "Bullet";
    obj["kind"]  = "bullet";

    auto [cx, cy] = WorldToMap(map, t.currentPos.x, t.currentPos.z);
    obj["x"] = cx;
    obj["y"] = cy;

    // Rotation from lastDir: atan2(x, z) gives bearing in radians; convert to degrees.
    float bearing = std::atan2(t.lastDir.x, t.lastDir.z) * (180.0f / kPi);
    obj["rotation"]  = bearing;
    obj["distance"]  = -1;
    obj["visibleOnMap"] = true;

    obj["isActive"]    = t.isActive;
    obj["isPhantom"]   = t.isPhantom;
    obj["isCompleted"] = t.isCompleted;

    obj["firstSeenAtUtcMs"] = t.firstSeenMs;
    obj["lastSeenAtUtcMs"]  = t.lastSeenMs;

    if (t.isCompleted)
        obj["finalizedAtUtcMs"] = t.finalizedMs;
    else
        obj["finalizedAtUtcMs"] = nullptr;

    obj["predictionSpeed"]  = t.speedMps;
    obj["directionSource"]  = "velocity";

    // initialPosition
    if (t.firstSeenMs > 0) {
        auto [ix, iy] = WorldToMap(map, t.initialPos.x, t.initialPos.z);
        obj["initialPosition"] = { {"x", ix}, {"y", iy} };
    } else {
        obj["initialPosition"] = nullptr;
    }

    // finalPosition
    if (t.isCompleted) {
        auto [fx, fy] = WorldToMap(map, t.finalPos.x, t.finalPos.z);
        obj["finalPosition"] = { {"x", fx}, {"y", fy} };
    } else {
        obj["finalPosition"] = nullptr;
    }

    // initialDirection (3-D, world space)
    obj["initialDirection"] = {
        {"x", t.initialDir.x},
        {"y", t.initialDir.y},
        {"z", t.initialDir.z}
    };

    // lastDirection
    obj["lastDirection"] = {
        {"x", t.lastDir.x},
        {"y", t.lastDir.y},
        {"z", t.lastDir.z}
    };

    // Path (map-projected 2-D)
    json path = json::array();
    for (const auto& [ts, pos] : t.points) {
        auto [px, py] = WorldToMap(map, pos.x, pos.z);
        path.push_back({ {"x", px}, {"y", py} });
    }
    obj["path"] = std::move(path);

    // Predicted path: 20 points at 15 m intervals, up to 300 m.
    json predictedPath = json::array();
    bool doPredict = !t.isCompleted && t.speedMps >= 1.0f;
    if (doPredict) {
        Vector3 cur = t.currentPos;
        float step = 15.0f;
        int   nPts = 20;
        for (int i = 1; i <= nPts; ++i) {
            float dist = step * static_cast<float>(i);
            if (dist > 300.0f) break;
            float px = cur.x + t.lastDir.x * dist;
            float py_val = cur.y + t.lastDir.y * dist;
            float pz = cur.z + t.lastDir.z * dist;
            (void)py_val;
            auto [mx, my] = WorldToMap(map, px, pz);
            predictedPath.push_back({ {"x", mx}, {"y", my} });
        }
    }
    obj["predictedPath"] = std::move(predictedPath);

    return obj;
}

// -------------------------------------------------------------------------
// WebSnapshotService::BuildStateJson
// -------------------------------------------------------------------------

std::string WebSnapshotService::BuildStateJson(const RuntimeUpdate&            update,
                                               const std::vector<BulletTrack>& bullets,
                                               const MapInfo*                  map,
                                               int64_t                         nowMs)
{
    (void)nowMs;
    int64_t seq = m_seq.fetch_add(1, std::memory_order_relaxed);

    int mapSize   = map ? map->worldSize : 15360;
    int tileCount = (mapSize + 511) / 512;

    json j;
    j["cacheRefreshSequence"]     = seq;
    j["fastCacheRefreshSequence"] = seq;
    j["slowCacheRefreshSequence"] = seq;

    j["mapId"]         = map ? map->id   : "";
    j["mapName"]       = map ? map->name : "";
    j["mapSize"]       = mapSize;
    j["tileSize"]      = 512;
    j["tileCountX"]    = tileCount;
    j["tileCountY"]    = tileCount;

    j["serverMapName"] = update.serverMapName.value_or("");
    j["serverName"]    = update.serverName.value_or("");
    j["gameVersion"]   = update.gameVersion.value_or("");

    j["isAttached"]       = update.areBaseObjectsReady;
    j["isUsingMockData"]  = false;
    j["status"]           = update.status;

    bool hasLocal = update.localPlayerPosition.has_value();
    j["hasLocalPlayer"] = hasLocal;

    if (hasLocal) {
        const Vector3& lp = *update.localPlayerPosition;
        auto [lx, ly] = WorldToMap(map, lp.x, lp.z);
        j["localPlayer"] = {
            { "x",              lx },
            { "y",              ly },
            { "rotation",       update.localPlayerLookDirection.value_or(0.0f) },
            { "cameraRotation", 0.0 }
        };
    } else {
        j["localPlayer"] = nullptr;
    }

    // playerList
    json playerList = json::array();
    for (const auto& p : update.players)
        playerList.push_back(PlayerListEntryJson(p));
    j["playerList"] = std::move(playerList);

    // players
    json players = json::array();
    for (const auto& p : update.players)
        players.push_back(PlayerEntityJson(p, map));
    j["players"] = std::move(players);

    // zombies
    json zombies = json::array();
    for (const auto& z : update.zombies)
        zombies.push_back(ZombieEntityJson(z, map));
    j["zombies"] = std::move(zombies);

    // animals
    json animals = json::array();
    for (const auto& a : update.animals)
        animals.push_back(AnimalEntityJson(a, map));
    j["animals"] = std::move(animals);

    // vehicles
    json vehicles = json::array();
    for (const auto& v : update.carsAndBoats)
        vehicles.push_back(VehicleEntityJson(v, map));
    j["vehicles"] = std::move(vehicles);

    // otherEntities
    json others = json::array();
    for (const auto& e : update.otherEntities)
        others.push_back(OtherEntityJson(e, map));
    j["otherEntities"] = std::move(others);

    // loot
    json loot = json::array();
    for (const auto& item : update.items)
        loot.push_back(LootEntityJson(item, map));
    j["loot"] = std::move(loot);

    // bullets
    json bulletArr = json::array();
    for (const auto& t : bullets)
        bulletArr.push_back(BulletJson(t, map));
    j["bullets"] = std::move(bulletArr);

    return j.dump();
}

// -------------------------------------------------------------------------
// WebSnapshotService::BuildBootstrapJson
// -------------------------------------------------------------------------

std::string WebSnapshotService::BuildBootstrapJson(const RuntimeUpdate& update,
                                                   const MapInfo*       map,
                                                   int                  port)
{
    int mapSize   = map ? map->worldSize : 15360;
    int tileCount = (mapSize + 511) / 512;

    json j;
    j["mapId"]         = map ? map->id   : "";
    j["mapName"]       = map ? map->name : "";
    j["serverMapName"] = update.serverMapName.value_or("");
    j["mapSize"]       = mapSize;
    j["tileSize"]      = 512;
    j["tileCountX"]    = tileCount;
    j["tileCountY"]    = tileCount;

    j["server"] = { { "port", port } };

    // All known maps.
    int mapCount = 0;
    const MapInfo* allMaps = MapRegistry::AllMaps(mapCount);

    json mapsArr = json::array();
    for (int i = 0; i < mapCount; ++i) {
        const MapInfo& m = allMaps[i];
        mapsArr.push_back({
            { "id",        m.id        },
            { "name",      m.name      },
            { "worldSize", m.worldSize },
            { "index",     m.index     },
            { "hasImage",  MapRegistry::HasMapImage(m) }
        });
    }
    j["maps"] = std::move(mapsArr);

    // Entity filter presets (fixed).
    json filters = json::array({
        { {"key","player"},  {"label","Players"}, {"kind","player"},  {"color","#ff8800"} },
        { {"key","zombie"},  {"label","Zombies"},  {"kind","zombie"},  {"color","#00cc44"} },
        { {"key","animal"},  {"label","Animals"},  {"kind","animal"},  {"color","#44aaff"} },
        { {"key","vehicle"}, {"label","Vehicles"}, {"kind","vehicle"}, {"color","#aa44ff"} },
        { {"key","bullet"},  {"label","Bullets"},  {"kind","bullet"},  {"color","#ffff00"} },
    });

    // Per-category loot filters — built from item_filters.json so that
    // app.js can map item.lootCategory → the correct colour/label.
    for (const auto& def : m_catalog.GetFilters()) {
        json f;
        f["key"]      = "loot_" + def.key;   // unique filter key
        f["label"]    = def.description.empty() ? def.key : def.description;
        f["kind"]     = "loot";
        f["category"] = def.key;             // must match item.lootCategory
        f["color"]    = def.color.value_or("#ffffff");
        filters.push_back(std::move(f));
    }

    // Fallback "other" loot bucket for items that don't match any filter.
    filters.push_back({
        {"key",      "loot_other"},
        {"label",    "Other"},
        {"kind",     "loot"},
        {"category", ItemFilterCatalog::kFallbackKey},
        {"color",    "#888888"}
    });

    j["filters"] = std::move(filters);

    return j.dump();
}
