#include "pch.hpp"
#include "common.hpp"
#include "core/memory.hpp"
#include "core/interfaces.hpp"
#include "core/sdk.hpp"
#include "core/schema.hpp"
#include "utils/fnv1a.hpp"
#include "sdk/entity.hpp"
#include "sdk/interfaces/game_entity_system.hpp"
#include "sdk/interfaces/global_vars.hpp"
#include "features/features.hpp"

static void resolve_game_rules_proxy()
{
    if (i::m_game_rules_proxy)
        return;

    static auto s_next_attempt = std::chrono::steady_clock::time_point{};
    const auto now = std::chrono::steady_clock::now();
    if (now < s_next_attempt)
        return;
    s_next_attempt = now + std::chrono::seconds(1);

    for (int32_t idx = 1; idx < 512; idx++)
    {
        const auto entity = i::m_game_entity_system->get(idx);
        if (!entity)
            continue;

        if (entity->get_schema_class_name() == "C_CSGameRulesProxy")
        {
            i::m_game_rules_proxy = reinterpret_cast<c_cs_game_rules_proxy*>(entity);
            return;
        }
    }
}

void f::run()
{
    m_frame_counter++;
    m_data = nlohmann::json{};
    m_player_data = nlohmann::json{};

    resolve_game_rules_proxy();
    f::roundinfo::get_round_info();
    get_map();

    // Always set default fields so the frontend never receives undefined/partial JSON.
    // This prevents React crashes on missing fields like m_players, m_local_team, etc.
    m_data["m_local_team"] = 0;
    m_data["m_players"] = nlohmann::json::array();
    m_data["m_alive_count"] = nlohmann::json::object();
    m_data["m_team_economy"] = nlohmann::json::object();
    m_data["m_bomb"] = nlohmann::json::object();
    m_data["m_grenades"] = nlohmann::json::array();
    m_data["m_dropped_weapons"] = nlohmann::json::array();

    if (!sdk::m_local_controller)
        return;

    const auto local_team = sdk::m_local_controller->m_iTeamNum();
    if (local_team == e_team::none || local_team == e_team::spec)
        return;

    m_data["m_local_team"] = local_team;

    // ── Player scan: every frame (0-64, lightweight) ──────────────────────
    get_player_info();
    compute_team_stats();

    // ── Planted bomb: read from cached pointer every frame ────────────────
    handle_planted_bomb();
    m_data["m_bomb"] = m_last_bomb;

    // ── Grenades: every 2nd frame (65-2048) ───────────────────────────────
    m_data["m_grenades"] = m_last_grenades;
    if (m_frame_counter % 2 == 0)
    {
        f::grenades::get_grenade_info();
        m_last_grenades = m_data["m_grenades"];
    }

    // ── Dropped weapons + carried/dropped C4: every 4th frame (65-2048) ──
    m_data["m_dropped_weapons"] = m_last_dropped_weapons;
    if (m_frame_counter % 4 == 0)
    {
        f::dropped_weapons::get_dropped_weapons();
        m_last_dropped_weapons = m_data["m_dropped_weapons"];
    }
}

void f::get_map()
{
    static bool s_reload_sent = false;
    static auto s_last_warn = std::chrono::steady_clock::time_point{};

    const auto map_name = i::m_global_vars->m_map_name();
    if (map_name.empty() || map_name.find("<empty>") != std::string::npos)
    {
        if (!s_reload_sent)
        {
            s_reload_sent = true;
        }

        m_data["m_map"] = "invalid";

        const auto now = std::chrono::steady_clock::now();
        if (now - s_last_warn > std::chrono::seconds(60))
        {
            s_last_warn = now;
            LOG_WARNING("failed to get map name! updating m_global_vars");
        }
        i::m_global_vars = m_memory->read_t<c_global_vars*>(
            m_memory->find_pattern(CLIENT_DLL, GET_GLOBAL_VARS)->rip().as<c_global_vars*>());
        i::m_game_rules_proxy = nullptr;
        return;
    }

    s_reload_sent = false;
    m_data["m_map"] = map_name;
}

void f::get_player_info()
{
    m_data["m_players"] = nlohmann::json::array();

    // Players are always at indices 0-64 (CCSPlayerController)
    constexpr int32_t highest_idx = 65;
    for (int32_t idx = 0; idx < highest_idx; idx++)
    {
        const auto entity = i::m_game_entity_system->get(idx);
        if (!entity)
            continue;

        const auto entity_handle = entity->get_ref_e_handle();
        if (!entity_handle.is_valid())
            continue;

        const auto class_name = entity->get_schema_class_name();
        if (class_name.empty())
            continue;

        const auto hashed_class_name = fnv1a::hash(class_name);

        if (hashed_class_name == fnv1a::hash("CCSPlayerController"))
        {
            const auto player = i::m_game_entity_system->get<c_cs_player_controller*>(entity_handle);
            if (!player)
                continue;

            const auto player_pawn = player->get_player_pawn();
            if (!player_pawn)
                continue;

            if (!f::players::get_data(idx, player, player_pawn))
                continue;

            f::players::get_weapons(player_pawn);
            f::players::get_active_weapon(player_pawn);

            m_player_data["m_is_local"] = (player == sdk::m_local_controller);

            m_data["m_players"].push_back(m_player_data);
        }
    }
}

void f::compute_team_stats()
{
    int32_t t_alive = 0, ct_alive = 0;
    int32_t t_money = 0, ct_money = 0;

    for (const auto& player : m_data["m_players"])
    {
        const auto team = player.value("m_team", 0);
        const auto dead = player.value("m_is_dead", false);
        const auto money = player.value("m_money", 0);

        if (team == static_cast<int>(e_team::t))
        {
            if (!dead) t_alive++;
            t_money += money;
        }
        else if (team == static_cast<int>(e_team::ct))
        {
            if (!dead) ct_alive++;
            ct_money += money;
        }
    }

    m_data["m_alive_count"] = { {"t", t_alive}, {"ct", ct_alive} };
    m_data["m_team_economy"] = { {"t", t_money}, {"ct", ct_money} };
}

void f::handle_planted_bomb()
{
    // If we have a cached C_PlantedC4 pointer, read timers every frame
    // for smooth countdown without re-scanning 2000 entities.
    if (f::m_planted_c4_ptr)
    {
        auto* planted = reinterpret_cast<c_planted_c4*>(f::m_planted_c4_ptr);

        // Quick validity: read schema class name
        const auto class_name = planted->get_schema_class_name();
        if (class_name == "C_PlantedC4" || class_name == "C_PlantedC4_")
        {
            const auto rules = i::m_game_rules_proxy
                ? i::m_game_rules_proxy->get_game_rules()
                : nullptr;
            if (rules && rules->m_bBombPlanted())
            {
                f::bomb::get_planted_bomb(planted);
                f::m_last_bomb = f::m_data["m_bomb"];
                return;
            }
        }

        // Bomb no longer valid — clear stale data so frontend doesn't show
        // old bomb timer / false defusing advisory on new round.
        f::m_planted_c4_ptr = 0;
        f::m_last_bomb = nlohmann::json::object();
    }
}
