/*
 * client.h — the implementation type behind the opaque `vc_client*` handle.
 */
#ifndef VOICECAT_CORE_CLIENT_H
#define VOICECAT_CORE_CLIENT_H

#include "voicecat.h"

#ifdef VOICECAT_HAS_NET

#include <atomic>
#include <condition_variable>
#include <deque>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>

#include "audio/audio_engine.h"
#include "codec/opus_codec.h"
#include "crypto/crypto.h"
#include "crypto/tofu_store.h"
#include "net/voice_frame.h"
#include "protocol/envelope.h"
#include "protocol/protocol.h"
#include "session/session.h"
#include "proto/voicecat.pb.h"

#endif  // VOICECAT_HAS_NET

struct vc_client {
    vc_client(const vc_config& cfg, vc_callbacks cb);
    ~vc_client();

    vc_client(const vc_client&) = delete;
    vc_client& operator=(const vc_client&) = delete;

    vc_result connect(const char* host, uint16_t port);
    vc_result disconnect();
    vc_result authenticate_guest(const char* nickname);
    vc_result authenticate_user(const char* username, const char* password);

    vc_result join_channel(uint32_t channel_id, const char* password);
    vc_result leave_channel();

    vc_result stream_start(const vc_stream_desc& desc, uint32_t* out_stream_id);
    vc_result stream_stop(uint32_t stream_id);
    vc_result set_input_device(uint32_t stream_id, const char* device_id);
    vc_result set_capture_channels(uint32_t stream_id, uint32_t channels);
    vc_result set_input_mode(vc_input_mode mode);
    vc_result set_vad_threshold(float threshold);
    vc_result set_push_to_talk(bool active);
    vc_result set_self_mute(bool mic_muted, bool deafened);
    vc_result set_output_volume(float gain);
    vc_result set_input_gain(float gain);
    vc_result set_input_noise_reduction(bool enable);
    vc_result set_remote_stream(uint32_t user_id, uint32_t stream_id, float gain, bool muted,
                                bool noise_reduction);
    vc_result get_remote_stream(uint32_t user_id, uint32_t stream_id,
                                vc_remote_stream_state* out);
    vc_result audio_suspend();
    vc_result audio_resume();
    vc_result audio_restart();

    vc_result send_text(vc_text_scope scope, uint32_t target_id, const char* utf8);

    vc_result list_devices(vc_device_kind kind, vc_device_list* out);

    // M4: channel/user/stream snapshot getters (read session_model_; see voicecat.h).
    vc_result list_channels(vc_channel_list* out);
    vc_result list_users(vc_user_list* out);
    vc_result list_user_streams(uint32_t user_id, vc_stream_summary_list* out);

    // M4: TOFU server-identity gate (see voicecat.h's VC_EVENT_SERVER_IDENTITY doc comment).
    vc_result confirm_server_identity(bool accept);
    vc_result get_server_identity_display(char* out_buf, size_t buf_cap, size_t* out_len);

    // M3: effective Opus config for a (user_id, stream_id) — our own pending/active local
    // streams, or any peer's broadcast StreamInfo.audio.
    vc_result get_stream_audio_config(uint32_t user_id, uint32_t stream_id,
                                      vc_audio_config* out);

    // External PCM feed (see voicecat.h: vc_stream_feed_pcm). Production API for driving a
    // local stream's encode pipeline without a hardware capture device. channels = 1 or 2.
    vc_result stream_feed_pcm(uint32_t stream_id, const int16_t* pcm,
                              size_t samples_per_channel, uint32_t channels);

    // External PCM sink (see voicecat.h: vc_set_pcm_sink). Delegates to AudioEngine.
    vc_result set_pcm_sink(vc_pcm_sink_cb cb, void* user);

    // External mixed-output sink + external-playback mode (iOS VPIO; see voicecat.h).
    vc_result set_mixed_output_sink(vc_mixed_output_cb cb, void* user);
    vc_result set_external_playback(bool enable);

    // TEST-ONLY (see voicecat.h) — deprecated alias for stream_feed_pcm(..., channels=1).
    vc_result test_inject_capture(uint32_t stream_id, const int16_t* pcm, size_t samples);

    // M5: moderation & admin.
    vc_result kick_user(uint32_t user_id, const char* reason);
    vc_result ban_user(uint32_t user_id, const char* reason, uint64_t expires_unix_ms);
    vc_result set_permission(uint32_t user_id, const vc_permissions* perms);
    vc_result set_server_mute(uint32_t user_id, bool muted, bool deafened);
    vc_result move_user(uint32_t user_id, uint32_t channel_id);
    vc_result create_channel(const vc_channel_info* info);
    vc_result edit_channel(const vc_channel_info* info);
    vc_result delete_channel(uint32_t channel_id);
    vc_result create_account(const char* username, const char* password);
    vc_result reset_password(const char* username, const char* new_password);
    vc_result delete_account(const char* username);
    vc_result list_accounts();
    vc_result get_account_list(vc_account_list* out);
    vc_result get_permissions(vc_permissions* out);

    vc_connection_state state() const {
#ifdef VOICECAT_HAS_NET
        return state_net_.load(std::memory_order_acquire);
#else
        return state_;
#endif
    }

   private:
    void emit(const vc_event& ev) const;

    vc_config    cfg_{};
    vc_callbacks cb_{};

#ifdef VOICECAT_HAS_NET
    // ── M1: TCP/TLS control channel ─────────────────────────────────────────────
    std::atomic<vc_connection_state> state_net_{VC_STATE_DISCONNECTED};

    // Blocking I/O thread (one per vc_client lifetime)
    std::thread      io_thread_;
    std::atomic<bool> io_stop_{false};

    // Send queue: pushed by any thread, drained by io_thread_
    std::mutex                        send_mutex_;
    std::condition_variable           send_cv_;
    std::deque<std::vector<uint8_t>>  send_queue_;

    // TLS context — created + used exclusively on io_thread_
    std::unique_ptr<voicecat::crypto::TlsContext> tls_;

    // Raw socket fd (stored after TCP connect; closed by disconnect())
    std::atomic<int> io_fd_{-1};

    // Pending auth stored before ServerHello arrives
    struct PendingAuth {
        bool        is_guest{true};
        std::string nick_or_user;
        std::string password;
    };
    std::mutex                    pending_auth_mutex_;
    std::optional<PendingAuth>    pending_auth_;

    // Self identity filled in after AuthResult
    uint32_t self_user_id_{0};
    uint64_t server_session_id_{0};
    std::atomic<uint64_t> next_req_id_{1};

    // Keepalive: client sends a Ping every ~15s (docs/protocol.md §7) so the server's
    // last_seen stays fresh and the reaper doesn't drop us. Pong echoes the nonce, which
    // we correlate to measure RTT. The read loop's 50ms TLS timeout means it spins fast
    // enough to check the ping interval with ample resolution.
    static constexpr int64_t kPingIntervalMs = 15000;
    std::atomic<int64_t>  last_ping_ms_{0};
    std::atomic<uint64_t> ping_nonce_{1};
    std::mutex            ping_mutex_;
    std::unordered_map<uint64_t, int64_t> pending_pings_;  // nonce → sent_ms
    std::atomic<int64_t>  last_rtt_ms_{0};

    // Graceful disconnect: set by disconnect() after queueing Disconnect{code=0}. The io
    // thread checks this after drain_sends() — when set, it sets io_stop_ and exits the
    // read loop, so the Disconnect is sent before the thread ends. The main thread just
    // joins; no socket close from the main thread (the io thread's cleanup closes it),
    // avoiding the double-close race that the send_cv_ wait approach exposed.
    std::atomic<bool> graceful_disconnect_pending_{false};

    // UDP keepalive: send a lightweight KEEPALIVE frame every ~5s to hold NAT bindings and
    // bump the server's last_seen independently of the TCP ping (docs/voice.md §6). Sent
    // as plaintext (no AEAD) — the server identifies the sender by its already-verified UDP
    // endpoint, and the TCP reaper is the real timeout authority. Avoids racing the
    // non-atomic send_counter_ in SodiumMediaCrypto::seal() with the audio callback thread.
    static constexpr int64_t kUdpKeepaliveIntervalMs = 5000;
    std::atomic<int64_t>  last_udp_keepalive_ms_{0};

    // Client-side session model. Mutated only on io_thread_ (handle_server_state/
    // handle_user_event/handle_channel_event), but read from any thread via the M4
    // list_channels/list_users/list_user_streams getters — session_model_mu_ guards both.
    voicecat::session::SessionModel session_model_;
    mutable std::mutex              session_model_mu_;

    // M5: last ListAccountsResult snapshot, populated on io_thread_ when
    // VC_EVENT_ACCOUNT_LIST fires and read by vc_get_account_list on caller threads.
    std::vector<voicecat::v1::AccountEntry> last_account_list_;
    mutable std::mutex                      account_list_mu_;

    // ── M4: TOFU server-identity gate ───────────────────────────────────────────
    std::unique_ptr<voicecat::crypto::TofuStore> tofu_store_;  // owns the pin file
    std::mutex              tofu_mu_;
    std::condition_variable tofu_cv_;
    bool                    tofu_decision_pending_{false};
    bool                    tofu_accept_{false};
    std::string             pending_identity_fp_hex_;  // ServerHello's Ed25519 fp, display-only

    // ── M2: UDP / media plane ────────────────────────────────────────────────────
    std::array<uint8_t, 16> udp_token_{};
    uint16_t                server_udp_port_{0};
    std::string              udp_host_;

    std::atomic<int>   udp_fd_{-1};
    std::thread        udp_thread_;
    std::atomic<bool>  udp_stop_{false};
    std::atomic<bool>  udp_ready_{false};

    std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> media_send_crypto_;
    std::unique_ptr<voicecat::crypto::SodiumMediaCrypto> media_recv_crypto_;

    voicecat::audio::AudioEngine audio_engine_;

    // M3: one LocalStream per concurrently-active stream kind (MIC/SCREEN_AUDIO/AUX_DEVICE
    // are each singletons for a given client — see docs/roadmap.md §M3). Replaces the M2
    // single-stream fields (local_encoder_/local_stream_active_/etc).
    struct LocalStream {
        voicecat::codec::OpusEncoder encoder;
        std::atomic<bool> active{false};
        bool              pending{false};       // announced, awaiting StreamAnnounceResult
        uint32_t          stream_id{0};
        uint32_t          ssrc{0};
        uint32_t          timestamp{0};
        uint16_t          frame_samples{960};
        uint64_t          pending_request_id{0};
        // Full effective OpusParams from the last StreamAnnounceResult — retained so
        // vc_get_stream_audio_config() has something to read back for our own streams.
        voicecat::codec::OpusParams effective_params;

        // Talk-indicator edge detection (docs/voice.md §7) — updated in on_capture_frame, after
        // the VAD/PTT gate so a gated-closed frame doesn't show as "talking".
        std::atomic<int64_t> last_capture_ms{0};
        bool                  talking = false;

        // Talkspurt marker: the sender's `timestamp` omits VAD/PTT/DTX silence, so the receiver
        // can't tell a continuation from a post-silence restart. encode_and_send_frame stamps
        // kFlagMarker on the first frame after a transmission gap (detected via last_send_ms) so
        // the receiver reseeds its playout clock cleanly. -1 = no frame sent yet (first frame is
        // always a marker).
        int64_t               last_send_ms = -1;

        // Device-enumeration follow-up: the device this stream's capture should use ("" =
        // default). Only meaningful for VC_STREAM_MIC today (the real capture device); set via
        // vc_set_input_device. Opaque id from AudioEngine::enumerate_devices — see
        // audio_engine.h's DeviceInfo doc comment.
        std::string capture_device_id;

        // Capture channel count (1 = mono, 2 = stereo interleaved). Only meaningful for
        // VC_STREAM_MIC. Set via vc_set_capture_channels; read by ensure_audio_running() to
        // configure AudioParams.capture_channels before the device opens. Defaults to 1 (mono).
        uint32_t capture_channels = 1;

        // If true, the caller is feeding PCM via vc_stream_feed_pcm — skip start/stop of the
        // core's WASAPI loopback device. Set from vc_stream_desc::external_feed at stream_start
        // time and checked in handle_stream_announce_result / stream_stop.
        bool external_feed = false;

        // Reframe buffer: the AudioEngine clock is fixed at 48 kHz / 20 ms, so capture/feed
        // always delivers 960-sample frames — but the channel's frame_ms (docs/voice.md §3)
        // can be 2.5…60 ms, so the encoder needs frame_samples per call (480 @10ms, 1920 @40ms,
        // …). on_capture_frame accumulates the engine's 960-sample frames here and emits
        // frame_samples-sized chunks. The 20 ms case (frame_samples == 960) bypasses this
        // entirely (fast path). Pre-sized at announce; never resized on the audio thread
        // (architecture.md §3 — no RT-thread allocation). encode_accum holds interleaved int16
        // at accum_channels; upmix_scratch is the pre-sized mono→stereo upmix target.
        std::vector<int16_t> encode_accum;
        size_t               accum_count    = 0;  // flat samples currently buffered
        int                  accum_channels = 0;  // channel count of buffered data; resets on change
        std::vector<int16_t> upmix_scratch;
    };
    mutable std::mutex                            local_streams_mu_;
    std::unordered_map<int, LocalStream>          local_streams_;       // keyed by vc_stream_kind
    std::unordered_map<uint64_t, int>             pending_announce_kind_;  // request_id -> kind
    uint32_t                                      next_local_stream_id_{1};

    // ssrc → (user_id, stream_id) for remote streams already wired into audio_engine_.
    mutable std::mutex                                            remote_streams_mu_;
    std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>>   remote_streams_;

    // M3: talk-indicator polling thread (separate from udp_thread_ / the miniaudio callback
    // thread — see architecture.md §3 real-time rule).
    std::thread        talk_timer_thread_;
    std::atomic<bool>  talk_timer_stop_{false};
    static constexpr int64_t kTalkPollMs     = 100;
    static constexpr int64_t kTalkHangoverMs = 300;

    // Local UDP destination (server media endpoint), resolved once during binding.
    uint32_t udp_dest_addr_{0};  // network byte order
    uint16_t udp_dest_port_{0};  // network byte order

    std::atomic<bool> self_mic_muted_{false};
    std::atomic<bool> self_deafened_{false};
    std::atomic<bool> server_muted_{false};
    std::atomic<bool> server_deafened_{false};

    // M5: permissions from last AuthResult.
    vc_permissions own_permissions_{};

    // Follow-up to M3: send-side input gate (docs/voice.md §11). MIC-only — SCREEN_AUDIO/
    // AUX_DEVICE are never gated (see PROGRESS.md for the rationale). Pure local state, no
    // protocol traffic. mic_vad_ is constructed once the MIC stream's StreamAnnounceResult
    // lands (handle_stream_announce_result, on io_thread_ — not the RT capture callback).
    std::atomic<vc_input_mode> current_input_mode_{VC_INPUT_VOICE_ACTIVATION};
    std::atomic<bool>          ptt_active_{false};
    std::atomic<float>         vad_threshold_{0.025f};  // remembered across mode switches
    std::atomic<float>         input_gain_{1.0f};  // send-side MIC gain (vc_set_input_gain)
    std::atomic<bool>          input_noise_reduction_{false};  // send-side MIC NS (vc_set_input_noise_reduction)

    // External-playback mode (iOS VPIO): when true, ensure_audio_running() configures the
    // AudioEngine to skip its hardware playback device and drive the mixer on a timer instead,
    // delivering the final mix to the mixed-output sink. Set via vc_set_external_playback.
    std::atomic<bool>          external_playback_{false};
    std::unique_ptr<voicecat::audio::ApmProcessor> mic_vad_;
    // Send-side mic noise suppressor (RNNoise). Constructed once with the MIC stream alongside
    // mic_vad_ (off the RT capture callback); toggling only flips input_noise_reduction_, so the
    // capture callback never allocates or races this pointer.
    std::unique_ptr<voicecat::audio::ApmProcessor> mic_ns_;

    // teardown_voice() is called both from run_io()'s own cleanup (on the io_thread_, when
    // the read loop exits) and from disconnect() (on the caller's thread) -- without
    // serializing those two call sites, both can see udp_thread_/talk_timer_thread_ as
    // joinable() at the same time and race to join() the same std::thread object (UB; an
    // intermittent "No such process" std::system_error on Windows is the typical symptom).
    // This mutex makes teardown_voice() idempotent under concurrent calls.
    std::mutex teardown_mu_;

    // ── io_thread_ entry point ──────────────────────────────────────────────────
    void run_io(std::string host, uint16_t port);

    // ── Protocol dispatch (called on io_thread_) ────────────────────────────────
    void handle_envelope(const voicecat::v1::Envelope& env);
    void handle_server_hello(const voicecat::v1::ServerHello& msg, uint64_t req_id);
    void handle_auth_result(const voicecat::v1::AuthResult& msg);
    void handle_server_state(const voicecat::v1::ServerStateSnapshot& snap);
    void handle_user_event(const voicecat::v1::UserEvent& ue);
    void handle_channel_event(const voicecat::v1::ChannelEvent& ce);
    void handle_join_channel_result(const voicecat::v1::JoinChannelResult& msg);
    void handle_text_message(const voicecat::v1::TextMessage& msg);
    void handle_disconnect(const voicecat::v1::Disconnect& msg);
    void handle_udp_binding_ack(const voicecat::v1::UdpBinding& msg);
    void handle_stream_announce_result(uint64_t req_id,
                                       const voicecat::v1::StreamAnnounceResult& msg);

    // ── M2: UDP / media helpers ──────────────────────────────────────────────────
    // Kicks off TCP UdpBinding request; called once after a successful AuthResult.
    void start_udp_binding();
    // Opens the UDP socket, sends the plaintext bootstrap packet, starts udp_thread_.
    void finish_udp_binding();
    // udp_thread_ entry point: recv loop, AEAD-open, decode, push to audio_engine_.
    void run_udp_recv();
    // Send a plaintext KEEPALIVE frame to the server media endpoint. Called from run_udp_recv
    // every kUdpKeepaliveIntervalMs to hold NAT bindings + bump the server's last_seen
    // (docs/voice.md §6). Plaintext — no AEAD — to avoid racing the audio thread's seal().
    void send_udp_keepalive();
    // capture_cb passed to audio_engine_.start(): encode + seal + send one frame for the
    // given local stream `kind` (M3: multiple concurrent local streams are possible).
    void on_capture_frame(int kind, const int16_t* pcm, int samples, int channels);
    // Encode one frame of exactly ls.frame_samples samples-per-channel (upmixing mono→stereo
    // for a stereo channel as needed), seal it, and send it over UDP, advancing ls.timestamp.
    // Called by on_capture_frame for each frame_samples-sized chunk. Assumes local_streams_mu_
    // is held and the send gate/crypto checks have already passed.
    void encode_and_send_frame(LocalStream& ls, const int16_t* pcm, int samples, int channels,
                               int fd);
    // Inspect a User proto's streams and wire up any new remote ssrc into audio_engine_,
    // emitting VC_EVENT_STREAM_STARTED/STOPPED as streams appear/disappear.
    void sync_remote_streams(const voicecat::v1::User& user);
    // Starts audio_engine_ (capture+playback) if not already running.
    void ensure_audio_running();
    // Joins udp_thread_, stops audio_engine_, clears media crypto/remote-stream state.
    // Safe to call multiple times. Called both from run_io()'s cleanup and disconnect().
    void teardown_voice();
    // talk_timer_thread_ entry point (M3): polls audio_engine_ for remote talk-state edges
    // and local capture activity, emitting VC_EVENT_TALK_STATE. Never the audio RT thread.
    void run_talk_timer();
    // Find a LocalStream by its client-assigned stream_id (held under local_streams_mu_ by
    // the caller, or taken internally). Returns nullptr if not found/not active.
    LocalStream* find_local_stream_by_id(uint32_t stream_id);

    // ── Helpers (io_thread_ and caller threads) ─────────────────────────────────
    // Queue an encoded envelope to be sent on io_thread_.
    void queue_envelope(const voicecat::v1::Envelope& env);

    // Keepalive: send a Ping envelope with a fresh nonce and record the sent time for
    // RTT measurement when the Pong arrives. Called from the read loop when kPingIntervalMs
    // has elapsed. (docs/protocol.md §7)
    void send_ping();

    // Drain send_queue_ by doing blocking TLS writes (called on io_thread_).
    void drain_sends();

    // Transition state + emit VC_EVENT_CONNECTION_STATE.
    void set_state(vc_connection_state s);

    // Convenience event emitters.
    void emit_error(vc_result r, const char* text);
    void emit_disconnected(vc_result r, const char* reason);

#else  // !VOICECAT_HAS_NET
    vc_connection_state state_{VC_STATE_DISCONNECTED};
#endif
};

#endif  // VOICECAT_CORE_CLIENT_H
