#include "network/link.h"

#include <unordered_map>
#include <memory>
#include <cmath>

namespace dofs {

// If gbps <= 0, return a very large sentinel duration (treated as "can't send").
Time Link::serialization_time(Bytes bytes, double gbps) noexcept {
        if (gbps <= 0.0) {
                return Time::from_ns(UINT64_C(0xFFFFFFFFFFFFFFFF));
        }

        const double ns = std::ceil(static_cast<double>(bytes) * 8.0 / gbps);
        return Time::from_ns(static_cast<uint64_t>(ns));
}

Link::Link(Simulator *const sim,
           LinkId id,
           NetworkNode *a, PortId a_port,
           NetworkNode *b, PortId b_port,
           Time latency,
           double bandwidth_gbps) noexcept
        : _sim(sim),
          _id(id),
          _a(a), _b(b),
          _a_id(a->id()),
          _b_id(b->id()),
          _a_port(a_port), _b_port(b_port),
          _latency_init(latency),
          _bandwidth_gbps_init(bandwidth_gbps),
          _latency_cur(latency),
          _bandwidth_gbps_cur(bandwidth_gbps),
          _status(LinkStatus::OK),
          _next_available_ab(Time(0)),
          _next_available_ba(Time(0)) {}

// Map sender to a direction; return nullopt if not an endpoint.
std::optional<Link::Dir> Link::_dir_from(NodeId sender) const noexcept {
        if (sender == _a_id)
                return Dir::AtoB;

        if (sender == _b_id)
                return Dir::BtoA;

        return std::nullopt;
}

Time Link::next_available(NodeId sender) const noexcept {
        auto d = _dir_from(sender);

        if (!d.has_value()) {
                // Unknown sender; be defensive: report "now" so caller won't stall forever.
                // The caller (switch) should treat this as a logic error.
                return _sim ? _sim->now() : Time(0);
        }

        return (d.value() == Dir::AtoB) ? _next_available_ab : _next_available_ba;
}

std::optional<Link::Reservation> Link::reserve(Bytes bytes,
                NodeId sender) noexcept {
        if (_status == LinkStatus::DOWN) {
                return std::nullopt;
        }

        if (!_sim) {
                log_error("ERROR", "Link: simulator instance not found");
                return std::nullopt;
        }

        auto d = _dir_from(sender);

        if (!d.has_value()) {
                log_error("ERROR", "Link::reserve called by non-endpoint node");
                return std::nullopt;
        }

        Time& cursor = (d.value() == Dir::AtoB) ? _next_available_ab :
                       _next_available_ba;

        const Time now = _sim->now();
        const Time start = (cursor > now) ? cursor : now;
        const Time ser = serialization_time(bytes, _bandwidth_gbps_cur);
        const Time finish = start + ser;

        cursor = finish;

        return Reservation{start, finish};
}

// --- Legacy path: send + schedule internally via the link ---
// This keeps existing callers working while the switch migrates to reserve().
void Link::send_pkt(Packet &pkt, NodeId caller) {
        if (_status == LinkStatus::DOWN) {
                // Drop silently
                return;
        }

        if (!_sim) {
                log_error("ERROR", "Link: simulator instance not found");
                return;
        }

        auto d = _dir_from(caller);

        if (!d.has_value()) {
                log_error("ERROR", "Link::send_pkt called by non-endpoint node");
                return;
        }

        auto rsv = reserve(pkt.total_size(), caller);

        if (!rsv.has_value()) {
                // DOWN or invalid sender; nothing to do.
                return;
        }

        const Time now = _sim->now();
        const Time to_finish = (rsv->finish > now) ?
                               rsv->finish.unsafe_sub(now) : Time(0);
        const Time total_delay = to_finish + _latency_cur;

        NetworkNode* target_node = (d.value() == Dir::AtoB) ? _b : _a;
        PortId target_ingress = (d.value() == Dir::AtoB) ? _b_port : _a_port;

        _sim->schedule_after(total_delay,
        [target_node, target_ingress, p = pkt]() mutable {
                target_node->recv_pkt(p, target_ingress);
        });
}

void Link::schedule_delivery_after(Packet &pkt, NodeId caller, Time after) {
        if (_status == LinkStatus::DOWN || !_sim)
                return;

        auto d = _dir_from(caller);

        if (!d.has_value()) {
                log_error("ERROR", "Link::schedule_delivery_after called by non-endpoint node");
                return;
        }

        NetworkNode* target_node = (d.value() == Dir::AtoB) ? _b : _a;
        PortId target_ingress = (d.value() == Dir::AtoB) ? _b_port : _a_port;

        _sim->schedule_after(after,
        [target_node, target_ingress, p = pkt]() mutable {
                target_node->recv_pkt(p, target_ingress);
        });
}

void Link::set_status(LinkStatus s, Time new_latency,
                      double new_bandwidth_gbps) {
        switch (s) {
        case LinkStatus::OK:
                _status = LinkStatus::OK;
                _latency_cur = _latency_init;
                _bandwidth_gbps_cur = _bandwidth_gbps_init;
                return;

        case LinkStatus::DOWN:
                _status = LinkStatus::DOWN;
                return;

        case LinkStatus::THROTTLING:
                if (new_latency < _latency_init ||
                    new_bandwidth_gbps > _bandwidth_gbps_init) {
                        log_error("ERROR",
                                  "Link::set_status(THROTTLING) ignored: invalid latency/bandwidth");
                        return;
                }

                _status = LinkStatus::THROTTLING;
                _latency_cur = new_latency;
                _bandwidth_gbps_cur = new_bandwidth_gbps;
                return;
        }
}

} // namespace dofs
