#include "simulation_server.h"

#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>

namespace tactical_shooter {

static constexpr float kPi = 3.14159265358979323846f;

using Clock = std::chrono::high_resolution_clock;

SimulationServer::SimulationServer()
    : entities_by_id_(kMaxEntities),
      queued_inputs_(kMaxEntities) {
}

SimulationServer::~SimulationServer() {
    stop();
}

// ---- Lifecycle ----------------------------------------------------------------

void SimulationServer::set_tick_rate(uint32_t hz) {
    tick_hz_ = std::clamp(hz, 1u, 1000u);
    tick_interval_ = 1.0f / static_cast<float>(tick_hz_);
}

void SimulationServer::start() {
    if (running_) return;
    running_ = true;
    current_tick_ = 0;
    time_accumulator_ = 0.0f;
    last_snapshots_.clear();
    hit_detection_.set_entities(living_entity_ptrs_);
    reset_stats();
    weapon_config_ = WeaponDamage();
    weapon_fire_rate_hz_ = 10.0f;

    // Initialize position history ring buffer
    position_history_.clear();
    position_history_.resize(history_depth_);
    history_write_index_ = 0;
    for (auto &entry : position_history_) {
        entry.tick = 0;
    }

    last_hit_result_ = LastHitResult{};
}

void SimulationServer::stop() {
    running_ = false;
    for (auto &entity : entities_by_id_) {
        entity = godot::Ref<Entity>();
    }
    living_entity_ptrs_.clear();
    queued_inputs_.clear();
    queued_inputs_.resize(kMaxEntities);
    pending_fires_.clear();
    last_snapshots_.clear();
    position_history_.clear();
    current_tick_ = 0;
    time_accumulator_ = 0.0f;
    last_hit_result_ = LastHitResult{};
}

bool SimulationServer::can_tick(float delta) {
    if (!running_) return false;
    time_accumulator_ += delta;
    return time_accumulator_ >= tick_interval_;
}

godot::PackedByteArray SimulationServer::tick() {
    if (!running_) return godot::PackedByteArray();

    // Drain as many fixed timestep ticks as accumulated
    while (time_accumulator_ >= tick_interval_) {
        time_accumulator_ -= tick_interval_;
        process_tick();
    }

    // Serialize and return the current state
    return serialize_state();
}

// ---- Entity Management -------------------------------------------------------

uint16_t SimulationServer::spawn_entity(const godot::Vector3 &position) {
    uint16_t id = allocate_entity_id();
    if (id >= kMaxEntities) return UINT16_MAX;

    auto entity = godot::Ref<Entity>(memnew(Entity));
    entity->set_entity_id(id);
    entity->reset(position);
    entities_by_id_[id] = entity;

    // Rebuild living ptrs list
    living_entity_ptrs_.clear();
    for (auto &e : entities_by_id_) {
        if (e.is_valid() && e->is_alive()) {
            living_entity_ptrs_.push_back(e.ptr());
        }
    }
    hit_detection_.set_entities(living_entity_ptrs_);

    return id;
}

void SimulationServer::despawn_entity(uint16_t entity_id) {
    if (entity_id < kMaxEntities && entities_by_id_[entity_id].is_valid()) {
        entities_by_id_[entity_id] = godot::Ref<Entity>();
        last_snapshots_.erase(entity_id);

        // Rebuild living ptrs
        living_entity_ptrs_.clear();
        for (auto &e : entities_by_id_) {
            if (e.is_valid() && e->is_alive()) {
                living_entity_ptrs_.push_back(e.ptr());
            }
        }
        hit_detection_.set_entities(living_entity_ptrs_);
    }
}

godot::Ref<Entity> SimulationServer::get_entity(uint16_t entity_id) {
    if (entity_id < kMaxEntities && entities_by_id_[entity_id].is_valid()) {
        return entities_by_id_[entity_id];
    }
    return godot::Ref<Entity>();
}

uint16_t SimulationServer::get_entity_count() const {
    uint16_t count = 0;
    for (auto &e : entities_by_id_) {
        if (e.is_valid()) ++count;
    }
    return count;
}

godot::Array SimulationServer::get_entity_ids() const {
    godot::Array ids;
    for (auto &e : entities_by_id_) {
        if (e.is_valid()) {
            ids.push_back(e->get_entity_id());
        }
    }
    return ids;
}

// ---- Input --------------------------------------------------------------------

void SimulationServer::apply_input(uint16_t entity_id, const godot::Dictionary &input_dict) {
    if (entity_id >= kMaxEntities) return;
    if (!entities_by_id_[entity_id].is_valid()) return;

    EntityInput input;

    if (input_dict.has("move_direction")) {
        input.move_direction = input_dict["move_direction"];
    }
    if (input_dict.has("look_yaw")) {
        input.look_yaw = static_cast<float>(static_cast<double>(input_dict["look_yaw"]));
    }
    if (input_dict.has("look_pitch")) {
        input.look_pitch = static_cast<float>(static_cast<double>(input_dict["look_pitch"]));
    }
    if (input_dict.has("jump")) {
        input.jump = input_dict["jump"];
    }
    if (input_dict.has("crouch")) {
        input.crouch = input_dict["crouch"];
    }
    if (input_dict.has("sprint")) {
        input.sprint = input_dict["sprint"];
    }
    if (input_dict.has("shoot")) {
        input.shoot = input_dict["shoot"];
    }
    if (input_dict.has("aim")) {
        input.aim = input_dict["aim"];
    }
    if (input_dict.has("input_sequence")) {
        input.input_sequence = static_cast<uint32_t>(static_cast<int64_t>(input_dict["input_sequence"]));
    }

    // If the shoot flag just transitioned from false→true, queue a weapon fire
    // with the tick of this input so lag compensation has the right time reference.
    if (input.shoot && !queued_inputs_[entity_id].input.shoot) {
        PendingFire fire;
        fire.shooter_id = entity_id;
        fire.fire_tick = current_tick_;
        fire.input_sequence = input.input_sequence;
        pending_fires_.push_back(fire);
    }

    queued_inputs_[entity_id].input = input;
    queued_inputs_[entity_id].pending = true;
}

void SimulationServer::fire_weapon(uint16_t entity_id) {
    if (entity_id < kMaxEntities && entities_by_id_[entity_id].is_valid()) {
        PendingFire fire;
        fire.shooter_id = entity_id;
        fire.fire_tick = current_tick_;
        // Use the input sequence from the shooter's last input if available
        if (entities_by_id_[entity_id].is_valid()) {
            fire.input_sequence = entities_by_id_[entity_id]->last_input().input_sequence;
        }
        pending_fires_.push_back(fire);
    }
}

// ---- Movement Configuration --------------------------------------------------

void SimulationServer::set_movement_walk_speed(float speed) {
    auto p = movement_.parameters();
    p.walk_speed = speed;
    movement_.set_parameters(p);
}

void SimulationServer::set_movement_sprint_speed(float speed) {
    auto p = movement_.parameters();
    p.sprint_speed = speed;
    movement_.set_parameters(p);
}

void SimulationServer::set_movement_crouch_speed(float speed) {
    auto p = movement_.parameters();
    p.crouch_speed = speed;
    movement_.set_parameters(p);
}

void SimulationServer::set_movement_acceleration(float accel) {
    auto p = movement_.parameters();
    p.acceleration = accel;
    movement_.set_parameters(p);
}

void SimulationServer::set_movement_jump_velocity(float vel) {
    auto p = movement_.parameters();
    p.jump_velocity = vel;
    movement_.set_parameters(p);
}

void SimulationServer::set_movement_gravity(float gravity) {
    auto p = movement_.parameters();
    p.gravity = gravity;
    movement_.set_parameters(p);
}

void SimulationServer::set_movement_config(const godot::Dictionary &config) {
    auto p = movement_.parameters();
    if (config.has("walk_speed"))     p.walk_speed = static_cast<float>(static_cast<double>(config["walk_speed"]));
    if (config.has("sprint_speed"))   p.sprint_speed = static_cast<float>(static_cast<double>(config["sprint_speed"]));
    if (config.has("crouch_speed"))   p.crouch_speed = static_cast<float>(static_cast<double>(config["crouch_speed"]));
    if (config.has("acceleration"))   p.acceleration = static_cast<float>(static_cast<double>(config["acceleration"]));
    if (config.has("air_acceleration")) p.air_acceleration = static_cast<float>(static_cast<double>(config["air_acceleration"]));
    if (config.has("jump_velocity"))  p.jump_velocity = static_cast<float>(static_cast<double>(config["jump_velocity"]));
    if (config.has("gravity"))        p.gravity = static_cast<float>(static_cast<double>(config["gravity"]));
    if (config.has("friction"))       p.friction = static_cast<float>(static_cast<double>(config["friction"]));
    if (config.has("max_velocity"))   p.max_velocity = static_cast<float>(static_cast<double>(config["max_velocity"]));
    movement_.set_parameters(p);
}

// ---- Benchmark / Stats -------------------------------------------------------

godot::Dictionary SimulationServer::get_stats() const {
    godot::Dictionary stats;
    stats["last_tick_usec"] = static_cast<int64_t>(last_tick_usec_);
    stats["tick_count"] = static_cast<int64_t>(tick_count_);
    stats["entity_count"] = static_cast<int64_t>(get_entity_count());

    float avg_usec = 0.0f;
    if (tick_count_ > 0) {
        avg_usec = static_cast<float>(total_tick_usec_) / static_cast<float>(tick_count_);
    }
    stats["avg_tick_usec"] = avg_usec;
    stats["peak_tick_usec"] = static_cast<float>(max_tick_usec_);

    // Also expose tick interval
    stats["tick_hz"] = static_cast<int64_t>(tick_hz_);
    stats["tick_interval_usec"] = static_cast<int64_t>(tick_interval_ * 1'000'000.0f);

    return stats;
}

void SimulationServer::reset_stats() {
    tick_count_ = 0;
    last_tick_usec_ = 0;
    total_tick_usec_ = 0;
    max_tick_usec_ = 0;
}

void SimulationServer::populate_bots(uint16_t count) {
    count = std::min(count, static_cast<uint16_t>(kMaxEntities));

    // Clear existing
    for (auto &e : entities_by_id_) {
        e = godot::Ref<Entity>();
    }
    last_snapshots_.clear();

    // Spawn bots in a grid pattern
    uint16_t per_row = static_cast<uint16_t>(std::ceil(std::sqrt(static_cast<float>(count))));
    for (uint16_t i = 0; i < count; ++i) {
        uint16_t row = i / per_row;
        uint16_t col = i % per_row;
        godot::Vector3 pos(
            static_cast<float>(col) * 3.0f,
            0.0f,
            static_cast<float>(row) * 3.0f
        );
        spawn_entity(pos);
    }
}

// ---- Private: Tick Logic -----------------------------------------------------

void SimulationServer::process_tick() {
    auto tick_start = Clock::now();

    // 1. Apply queued inputs from clients
    for (uint16_t i = 0; i < kMaxEntities; ++i) {
        if (queued_inputs_[i].pending && entities_by_id_[i].is_valid()) {
            entities_by_id_[i]->apply_input(queued_inputs_[i].input);
            queued_inputs_[i].pending = false;
        }
    }

    // 2. Record pre-move positions for lag compensation (BEFORE movement)
    record_position_history(current_tick_);

    // 3. Update entity positions
    update_movement();

    // 4. Process combat with lag compensation
    update_combat();

    // 5. Process queued weapon fires from fire_weapon() calls (deprecated path,
    //    kept for backwards compat — modern path queues fires via apply_input)
    //    This is now handled inside update_combat() which drains pending_fires_

    // 6. Clean up dead entities after combat
    for (auto &entity : entities_by_id_) {
        if (entity.is_valid() && !entity->is_alive()) {
            // Keep dead entities in list for ragdoll/corpse but mark
            // We could despawn here if needed
        }
    }

    // 7. Rebuild living entity ptrs for hit detection next tick
    living_entity_ptrs_.clear();
    for (auto &e : entities_by_id_) {
        if (e.is_valid() && e->is_alive()) {
            living_entity_ptrs_.push_back(e.ptr());
        }
    }
    hit_detection_.set_entities(living_entity_ptrs_);

    ++current_tick_;
    ++tick_count_;

    // Timing
    auto tick_end = Clock::now();
    last_tick_usec_ = std::chrono::duration_cast<std::chrono::microseconds>(
        tick_end - tick_start
    ).count();
    total_tick_usec_ += last_tick_usec_;
    max_tick_usec_ = std::max(max_tick_usec_, last_tick_usec_);
}

void SimulationServer::update_movement() {
    float delta = tick_interval_;

    for (auto &entity_ref : living_entity_ptrs_) {
        movement_.update(*entity_ref, delta);
    }
}

void SimulationServer::update_combat() {
    if (pending_fires_.empty()) return;

    // Sort fires by fire_tick (oldest first) for deterministic processing
    std::sort(pending_fires_.begin(), pending_fires_.end(),
        [](const PendingFire &a, const PendingFire &b) {
            return a.fire_tick < b.fire_tick;
        });

    // Process each fire with lag compensation
    for (auto &fire : pending_fires_) {
        process_compensated_fire(fire);
    }

    pending_fires_.clear();
}

// ---- Lag Compensation ---------------------------------------------------------

void SimulationServer::record_position_history(uint32_t tick) {
    TickPositionSnapshot &slot = position_history_[history_write_index_];
    slot.tick = tick;
    slot.entries.clear();

    for (Entity *entity : living_entity_ptrs_) {
        if (!entity) continue;
        TickPositionSnapshot::Entry entry;
        entry.entity_id = entity->get_entity_id();
        entry.position = entity->get_position();
        entry.flags = entity->get_flags();
        slot.entries.push_back(entry);
    }

    history_write_index_ = (history_write_index_ + 1) % history_depth_;
}

SimulationServer::RewindState SimulationServer::begin_rewind(uint32_t target_tick) {
    RewindState state;

    // Find the closest position snapshot at or before the target tick
    // Search backward from the write index
    for (uint32_t i = 0; i < history_depth_; ++i) {
        uint32_t idx = (history_write_index_ + history_depth_ - 1 - i) % history_depth_;
        const TickPositionSnapshot &snap = position_history_[idx];
        if (snap.tick == 0) break;  // uninitialized slot

        if (snap.tick <= target_tick) {
            // Save current positions, then rewind to snapshot positions
            for (auto &entry : snap.entries) {
                if (entry.entity_id < kMaxEntities &&
                    entities_by_id_[entry.entity_id].is_valid()) {
                    // Save current position
                    TickPositionSnapshot::Entry saved;
                    saved.entity_id = entry.entity_id;
                    saved.position = entities_by_id_[entry.entity_id]->get_position();
                    saved.flags = entities_by_id_[entry.entity_id]->get_flags();
                    state.saved.push_back(saved);

                    // Rewind to snapshot position
                    entities_by_id_[entry.entity_id]->set_position(entry.position);
                    entities_by_id_[entry.entity_id]->set_flags(entry.flags);
                }
            }
            break;
        }
    }

    // Update hit detection with current (now rewound) positions
    // The living_entity_ptrs_ haven't changed, only positions have
    return state;
}

void SimulationServer::end_rewind(const RewindState &state) {
    // Restore all saved positions
    for (auto &saved : state.saved) {
        if (saved.entity_id < kMaxEntities &&
            entities_by_id_[saved.entity_id].is_valid()) {
            entities_by_id_[saved.entity_id]->set_position(saved.position);
            entities_by_id_[saved.entity_id]->set_flags(saved.flags);
        }
    }
}

void SimulationServer::process_compensated_fire(const PendingFire &fire) {
    auto &shooter = entities_by_id_[fire.shooter_id];
    if (!shooter.is_valid() || !shooter->is_alive()) return;

    // Calculate fire direction from shooter's yaw/pitch at time of fire
    float yaw_rad = shooter->get_yaw() * (kPi / 180.0f);
    float pitch_rad = shooter->get_pitch() * (kPi / 180.0f);

    godot::Vector3 direction(
        std::cos(pitch_rad) * std::sin(yaw_rad),
        -std::sin(pitch_rad),
        std::cos(pitch_rad) * std::cos(yaw_rad)
    );

    // Apply spread (random cone)
    if (weapon_config_.spread_degrees > 0.0f) {
        float spread_rad = weapon_config_.spread_degrees * (kPi / 180.0f);
        float theta = static_cast<float>(rand() % 360) * (kPi / 180.0f);
        float phi = static_cast<float>(rand() % 100) / 100.0f * spread_rad;
        godot::Vector3 up(0.0f, 1.0f, 0.0f);
        godot::Vector3 right = direction.cross(up).normalized();
        if (right.length_squared() < 0.001f) {
            right = godot::Vector3(1.0f, 0.0f, 0.0f);
        }
        up = right.cross(direction).normalized();
        godot::Vector3 spread_offset = right * std::sin(theta) * std::sin(phi)
                                      + up * std::cos(theta) * std::sin(phi);
        direction = (direction + spread_offset).normalized();
    }

    // Shooter's eye position (current — not rewound)
    godot::Vector3 origin = shooter->get_position();
    origin.y += 1.5f; // eye height

    // BEGIN LAG COMPENSATION: rewind targets to fire_tick positions
    RewindState rewind_state = begin_rewind(fire.fire_tick);

    // Restore shooter to current position immediately (don't rewind the shooter)
    for (auto it = rewind_state.saved.begin(); it != rewind_state.saved.end(); ) {
        if (it->entity_id == fire.shooter_id) {
            // Put shooter back to current position
            entities_by_id_[fire.shooter_id]->set_position(it->position);
            entities_by_id_[fire.shooter_id]->set_flags(it->flags);
            // Remove from rewind set so end_rewind doesn't re-restore it
            it = rewind_state.saved.erase(it);
        } else {
            ++it;
        }
    }

    // Update hit detection reference (pointers are still valid, positions changed)
    hit_detection_.set_entities(living_entity_ptrs_);

    // Fire the shot against rewound target positions
    HitResult hit = hit_detection_.process_shot(
        origin, direction, fire.shooter_id, weapon_config_
    );

    // END LAG COMPENSATION: restore all entity positions
    end_rewind(rewind_state);
    hit_detection_.set_entities(living_entity_ptrs_);

    // Apply damage if hit
    if (hit.hit && hit.entity_id < kMaxEntities) {
        auto &target_ref = entities_by_id_[hit.entity_id];
        if (target_ref.is_valid()) {
            Entity *target_ptr = target_ref.ptr();
            HitDetection::apply_damage(*target_ptr, hit.damage, 1.0f);

            // Record hit result for GDScript feedback
            last_hit_result_.hit = true;
            last_hit_result_.entity_id = hit.entity_id;
            last_hit_result_.damage = hit.damage;
            last_hit_result_.distance = hit.distance;
            last_hit_result_.hitbox_id = hit.hitbox_id;
            last_hit_result_.killed = !target_ptr->is_alive();
        }
    } else {
        last_hit_result_.hit = false;
    }
}

// ---- Weapon Config ------------------------------------------------------------

void SimulationServer::set_weapon_config(const godot::Dictionary &config) {
    WeaponDamage wd;
    if (config.has("base_damage"))       wd.base_damage = static_cast<float>(static_cast<double>(config["base_damage"]));
    if (config.has("head_multiplier"))   wd.head_multiplier = static_cast<float>(static_cast<double>(config["head_multiplier"]));
    if (config.has("body_multiplier"))   wd.body_multiplier = static_cast<float>(static_cast<double>(config["body_multiplier"]));
    if (config.has("arm_multiplier"))    wd.arm_multiplier = static_cast<float>(static_cast<double>(config["arm_multiplier"]));
    if (config.has("leg_multiplier"))    wd.leg_multiplier = static_cast<float>(static_cast<double>(config["leg_multiplier"]));
    if (config.has("max_range"))         wd.max_range = static_cast<float>(static_cast<double>(config["max_range"]));
    if (config.has("spread_degrees"))    wd.spread_degrees = static_cast<float>(static_cast<double>(config["spread_degrees"]));
    if (config.has("fire_rate_hz"))      weapon_fire_rate_hz_ = static_cast<float>(static_cast<double>(config["fire_rate_hz"]));
    weapon_config_ = wd;
}

void SimulationServer::set_history_depth(uint32_t ticks) {
    history_depth_ = std::clamp(ticks, 16u, 256u);
}

godot::Dictionary SimulationServer::get_last_hit_result() const {
    godot::Dictionary result;
    result["hit"] = last_hit_result_.hit;
    result["entity_id"] = static_cast<int64_t>(last_hit_result_.entity_id);
    result["damage"] = last_hit_result_.damage;
    result["distance"] = last_hit_result_.distance;
    result["hitbox_id"] = static_cast<int64_t>(last_hit_result_.hitbox_id);
    result["killed"] = last_hit_result_.killed;
    return result;
}

godot::PackedByteArray SimulationServer::serialize_state() {
    const uint16_t count = static_cast<uint16_t>(living_entity_ptrs_.size());
    if (count == 0) return godot::PackedByteArray();

    // Pre-allocate a reasonable buffer
    Bitstream stream;
    stream.reserve(1024); // 1KB should cover a full snapshot of 32 entities

    // Use delta compression if we have a base snapshot
    if (!last_snapshots_.empty()) {
        serializer_.write_delta_snapshot(
            stream, current_tick_,
            living_entity_ptrs_.data(), count,
            last_snapshots_
        );
    } else {
        serializer_.write_full_snapshot(
            stream, current_tick_,
            living_entity_ptrs_.data(), count
        );
    }

    // Update last snapshots for next delta
    for (Entity *entity : living_entity_ptrs_) {
        if (entity) {
            last_snapshots_[entity->get_entity_id()] = entity->capture_snapshot();
        }
    }

    // Convert to PackedByteArray
    godot::PackedByteArray result;
    size_t byte_size = stream.byte_size();
    result.resize(static_cast<int64_t>(byte_size));
    if (byte_size > 0) {
        uint8_t *dst = result.ptrw();
        memcpy(dst, stream.data(), byte_size);
    }

    return result;
}

// ---- Private: Helpers --------------------------------------------------------

uint16_t SimulationServer::allocate_entity_id() {
    for (uint16_t i = 0; i < kMaxEntities; ++i) {
        if (!entities_by_id_[i].is_valid()) {
            return i;
        }
    }
    return UINT16_MAX;
}

// ---- GDScript Bindings -------------------------------------------------------

void SimulationServer::_bind_methods() {
    using namespace godot;

    // Lifecycle
    ClassDB::bind_method(D_METHOD("set_tick_rate", "hz"), &SimulationServer::set_tick_rate);
    ClassDB::bind_method(D_METHOD("get_tick_rate"), &SimulationServer::get_tick_rate);
    ADD_PROPERTY(PropertyInfo(Variant::INT, "tick_rate"), "set_tick_rate", "get_tick_rate");

    ClassDB::bind_method(D_METHOD("start"), &SimulationServer::start);
    ClassDB::bind_method(D_METHOD("stop"), &SimulationServer::stop);
    ClassDB::bind_method(D_METHOD("is_running"), &SimulationServer::is_running);
    ClassDB::bind_method(D_METHOD("can_tick", "delta"), &SimulationServer::can_tick);
    ClassDB::bind_method(D_METHOD("tick"), &SimulationServer::tick);

    // Entity management
    ClassDB::bind_method(D_METHOD("spawn_entity", "position"), &SimulationServer::spawn_entity);
    ClassDB::bind_method(D_METHOD("despawn_entity", "entity_id"), &SimulationServer::despawn_entity);
    ClassDB::bind_method(D_METHOD("get_entity", "entity_id"), &SimulationServer::get_entity);
    ClassDB::bind_method(D_METHOD("get_entity_count"), &SimulationServer::get_entity_count);
    ClassDB::bind_method(D_METHOD("get_entity_ids"), &SimulationServer::get_entity_ids);

    // Input
    ClassDB::bind_method(D_METHOD("apply_input", "entity_id", "input_dict"), &SimulationServer::apply_input);
    ClassDB::bind_method(D_METHOD("fire_weapon", "entity_id"), &SimulationServer::fire_weapon);

    // Movement config
    ClassDB::bind_method(D_METHOD("set_movement_walk_speed", "speed"), &SimulationServer::set_movement_walk_speed);
    ClassDB::bind_method(D_METHOD("set_movement_sprint_speed", "speed"), &SimulationServer::set_movement_sprint_speed);
    ClassDB::bind_method(D_METHOD("set_movement_crouch_speed", "speed"), &SimulationServer::set_movement_crouch_speed);
    ClassDB::bind_method(D_METHOD("set_movement_acceleration", "accel"), &SimulationServer::set_movement_acceleration);
    ClassDB::bind_method(D_METHOD("set_movement_jump_velocity", "vel"), &SimulationServer::set_movement_jump_velocity);
    ClassDB::bind_method(D_METHOD("set_movement_gravity", "gravity"), &SimulationServer::set_movement_gravity);
    ClassDB::bind_method(D_METHOD("set_movement_config", "config"), &SimulationServer::set_movement_config);

    // Weapon config
    ClassDB::bind_method(D_METHOD("set_weapon_config", "config"), &SimulationServer::set_weapon_config);

    // Lag compensation
    ClassDB::bind_method(D_METHOD("set_history_depth", "ticks"), &SimulationServer::set_history_depth);

    // Hit feedback
    ClassDB::bind_method(D_METHOD("get_last_hit_result"), &SimulationServer::get_last_hit_result);

    // Benchmark
    ClassDB::bind_method(D_METHOD("get_stats"), &SimulationServer::get_stats);
    ClassDB::bind_method(D_METHOD("reset_stats"), &SimulationServer::reset_stats);
    ClassDB::bind_method(D_METHOD("populate_bots", "count"), &SimulationServer::populate_bots);
}

} // namespace tactical_shooter
