#pragma once

// -----------------------------------------------------------------------------
// webradar.h
// -----------------------------------------------------------------------------
// WebRadar: publishes the match state to the valthrun web radar (central
// server model). The cheat is the PUBLISHER only - the browser frontend and
// the WebSocket server are hosted by radar.valth.run, so the shared viewer
// URL (https://radar.valth.run/session/<session_id>) works without opening
// any local port.
//
// Transport: IXWebSocket (vcpkg, x64-windows) with the mbedTLS backend for
// wss://. The publisher thread runs the state loop; the WebSocket object
// lives on its own internal thread and dispatches server messages to the
// handshake state machine (HandleServerMessage).
//
// Protocol (valthrun radar protocol v2, WebSocket + JSON text frames):
//   1. Connect to wss://radar.valth.run/publish
//   2. Handshake:  {"type":"request-initialize","payload":{"clientVersion":2}}
//      ->          {"type":"response-success","payload":{"serverVersion":2}}
//   3. Session:    {"type":"initialize-publish","payload":{}}
//                  (reconnects add "session_auth_token" to resume the URL)
//      ->          {"type":"response-initialize-publish",
//                   "payload":{"session_id":"...","session_auth_token":"..."}}
//   4. Every 50ms: {"type":"notify-radar-state","payload":{"state":{...}}}
//
// Field names match radar/shared/src/protocol.rs of valthrun-cs2 (serde,
// snake_case payload fields, kebab-case type tags).
// -----------------------------------------------------------------------------

#include <condition_variable>
#include <memory>
#include <mutex>
#include <string>

#include <ixwebsocket/IXWebSocket.h>

#include "../core/memory.h"
#include "../core/offsets.h"
#include "../sdk/entitylist.h"
#include "../sdk/localplayer.h"

namespace features {

// Publishes the radar state to the valthrun web radar.
class WebRadar
{
public:
	// Binds the memory reader, offsets, entity list, local player and the
	// module bases (client.dll for dwGlobalVars, engine2.dll for
	// engine2-relative offsets like the CNetworkGameClient instance).
	void Initialize(const core::ProcessMemory* memory, const core::GameOffsets* offsets,
		const sdk::EntityList* entities, const sdk::LocalPlayer* player,
		uintptr_t clientBase, uintptr_t engineBase);

	// Starts the publisher thread (and the first connection).
	void Start();

	// Creates a fresh WebSocket, installs the handshake callback and starts
	// the internal thread. Called for the first connection and again after
	// every Disconnect() (which destroys the socket): each (re)connect
	// looks exactly like the first launch, except that the stored session
	// auth token reclaims the unbound session.
	void CreateSocketAndStart();

	// Thread entry point.
	static unsigned long __stdcall ThreadProc(void* param);

	// Returns the current session viewer URL ("" when not connected).
	// Called by the menu on every redraw.
	std::string GetSessionUrl() const;

private:
	// Publishes the current radar state as a JSON string. Called every 50 ms
	// by the thread. Returns false when the state could not be produced or
	// the send failed (the caller then drops the connection).
	bool PublishState();

	// Waits until the handshake + session registration that the socket's own
	// thread drives completes. The socket is started once in Start() and
	// reconnects by itself (automatic reconnection), so this never touches
	// start()/stop() again: it just waits on the handshake condition. Returns
	// immediately when the current connection is already handshaken (radar
	// toggled off and back on - the live connection is reused as-is). On
	// success fills m_connected + the session state. Returns false on failure
	// or timeout.
	bool Connect();

	// Sends one JSON text frame over the open WebSocket.
	bool SendFrame(const std::string& json);

	// Stops publishing and destroys the socket (its destructor closes the
	// connection cleanly). No goodbye message is sent on purpose: the
	// server then keeps the session as "unbound" (radar-server
	// pub_session_unbound) instead of closing it, and the stored auth token
	// reclaims it on the next connect - the viewer URL stays the same
	// across toggles. The session is only kept while the radar is toggled
	// off briefly: ResetSession() drops it after the radar stayed off for
	// over a minute, so the next enable gets a fresh id. Stale tokens are
	// also dropped by NoteResumeFailure after repeated rejections.
	void Disconnect();

	// Marks the handshake as failed so the next Connect() waits for the
	// reconnection handshake instead of assuming the (stale) current one.
	// Called when a send fails and the connection is considered dropped.
	void InvalidateHandshake();

	// Counts rejected handshakes that used a stored session auth token; after
	// 3 consecutive rejections the token is cleared so the next attempt
	// registers a fresh session (see m_resumeFailures). No-op when no token
	// is stored.
	void NoteResumeFailure();

	// Drops the stored session (id + auth token + viewer URL) so the next
	// enable registers a brand-new session id. Called by the publisher
	// thread when the radar stayed off for over a minute (the old unbound
	// session is considered dead). No-op when no session is stored.
	void ResetSession();

	// Turns the radar off after an unexpected error (exception). Only the
	// config flag is flipped here: the publisher thread notices it on its
	// next iteration and performs the actual teardown (Disconnect), so this
	// is safe to call from the socket's own callback thread. Never tears the
	// socket down directly - only the publisher thread may reset it.
	void ForceOff(const std::string& reason);

	// Parses one server message and drives the handshake state machine
	// (runs on the socket's own thread). Returns true only when the
	// handshake + session phase finished successfully; the callback then
	// wakes Connect(). Rejections do not end the handshake: the machine
	// retries on the same live connection (rate-limited), only a connection
	// drop (Error/Close) or the Connect() timeout ends the wait.
	bool HandleServerMessage(const std::string& json);

	// Extracts the string value of a JSON member ("key":"value") from a
	// small server response. Returns "" when the key is absent.
	static std::string ExtractJsonString(const std::string& json, const std::string& key);

	// -----------------------------------------------------------------------
	// Radar state generation - implemented, no WebSocket needed.
	// -----------------------------------------------------------------------

	// Reads the current map name (e.g. "de_dust2") from the game. The map
	// only changes on map transitions, so the value is cached and re-read
	// at most every 5 seconds.
	std::string ReadWorldName();

	// Returns the local player's controller entity index (0..63), or -1
	// when the local pawn is not resolvable.
	int GetLocalControllerIndex();

	// Reads one player pawn entry into a JSON object fragment (without the
	// surrounding array braces). Returns false when the slot is empty.
	bool BuildPlayerJson(int slot, std::string& outFragment);

	// Fills the player name from the controller's m_iszPlayerName.
	std::string ReadPlayerName(int slot);

	// Weapon id of the pawn's active weapon (m_iItemDefinitionIndex).
	// TODO: not implemented - needs the m_pWeaponServices ->
	// m_hActiveWeapon -> m_iItemDefinitionIndex chain. Returns 0 until then.
	unsigned short ReadWeaponId(uintptr_t pawn);

	// Reads the defuser flag / flash time.
	// TODO: not implemented - needs m_pItemServices->m_bHasDefuser and
	// m_flFlashBangTime. Returns false / 0.0 until then.
	bool ReadHasDefuser(uintptr_t pawn);
	float ReadFlashTime(uintptr_t pawn);

	// Builds the JSON fragments for the bomb state: outPlantedC4 is the
	// "plantedC4" object (position, bomb site, state/timer) or "" when no
	// planted bomb exists; outC4Entities holds the comma-separated
	// "c4Entities" object fragments (entity id, position, owner) for every
	// carried/dropped C4. The entity indices come from a 1 Hz class-name
	// scan over the raw entity list (cached).
	void BuildBombJson(std::string& outPlantedC4, std::string& outC4Entities);

	// Re-locates the bomb entities (carried/dropped C4 + planted C4) with
	// a class-name scan. Runs at most once per second.
	void ScanBombs();

	// Returns the absolute world position of an entity via its game scene
	// node (m_pGameSceneNode -> m_vecAbsOrigin). False when unresolved or
	// all-zero (stale entity).
	bool GetEntityPosition(uintptr_t entity, sdk::Vec3& outPosition) const;

private:
	const core::ProcessMemory* m_memory = nullptr;
	const core::GameOffsets* m_offsets = nullptr;
	const sdk::EntityList* m_entities = nullptr;
	const sdk::LocalPlayer* m_player = nullptr;
	uintptr_t m_clientBase = 0;      // client.dll base (dwGlobalVars)
	uintptr_t m_engineBase = 0;      // engine2.dll base (CNetworkGameClient)

	// Cached map name (re-read every 5 seconds, see ReadWorldName).
	std::string m_worldName;
	unsigned long m_lastWorldNameRead = 0;

	// WebSocket transport. Created per connection in CreateSocketAndStart()
	// and destroyed by Disconnect() (m_websocket.reset()). The destructor's
// internal stop() is clean in this library build (the EXPLICIT stop()
// throws std::length_error / std::bad_alloc once its thread has run -
// reproduced in isolation - so it is never called here); it joins the
// internal thread before the object is freed, so the callback (which reads
// this member) can never run against a dead object.
	std::shared_ptr<ix::WebSocket> m_websocket;

	// Set when the handshake + session registration succeeded. Only the
	// publisher thread reads/writes it; the message callback never touches
	// it (it signals through the handshake state below).
	bool m_connected = false;

	// Handshake synchronization: the WebSocket's own thread signals these
	// from the message callback; Connect() (publisher thread) waits on
	// them. They stay members because the callback outlives Connect():
	// messages arriving after the handshake (notify-view-count, ...) must
	// not touch Connect()'s stack. m_handshakeOk is true only while the
	// CURRENT connection has completed the handshake: it is reset on every
	// Open/Error/Close and by InvalidateHandshake, so Connect() can use it
	// to detect "already connected".
	mutable std::mutex m_handshakeMutex;
	std::condition_variable m_handshakeCv;
	bool m_handshakeDone = false;
	bool m_handshakeOk = false;

	// Session state (written by the message callback / Connect, read by the
	// menu through GetSessionUrl): guarded by m_sessionMutex.
	mutable std::mutex m_sessionMutex;
	std::string m_sessionId;
	std::string m_sessionToken;  // resumed on reconnect (keeps the URL)
	std::string m_sessionUrl;    // https://radar.valth.run/session/<id>

	// True once the session initialization was sent on the CURRENT
	// connection. The server acks every notify-radar-state with a
	// response-success; without this flag each ack would trigger another
	// initialize-publish, which the server rejects (the session is already
	// owned) - a hot reconnect flood. Reset on every Open and after a
	// rejection.
	bool m_initialized = false;

	// Consecutive handshake rejections while resuming a stored session auth
	// token. When the server rejects the token repeatedly (stale session),
	// the token is cleared after 3 attempts so a fresh session is created
	// instead of an endless rejection loop. Reset on a successful session
	// registration. Only touched by the message callback.
	int m_resumeFailures = 0;

	// GetTickCount() of the last request-initialize send, used to rate-limit
	// the rejection retry loop (the server answers rejections immediately,
	// which would otherwise flood the connection).
	unsigned long m_lastHandshakeRequest = 0;

	// GetTickCount() of the moment the radar was turned off (set when the
	// publisher thread first notices the radar disabled; refreshed on every
	// off transition). Used to drop the stored session when the radar stays
	// off for over a minute, so the next enable creates a fresh session id.
	unsigned long m_radarOffSince = 0;

	// Cached bomb entities (entity pointer + entity index), refreshed by
	// ScanBombs once per second.
	uintptr_t m_c4Entity = 0;         // carried/dropped bomb (C_C4)
	uintptr_t m_c4Index = 0;
	uintptr_t m_plantedC4Entity = 0;  // planted bomb (C_PlantedC4)
	uintptr_t m_plantedC4Index = 0;
	unsigned long m_lastBombScan = 0; // GetTickCount of the last ScanBombs
};

} // namespace features