/*
 *  Copyright (C) 2004-2026 Savoir-faire Linux Inc.
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

#include "sip/sipvoiplink.h"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "sdp.h"
#include "sip/sipcall.h"
#include "sip/sipaccount.h"

#include "jamidht/jamiaccount.h"

#include "manager.h"

#include "im/instant_messaging.h"

#include "pres_sub_server.h"

#include "connectivity/sip_utils.h"
#include "string_utils.h"
#include "logger.h"

#include <dhtnet/ip_utils.h>
#include <opendht/thread_pool.h>

#include <pjsip/sip_endpoint.h>
#include <pjsip/sip_uri.h>

#include <pjsip-simple/presence.h>
#include <pjsip-simple/publish.h>

// Only PJSIP 2.10 is supported.
#if PJ_VERSION_NUM < (2 << 24 | 10 << 16)
#error "Unsupported PJSIP version (requires version 2.10+)"
#endif

#include <cstddef>
#include <regex>

// On Windows, gai_strerror is a macro that expands to a wide-string
// version; use the narrow-string variant explicitly.
#ifdef _WIN32
#define jami_gai_strerror gai_strerrorA
#else
#define jami_gai_strerror gai_strerror
#endif

namespace jami {

using sip_utils::CONST_PJ_STR;

/**************** EXTERN VARIABLES AND FUNCTIONS (callbacks) **************************/

static pjsip_endpoint* endpt_;
static pjsip_module mod_ua_;

static void invite_session_state_changed_cb(pjsip_inv_session* inv, pjsip_event* e);
static void outgoing_request_forked_cb(pjsip_inv_session* inv, pjsip_event* e);
static void transaction_state_changed_cb(pjsip_inv_session* inv, pjsip_transaction* tsx, pjsip_event* e);
// Called when an SDP offer is found in answer. This will occur
// when we send an empty invite (no SDP). In this case, we should
// expect an offer in a the 200 OK message
static void on_rx_offer2(pjsip_inv_session* inv, struct pjsip_inv_on_rx_offer_cb_param* param);
// Called when a re-invite is received
static pj_status_t reinvite_received_cb(pjsip_inv_session* inv, const pjmedia_sdp_session* offer, pjsip_rx_data* rdata);
// Called to request an SDP offer if the peer sent an invite or
// a re-invite with no SDP. In this, we must provide an offer in
// the answer (200 OK) if we accept the call
static void sdp_create_offer_cb(pjsip_inv_session* inv, pjmedia_sdp_session** p_offer);
// Called to report media (SDP) negotiation result
static void sdp_media_update_cb(pjsip_inv_session* inv, pj_status_t status);

static std::shared_ptr<SIPCall> getCallFromInvite(pjsip_inv_session* inv);
#ifdef DEBUG_SIP_REQUEST_MSG
static void processInviteResponseHelper(pjsip_inv_session* inv, pjsip_event* e);
#endif

static pj_bool_t
handleIncomingOptions(pjsip_rx_data* rdata)
{
    pjsip_tx_data* tdata;

    auto* dlg = pjsip_rdata_get_dlg(rdata);
    if (dlg) {
        JAMI_LOG("Processing in-dialog option request");
        if (pjsip_dlg_create_response(dlg, rdata, PJSIP_SC_OK, NULL, &tdata) != PJ_SUCCESS) {
            JAMI_ERROR("Failed to create in-dialog response for option request");
            return PJ_FALSE;
        }
    } else {
        JAMI_LOG("Processing out-of-dialog option request");
        if (pjsip_endpt_create_response(endpt_, rdata, PJSIP_SC_OK, NULL, &tdata) != PJ_SUCCESS) {
            JAMI_ERROR("Failed to create out-of-dialog response for option request");
            return PJ_FALSE;
        }
    }

#define ADD_HDR(hdr) \
    do { \
        const pjsip_hdr* cap_hdr = hdr; \
        if (cap_hdr) \
            pjsip_msg_add_hdr(tdata->msg, (pjsip_hdr*) pjsip_hdr_clone(tdata->pool, cap_hdr)); \
    } while (0)
#define ADD_CAP(cap) ADD_HDR(pjsip_endpt_get_capability(endpt_, cap, NULL));

    ADD_CAP(PJSIP_H_ALLOW);
    ADD_CAP(PJSIP_H_ACCEPT);
    ADD_CAP(PJSIP_H_SUPPORTED);
    ADD_HDR(pjsip_evsub_get_allow_events_hdr(NULL));

    if (dlg) {
        if (pjsip_dlg_send_response(dlg, pjsip_rdata_get_tsx(rdata), tdata) != PJ_SUCCESS) {
            JAMI_ERROR("Failed to send in-dialog response for option request");
            return PJ_FALSE;
        }

        JAMI_LOG("Sent in-dialog response for option request");
        return PJ_TRUE;
    }

    pjsip_response_addr res_addr;
    pjsip_get_response_addr(tdata->pool, rdata, &res_addr);

    if (pjsip_endpt_send_response(endpt_, &res_addr, tdata, NULL, NULL) != PJ_SUCCESS) {
        pjsip_tx_data_dec_ref(tdata);
        JAMI_ERROR("Failed to send out-of-dialog response for option request");
        return PJ_FALSE;
    }

    JAMI_LOG("Sent out-of-dialog response for option request");
    return PJ_TRUE;
}

// return PJ_FALSE so that eventually other modules will handle these requests
// TODO: move Voicemail to separate module
static pj_bool_t
transaction_response_cb(pjsip_rx_data* rdata)
{
    pjsip_dialog* dlg = pjsip_rdata_get_dlg(rdata);

    if (!dlg)
        return PJ_FALSE;

    pjsip_transaction* tsx = pjsip_rdata_get_tsx(rdata);

    if (!tsx or tsx->method.id != PJSIP_INVITE_METHOD)
        return PJ_FALSE;

    if (tsx->status_code / 100 == 2) {
        /**
         * Send an ACK message inside a transaction. PJSIP send automatically, non-2xx ACK response.
         * ACK for a 2xx response must be send using this method.
         */
        pjsip_tx_data* tdata;

        if (rdata->msg_info.cseq) {
            pjsip_dlg_create_request(dlg, &pjsip_ack_method, rdata->msg_info.cseq->cseq, &tdata);
            pjsip_dlg_send_request(dlg, tdata, -1, NULL);
        }
    }

    return PJ_FALSE;
}

static pj_status_t
try_respond_stateless(pjsip_endpoint* endpt,
                      pjsip_rx_data* rdata,
                      int st_code,
                      const pj_str_t* st_text,
                      const pjsip_hdr* hdr_list,
                      const pjsip_msg_body* body)
{
    /* Check that no UAS transaction has been created for this request.
     * If UAS transaction has been created for this request, application
     * MUST send the response statefully using that transaction.
     */
    if (!pjsip_rdata_get_tsx(rdata))
        return pjsip_endpt_respond_stateless(endpt, rdata, st_code, st_text, hdr_list, body);
    else
        JAMI_ERROR("Transaction has been created for this request, send response statefully instead");

    return !PJ_SUCCESS;
}

template<typename T>
static bool
is_uninitialized(std::weak_ptr<T> const& weak)
{
    using wt = std::weak_ptr<T>;
    return !weak.owner_before(wt {}) && !wt {}.owner_before(weak);
}

static pj_bool_t
transaction_request_cb(pjsip_rx_data* rdata)
{
    if (!rdata or !rdata->msg_info.msg) {
        JAMI_ERROR("rx_data is NULL");
        return PJ_FALSE;
    }

    pjsip_method* method = &rdata->msg_info.msg->line.req.method;

    if (!method) {
        JAMI_ERROR("method is NULL");
        return PJ_FALSE;
    }

    if (method->id == PJSIP_ACK_METHOD && pjsip_rdata_get_dlg(rdata))
        return PJ_FALSE;

    if (!rdata->msg_info.to or !rdata->msg_info.from or !rdata->msg_info.via) {
        JAMI_ERROR("Missing From, To or Via fields");
        return PJ_FALSE;
    }

    auto* const sip_to_uri = reinterpret_cast<pjsip_sip_uri*>(pjsip_uri_get_uri(rdata->msg_info.to->uri));
    auto* const sip_from_uri = reinterpret_cast<pjsip_sip_uri*>(pjsip_uri_get_uri(rdata->msg_info.from->uri));
    const pjsip_host_port& sip_via = rdata->msg_info.via->sent_by;

    if (!sip_to_uri or !sip_from_uri or !sip_via.host.ptr) {
        JAMI_ERROR("NULL URI");
        return PJ_FALSE;
    }

    std::string_view toUsername(sip_to_uri->user.ptr, sip_to_uri->user.slen);
    std::string_view toHost(sip_to_uri->host.ptr, sip_to_uri->host.slen);
    std::string_view viaHostname(sip_via.host.ptr, sip_via.host.slen);
    const std::string_view remote_user(sip_from_uri->user.ptr, sip_from_uri->user.slen);
    const std::string_view remote_hostname(sip_from_uri->host.ptr, sip_from_uri->host.slen);
    std::string peerNumber;
    if (not remote_user.empty() and not remote_hostname.empty())
        peerNumber = remote_user + "@" + remote_hostname;
    else {
        char tmp[PJSIP_MAX_URL_SIZE];
        size_t length = pjsip_uri_print(PJSIP_URI_IN_FROMTO_HDR, sip_from_uri, tmp, PJSIP_MAX_URL_SIZE);
        peerNumber = sip_utils::stripSipUriPrefix(std::string_view(tmp, length));
    }

    auto transport = Manager::instance().sipVoIPLink().sipTransportBroker->addTransport(rdata->tp_info.transport);

    std::shared_ptr<SIPAccountBase> account;
    // If transport account is default-constructed, guessing account is allowed
    const auto& waccount = transport ? transport->getAccount() : std::weak_ptr<SIPAccountBase> {};
    if (is_uninitialized(waccount)) {
        account = Manager::instance().sipVoIPLink().guessAccount(toUsername, viaHostname, remote_hostname);
        if (not account)
            return PJ_FALSE;
        if (not transport and account->getAccountType() == SIPAccount::ACCOUNT_TYPE) {
            if (not(transport = std::static_pointer_cast<SIPAccount>(account)->getTransport())) {
                JAMI_ERROR("No suitable transport to answer this call.");
                return PJ_FALSE;
            }
            JAMI_WARNING("Using transport from account.");
        }
    } else if (!(account = waccount.lock())) {
        JAMI_ERROR("Dropping SIP request: account is expired.");
        return PJ_FALSE;
    }

    pjsip_msg_body* body = rdata->msg_info.msg->body;

    if (method->id == PJSIP_OTHER_METHOD) {
        std::string_view request = sip_utils::as_view(method->name);

        if (request.find(sip_utils::SIP_METHODS::NOTIFY) != std::string_view::npos) {
            if (body and body->data) {
                std::string_view body_view(static_cast<char*>(body->data), body->len);
                auto pos = body_view.find("Voice-Message: ");
                if (pos != std::string_view::npos) {
                    int newCount {0};
                    int oldCount {0};
                    int urgentCount {0};
                    std::string sp(body_view.substr(pos));
                    int ret = sscanf(sp.c_str(), "Voice-Message: %d/%d (%d/", &newCount, &oldCount, &urgentCount);

                    // According to rfc3842
                    // urgent messages are optional
                    if (ret >= 2)
                        emitSignal<libjami::CallSignal::VoiceMailNotify>(account->getAccountID(),
                                                                         newCount,
                                                                         oldCount,
                                                                         urgentCount);
                }
            }
        } else if (request.find(sip_utils::SIP_METHODS::MESSAGE) != std::string_view::npos) {
            // Reply 200 immediately (RFC 3428, ch. 7)
            try_respond_stateless(endpt_, rdata, PJSIP_SC_OK, nullptr, nullptr, nullptr);
            // Process message content in case of multi-part body
            auto payloads = im::parseSipMessage(rdata->msg_info.msg);
            if (payloads.size() > 0) {
                constexpr pj_str_t STR_MESSAGE_ID = jami::sip_utils::CONST_PJ_STR("Message-ID");
                auto* msgId = (pjsip_generic_string_hdr*) pjsip_msg_find_hdr_by_name(rdata->msg_info.msg,
                                                                                     &STR_MESSAGE_ID,
                                                                                     nullptr);
                std::string id = {};
                if (!msgId) {
                    // Supports IMDN message format https://tools.ietf.org/html/rfc5438#section-7.1.1.3
                    constexpr pj_str_t STR_IMDN_MESSAGE_ID = jami::sip_utils::CONST_PJ_STR("imdn.Message-ID");
                    msgId = (pjsip_generic_string_hdr*) pjsip_msg_find_hdr_by_name(rdata->msg_info.msg,
                                                                                   &STR_IMDN_MESSAGE_ID,
                                                                                   nullptr);
                }
                if (msgId)
                    id = std::string(msgId->hvalue.ptr, msgId->hvalue.slen);

                if (not id.empty()) {
                    try {
                        // Mark message as treated
                        auto intid = from_hex_string(id);
                        auto acc = std::dynamic_pointer_cast<JamiAccount>(account);
                        if (acc and acc->isMessageTreated(intid))
                            return PJ_FALSE;
                    } catch (const std::exception& e) {
                        JAMI_WARNING("[Account {}] Couldn't treat message {}: {}",
                                     account->getAccountID(),
                                     from_hex_string(id),
                                     e.what());
                    }
                }
                account->onTextMessage(id, peerNumber, transport->getTlsInfos().peerCert, payloads);
            }
            return PJ_FALSE;
        }

        try_respond_stateless(endpt_, rdata, PJSIP_SC_OK, NULL, NULL, NULL);

        return PJ_FALSE;
    } else if (method->id == PJSIP_OPTIONS_METHOD) {
        return handleIncomingOptions(rdata);
    } else if (method->id != PJSIP_INVITE_METHOD && method->id != PJSIP_ACK_METHOD) {
        try_respond_stateless(endpt_, rdata, PJSIP_SC_METHOD_NOT_ALLOWED, NULL, NULL, NULL);
        return PJ_FALSE;
    }

    if (method->id == PJSIP_INVITE_METHOD) {
        // Log headers of received INVITE
        JAMI_LOG("Received a SIP INVITE request");
        sip_utils::logMessageHeaders(&rdata->msg_info.msg->hdr);
    }

    pjmedia_sdp_session* r_sdp {nullptr};
    if (body) {
        if (pjmedia_sdp_parse(rdata->tp_info.pool, (char*) body->data, body->len, &r_sdp) != PJ_SUCCESS) {
            JAMI_WARNING("Failed to parse the SDP in offer");
            r_sdp = nullptr;
        }
    }

    if (not account->hasActiveCodec(MEDIA_AUDIO)) {
        try_respond_stateless(endpt_, rdata, PJSIP_SC_NOT_ACCEPTABLE_HERE, NULL, NULL, NULL);
        return PJ_FALSE;
    }

    // Verify that we can handle the request
    unsigned options = 0;

    if (pjsip_inv_verify_request(rdata, &options, NULL, NULL, endpt_, NULL) != PJ_SUCCESS) {
        JAMI_ERROR("Unable to verify INVITE request in secure dialog.");
        try_respond_stateless(endpt_, rdata, PJSIP_SC_METHOD_NOT_ALLOWED, NULL, NULL, NULL);
        return PJ_FALSE;
    }

    // Build the initial media using the remote offer.
    auto localMediaList = Sdp::getMediaAttributeListFromSdp(r_sdp);

    // To enable video, it must be enabled in the remote and locally (i.e. in the account)
    for (auto& media : localMediaList) {
        if (media.type_ == MediaType::MEDIA_VIDEO) {
            media.enabled_ &= account->isVideoEnabled();
        }
    }

    auto call = account->newIncomingCall(std::string(remote_user),
                                         MediaAttribute::mediaAttributesToMediaMaps(localMediaList),
                                         transport);
    if (!call) {
        return PJ_FALSE;
    }

    call->setPeerUaVersion(sip_utils::getPeerUserAgent(rdata));
    // The username can be used to join specific calls in conversations
    call->toUsername(std::string(toUsername));

    // FIXME: for now, use the same address family as the SIP transport
    auto family = pjsip_transport_type_get_af(pjsip_transport_get_type_from_flag(transport->get()->flag));

    dhtnet::IpAddr addrSdp;
    if (account->getUPnPActive()) {
        /* use UPnP addr, or published addr if its set */
        addrSdp = account->getPublishedSameasLocal() ? account->getUPnPIpAddress() : account->getPublishedIpAddress();
    } else {
        addrSdp = account->isStunEnabled() or (not account->getPublishedSameasLocal())
                      ? account->getPublishedIpAddress()
                      : dhtnet::ip_utils::getInterfaceAddr(account->getLocalInterface(), family);
    }

    /* fallback on local address */
    if (not addrSdp)
        addrSdp = dhtnet::ip_utils::getInterfaceAddr(account->getLocalInterface(), family);

    // Try to obtain display name from From: header first, fallback on Contact:
    auto peerDisplayName = sip_utils::parseDisplayName(rdata->msg_info.from);
    if (peerDisplayName.empty()) {
        if (const auto* hdr = static_cast<const pjsip_contact_hdr*>(
                pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CONTACT, nullptr))) {
            peerDisplayName = sip_utils::parseDisplayName(hdr);
        }
    }

    call->setPeerNumber(peerNumber);
    call->setPeerUri(account->getToUri(peerNumber));
    call->setPeerDisplayName(peerDisplayName);
    call->setState(Call::ConnectionState::PROGRESSING);
    call->getSDP().setPublishedIP(addrSdp);
    call->setPeerAllowMethods(sip_utils::getPeerAllowMethods(rdata));

    // Set the temporary media list. Might change when we receive
    // the accept from the client.
    if (r_sdp != nullptr) {
        call->getSDP().setReceivedOffer(r_sdp);
    }

    pjsip_dialog* dialog = nullptr;
    if (pjsip_dlg_create_uas_and_inc_lock(pjsip_ua_instance(), rdata, nullptr, &dialog) != PJ_SUCCESS) {
        JAMI_ERROR("Unable to create UAS");
        call.reset();
        try_respond_stateless(endpt_, rdata, PJSIP_SC_INTERNAL_SERVER_ERROR, nullptr, nullptr, nullptr);
        return PJ_FALSE;
    }

    pjsip_tpselector tp_sel = SIPVoIPLink::getTransportSelector(transport->get());
    if (!dialog or pjsip_dlg_set_transport(dialog, &tp_sel) != PJ_SUCCESS) {
        JAMI_ERROR("Unable to set transport for dialog");
        if (dialog)
            pjsip_dlg_dec_lock(dialog);
        return PJ_FALSE;
    }

    pjsip_inv_session* inv = nullptr;
    // Create UAS for the invite.
    // The SDP is not set here, it will be done when the call is
    // accepted and the media attributes of the answer are known.
    pjsip_inv_create_uas(dialog, rdata, NULL, PJSIP_INV_SUPPORT_ICE, &inv);
    if (!inv) {
        JAMI_ERROR("Call invite is not initialized");
        pjsip_dlg_dec_lock(dialog);
        return PJ_FALSE;
    }

    // dialog is now owned by invite
    pjsip_dlg_dec_lock(dialog);

    inv->mod_data[mod_ua_.id] = call.get();
    call->setInviteSession(inv);

    // Check whether Replaces header is present in the request and process accordingly.
    pjsip_dialog* replaced_dlg;
    pjsip_tx_data* response;

    if (pjsip_replaces_verify_request(rdata, &replaced_dlg, PJ_FALSE, &response) != PJ_SUCCESS) {
        JAMI_ERROR("Something wrong with Replaces request.");
        call.reset();

        // Something wrong with the Replaces header.
        if (response) {
            pjsip_response_addr res_addr;
            pjsip_get_response_addr(response->pool, rdata, &res_addr);
            pjsip_endpt_send_response(endpt_, &res_addr, response, NULL, NULL);
        } else {
            try_respond_stateless(endpt_, rdata, PJSIP_SC_INTERNAL_SERVER_ERROR, NULL, NULL, NULL);
        }

        return PJ_FALSE;
    }

    // Check if call has been transferred
    pjsip_tx_data* tdata = nullptr;

    if (pjsip_inv_initial_answer(call->inviteSession_.get(), rdata, PJSIP_SC_TRYING, NULL, NULL, &tdata) != PJ_SUCCESS) {
        JAMI_ERROR("Unable to create answer TRYING");
        return PJ_FALSE;
    }

    // Add user-agent header
    sip_utils::addUserAgentHeader(account->getUserAgentName(), tdata);

    if (pjsip_inv_send_msg(call->inviteSession_.get(), tdata) != PJ_SUCCESS) {
        JAMI_ERROR("Unable to send msg TRYING");
        return PJ_FALSE;
    }

    call->setState(Call::ConnectionState::TRYING);

    if (pjsip_inv_answer(call->inviteSession_.get(), PJSIP_SC_RINGING, NULL, NULL, &tdata) != PJ_SUCCESS) {
        JAMI_ERROR("Unable to create answer RINGING");
        return PJ_FALSE;
    }

    sip_utils::addContactHeader(call->getContactHeader(), tdata);
    if (pjsip_inv_send_msg(call->inviteSession_.get(), tdata) != PJ_SUCCESS) {
        JAMI_ERROR("Unable to send msg RINGING");
        return PJ_FALSE;
    }

    call->setState(Call::ConnectionState::RINGING);

    Manager::instance().incomingCall(account->getAccountID(), *call);

    if (replaced_dlg) {
        // Get the INVITE session associated with the replaced dialog.
        auto* replaced_inv = pjsip_dlg_get_inv_session(replaced_dlg);

        // Disconnect the "replaced" INVITE session.
        if (pjsip_inv_end_session(replaced_inv, PJSIP_SC_GONE, nullptr, &tdata) == PJ_SUCCESS && tdata) {
            pjsip_inv_send_msg(replaced_inv, tdata);
        }

        // Close call at application level
        if (auto replacedCall = getCallFromInvite(replaced_inv))
            replacedCall->hangup(PJSIP_SC_OK);
    }

    return PJ_FALSE;
}

static void
tp_state_callback(pjsip_transport* tp, pjsip_transport_state state, const pjsip_transport_state_info* info)
{
    if (auto& broker = Manager::instance().sipVoIPLink().sipTransportBroker)
        broker->transportStateChanged(tp, state, info);
    else
        JAMI_ERROR("SIPVoIPLink with invalid SipTransportBroker");
}

/*************************************************************************************************/

pjsip_endpoint*
SIPVoIPLink::getEndpoint()
{
    return endpt_;
}

pjsip_module*
SIPVoIPLink::getMod()
{
    return &mod_ua_;
}

pj_pool_t*
SIPVoIPLink::getPool() noexcept
{
    return pool_.get();
}

pj_caching_pool*
SIPVoIPLink::getCachingPool() noexcept
{
    return &cp_;
}

SIPVoIPLink::SIPVoIPLink()
    : pool_(nullptr)
{
#define TRY(ret) \
    do { \
        if ((ret) != PJ_SUCCESS) \
            throw VoipLinkException(#ret " failed"); \
    } while (0)

    pj_caching_pool_init(&cp_, &pj_pool_factory_default_policy, 0);
    pool_.reset(pj_pool_create(&cp_.factory, PACKAGE, static_cast<long>(64) * 1024, 4096, nullptr));
    if (!pool_)
        throw VoipLinkException("UserAgent: Unable to initialize memory pool");

    TRY(pjsip_endpt_create(&cp_.factory, pj_gethostname()->ptr, &endpt_));

    auto ns = dhtnet::ip_utils::getLocalNameservers();
    if (not ns.empty()) {
        std::vector<pj_str_t> dns_nameservers(ns.size());
        std::vector<pj_uint16_t> dns_ports(ns.size());
        for (unsigned i = 0, n = ns.size(); i < n; i++) {
            char hbuf[NI_MAXHOST];
            if (auto ret
                = getnameinfo((sockaddr*) &ns[i], ns[i].getLength(), hbuf, sizeof(hbuf), nullptr, 0, NI_NUMERICHOST)) {
                JAMI_WARNING("Error printing SIP nameserver: {}", jami_gai_strerror(ret));
            } else {
                JAMI_LOG("Using SIP nameserver: {}", hbuf);
                pj_strdup2(pool_.get(), &dns_nameservers[i], hbuf);
                dns_ports[i] = ns[i].getPort();
            }
        }
        pj_dns_resolver* resv;
        if (auto ret = pjsip_endpt_create_resolver(endpt_, &resv)) {
            JAMI_WARNING("Error creating SIP DNS resolver: {}", sip_utils::sip_strerror(ret));
        } else {
            if (auto ret = pj_dns_resolver_set_ns(resv,
                                                  dns_nameservers.size(),
                                                  dns_nameservers.data(),
                                                  dns_ports.data())) {
                JAMI_WARNING("Error setting SIP DNS servers: {}", sip_utils::sip_strerror(ret));
            } else {
                if (auto ret = pjsip_endpt_set_resolver(endpt_, resv)) {
                    JAMI_WARNING("Error setting PJSIP DNS resolver: {}", sip_utils::sip_strerror(ret));
                }
            }
        }
    }

    sipTransportBroker.reset(new SipTransportBroker(endpt_));

    auto status = pjsip_tpmgr_set_state_cb(pjsip_endpt_get_tpmgr(endpt_), tp_state_callback);
    if (status != PJ_SUCCESS)
        JAMI_ERROR("Unable to set transport callback: {}", sip_utils::sip_strerror(status));

    TRY(pjsip_tsx_layer_init_module(endpt_));
    TRY(pjsip_ua_init_module(endpt_, nullptr));
    TRY(pjsip_replaces_init_module(endpt_)); // See the Replaces specification in RFC 3891
    TRY(pjsip_100rel_init_module(endpt_));

    // Initialize and register ring module
    mod_ua_.name = sip_utils::CONST_PJ_STR(PACKAGE);
    mod_ua_.id = -1;
    mod_ua_.priority = PJSIP_MOD_PRIORITY_APPLICATION;
    mod_ua_.on_rx_request = &transaction_request_cb;
    mod_ua_.on_rx_response = &transaction_response_cb;
    TRY(pjsip_endpt_register_module(endpt_, &mod_ua_));

    TRY(pjsip_evsub_init_module(endpt_));
    TRY(pjsip_xfer_init_module(endpt_));

    // presence/publish management
    TRY(pjsip_pres_init_module(endpt_, pjsip_evsub_instance()));
    TRY(pjsip_endpt_register_module(endpt_, &PresSubServer::mod_presence_server));

    static const pjsip_inv_callback inv_cb = {
        invite_session_state_changed_cb,
        outgoing_request_forked_cb,
        transaction_state_changed_cb,
        nullptr /* on_rx_offer */,
        on_rx_offer2,
        reinvite_received_cb,
        sdp_create_offer_cb,
        sdp_media_update_cb,
        nullptr /* on_send_ack */,
        nullptr /* on_redirected */,
    };
    TRY(pjsip_inv_usage_init(endpt_, &inv_cb));

    static constexpr pj_str_t allowed[] = {
        CONST_PJ_STR(sip_utils::SIP_METHODS::INFO),
        CONST_PJ_STR(sip_utils::SIP_METHODS::OPTIONS),
        CONST_PJ_STR(sip_utils::SIP_METHODS::MESSAGE),
        CONST_PJ_STR(sip_utils::SIP_METHODS::PUBLISH),
    };

    pjsip_endpt_add_capability(endpt_, &mod_ua_, PJSIP_H_ALLOW, nullptr, PJ_ARRAY_SIZE(allowed), allowed);

    static constexpr pj_str_t text_plain = CONST_PJ_STR("text/plain");
    pjsip_endpt_add_capability(endpt_, &mod_ua_, PJSIP_H_ACCEPT, nullptr, 1, &text_plain);

    static constexpr pj_str_t accepted = CONST_PJ_STR("application/sdp");
    pjsip_endpt_add_capability(endpt_, &mod_ua_, PJSIP_H_ACCEPT, nullptr, 1, &accepted);

    static constexpr pj_str_t iscomposing = CONST_PJ_STR("application/im-iscomposing+xml");
    pjsip_endpt_add_capability(endpt_, &mod_ua_, PJSIP_H_ACCEPT, nullptr, 1, &iscomposing);

    TRY(pjsip_replaces_init_module(endpt_));
#undef TRY

    sipThread_ = std::thread([this] {
        while (running_)
            handleEvents();
    });

    JAMI_LOG("SIPVoIPLink@{}", fmt::ptr(this));
}

SIPVoIPLink::~SIPVoIPLink() {}

void
SIPVoIPLink::shutdown()
{
    JAMI_LOG("Shutting down SIPVoIPLink@{}…", fmt::ptr(this));
    // Remaining calls should not happen as possible upper callbacks
    // may be called and another instance of SIPVoIPLink can be re-created!

    if (not Manager::instance().callFactory.empty(Call::LinkType::SIP))
        JAMI_ERROR("{} SIP calls remains!", Manager::instance().callFactory.callCount(Call::LinkType::SIP));

    sipTransportBroker->shutdown();
    pjsip_tpmgr_set_state_cb(pjsip_endpt_get_tpmgr(endpt_), nullptr);

    running_ = false;
    sipThread_.join();
    pjsip_endpt_destroy(endpt_);
    pool_.reset();
    pj_caching_pool_destroy(&cp_);
    sipTransportBroker.reset();

    JAMI_LOG("SIPVoIPLink@{} shutdown successfully completed", fmt::ptr(this));
}

std::shared_ptr<SIPAccountBase>
SIPVoIPLink::guessAccount(std::string_view userName, std::string_view server, std::string_view fromUri) const
{
    JAMI_LOG("username = {}, server = {}, from = {}", userName, server, fromUri);
    // Attempt to find the account id from username and server name by full match

    std::shared_ptr<SIPAccountBase> result;
    std::shared_ptr<SIPAccountBase> IP2IPAccount;
    MatchRank best = MatchRank::NONE;

    // SIP accounts
    for (const auto& account : Manager::instance().getAllAccounts<SIPAccount>()) {
        const MatchRank match(account->matches(userName, server));

        // return right away if this is a full match
        if (match == MatchRank::FULL) {
            return account;
        } else if (match > best) {
            best = match;
            result = account;
        } else if (!IP2IPAccount && account->isIP2IP()) {
            // Allow IP2IP calls if an account exists for this type of calls
            IP2IPAccount = account;
        }
    }

    return result ? result : IP2IPAccount;
}

// Called from EventThread::run (not main thread)
void
SIPVoIPLink::handleEvents()
{
    const pj_time_val timeout = {1, 0};
    if (auto ret = pjsip_endpt_handle_events(endpt_, &timeout))
        JAMI_ERROR("pjsip_endpt_handle_events failed with error {}", sip_utils::sip_strerror(ret));
}

void
SIPVoIPLink::registerKeepAliveTimer(pj_timer_entry& timer, pj_time_val& delay)
{
    JAMI_DEBUG("Register new keepalive timer {:d} with delay {:d}", timer.id, delay.sec);

    if (timer.id == -1)
        JAMI_WARNING("Timer already scheduled");

    switch (pjsip_endpt_schedule_timer(endpt_, &timer, &delay)) {
    case PJ_SUCCESS:
        break;

    default:
        JAMI_ERROR("Unable to schedule new timer in pjsip endpoint");

        /* fallthrough */
    case PJ_EINVAL:
        JAMI_ERROR("Invalid timer or delay entry");
        break;

    case PJ_EINVALIDOP:
        JAMI_ERROR("Invalid timer entry, maybe already scheduled");
        break;
    }
}

void
SIPVoIPLink::cancelKeepAliveTimer(pj_timer_entry& timer)
{
    pjsip_endpt_cancel_timer(endpt_, &timer);
}

///////////////////////////////////////////////////////////////////////////////
// Private functions
///////////////////////////////////////////////////////////////////////////////

static std::shared_ptr<SIPCall>
getCallFromInvite(pjsip_inv_session* inv)
{
    if (auto* call_ptr = static_cast<SIPCall*>(inv->mod_data[mod_ua_.id]))
        return std::static_pointer_cast<SIPCall>(call_ptr->shared_from_this());
    return nullptr;
}

static void
invite_session_state_changed_cb(pjsip_inv_session* inv, pjsip_event* ev)
{
    if (inv == nullptr or ev == nullptr) {
        throw VoipLinkException("Unexpected null pointer");
    }

    auto call = getCallFromInvite(inv);
    if (not call)
        return;

    if (ev->type != PJSIP_EVENT_TSX_STATE and ev->type != PJSIP_EVENT_TX_MSG and ev->type != PJSIP_EVENT_RX_MSG) {
        JAMI_WARNING("[call:{}] INVITE@{} state changed to {} ({}): unexpected event type {}",
                     call->getCallId(),
                     fmt::ptr(inv),
                     pjsip_inv_state_name(inv->state),
                     pjsip_inv_state_name(inv->state),
                     static_cast<int>(ev->type));
        return;
    }

    decltype(pjsip_transaction::status_code) status_code = 0;

    if (ev->type == PJSIP_EVENT_TSX_STATE) {
        auto* const tsx = ev->body.tsx_state.tsx;
        status_code = tsx ? tsx->status_code : PJSIP_SC_NOT_FOUND;
        const pj_str_t* description = pjsip_get_status_text(status_code);

        JAMI_LOG("[call:{}] INVITE@{:p} state changed to {:d} ({}): cause={:d}, tsx@{:p} status {:d} ({:s})",
                 call->getCallId(),
                 fmt::ptr(inv),
                 (int) inv->state,
                 pjsip_inv_state_name(inv->state),
                 (int) inv->cause,
                 fmt::ptr(tsx),
                 status_code,
                 sip_utils::as_view(*description));
    } else if (ev->type == PJSIP_EVENT_TX_MSG) {
        JAMI_LOG("[call:{}] INVITE@{:p} state changed to {:d} ({:s}): cause={:d} (TX_MSG)",
                 call->getCallId(),
                 fmt::ptr(inv),
                 (int) inv->state,
                 pjsip_inv_state_name(inv->state),
                 (int) inv->cause);
    }
    pjsip_rx_data* rdata {nullptr};
    if (ev->type == PJSIP_EVENT_RX_MSG) {
        rdata = ev->body.rx_msg.rdata;
    } else if (ev->type == PJSIP_EVENT_TSX_STATE and ev->body.tsx_state.type == PJSIP_EVENT_RX_MSG) {
        rdata = ev->body.tsx_state.src.rdata;
    }
    if (rdata != nullptr) {
        call->setPeerUaVersion(sip_utils::getPeerUserAgent(rdata));
        auto methods = sip_utils::getPeerAllowMethods(rdata);
        if (not methods.empty()) {
            call->setPeerAllowMethods(std::move(methods));
        }
    }

    switch (inv->state) {
    case PJSIP_INV_STATE_EARLY:
        if (status_code == PJSIP_SC_RINGING)
            call->onPeerRinging();
        break;

    case PJSIP_INV_STATE_CONFIRMED:
        // After we sent or received a ACK - The connection is established
        call->onAnswered();
        break;

    case PJSIP_INV_STATE_DISCONNECTED:
        switch (inv->cause) {
        // When a peer's device replies busy
        case PJSIP_SC_BUSY_HERE:
            call->onBusyHere();
            break;
        // When the peer manually refuse the call
        case PJSIP_SC_DECLINE:
        case PJSIP_SC_BUSY_EVERYWHERE:
            if (inv->role != PJSIP_ROLE_UAC)
                break;
            // close call
            call->onClosed();
            break;
        // The call terminates normally - BYE / CANCEL
        case PJSIP_SC_OK:
        case PJSIP_SC_REQUEST_TERMINATED:
            call->onClosed();
            break;

        // Error/unhandled conditions
        default:
            call->onFailure(inv->cause);
            break;
        }
        break;

    default:
        break;
    }
}

static void
on_rx_offer2(pjsip_inv_session* inv, struct pjsip_inv_on_rx_offer_cb_param* param)
{
    if (not param or not param->offer) {
        JAMI_ERROR("Invalid offer");
        return;
    }

    auto call = getCallFromInvite(inv);
    if (not call)
        return;

    // This callback is called whenever a new media offer is found in a
    // SIP message, typically in a re-invite and in a '200 OK' (as a
    // response to an empty invite).
    // Here we only handle the second case. The first case is handled
    // in reinvite_received_cb.
    if (inv->cause != PJSIP_SC_OK) {
        // Silently ignore if it's not a '200 OK'
        return;
    }

    if (auto call = getCallFromInvite(inv)) {
        if (auto const& account = call->getAccount().lock()) {
            call->onReceiveOfferIn200OK(param->offer);
        }
    }
}

static pj_status_t
reinvite_received_cb(pjsip_inv_session* inv, const pjmedia_sdp_session* offer, pjsip_rx_data* rdata)
{
    if (!offer)
        return !PJ_SUCCESS;
    if (auto call = getCallFromInvite(inv)) {
        if (auto const& account = call->getAccount().lock()) {
            return call->onReceiveReinvite(offer, rdata);
        }
    }

    // Return success if there is no matching call. The re-invite
    // should be ignored.
    return PJ_SUCCESS;
}

static void
sdp_create_offer_cb(pjsip_inv_session* inv, pjmedia_sdp_session** p_offer)
{
    auto call = getCallFromInvite(inv);
    if (not call)
        return;

    auto account = call->getSIPAccount();
    if (not account) {
        JAMI_ERROR("No account detected");
        return;
    }

    if (account->isEmptyOffersEnabled()) {
        // Skip if the client wants to send an empty offer.
        JAMI_LOG("Client requested to send an empty offer (no SDP)");
        return;
    }

    auto family = pj_AF_UNSPEC();
    // Use the same address family as the SIP transport
    if (auto* dlg = inv->dlg) {
        if (dlg->tp_sel.type == PJSIP_TPSELECTOR_TRANSPORT) {
            if (auto* tr = dlg->tp_sel.u.transport)
                family = tr->local_addr.addr.sa_family;
        } else if (dlg->tp_sel.type == PJSIP_TPSELECTOR_LISTENER) {
            if (auto* tr = dlg->tp_sel.u.listener)
                family = tr->local_addr.addr.sa_family;
        }
    }
    // If transport family is unknown, detect from available addresses
    if (family == pj_AF_UNSPEC()) {
        auto addr4 = dhtnet::ip_utils::getInterfaceAddr(account->getLocalInterface(), pj_AF_INET());
        family = addr4 ? pj_AF_INET() : pj_AF_INET6();
    }
    auto ifaceAddr = dhtnet::ip_utils::getInterfaceAddr(account->getLocalInterface(), family);

    dhtnet::IpAddr address;
    if (account->getUPnPActive()) {
        /* use UPnP addr, or published addr if it's set */
        address = account->getPublishedSameasLocal() ? account->getUPnPIpAddress() : account->getPublishedIpAddress();
    } else {
        address = account->getPublishedSameasLocal() ? ifaceAddr : account->getPublishedIpAddress();
    }

    /* fallback on local address */
    if (not address)
        address = ifaceAddr;

    auto& sdp = call->getSDP();
    sdp.setPublishedIP(address);

    // This list should be provided by the client. Kept for backward compatibility.
    auto const& mediaList = call->getMediaAttributeList();
    if (mediaList.empty()) {
        throw VoipLinkException("Unexpected empty media attribute list");
    }

    JAMI_LOG("Creating a SDP offer using the following media:");
    for (auto const& media : mediaList) {
        JAMI_LOG("[call {}] Media {}", call->getCallId(), media.toString(true));
    }

    const bool created = sdp.createOffer(mediaList);

    if (created and p_offer != nullptr)
        *p_offer = sdp.getLocalSdpSession();
}

static const pjmedia_sdp_session*
get_active_remote_sdp(pjsip_inv_session* inv)
{
    const pjmedia_sdp_session* sdp_session {};

    if (pjmedia_sdp_neg_get_active_remote(inv->neg, &sdp_session) != PJ_SUCCESS) {
        JAMI_ERROR("Active remote not present");
        return nullptr;
    }

    if (pjmedia_sdp_validate(sdp_session) != PJ_SUCCESS) {
        JAMI_ERROR("Invalid remote SDP session");
        return nullptr;
    }

    return sdp_session;
}

static const pjmedia_sdp_session*
get_active_local_sdp(pjsip_inv_session* inv)
{
    const pjmedia_sdp_session* sdp_session {};

    if (pjmedia_sdp_neg_get_active_local(inv->neg, &sdp_session) != PJ_SUCCESS) {
        JAMI_ERROR("Active local not present");
        return nullptr;
    }

    if (pjmedia_sdp_validate(sdp_session) != PJ_SUCCESS) {
        JAMI_ERROR("Invalid local SDP session");
        return nullptr;
    }

    return sdp_session;
}

// This callback is called after SDP offer/answer session has completed.
static void
sdp_media_update_cb(pjsip_inv_session* inv, pj_status_t status)
{
    auto call = getCallFromInvite(inv);
    if (not call)
        return;

    JAMI_LOG("[call:{}] INVITE@{} media update: status {}", call->getCallId(), fmt::ptr(inv), status);

    if (status != PJ_SUCCESS) {
        const int reason = inv->state != PJSIP_INV_STATE_NULL and inv->state != PJSIP_INV_STATE_CONFIRMED
                               ? PJSIP_SC_UNSUPPORTED_MEDIA_TYPE
                               : 0;

        JAMI_WARNING("[call:{}] SDP offer failed, reason {}", call->getCallId(), reason);

        call->hangup(reason);
        return;
    }

    // Fetch SDP data from request
    const auto* const localSDP = get_active_local_sdp(inv);
    const auto* const remoteSDP = get_active_remote_sdp(inv);

    // Update our SDP manager
    auto& sdp = call->getSDP();
    sdp.setActiveLocalSdpSession(localSDP);
    if (localSDP != nullptr) {
        Sdp::printSession(localSDP, "Local active session:", sdp.getSdpDirection());
    }

    sdp.setActiveRemoteSdpSession(remoteSDP);
    if (remoteSDP != nullptr) {
        Sdp::printSession(remoteSDP, "Remote active session:", sdp.getSdpDirection());
    }

    call->onMediaNegotiationComplete();
}

static void
outgoing_request_forked_cb(pjsip_inv_session* /*inv*/, pjsip_event* /*e*/)
{}

static bool
handleMediaControl(SIPCall& call, pjsip_msg_body* body)
{
    /*
     * Incoming INFO request for media control.
     */
    constexpr pj_str_t STR_APPLICATION = CONST_PJ_STR("application");
    constexpr pj_str_t STR_MEDIA_CONTROL_XML = CONST_PJ_STR("media_control+xml");

    if (body->len and pj_stricmp(&body->content_type.type, &STR_APPLICATION) == 0
        and pj_stricmp(&body->content_type.subtype, &STR_MEDIA_CONTROL_XML) == 0) {
        auto body_msg = std::string_view((char*) body->data, (size_t) body->len);

        /* Apply and answer the INFO request */
        static constexpr auto PICT_FAST_UPDATE = "picture_fast_update"sv;
        static constexpr auto STREAM_ID = "stream_id"sv;
        static constexpr auto DEVICE_ORIENTATION = "device_orientation"sv;
        static constexpr auto RECORDING_STATE = "recording_state"sv;
        static constexpr auto MUTE_STATE = "mute_state"sv;
        static constexpr auto VOICE_ACTIVITY = "voice_activity"sv;

        int streamIdx = -1;
        if (body_msg.find(STREAM_ID) != std::string_view::npos) {
            // Note: here we use the index of the RTP stream, not its label!
            // Indeed, both sides will have different labels as they have different call IDs
            static const std::regex STREAMID_REGEX("<stream_id>([0-9]+)</stream_id>");
            std::svmatch matched_pattern;
            std::regex_search(body_msg, matched_pattern, STREAMID_REGEX);
            if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
                try {
                    streamIdx = std::stoi(matched_pattern[1]);
                } catch (const std::exception& e) {
                    JAMI_WARNING("Error parsing stream index: {}", e.what());
                }
            }
        }

        if (body_msg.find(PICT_FAST_UPDATE) != std::string_view::npos) {
            call.sendKeyframe(streamIdx);
            return true;
        } else if (body_msg.find(DEVICE_ORIENTATION) != std::string_view::npos) {
            static const std::regex ORIENTATION_REGEX("device_orientation=([-+]?[0-9]+)");

            std::svmatch matched_pattern;
            std::regex_search(body_msg, matched_pattern, ORIENTATION_REGEX);

            if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
                try {
                    int rotation = -std::stoi(matched_pattern[1]);
                    while (rotation <= -180)
                        rotation += 360;
                    while (rotation > 180)
                        rotation -= 360;
                    JAMI_WARNING("Rotate video {} deg.", rotation);
#ifdef ENABLE_VIDEO
                    call.setRotation(streamIdx, rotation);
#endif
                } catch (const std::exception& e) {
                    JAMI_WARNING("Error parsing angle: {}", e.what());
                }
                return true;
            }
        } else if (body_msg.find(RECORDING_STATE) != std::string_view::npos) {
            static const std::regex REC_REGEX("recording_state=([0-1])");
            std::svmatch matched_pattern;
            std::regex_search(body_msg, matched_pattern, REC_REGEX);

            if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
                try {
                    bool state = std::stoi(matched_pattern[1]);
                    call.peerRecording(state);
                } catch (const std::exception& e) {
                    JAMI_WARNING("Error parsing state remote recording: {}", e.what());
                }
                return true;
            }
        } else if (body_msg.find(MUTE_STATE) != std::string_view::npos) {
            static const std::regex REC_REGEX("mute_state=([0-1])");
            std::svmatch matched_pattern;
            std::regex_search(body_msg, matched_pattern, REC_REGEX);

            if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
                try {
                    bool state = std::stoi(matched_pattern[1]);
                    call.peerMuted(state, streamIdx);
                } catch (const std::exception& e) {
                    JAMI_WARNING("Error parsing state remote mute: {}", e.what());
                }
                return true;
            }
        } else if (body_msg.find(VOICE_ACTIVITY) != std::string_view::npos) {
            static const std::regex REC_REGEX("voice_activity=([0-1])");
            std::svmatch matched_pattern;
            std::regex_search(body_msg, matched_pattern, REC_REGEX);

            if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
                try {
                    bool state = std::stoi(matched_pattern[1]);
                    call.peerVoice(state);
                } catch (const std::exception& e) {
                    JAMI_WARNING("Error parsing state remote voice: {}", e.what());
                }
                return true;
            }
        }
    }

    return false;
}

/**
 * Helper function to process refer function on call transfer
 */
static bool
transferCall(SIPCall& call, const std::string& refer_to)
{
    const auto& callId = call.getCallId();
    JAMI_WARNING("[call:{}] Attempting to transfer to {}", callId, refer_to);
    try {
        Manager::instance().newOutgoingCall(refer_to,
                                            call.getAccountId(),
                                            MediaAttribute::mediaAttributesToMediaMaps(call.getMediaAttributeList()));
        Manager::instance().hangupCall(call.getAccountId(), callId);
    } catch (const std::exception& e) {
        JAMI_ERROR("[call:{}] SIP transfer failed: {}", callId, e.what());
        return false;
    }
    return true;
}

static void
replyToRequest(pjsip_inv_session* inv, pjsip_rx_data* rdata, int status_code)
{
    const auto ret = pjsip_dlg_respond(inv->dlg, rdata, status_code, nullptr, nullptr, nullptr);
    if (ret != PJ_SUCCESS)
        JAMI_WARNING("SIP: Failed to reply {} to request", status_code);
}

static void
onRequestRefer(pjsip_inv_session* inv, pjsip_rx_data* rdata, pjsip_msg* msg, SIPCall& call)
{
    static constexpr pj_str_t str_refer_to = CONST_PJ_STR("Refer-To");

    if (auto* refer_to = static_cast<pjsip_generic_string_hdr*>(
            pjsip_msg_find_hdr_by_name(msg, &str_refer_to, nullptr))) {
        // RFC 3515, 2.4.2: reply bad request if no or too many refer-to header.
        if (static_cast<void*>(refer_to->next) == static_cast<void*>(&msg->hdr)
            or !pjsip_msg_find_hdr_by_name(msg, &str_refer_to, refer_to->next)) {
            replyToRequest(inv, rdata, PJSIP_SC_ACCEPTED);
            transferCall(call, std::string(refer_to->hvalue.ptr, refer_to->hvalue.slen));

            // RFC 3515, 2.4.4: we MUST handle the processing using NOTIFY msgs
            // But your current design doesn't permit that
            return;
        } else
            JAMI_ERROR("[call:{}] REFER: too many Refer-To headers", call.getCallId());
    } else
        JAMI_ERROR("[call:{}] REFER: no Refer-To header", call.getCallId());

    replyToRequest(inv, rdata, PJSIP_SC_BAD_REQUEST);
}

static void
onRequestInfo(pjsip_inv_session* inv, pjsip_rx_data* rdata, pjsip_msg* msg, SIPCall& call)
{
    if (!msg->body or handleMediaControl(call, msg->body))
        replyToRequest(inv, rdata, PJSIP_SC_OK);
}

static void
onRequestNotify(pjsip_inv_session* /*inv*/, pjsip_rx_data* /*rdata*/, pjsip_msg* msg, SIPCall& call)
{
    if (!msg->body)
        return;

    const std::string_view bodyText {static_cast<char*>(msg->body->data), msg->body->len};
    JAMI_LOG("[call:{}] NOTIFY body start - {}\n{}\n[call:{}] NOTIFY body end - {}",
             call.getCallId(),
             fmt::ptr(msg->body),
             bodyText,
             call.getCallId(),
             fmt::ptr(msg->body));

    // TODO
}

static void
transaction_state_changed_cb(pjsip_inv_session* inv, pjsip_transaction* tsx, pjsip_event* event)
{
    auto call = getCallFromInvite(inv);
    if (not call)
        return;

#ifdef DEBUG_SIP_REQUEST_MSG
    processInviteResponseHelper(inv, event);
#endif

    // We process here only incoming request message
    if (tsx->role != PJSIP_ROLE_UAS or tsx->state != PJSIP_TSX_STATE_TRYING
        or event->body.tsx_state.type != PJSIP_EVENT_RX_MSG) {
        return;
    }

    auto* const rdata = event->body.tsx_state.src.rdata;
    if (!rdata) {
        JAMI_ERROR("[INVITE:{:p}] SIP RX request without rx data", fmt::ptr(inv));
        return;
    }

    auto* const msg = rdata->msg_info.msg;
    if (msg->type != PJSIP_REQUEST_MSG) {
        JAMI_ERROR("[INVITE:{:p}] SIP RX request without msg", fmt::ptr(inv));
        return;
    }

    // Using method name to dispatch
    auto methodName = sip_utils::as_view(msg->line.req.method.name);
    JAMI_LOG("[INVITE:{:p}] RX SIP method {:d} ({:s})", fmt::ptr(inv), (int) msg->line.req.method.id, methodName);

#ifdef DEBUG_SIP_REQUEST_MSG
    char msgbuf[1000];
    auto msgsize = pjsip_msg_print(msg, msgbuf, sizeof msgbuf);
    if (msgsize > 0)
        JAMI_LOG("{:s}", std::string_view(msgbuf, msgsize));
#endif // DEBUG_SIP_REQUEST_MSG

    if (methodName == sip_utils::SIP_METHODS::REFER)
        onRequestRefer(inv, rdata, msg, *call);
    else if (methodName == sip_utils::SIP_METHODS::INFO)
        onRequestInfo(inv, rdata, msg, *call);
    else if (methodName == sip_utils::SIP_METHODS::NOTIFY)
        onRequestNotify(inv, rdata, msg, *call);
    else if (methodName == sip_utils::SIP_METHODS::OPTIONS)
        handleIncomingOptions(rdata);
    else if (methodName == sip_utils::SIP_METHODS::MESSAGE) {
        if (msg->body)
            runOnMainThread([call, m = im::parseSipMessage(msg)]() mutable { call->onTextMessage(std::move(m)); });
    }
}

#ifdef DEBUG_SIP_REQUEST_MSG
static void
processInviteResponseHelper(pjsip_inv_session* inv, pjsip_event* event)
{
    if (event->body.tsx_state.type != PJSIP_EVENT_RX_MSG)
        return;

    const auto rdata = event->body.tsx_state.src.rdata;
    if (rdata == nullptr or rdata->msg_info.msg == nullptr)
        return;

    const auto msg = rdata->msg_info.msg;
    if (msg->type != PJSIP_RESPONSE_MSG)
        return;

    // Only handle the following responses
    switch (msg->line.status.code) {
    case PJSIP_SC_TRYING:
    case PJSIP_SC_RINGING:
    case PJSIP_SC_OK:
        break;
    default:
        return;
    }

    JAMI_LOG("[INVITE:{:p}] SIP RX response: reason {:s}, status code {:d} {:s}",
             fmt::ptr(inv),
             sip_utils::as_view(msg->line.status.reason),
             msg->line.status.code,
             sip_utils::as_view(*pjsip_get_status_text(msg->line.status.code)));

    sip_utils::logMessageHeaders(&msg->hdr);
}
#endif

int
SIPVoIPLink::getModId()
{
    return mod_ua_.id;
}

void
SIPVoIPLink::createSDPOffer(pjsip_inv_session* inv)
{
    if (inv == nullptr) {
        throw VoipLinkException("Invite session is unable to be null");
    }
    sdp_create_offer_cb(inv, nullptr);
}

// Thread-safe DNS resolver callback mapping
class SafeResolveCallbackMap
{
public:
    using ResolveCallback = std::function<void(pj_status_t, const pjsip_server_addresses*)>;

    void registerCallback(uintptr_t key, ResolveCallback&& cb)
    {
        std::lock_guard lk(mutex_);
        cbMap_.emplace(key, std::move(cb));
    }

    void process(uintptr_t key, pj_status_t status, const pjsip_server_addresses* addr)
    {
        std::lock_guard lk(mutex_);
        auto it = cbMap_.find(key);
        if (it != cbMap_.end()) {
            it->second(status, addr);
            cbMap_.erase(it);
        }
    }

private:
    std::mutex mutex_;
    std::map<uintptr_t, ResolveCallback> cbMap_;
};

static SafeResolveCallbackMap&
getResolveCallbackMap()
{
    static SafeResolveCallbackMap map;
    return map;
}

static void
resolver_callback(pj_status_t status, void* token, const struct pjsip_server_addresses* addr)
{
    getResolveCallbackMap().process((uintptr_t) token, status, addr);
}

void
SIPVoIPLink::resolveSrvName(const std::string& name, pjsip_transport_type_e type, SrvResolveCallback&& cb)
{
    // PJSIP limits hostname to be longer than PJ_MAX_HOSTNAME.
    // But, resolver prefix the given name by a string like "_sip._udp."
    // causing a check against PJ_MAX_HOSTNAME to be useless.
    // It's not easy to pre-determinate as it's implementation dependent.
    // So we just choose a security marge enough for most cases, preventing a crash later
    // in the call of pjsip_endpt_resolve().
    if (name.length() > (PJ_MAX_HOSTNAME - 12)) {
        JAMI_ERROR("Hostname is too long");
        cb({});
        return;
    }

    // extract port if name is in form "server:port"
    int port;
    pj_ssize_t name_size;
    const auto n = name.rfind(':');
    if (n != std::string::npos) {
        port = std::atoi(name.c_str() + n + 1);
        name_size = static_cast<pj_ssize_t>(n);
    } else {
        port = 0;
        name_size = static_cast<pj_ssize_t>(name.size());
    }
    JAMI_LOG("Attempt to resolve '{}' (port: {})", name, port);

    pjsip_host_info host_info {
        /*.flag = */ 0,
        /*.type = */ type,
        /*.addr = */ {{(char*) name.c_str(), name_size}, port},
    };

    const auto token = std::hash<std::string>()(name + std::to_string(type));
    getResolveCallbackMap()
        .registerCallback(token, [=, cb = std::move(cb)](pj_status_t s, const pjsip_server_addresses* r) {
            try {
                if (s != PJ_SUCCESS || !r) {
                    JAMI_WARNING("Unable to resolve \"{}\" using pjsip_endpt_resolve, attempting getaddrinfo.", name);
                    dht::ThreadPool::io().run([=, cb = std::move(cb)]() {
                        auto ips = dhtnet::ip_utils::getAddrList(name.c_str());
                        runOnMainThread(std::bind(cb, std::move(ips)));
                    });
                } else {
                    std::vector<dhtnet::IpAddr> ips;
                    ips.reserve(r->count);
                    for (unsigned i = 0; i < r->count; i++)
                        ips.push_back(r->entry[i].addr);
                    cb(ips);
                }
            } catch (const std::exception& e) {
                JAMI_ERROR("Error resolving address: {}", e.what());
                cb({});
            }
        });

    pjsip_endpt_resolve(endpt_, pool_.get(), &host_info, (void*) token, resolver_callback);
}

// Workaround for MSVC where __VA_ARGS__ is treated as a single token
// when passed to another variadic macro. This forces a re-scan.
#define VA_EXPAND(x) x

#define RETURN_IF_NULL(A, ...) \
    if ((A) == NULL) { \
        VA_EXPAND(JAMI_WARNING(__VA_ARGS__)); \
        return; \
    }

#define RETURN_FALSE_IF_NULL(A, ...) \
    if ((A) == NULL) { \
        VA_EXPAND(JAMI_WARNING(__VA_ARGS__)); \
        return false; \
    }

void
SIPVoIPLink::findLocalAddressFromTransport(pjsip_transport* transport,
                                           pjsip_transport_type_e transportType,
                                           const std::string& host,
                                           std::string& addr,
                                           pj_uint16_t& port) const
{
    // Initialize the SIP port with the default SIP port
    port = pjsip_transport_get_default_port_for_type(transportType);

    // Initialize the SIP address with the hostname
    addr = sip_utils::as_view(*pj_gethostname());

    // Update address and port with active transport
    RETURN_IF_NULL(transport, "Transport is NULL in findLocalAddress, using local address {} :{}", addr, port);

    // get the transport manager associated with the SIP enpoint
    auto* tpmgr = pjsip_endpt_get_tpmgr(endpt_);
    RETURN_IF_NULL(tpmgr, "Transport manager is NULL in findLocalAddress, using local address {} :{}", addr, port);

    const pj_str_t pjstring(CONST_PJ_STR(host));

    auto tp_sel = getTransportSelector(transport);
    pjsip_tpmgr_fla2_param param = {transportType, &tp_sel, pjstring, PJ_FALSE, {nullptr, 0}, 0, nullptr};
    if (pjsip_tpmgr_find_local_addr2(tpmgr, pool_.get(), &param) != PJ_SUCCESS) {
        JAMI_WARNING("Unable to retrieve local address and port from transport, using {} :{}", addr, port);
        return;
    }

    // Update local address based on the transport type
    addr = sip_utils::as_view(param.ret_addr);

    // Determine the local port based on transport information
    port = param.ret_port;
}

bool
SIPVoIPLink::findLocalAddressFromSTUN(
    pjsip_transport* transport, pj_str_t* stunServerName, int stunPort, std::string& addr, pj_uint16_t& port) const
{
    // WARN: pjstun_get_mapped_addr2 works in IPv4 only.
    // For IPv6 transports, STUN is not needed (no NAT), return local address.
    // WARN: this function is blocking (network request).

    // Initialize the sip port with the default SIP port
    port = sip_utils::DEFAULT_SIP_PORT;

    // Detect transport family - skip STUN for IPv6 (no NAT on IPv6)
    if (transport && transport->local_addr.addr.sa_family == pj_AF_INET6()) {
        auto localIp6 = dhtnet::ip_utils::getLocalAddr(pj_AF_INET6());
        if (localIp6) {
            addr = localIp6.toString();
            port = pj_sockaddr_get_port(&transport->local_addr);
            if (port == 0)
                port = sip_utils::DEFAULT_SIP_PORT;
            JAMI_DEBUG("IPv6 transport detected, skipping STUN, using local address {}:{}", addr, port);
            return true;
        }
    }

    // Get Local IPv4 address for STUN
    auto localIp = dhtnet::ip_utils::getLocalAddr(pj_AF_INET());
    if (not localIp) {
        JAMI_WARNING("Failed to find local IP");
        return false;
    }

    addr = localIp.toString();

    // Update address and port with active transport
    RETURN_FALSE_IF_NULL(transport, "Transport is NULL in findLocalAddress, using local address {}:{}", addr, port);

    JAMI_LOG("STUN mapping of '{}:{}'", addr, port);

    pj_sockaddr_in mapped_addr;
    pj_sock_t sipSocket = pjsip_udp_transport_get_socket(transport);
    const pjstun_setting stunOpt = {PJ_TRUE, localIp.getFamily(), *stunServerName, stunPort, *stunServerName, stunPort};
    pj_status_t stunStatus = pjstun_get_mapped_addr2(&cp_.factory, &stunOpt, 1, &sipSocket, &mapped_addr);

    switch (stunStatus) {
    case PJLIB_UTIL_ESTUNNOTRESPOND:
        JAMI_ERROR("No response from STUN server {:s}", sip_utils::as_view(*stunServerName));
        return false;

    case PJLIB_UTIL_ESTUNSYMMETRIC:
        JAMI_ERROR("Different mapped addresses are returned by servers.");
        return false;

    case PJ_SUCCESS:
        port = pj_sockaddr_in_get_port(&mapped_addr);
        addr = dhtnet::IpAddr((const sockaddr_in&) mapped_addr).toString(true);
        JAMI_DEBUG("STUN server {:s} replied '{}'", sip_utils::as_view(*stunServerName), addr);
        return true;

    default: // use given address, silent any not handled error
        JAMI_WARNING("Error from STUN server {:s}, using source address", sip_utils::as_view(*stunServerName));
        return false;
    }
}
#undef RETURN_IF_NULL
#undef RETURN_FALSE_IF_NULL
} // namespace jami
