// src/Services/WhipClient.h
#pragma once

#include <memory>
#include <string>
#include <string_view>
#include <atomic>
#include <mutex>
#include <optional>
#include <filesystem>
#include <vector>
#include <regex>
#include <array>

#include <spdlog/spdlog.h>
#include <nlohmann/json.hpp>
#include <httplib.h> // build with CPPHTTPLIB_OPENSSL_SUPPORT

#include <rtc/rtc.hpp> // libdatachannel

namespace snoop {

class WhipClient {
public:
    struct Params {
        std::string whipUrl;   // full WHIP endpoint (may already include ?token=...)
        std::string caPath;    // CA chain
        std::string crtPath;   // client cert
        std::string keyPath;   // client key (temp extracted from keyctl)
        int sampleRate = 48000;
        int channels   = 1;
    };

    explicit WhipClient(Params p)
        : m_p(std::move(p)) {}

    ~WhipClient() {
        Stop();
    }

    void Start() {
        std::lock_guard lk(m_mtx);
        if (m_started) return;

        // ----- PeerConnection -----
        rtc::Configuration cfg;
        // No STUN/TURN required for MediaMTX WHIP (server is public/ICE-lite).
        // cfg.iceServers = { }; // default

        m_pc = std::make_shared<rtc::PeerConnection>(cfg);

        // Optional: observe connection state / ICE
        m_pc->onStateChange([this](rtc::PeerConnection::State s){
            spdlog::info("WHIP pc state: {}", (int)s);
        });
        m_pc->onGatheringStateChange([this](rtc::PeerConnection::GatheringState s){
            spdlog::info("WHIP gathering state: {}", (int)s);
        });
        m_pc->onLocalDescription([this](rtc::Description desc){
            // When we have the local SDP, POST (WHIP) to get the answer
            try {
                const std::string offer = std::string(desc);
                auto [answer, resourceUrl] = PostOfferWHIP(offer);
                m_resourceUrl = resourceUrl;
                m_pc->setRemoteDescription(rtc::Description(answer, "answer"));
                spdlog::info("WHIP remote description set");
            } catch (const std::exception& e) {
                spdlog::error("WHIP POST offer failed: {}", e.what());
            }
        });
        m_pc->onLocalCandidate([this](rtc::Candidate c) {
            // Trickle via PATCH to WHIP resource (if server requires)
            if (!m_resourceUrl.has_value()) return;
            try {
                PatchCandidateWHIP(c);
            } catch (const std::exception& e) {
                spdlog::warn("WHIP PATCH candidate failed: {}", e.what());
            }
        });

        // ----- Audio track / source -----
        m_audioSource = rtc::CreateAudioSource(m_p.sampleRate, m_p.channels);
        m_track = m_pc->addTrack(m_audioSource);

        // ----- Create offer -----
        m_pc->setLocalDescription(); // triggers onLocalDescription callback

        m_started = true;
    }

    void Stop() {
        std::lock_guard lk(m_mtx);
        if (!m_started) return;

        if (m_track)      m_track = nullptr;
        if (m_audioSource) m_audioSource = nullptr;
        if (m_pc)         m_pc = nullptr;

        // No explicit WHIP DELETE here, but you can add it if server supports deleting the resource.

        // cleanup resource url
        m_resourceUrl.reset();
        m_started = false;
    }

    // Feed PCM (float32) in interleaved format. frames = samples per channel.
    void PushPCM(const float* interleaved, size_t frames) {
        std::lock_guard lk(m_mtx);
        if (!m_audioSource) return;
        // libdatachannel expects planar/int16? Actually CreateAudioSource accepts float32
        // and uses (frames, channels) to encode Opus internally.
        // Push signature is: audioSource->pushFloat(interleaved, frames, channels, sampleRate)
        m_audioSource->pushFloat(interleaved, (int)frames, m_p.channels, m_p.sampleRate);
    }

private:
    Params m_p;
    std::shared_ptr<rtc::PeerConnection> m_pc;
    std::shared_ptr<rtc::AudioSource>    m_audioSource;
    std::shared_ptr<rtc::Track>          m_track;
    std::optional<std::string>           m_resourceUrl;
    std::mutex m_mtx;
    std::atomic<bool> m_started{false};

    // --- WHIP HTTP helpers (mTLS-capable) ---

    static std::tuple<std::string,std::string> ExtractAnswerAndLocation(const httplib::Result& r) {
        // WHIP server responds 201 Created, body = SDP answer (text/plain),
        // Location header = resource URL for PATCH candidates.
        if (!r) throw std::runtime_error("No HTTP result");
        if (r->status != 201 && r->status != 200)
            throw std::runtime_error("Unexpected WHIP status: " + std::to_string(r->status));
        std::string answer = r->body;
        std::string resourceUrl;
        if (r->has_header("Location")) resourceUrl = r->get_header_value("Location");
        return {answer, resourceUrl};
    }

    std::pair<std::string,std::string> PostOfferWHIP(const std::string& sdpOffer) {
        auto [cli, path] = MakeClientForUrl(m_p.whipUrl);
        // WHIP requires:
        // POST <endpoint>  Content-Type: application/sdp    Body: offer SDP
        // Response: 201 Created, body: answer SDP, Location: resource URL
        auto res = cli->Post(path.c_str(), sdpOffer, "application/sdp");
        auto [answer, resUrl] = ExtractAnswerAndLocation(res);
        return {answer, resUrl};
    }

    void PatchCandidateWHIP(const rtc::Candidate& cand) {
        if (!m_resourceUrl) return;
        auto [cli, path] = MakeClientForUrl(*m_resourceUrl);
        // WHIP trickle: PATCH resource with Content-Type: application/trickle-ice-sdpfrag
        // Body is an SDP fragment with "a=candidate:..."
        std::string frag = "a=" + std::string(cand);
        auto res = cli->Patch(path.c_str(), frag, "application/trickle-ice-sdpfrag");
        if (!res || (res->status < 200 || res->status >= 300)) {
            spdlog::warn("WHIP PATCH {} -> {}", path, res ? res->status : -1);
        }
    }

    // Parse URL, return httplib client + path, configured with mTLS if https
    static std::pair<std::unique_ptr<httplib::Client>, std::string>
    MakeClientForUrl(const std::string& url) {
        // scheme://host[:port]/path...
        static const std::regex re(R"(^(https?)://([^/:]+)(?::(\d+))?(/.*)?$)");
        std::smatch m;
        if (!std::regex_match(url, m, re)) {
            throw std::runtime_error("Invalid URL: " + url);
        }
        std::string scheme = m[1].str();
        std::string host   = m[2].str();
        int port           = m[3].matched ? std::stoi(m[3].str()) : (scheme=="https"?443:80);
        std::string path   = m[4].matched ? m[4].str() : "/";

        // We need the mTLS params; capture via thread-local? We’ll pass via this pointer.
        // To access instance fields here, make this non-static or pass params through a lambda.
        // Simpler: look up global thread-local; or we restructure to capture this.
        // For cleanliness, we’ll make a non-static wrapper below.
        throw std::logic_error("Use MakeClientForUrl_inst instead");
    }

    std::pair<std::unique_ptr<httplib::Client>, std::string>
    MakeClientForUrl_inst(const std::string& url) {
        static const std::regex re(R"(^(https?)://([^/:]+)(?::(\d+))?(/.*)?$)");
        std::smatch m;
        if (!std::regex_match(url, m, re)) {
            throw std::runtime_error("Invalid URL: " + url);
        }
        std::string scheme = m[1].str();
        std::string host   = m[2].str();
        int port           = m[3].matched ? std::stoi(m[3].str()) : (scheme=="https"?443:80);
        std::string path   = m[4].matched ? m[4].str() : "/";

        if (scheme == "https") {
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
            throw std::runtime_error("https URL but CPPHTTPLIB_OPENSSL_SUPPORT not enabled");
#else
            auto cli = std::make_unique<httplib::SSLClient>(host.c_str(), port);
            cli->enable_server_certificate_verification(true);
            cli->set_ca_cert_path(m_p.caPath.c_str());
            cli->set_client_cert_file(m_p.crtPath.c_str(), m_p.keyPath.c_str(), nullptr);
            cli->set_connection_timeout(10);
            cli->set_read_timeout(60);
            cli->set_write_timeout(60);
            return {std::move(cli), path};
#endif
        } else {
            auto cli = std::make_unique<httplib::Client>(host.c_str(), port);
            cli->set_connection_timeout(10);
            cli->set_read_timeout(60);
            cli->set_write_timeout(60);
            return {std::move(cli), path};
        }
    }

    // Overload to call instance version
    std::pair<std::unique_ptr<httplib::Client>, std::string>
    MakeClientForUrl(const std::string& url) {
        return MakeClientForUrl_inst(url);
    }
};

} // namespace snoop
