package com.foxx.androidcast.network.control;

/*********************************************************************
 * PassThroughNetworkControlPlane.java
 * Created at: Fri 15 May 2026 17:47:09 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class PassThroughNetworkControlPlane
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (7 commits, 143 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	55fde627b9a8bb3fc5c27b4e3d5f7c79760d4037f477d4a578f3699b6349fb6e
**********************************************************************/
import com.foxx.androidcast.CastConfig;

/**
 * Default no-op controller: emits heartbeat stats, ignores congestion, never switches transport.
 * Swap this class for smart logic without changing protocol or session code.
 */
public class PassThroughNetworkControlPlane implements NetworkControlPlane {
    private static final long STATS_INTERVAL_MS = 2_000;

    private long sequence;
    private long lastStatsMs;
    private NetworkStatsSnapshot lastPeer;
    private final NetworkControlAdvice outbound = NetworkControlAdvice.none();
    private long statsWindowBytes;
    private long statsWindowStartMs;
    private long sendWindowBytes;
    private long sendWindowStartMs;
    private StatsEnricher statsEnricher;
    private volatile int diagPingRttMs;

    /** Optional hook to attach encode/decode metrics before stats are sent. */
    public interface StatsEnricher {
        void enrich(NetworkStatsSnapshot snapshot, long nowMs);
    }

    public void setStatsEnricher(StatsEnricher enricher) {
        this.statsEnricher = enricher;
    }

    public void setDiagPingRttMs(int rttMs) {
        if (rttMs > 0 && rttMs < 30_000) {
            diagPingRttMs = rttMs;
        }
    }

    public int getDiagPingRttMs() {
        return diagPingRttMs;
    }

    @Override
    public NetworkControlAdvice tick(long nowMs) {
        outbound.sendStats = nowMs - lastStatsMs >= STATS_INTERVAL_MS;
        outbound.sendControl = false;
        outbound.suggestedBitrateKbps = 0;
        outbound.suggestedTransport = CastConfig.DEFAULT_TRANSPORT;
        outbound.switchTransport = false;
        outbound.congestionLevel = 0;
        if (outbound.sendStats) {
            lastStatsMs = nowMs;
        }
        return outbound;
    }

    public NetworkStatsSnapshot getLastPeerStats() {
        return lastPeer != null ? lastPeer.copy() : null;
    }

    @Override
    public void onPeerStats(NetworkStatsSnapshot stats) {
        lastPeer = stats != null ? stats.copy() : null;
        if (lastPeer != null && lastPeer.timestampMs > 0) {
            long rtt = System.currentTimeMillis() - lastPeer.timestampMs;
            if (rtt > 0 && rtt < 10_000) {
                lastPeer.rttMs = (int) rtt;
            }
        }
    }

    /** Record inbound (receiver) or outbound (sender) payload bytes for bitrate stats. */
    public void recordBytes(int bytes) {
        long now = System.currentTimeMillis();
        if (statsWindowStartMs == 0) {
            statsWindowStartMs = now;
        }
        statsWindowBytes += bytes;
    }

    public void recordSendBytes(int bytes) {
        long now = System.currentTimeMillis();
        if (sendWindowStartMs == 0) {
            sendWindowStartMs = now;
        }
        sendWindowBytes += bytes;
    }

    @Override
    public NetworkStatsSnapshot buildLocalStats(long nowMs) {
        NetworkStatsSnapshot s = new NetworkStatsSnapshot();
        s.sequence = ++sequence;
        s.timestampMs = nowMs;
        if (diagPingRttMs > 0) {
            s.rttMs = diagPingRttMs;
        } else {
            s.rttMs = lastPeer != null ? lastPeer.rttMs : 0;
        }
        s.lossRatio = 0f;
        s.sendBitrateKbps = estimateSendKbps(nowMs);
        s.recvBitrateKbps = estimateRecvKbps(nowMs);
        s.congestionLevel = 0;
        s.jitterMs = 0;
        if (statsEnricher != null) {
            statsEnricher.enrich(s, nowMs);
        }
        return s;
    }

    private int estimateSendKbps(long nowMs) {
        if (sendWindowStartMs == 0 || sendWindowBytes == 0) {
            return 0;
        }
        long elapsed = nowMs - sendWindowStartMs;
        if (elapsed < 200) {
            return 0;
        }
        int kbps = (int) (sendWindowBytes * 8L / elapsed);
        if (elapsed > 4000) {
            sendWindowStartMs = nowMs;
            sendWindowBytes = 0;
        }
        return kbps;
    }

    private int estimateRecvKbps(long nowMs) {
        if (statsWindowStartMs == 0 || statsWindowBytes == 0) {
            return 0;
        }
        long elapsed = nowMs - statsWindowStartMs;
        if (elapsed < 200) {
            return 0;
        }
        int kbps = (int) (statsWindowBytes * 8L / elapsed);
        if (elapsed > 4000) {
            statsWindowStartMs = nowMs;
            statsWindowBytes = 0;
        }
        return kbps;
    }

    @Override
    public void onPeerControl(int suggestedBitrateKbps, String suggestedTransport,
                              boolean switchTransport, int congestionLevel) {
        // Pass-through: no local adjustments yet.
    }

    @Override
    public NetworkControlAdvice getOutboundAdvice() {
        return outbound;
    }
}
