#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/mobility-module.h"
#include "ns3/internet-module.h"
#include "ns3/applications-module.h"
#include "ns3/antenna-module.h"
#include "ns3/nr-module.h"
#include "ns3/flow-monitor-module.h"
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <cerrno>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <fstream>
#include <vector>
#include <algorithm>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
using namespace ns3;
NS_LOG_COMPONENT_DEFINE("Ns3A3Handover");

struct ConfigData {
    uint16_t sourceCell=1, targetCell=2, numerology=1;
    double a3Offset=3, hysteresis=1, effectiveMargin=4, speedKmh=90;
    double movementStart=2, simTime=10, udpStart=1;
    double frequency=3.5e9, bandwidth=20e6, gnbTxPower=30;
    double packetInterval=0.001;
    uint32_t tttMs=320, packetSize=1000;
    std::string apiIp="127.0.0.1";
    uint16_t apiPort=8080;
    std::string outcomePath="/api/v1/a3-handover-output-kpis";
    std::string livePath="/api/v1/a3-handover-live-kpis";
    bool enableHttpPost=true;
};
static ConfigData cfg;
struct Measurement { bool valid=false; double rsrp=0, rsrq=0, time=-1; };
static Measurement meas[3];
static Ptr<MobilityModel> ueMobility;
static uint64_t imsi=0;
static uint16_t rnti=0, servingCell=0, hoSource=0, hoTarget=0;
static bool ueAttached=false, gnbConnected=false, hoStarted=false, hoCompleted=false;
static bool servingRsrpValid=false, targetRsrpValid=false;
static double attachTime=-1, hoStart=-1, hoEnd=-1, hoX=0;
static double servingRsrp=0, targetRsrp=0, rsrpDelta=0;
static double sinrSum=0, mcsSum=0;
static uint64_t sinrSamples=0, mcsSamples=0, harqAck=0, harqNack=0;
static uint32_t successfulHos=0, periodicLivePosts=0, a3TriggerLivePosts=0;
struct UdpData {
    Ptr<FlowMonitor> monitor;
    Ptr<Ipv4FlowClassifier> classifier;
    Ipv4Address ueIp;
    uint16_t port=1234;
    uint64_t previousTx=0, previousRx=0, previousBytes=0;
    double previousTime=1;
};
static UdpData udp;

// Positions use command-line strings in X,Y,Z meters format.
static std::string sourcePosition="0,0,10";
static std::string targetPosition="200,0,10";
static std::string uePosition="20,0,1.5";
// constant = original straight-line model; sumo = replay time,x,y,z CSV.
static std::string mobilitySource="constant";
static std::string sumoTraceFile="mobility/applied_offset_trajectory.csv";
struct SumoSample { double time; Vector position; };
static std::vector<SumoSample> sumoSamples;

static std::vector<SumoSample> LoadSumoCsv(const std::string& file) {
    std::ifstream in(file);
    NS_ABORT_MSG_IF(!in.is_open(), "Cannot open SUMO trajectory CSV: " << file);
    std::string line;
    NS_ABORT_MSG_IF(!std::getline(in,line), "Empty SUMO CSV: " << file);
    if (!line.empty() && line.back()=='\r') line.pop_back();
    NS_ABORT_MSG_IF(line!="time,x,y,z", "SUMO CSV must have header time,x,y,z");
    std::vector<SumoSample> result;
    unsigned lineNo=1;
    while (std::getline(in,line)) {
        ++lineNo;
        if (!line.empty() && line.back()=='\r') line.pop_back();
        if (line.empty()) continue;
        std::istringstream row(line);
        std::string field;
        double v[4];
        for (unsigned i=0;i<4;++i) {
            NS_ABORT_MSG_IF(!std::getline(row,field,','),
                            "Missing column in SUMO CSV line " << lineNo);
            std::istringstream num(field);
            num >> std::ws;
            NS_ABORT_MSG_IF(!(num >> v[i]) || !std::isfinite(v[i]),
                            "Invalid numeric value in SUMO CSV line " << lineNo);
            num >> std::ws;
            NS_ABORT_MSG_IF(!num.eof(), "Invalid numeric field in SUMO CSV line " << lineNo);
        }
        NS_ABORT_MSG_IF(std::getline(row,field,','),
                        "Too many columns in SUMO CSV line " << lineNo);
        NS_ABORT_MSG_IF(v[0]<0 || (!result.empty() && v[0]<=result.back().time),
                        "SUMO CSV times must be nonnegative and strictly increasing");
        result.push_back({v[0],Vector(v[1],v[2],v[3])});
    }
    NS_ABORT_MSG_IF(result.size()<2, "SUMO CSV needs at least two samples");
    NS_ABORT_MSG_IF(std::abs(result.front().time)>1e-9,
                    "SUMO CSV first timestamp must be 0.0 s");
    return result;
}


static Vector ParsePosition(const std::string& label,const std::string& input) {
    std::istringstream ss(input);
    double x=0,y=0,z=0;
    char comma1=0,comma2=0;
    ss>>std::ws;
    bool valid=static_cast<bool>(ss>>x>>comma1>>y>>comma2>>z);
    ss>>std::ws;
    NS_ABORT_MSG_IF(!valid || !ss.eof() || comma1!=',' || comma2!=',' ||
                    !std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z),
                    "Invalid " << label << ": expected X,Y,Z (meters)");
    return Vector(x,y,z);
}
static std::string BandName(double hz) {
    return hz>=3.3e9&&hz<=3.8e9?"n78":"Not mapped";
}
static void ValidateConfig() {
    cfg.effectiveMargin=cfg.a3Offset+cfg.hysteresis;
    NS_ABORT_MSG_IF(mobilitySource!="constant" && mobilitySource!="sumo",
                    "mobilitySource must be constant or sumo");
    NS_ABORT_MSG_IF(cfg.sourceCell!=1 || cfg.targetCell!=2,
                    "This two-gNB scenario supports sourceCellId=1 and targetCellId=2 only");
    NS_ABORT_MSG_IF(!std::isfinite(cfg.a3Offset) || !std::isfinite(cfg.hysteresis) ||
                    !std::isfinite(cfg.speedKmh) || !std::isfinite(cfg.movementStart) ||
                    !std::isfinite(cfg.simTime) || cfg.a3Offset<0 || cfg.hysteresis<0 ||
                    cfg.effectiveMargin>15 ||
                    std::abs(2*cfg.effectiveMargin-std::round(2*cfg.effectiveMargin))>1e-6 ||
                    (mobilitySource=="constant" && cfg.speedKmh<=0) || cfg.movementStart<0 ||
                    cfg.simTime<=cfg.movementStart || cfg.simTime<=cfg.udpStart,
                    "Invalid handover or simulation parameters");
    bool supportedTtt=cfg.tttMs==0 || cfg.tttMs==40 || cfg.tttMs==64 ||
        cfg.tttMs==80 || cfg.tttMs==100 || cfg.tttMs==128 || cfg.tttMs==160 ||
        cfg.tttMs==256 || cfg.tttMs==320 || cfg.tttMs==480 || cfg.tttMs==512 ||
        cfg.tttMs==640 || cfg.tttMs==1024 || cfg.tttMs==1280 ||
        cfg.tttMs==2560 || cfg.tttMs==5120;
    NS_ABORT_MSG_IF(!supportedTtt,"Unsupported RRC Time-to-Trigger (ms)");
    std::cout<<"\n========== EVENT A3 COMMAND-LINE CONFIGURATION ==========\n"
             <<"Source Cell ID       : "<<cfg.sourceCell<<"\n"
             <<"Target Cell ID       : "<<cfg.targetCell<<"\n"
             <<"A3 Offset            : "<<cfg.a3Offset<<" dB\n"
             <<"Hysteresis           : "<<cfg.hysteresis<<" dB\n"
             <<"Effective A3 Margin  : "<<cfg.effectiveMargin<<" dB\n"
             <<"TTT                  : "<<cfg.tttMs<<" ms\n"
             <<"UE Speed (constant)  : "<<cfg.speedKmh<<" km/h\n"
             <<"Mobility Source      : "<<mobilitySource<<"\n"
             <<"SUMO CSV (if used)   : "<<sumoTraceFile<<"\n"
             <<"Mobility Start        : "<<cfg.movementStart<<" s\n"
             <<"Simulation Time      : "<<cfg.simTime<<" s\n"
             <<"Source gNB Position  : "<<sourcePosition<<" m\n"
             <<"Target gNB Position  : "<<targetPosition<<" m\n"
             <<"UE Start Position    : "<<uePosition<<" m\n"
             <<"=========================================================\n";
}
static double Missing() { return std::numeric_limits<double>::quiet_NaN(); }
static std::string JsonNumber1(double v) {
    if(!std::isfinite(v)) return "null";
    std::ostringstream s; s<<std::fixed<<std::setprecision(1)<<v; return s.str();
}
static std::string JsonKpi(double v,const std::string& unit) {
    return "{\"value\": "+JsonNumber1(v)+", \"unit\": \""+unit+"\"}";
}
static std::string JsonId(uint16_t v,bool valid) {
    return "{\"value\": "+(valid?JsonNumber1(v):"null")+", \"unit\": null}";
}
static std::string BuildOutcomeJson(bool final) {
    std::ostringstream j;
    j<<"{\n  \"a3_handover_output_kpis\": ";
    if(!final) j<<"{},\n";
    else {
        j<<"{\n"
         <<"    \"effective_a3_margin\": "<<JsonKpi(cfg.effectiveMargin,"dB")<<",\n"
         <<"    \"ho_start_time\": "<<JsonKpi(hoStarted?hoStart:Missing(),"s")<<",\n"
         <<"    \"ue_position_at_ho\": "<<JsonKpi(hoStarted?hoX:Missing(),"m")<<",\n"
         <<"    \"serving_rsrp_at_ho\": "<<JsonKpi(hoStarted&&servingRsrpValid?servingRsrp:Missing(),"dBm")<<",\n"
         <<"    \"target_rsrp_at_ho\": "<<JsonKpi(hoStarted&&targetRsrpValid?targetRsrp:Missing(),"dBm")<<",\n"
         <<"    \"rsrp_delta_at_ho\": "<<JsonKpi(hoStarted&&servingRsrpValid&&targetRsrpValid?rsrpDelta:Missing(),"dB")
         <<"\n  },\n";
    }
    j<<"  \"radio_configuration_kpis\": {\n"
     <<"    \"frequency\": "<<JsonKpi(cfg.frequency/1e9,"GHz")<<",\n"
     <<"    \"band\": {\"value\": \""<<BandName(cfg.frequency)<<"\", \"unit\": null},\n"
     <<"    \"bandwidth\": "<<JsonKpi(cfg.bandwidth/1e6,"MHz")<<",\n"
     <<"    \"subcarrier_spacing\": "<<JsonKpi(15u*(1u<<cfg.numerology),"kHz")<<",\n"
     <<"    \"a3_offset\": "<<JsonKpi(cfg.a3Offset,"dB")<<"\n  }\n}\n";
    return j.str();
}
static std::string BuildLiveJson() {
    bool hasServing=servingCell==1||servingCell==2;
    uint16_t neighbor=hasServing?(servingCell==1?2:1):0;
    bool sv=hasServing&&meas[servingCell].valid;
    bool nv=hasServing&&meas[neighbor].valid;
    double delta=sv&&nv?meas[neighbor].rsrp-meas[servingCell].rsrp:Missing();
    std::ostringstream j;
    j<<"{\n  \"a3_handover_live_kpis\": {\n"
     <<"    \"serving_cell_id\": "<<JsonId(servingCell,hasServing)<<",\n"
     <<"    \"serving_cell_rsrp\": "<<JsonKpi(sv?meas[servingCell].rsrp:Missing(),"dBm")<<",\n"
     <<"    \"neighbor_cell_id\": "<<JsonId(neighbor,hasServing)<<",\n"
     <<"    \"neighbor_cell_rsrp\": "<<JsonKpi(nv?meas[neighbor].rsrp:Missing(),"dBm")<<",\n"
     <<"    \"rsrp_delta\": "<<JsonKpi(delta,"dB")<<",\n"
     <<"    \"ho_trigger_condition\": "<<JsonKpi(cfg.effectiveMargin,"dB")<<"\n  }\n}\n";
    return j.str();
}
static bool HttpPostJson(const std::string& path,const std::string& body) {
    if(path.empty()||path[0]!='/'||path.find('\r')!=std::string::npos||path.find('\n')!=std::string::npos) {
        std::cerr<<"[HTTP ERROR] Invalid URL path\n"; return false;
    }
    int fd=::socket(AF_INET,SOCK_STREAM,0);
    if(fd<0) { std::cerr<<"[HTTP ERROR] socket: "<<std::strerror(errno)<<"\n"; return false; }
    timeval timeout{2,0};
    ::setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
    ::setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));
    sockaddr_in addr{}; addr.sin_family=AF_INET; addr.sin_port=htons(cfg.apiPort);
    if(::inet_pton(AF_INET,cfg.apiIp.c_str(),&addr.sin_addr)!=1) {
        std::cerr<<"[HTTP ERROR] Invalid IPv4 address\n"; ::close(fd); return false;
    }
    if(::connect(fd,reinterpret_cast<sockaddr*>(&addr),sizeof(addr))<0) {
        std::cerr<<"[HTTP ERROR] connect: "<<std::strerror(errno)<<"\n"; ::close(fd); return false;
    }
    std::ostringstream req;
    req<<"POST "<<path<<" HTTP/1.1\r\nHost: "<<cfg.apiIp<<":"<<cfg.apiPort
       <<"\r\nContent-Type: application/json\r\nAccept: application/json\r\nContent-Length: "
       <<body.size()<<"\r\nConnection: close\r\n\r\n"<<body;
    std::string data=req.str(); size_t offset=0;
    while(offset<data.size()) {
        ssize_t sent=::send(fd,data.data()+offset,data.size()-offset,MSG_NOSIGNAL);
        if(sent<=0) { std::cerr<<"[HTTP ERROR] send failed\n"; ::close(fd); return false; }
        offset+=static_cast<size_t>(sent);
    }
    std::string response; char buffer[2048];
    while(response.size()<65536&&response.find("\r\n\r\n")==std::string::npos) {
        ssize_t n=::recv(fd,buffer,sizeof(buffer),0);
        if(n<=0) break;
        response.append(buffer,static_cast<size_t>(n));
    }
    ::close(fd);
    std::istringstream status(response); std::string protocol; int code=0;
    status>>protocol>>code;
    std::cout<<"[HTTP] "<<path<<" status="<<code<<(code>=200&&code<300?" SUCCESS":" FAILED")<<"\n";
    return code>=200&&code<300;
}
static void Publish(const std::string& label,const std::string& path,const std::string& json) {
    std::cout<<"\n========== "<<label<<" JSON PAYLOAD ==========\n"<<json
             <<"========================================\n";
    if(cfg.enableHttpPost) {
        std::cout<<"[HTTP] POST http://"<<cfg.apiIp<<":"<<cfg.apiPort<<path<<"\n";
        HttpPostJson(path,json);
    } else std::cout<<"[HTTP] Publishing disabled\n";
}
static void PublishA3TriggerLiveKpis() {
    ++a3TriggerLivePosts;
    std::cout<<std::fixed<<std::setprecision(6)
             <<"\n========== A3 TRIGGER LIVE KPI EVENT ==========\n"
             <<"Simulation Time : "<<Simulator::Now().GetSeconds()<<" s\n"
             <<"Source Cell ID  : "<<hoSource<<"\nTarget Cell ID  : "<<hoTarget
             <<"\nA3 Offset      : "<<cfg.a3Offset<<" dB\nHysteresis     : "<<cfg.hysteresis
             <<" dB\nTTT            : "<<cfg.tttMs<<" ms\n===============================================\n";
    Publish("A3 TRIGGER LIVE KPIS",cfg.livePath,BuildLiveJson());
}
static void StartMobility(Vector v) {
    DynamicCast<ConstantVelocityMobilityModel>(ueMobility)->SetVelocity(v);
    std::cout<<"\n[MOBILITY START] Time="<<Simulator::Now().GetSeconds()<<" s, Speed="<<cfg.speedKmh<<" km/h\n";
}
static void UeMeasurement(uint16_t rr,uint16_t cell,double rsrp,double rsrq,bool serving,uint8_t cc) {
    if(cell<1||cell>2||!std::isfinite(rsrp)) return;
    meas[cell]={true,rsrp,rsrq,Simulator::Now().GetSeconds()};
}
static void UeSinr(uint16_t cell,uint16_t rr,double sinr,uint16_t bwp) {
    if(std::isfinite(sinr)&&sinr>0) { sinrSum+=sinr; ++sinrSamples; }
}
static void DlScheduling(NrSchedulingCallbackInfo info) {
    if(rnti&&info.m_rnti==rnti) { mcsSum+=info.m_mcs; ++mcsSamples; }
}
static void DlHarq(const DlHarqInfo& info) {
    if(info.m_rnti!=rnti) return;
    if(info.m_harqStatus==DlHarqInfo::ACK) ++harqAck;
    else if(info.m_harqStatus==DlHarqInfo::NACK) ++harqNack;
}
static void UeConnected(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
    imsi=id; servingCell=cell; rnti=rr; ueAttached=true; attachTime=Simulator::Now().GetSeconds();
    std::cout<<"\n[UE ATTACH] Time="<<attachTime<<" s, IMSI="<<imsi<<", Cell="<<cell<<", RNTI="<<rr<<"\n";
}
static void GnbConnected(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
    if(cell==cfg.sourceCell) gnbConnected=true;
    std::cout<<"[gNB RRC] Time="<<Simulator::Now().GetSeconds()<<" s, Cell="<<cell<<", RNTI="<<rr<<"\n";
}
static bool GetDlFlow(FlowMonitor::FlowStats& out) {
    if(!udp.monitor||!udp.classifier) return false;
    for(const auto& entry:udp.monitor->GetFlowStats()) {
        auto tuple=udp.classifier->FindFlow(entry.first);
        if(tuple.protocol==17&&tuple.destinationAddress==udp.ueIp&&tuple.destinationPort==udp.port) {
            out=entry.second; return true;
        }
    }
    return false;
}
static void PrintA3() {
    if(servingCell!=1&&servingCell!=2) return;
    uint16_t neighbor=servingCell==1?2:1;
    bool both=meas[servingCell].valid&&meas[neighbor].valid;
    double delta=both?meas[neighbor].rsrp-meas[servingCell].rsrp:Missing();
    bool fresh=both&&Simulator::Now().GetSeconds()-meas[servingCell].time<=0.5&&
               Simulator::Now().GetSeconds()-meas[neighbor].time<=0.5;
    std::cout<<std::fixed<<std::setprecision(3)<<"[A3] Time="<<Simulator::Now().GetSeconds()
             <<" s, Serving Cell ID ="<<servingCell<<", Neighbor Cell ID ="<<neighbor
             <<", Serving Cell RSRP=";
    if(meas[servingCell].valid) std::cout<<meas[servingCell].rsrp; else std::cout<<"N/A";
    std::cout<<" dBm, Neighbor Cell RSRP=";
    if(meas[neighbor].valid) std::cout<<meas[neighbor].rsrp; else std::cout<<"N/A";
    std::cout<<" dBm, RSRP Delta=";
    if(both) std::cout<<delta; else std::cout<<"N/A";
    std::cout<<" dB, HO Trigger Condition (Offset+Hys)="<<cfg.effectiveMargin<<" dB, Entry="
             <<(!both?"N/A":!fresh?"STALE":delta>cfg.effectiveMargin?"MET":"NOT MET")<<"\n";
}
static void GnbHoStart(std::string context,uint64_t id,uint16_t cell,uint16_t rr,uint16_t target) {
    if(hoStarted) return;
    hoStarted=true; hoStart=Simulator::Now().GetSeconds(); hoSource=cell; hoTarget=target;
    hoX=ueMobility->GetPosition().x;
    if(cell>=1&&cell<=2&&meas[cell].valid) { servingRsrpValid=true; servingRsrp=meas[cell].rsrp; }
    if(target>=1&&target<=2&&meas[target].valid) { targetRsrpValid=true; targetRsrp=meas[target].rsrp; }
    if(servingRsrpValid&&targetRsrpValid) rsrpDelta=targetRsrp-servingRsrp;
    std::cout<<std::fixed<<std::setprecision(6)<<"\n========== A3 HANDOVER START ==========\n"
             <<"Time: "<<hoStart<<" s\nSource Cell: "<<cell<<"\nTarget Cell: "<<target
             <<"\nUE X: "<<hoX<<" m\nA3 Offset: "<<cfg.a3Offset
             <<" dB\nEffective Margin: "<<cfg.effectiveMargin<<" dB\nTTT: "<<cfg.tttMs<<" ms\n";
    PrintA3();
    // Event-driven publication in addition to one-second reports.
    PublishA3TriggerLiveKpis();
}
static void UeHoStart(std::string context,uint64_t id,uint16_t cell,uint16_t rr,uint16_t target) {
    std::cout<<"[UE RRC HO START] "<<Simulator::Now().GetSeconds()<<" s, "<<cell<<" -> "<<target<<"\n";
}
static void UeHoEnd(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
    hoCompleted=true; hoEnd=Simulator::Now().GetSeconds(); servingCell=cell; rnti=rr; ++successfulHos;
    std::cout<<"\n========== A3 HANDOVER COMPLETE ==========\nTime: "<<hoEnd
             <<" s\nServing Cell: "<<cell<<"\nHO Duration: "<<(hoEnd-hoStart)*1000
             <<" ms\nStatus: SUCCESS\n";
}
static void GnbHoEnd(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
    std::cout<<"[gNB RRC HO COMPLETE] "<<Simulator::Now().GetSeconds()<<" s, Cell="<<cell<<"\n";
}
static void PeriodicReport() {
    double now=Simulator::Now().GetSeconds(); Vector p=ueMobility->GetPosition();
    std::cout<<std::fixed<<std::setprecision(3)<<"\n[MOBILITY] Time="<<now
             <<" s, UE Position=("<<p.x<<", "<<p.y<<", "<<p.z
             <<") m, Serving Cell="<<servingCell<<"\n";
    PrintA3();
    std::cout<<"[RADIO] SINR=";
    if(sinrSamples) std::cout<<10*std::log10(sinrSum/sinrSamples)<<" dB"; else std::cout<<"N/A";
    std::cout<<", MCS=";
    if(mcsSamples) std::cout<<mcsSum/mcsSamples; else std::cout<<"N/A";
    std::cout<<", HARQ ACK="<<harqAck<<", NACK="<<harqNack<<"\n";
    FlowMonitor::FlowStats stats;
    if(GetDlFlow(stats)) {
        double interval=now-udp.previousTime;
        double throughput=interval>0?(stats.rxBytes-udp.previousBytes)*8.0/(interval*1e6):0;
        std::cout<<"[UDP] Time="<<now<<" s, Tx Pkts="<<stats.txPackets-udp.previousTx
                 <<", Rx Pkts="<<stats.rxPackets-udp.previousRx<<", Throughput="<<throughput
                 <<" Mbps, Cell="<<servingCell<<"\n";
        udp.previousTx=stats.txPackets; udp.previousRx=stats.rxPackets; udp.previousBytes=stats.rxBytes;
    }
    udp.previousTime=now; sinrSum=0; mcsSum=0; sinrSamples=0; mcsSamples=0; harqAck=0; harqNack=0;
}
static void PublishLiveKpis() {
    double now=Simulator::Now().GetSeconds(); ++periodicLivePosts;
    std::cout<<"\n[PERIODIC LIVE KPI] Simulation Time: "<<std::fixed<<std::setprecision(3)<<now<<" s\n";
    Publish("PERIODIC A3 HANDOVER LIVE KPIS",cfg.livePath,BuildLiveJson());
    if(now+1.0<cfg.simTime-1e-9) Simulator::Schedule(Seconds(1.0),&PublishLiveKpis);
}
int main(int argc,char* argv[]) {
    CommandLine cmd(__FILE__);
    cmd.AddValue("sourceCellId","Source cell ID (default 1)",cfg.sourceCell);
    cmd.AddValue("targetCellId","Target cell ID (default 2)",cfg.targetCell);
    cmd.AddValue("a3Offset","A3 offset in dB (default 3)",cfg.a3Offset);
    cmd.AddValue("hysteresis","A3 hysteresis in dB (default 1)",cfg.hysteresis);
    cmd.AddValue("ttt","Time-to-trigger in ms (default 320)",cfg.tttMs);
    cmd.AddValue("sourceCellPos","Source gNB X,Y,Z meters (default 0,0,10)",sourcePosition);
    cmd.AddValue("targetCellPos","Target gNB X,Y,Z meters (default 200,0,10)",targetPosition);
    cmd.AddValue("ueStartPos","UE initial X,Y,Z meters (default 20,0,1.5)",uePosition);
    cmd.AddValue("mobilityTime","UE mobility start time in seconds (default 2)",cfg.movementStart);
    cmd.AddValue("simulationTime","Simulation duration in seconds (default 10)",cfg.simTime);
    cmd.AddValue("speedKmh","UE speed in km/h for constant mode (default 90)",cfg.speedKmh);
    cmd.AddValue("mobilitySource","constant or sumo (default constant)",mobilitySource);
    cmd.AddValue("sumoTraceFile","CSV path with time,x,y,z for sumo mode",sumoTraceFile);
    cmd.AddValue("apiIp","HTTP API IPv4 address",cfg.apiIp);
    cmd.AddValue("apiPort","HTTP API TCP port",cfg.apiPort);
    cmd.AddValue("enableHttpPost","Enable host-side HTTP publishing",cfg.enableHttpPost);
    cmd.Parse(argc,argv);
    ValidateConfig();
    Vector p1=ParsePosition("sourceCellPos",sourcePosition);
    Vector p2=ParsePosition("targetCellPos",targetPosition);
    Vector pu=ParsePosition("ueStartPos",uePosition);
    if (mobilitySource=="sumo") {
        sumoSamples=LoadSumoCsv(sumoTraceFile);
        pu=sumoSamples.front().position;
        std::cout<<"[SUMO] Loaded "<<sumoSamples.size()<<" samples from "<<sumoTraceFile
                 <<"; first position=("<<pu.x<<","<<pu.y<<","<<pu.z<<") m"
                 <<"; last CSV time="<<sumoSamples.back().time<<" s\n";
        NS_ABORT_MSG_IF(cfg.simTime<=cfg.movementStart,
                        "Simulation must extend beyond SUMO mobility start");
        if (cfg.simTime<cfg.movementStart+sumoSamples.back().time)
            std::cout<<"[SUMO] Simulation ends before CSV route finishes; route will be truncated.\n";
    }
    double dx=p2.x-pu.x,dy=p2.y-pu.y,distance=std::hypot(dx,dy);
    NS_ABORT_MSG_IF(mobilitySource=="constant" && distance<=0,
                    "UE must not start at target gNB");
    double speed=cfg.speedKmh/3.6;
    Vector velocity(0,0,0);
    if (mobilitySource=="constant")
        velocity=Vector(speed*dx/distance,speed*dy/distance,0);
    NodeContainer gnbs,ues; gnbs.Create(2); ues.Create(1);
    MobilityHelper gm;
    Ptr<ListPositionAllocator> gp=CreateObject<ListPositionAllocator>(); gp->Add(p1); gp->Add(p2);
    gm.SetPositionAllocator(gp); gm.SetMobilityModel("ns3::ConstantPositionMobilityModel"); gm.Install(gnbs);
    MobilityHelper um;
    Ptr<ListPositionAllocator> up=CreateObject<ListPositionAllocator>(); up->Add(pu);
    um.SetPositionAllocator(up);
    um.SetMobilityModel(mobilitySource=="sumo" ? "ns3::WaypointMobilityModel"
                                                    : "ns3::ConstantVelocityMobilityModel");
    um.Install(ues);
    ueMobility=ues.Get(0)->GetObject<MobilityModel>();
    if (mobilitySource=="constant") {
        DynamicCast<ConstantVelocityMobilityModel>(ueMobility)->SetVelocity(Vector(0,0,0));
    } else {
        Ptr<WaypointMobilityModel> waypoint=DynamicCast<WaypointMobilityModel>(ueMobility);
        NS_ABORT_MSG_IF(!waypoint,"SUMO mode requires WaypointMobilityModel");
        // Hold at first position until mobilityTime, then replay SUMO time offsets.
        waypoint->AddWaypoint(Waypoint(Seconds(0),pu));
        for (const auto& sample:sumoSamples) {
            double when=cfg.movementStart+sample.time;
            if (when>0 && when<=cfg.simTime)
                waypoint->AddWaypoint(Waypoint(Seconds(when),sample.position));
        }
        std::cout<<"[SUMO] Waypoint mobility configured, time offset="
                 <<cfg.movementStart<<" s; UE speed comes from CSV positions.\n";
    }
    Ptr<NrPointToPointEpcHelper> epc=CreateObject<NrPointToPointEpcHelper>();
    Ptr<IdealBeamformingHelper> beam=CreateObject<IdealBeamformingHelper>();
    Ptr<NrHelper> nr=CreateObject<NrHelper>();
    nr->SetEpcHelper(epc); nr->SetBeamformingHelper(beam);
    nr->SetAttribute("UseIdealRrc",BooleanValue(false));
    nr->SetSchedulerTypeId(TypeId::LookupByName("ns3::NrMacSchedulerTdmaRR"));
    beam->SetAttribute("BeamformingMethod",TypeIdValue(DirectPathBeamforming::GetTypeId()));
    nr->SetHandoverAlgorithmType("ns3::NrA3RsrpHandoverAlgorithm");
    nr->SetHandoverAlgorithmAttribute("Hysteresis",DoubleValue(cfg.effectiveMargin));
    nr->SetHandoverAlgorithmAttribute("TimeToTrigger",TimeValue(MilliSeconds(cfg.tttMs)));
    CcBwpCreator creator;
    CcBwpCreator::SimpleOperationBandConf bandConf(cfg.frequency,cfg.bandwidth,1);
    OperationBandInfo band=creator.CreateOperationBandContiguousCc(bandConf);
    Ptr<NrChannelHelper> channel=CreateObject<NrChannelHelper>();
    channel->ConfigureFactories("UMi","LOS","ThreeGpp");
    channel->SetPathlossAttribute("ShadowingEnabled",BooleanValue(false));
    channel->AssignChannelsToBands({band});
    BandwidthPartInfoPtrVector bwps=CcBwpCreator::GetAllBwps({band});
    nr->SetGnbAntennaAttribute("NumRows",UintegerValue(4));
    nr->SetGnbAntennaAttribute("NumColumns",UintegerValue(8));
    nr->SetGnbAntennaAttribute("AntennaElement",PointerValue(CreateObject<IsotropicAntennaModel>()));
    nr->SetUeAntennaAttribute("NumRows",UintegerValue(2));
    nr->SetUeAntennaAttribute("NumColumns",UintegerValue(4));
    nr->SetUeAntennaAttribute("AntennaElement",PointerValue(CreateObject<IsotropicAntennaModel>()));
    NetDeviceContainer gdev=nr->InstallGnbDevice(gnbs,bwps);
    NetDeviceContainer udev=nr->InstallUeDevice(ues,bwps);
    for(uint32_t i=0;i<2;++i) {
        Ptr<NrGnbPhy> phy=NrHelper::GetGnbPhy(gdev.Get(i),0);
        phy->SetAttribute("Numerology",UintegerValue(cfg.numerology));
        phy->SetAttribute("TxPower",DoubleValue(cfg.gnbTxPower));
    }
    Ptr<NrGnbNetDevice> g1=DynamicCast<NrGnbNetDevice>(gdev.Get(0));
    Ptr<NrGnbNetDevice> g2=DynamicCast<NrGnbNetDevice>(gdev.Get(1));
    NS_ABORT_MSG_IF(g1->GetCellId()!=1||g2->GetCellId()!=2,"Unexpected cell IDs");
    std::cout<<"\n========== NR CELL CONFIGURATION ==========\n"
             <<"gNB 1 Cell ID       : "<<g1->GetCellId()<<"\n"
             <<"gNB 2 Cell ID       : "<<g2->GetCellId()<<"\n"
             <<"NR Band             : "<<BandName(cfg.frequency)<<"\n"
             <<"Carrier Frequency   : "<<cfg.frequency/1e9<<" GHz\n"
             <<"Bandwidth           : "<<cfg.bandwidth/1e6<<" MHz\n"
             <<"Numerology          : "<<cfg.numerology<<"\n"
             <<"Subcarrier Spacing  : "<<15u*(1u<<cfg.numerology)<<" kHz\n"
             <<"Channel Model       : UMi LOS\nScheduler           : TDMA Round Robin\n"
             <<"RRC Protocol        : REAL\n===========================================\n";
    Publish("INITIAL RADIO CONFIGURATION",cfg.outcomePath,BuildOutcomeJson(false));
    auto [remote,remoteIp]=epc->SetupRemoteHost("100Gb/s",2500,Seconds(0)); (void)remoteIp;
    InternetStackHelper internet; internet.Install(ues);
    Ipv4InterfaceContainer ip=epc->AssignUeIpv4Address(udev);
    Ipv4StaticRoutingHelper routing;
    routing.GetStaticRouting(ues.Get(0)->GetObject<Ipv4>())
           ->SetDefaultRoute(epc->GetUeDefaultGatewayAddress(),1);
    udp.ueIp=ip.GetAddress(0); udp.previousTime=cfg.udpStart;
    Config::Connect("/NodeList/*/DeviceList/*/NrUeRrc/ConnectionEstablished",MakeCallback(&UeConnected));
    Config::Connect("/NodeList/*/DeviceList/*/NrGnbRrc/ConnectionEstablished",MakeCallback(&GnbConnected));
    Config::Connect("/NodeList/*/DeviceList/*/NrUeRrc/HandoverStart",MakeCallback(&UeHoStart));
    Config::Connect("/NodeList/*/DeviceList/*/NrUeRrc/HandoverEndOk",MakeCallback(&UeHoEnd));
    Config::Connect("/NodeList/*/DeviceList/*/NrGnbRrc/HandoverStart",MakeCallback(&GnbHoStart));
    Config::Connect("/NodeList/*/DeviceList/*/NrGnbRrc/HandoverEndOk",MakeCallback(&GnbHoEnd));
    Ptr<NrUePhy> uePhy=NrHelper::GetUePhy(udev.Get(0),0);
    NS_ABORT_MSG_IF(!uePhy->TraceConnectWithoutContext("ReportUeMeasurements",MakeCallback(&UeMeasurement)),"RSRP trace failed");
    NS_ABORT_MSG_IF(!uePhy->TraceConnectWithoutContext("DlDataSinr",MakeCallback(&UeSinr)),"SINR trace failed");
    for(uint32_t i=0;i<2;++i) {
        Ptr<NrGnbMac> mac=NrHelper::GetGnbMac(gdev.Get(i),0);
        NS_ABORT_MSG_IF(!mac->TraceConnectWithoutContext("DlScheduling",MakeCallback(&DlScheduling)),"Scheduling trace failed");
        NS_ABORT_MSG_IF(!mac->TraceConnectWithoutContext("DlHarqFeedback",MakeCallback(&DlHarq)),"HARQ trace failed");
    }
    nr->AddX2Interface(gnbs);
    nr->AttachToGnb(udev.Get(0),gdev.Get(0));
    UdpServerHelper server(udp.port);
    ApplicationContainer serverApps=server.Install(ues.Get(0));
    UdpClientHelper client(udp.ueIp,udp.port);
    client.SetAttribute("MaxPackets",UintegerValue(0xFFFFFFFF));
    client.SetAttribute("PacketSize",UintegerValue(cfg.packetSize));
    client.SetAttribute("Interval",TimeValue(Seconds(cfg.packetInterval)));
    ApplicationContainer clientApps=client.Install(remote);
    NrQosFlow qos(NrQosFlow::NGBR_LOW_LAT_EMBB);
    Ptr<NrQosRule> rule=Create<NrQosRule>();
    NrQosRule::PacketFilter filter; filter.localPortStart=udp.port; filter.localPortEnd=udp.port;
    rule->Add(filter); nr->ActivateDedicatedQosFlow(udev.Get(0),qos,rule);
    serverApps.Start(Seconds(0.5)); clientApps.Start(Seconds(cfg.udpStart));
    serverApps.Stop(Seconds(cfg.simTime)); clientApps.Stop(Seconds(cfg.simTime));
    FlowMonitorHelper flowHelper;
    NodeContainer nodes; nodes.Add(remote); nodes.Add(ues);
    udp.monitor=flowHelper.Install(nodes);
    udp.classifier=DynamicCast<Ipv4FlowClassifier>(flowHelper.GetClassifier());
    if (mobilitySource=="constant") {
        Simulator::Schedule(Seconds(cfg.movementStart),&StartMobility,velocity);
    } else {
        std::cout<<"[SUMO] No constant-velocity start event; waypoints control movement.\n";
    }
    Simulator::Schedule(Seconds(cfg.movementStart),&PublishLiveKpis);
    for(double t=cfg.udpStart+1;t<cfg.simTime-1e-9;t+=1)
        Simulator::Schedule(Seconds(t),&PeriodicReport);
    std::cout<<"\nStarting Event A3 handover simulation...\n";
    Simulator::Stop(Seconds(cfg.simTime)); Simulator::Run(); PeriodicReport();
    std::cout<<"\n========== FINAL HANDOVER SUMMARY ==========\n"
             <<"Initial attachment: "<<(ueAttached&&gnbConnected?"SUCCESS":"NOT CONFIRMED")
             <<"\nHO started: "<<(hoStarted?"YES":"NO")
             <<"\nHO completed: "<<(hoCompleted?"YES":"NO")
             <<"\nHO start time: "<<hoStart<<" s\nHO X: "<<(hoStarted?hoX:0)
             <<" m\nFinal serving cell: "<<servingCell<<"\nSuccessful HOs: "<<successfulHos<<"\n";
    if(hoStarted&&hoCompleted) std::cout<<"HO duration: "<<(hoEnd-hoStart)*1000<<" ms\n";
    std::cout<<"\n========== LIVE KPI PUBLICATION SUMMARY ==========\n"
             <<"Periodic live POST attempts : "<<periodicLivePosts
             <<"\nA3 trigger POST attempts    : "<<a3TriggerLivePosts
             <<"\nTotal live POST attempts    : "<<periodicLivePosts+a3TriggerLivePosts
             <<"\n==================================================\n";
    udp.monitor->CheckForLostPackets(); FlowMonitor::FlowStats stats;
    if(GetDlFlow(stats)) {
        double duration=cfg.simTime-cfg.udpStart;
        double pdr=stats.txPackets?100.0*stats.rxPackets/stats.txPackets:0;
        double throughput=stats.rxBytes*8.0/(duration*1e6);
        std::cout<<"\n[UDP FINAL] Tx="<<stats.txPackets<<", Rx="<<stats.rxPackets
                 <<", PDR="<<pdr<<" %, Throughput="<<throughput<<" Mbps\n";
    }
    std::cout<<"[UDP SERVER] Received="<<DynamicCast<UdpServer>(serverApps.Get(0))->GetReceived()<<"\n";
    udp.monitor->SerializeToXmlFile("nr-a3-handover-flowmon.xml",true,true);
    std::string finalJson=BuildOutcomeJson(true);
    Simulator::Destroy();
    Publish("FINAL A3 HANDOVER OUTCOME",cfg.outcomePath,finalJson);
    std::cout<<"Simulation completed.\n";
    return 0;
}

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: