package com.foxx.androidcast.stats.ui;

/*********************************************************************
 * SessionRecord.java
 * Created at: Tue 19 May 2026 19:43:06 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class SessionRecord
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 235 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	851bf289ce0cfb533f555e8101cf087211cae6d9f65f3b76130cb025556afaa0
**********************************************************************/
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/** Parsed session JSON for the in-app statistics analyzer. */
public final class SessionRecord {
    public final String fileName;
    public final int schemaVersion;
    public final long startedAtEpochMs;
    public final long endedAtEpochMs;
    public final long durationMs;
    /** {@code send} or {@code recv}; inferred from filename when absent. */
    public final String direction;
    public final String role;
    public final String transport;
    public final int clientCount;
    public final int disruptCount;
    public final int idleTimeoutCount;
    public final String videoCodec;
    public final String audioCodec;
    public final String captureMode;
    public final String resolution;
    public final int networkSamples;
    public final long avgRttMs;
    public final double avgLossRatio;
    public final long avgVideoBitrateKbps;
    public final double avgEncodeFps;
    public final ProtectionStats protection;
    public final UdpStats udp;
    public final List<SamplePoint> samples;

    public SessionRecord(String fileName, JSONObject root) {
        this.fileName = fileName;
        schemaVersion = root.optInt("schema_version", 0);
        startedAtEpochMs = root.optLong("started_at_epoch_ms", 0L);
        endedAtEpochMs = root.optLong("ended_at_epoch_ms", 0L);
        durationMs = root.optLong("duration_ms", 0L);
        direction = inferDirection(fileName, root);
        role = root.optString("role", direction.equals("recv") ? "receiver" : "sender");
        transport = root.optString("transport", "?");
        clientCount = root.optInt("client_count", 1);
        disruptCount = root.optInt("disrupt_count", 0);
        idleTimeoutCount = root.optInt("idle_timeout_count", 0);
        videoCodec = root.optString("video_codec", "");
        audioCodec = root.optString("audio_codec", "");
        captureMode = root.optString("capture_mode", "");
        resolution = root.optString("resolution", "");
        networkSamples = root.optInt("network_samples", 0);
        avgRttMs = root.optLong("avg_rtt_ms", 0L);
        avgLossRatio = root.optDouble("avg_loss_ratio", 0.0);
        avgVideoBitrateKbps = root.optLong("avg_video_bitrate_kbps", 0L);
        avgEncodeFps = root.optDouble("avg_encode_fps", 0.0);
        protection = ProtectionStats.fromJson(root.optJSONObject("stream_protection"));
        udp = UdpStats.fromJson(root.optJSONObject("udp"));
        samples = SamplePoint.parseArray(root.optJSONArray("samples"));
    }

    public boolean hasTimeSeries() {
        return samples.size() >= 2;
    }

    public String listSubtitle() {
        long min = durationMs / 60_000L;
        long sec = (durationMs / 1000L) % 60L;
        return direction + " · " + transport + " · " + min + "m " + sec + "s";
    }

    private static String inferDirection(String fileName, JSONObject root) {
        String d = root.optString("direction", "");
        if ("send".equals(d) || "recv".equals(d)) {
            return d;
        }
        if (fileName != null) {
            if (fileName.startsWith("session_send_")) {
                return "send";
            }
            if (fileName.startsWith("session_recv_")) {
                return "recv";
            }
        }
        String role = root.optString("role", "");
        return "receiver".equals(role) ? "recv" : "send";
    }

    public static final class SamplePoint {
        public final long tMs;
        public final double videoBitrateKbps;
        public final int rttMs;
        public final double lossRatio;
        public final double encodeFps;
        public final int targetBitrateKbps;
        public final int congestionLevel;

        public SamplePoint(long tMs, double videoBitrateKbps, int rttMs, double lossRatio,
                double encodeFps, int targetBitrateKbps, int congestionLevel) {
            this.tMs = tMs;
            this.videoBitrateKbps = videoBitrateKbps;
            this.rttMs = rttMs;
            this.lossRatio = lossRatio;
            this.encodeFps = encodeFps;
            this.targetBitrateKbps = targetBitrateKbps;
            this.congestionLevel = congestionLevel;
        }

        static SamplePoint fromJson(JSONObject o) {
            return new SamplePoint(
                    o.optLong("t_ms", 0L),
                    o.optDouble("video_bitrate_kbps", 0.0),
                    o.optInt("rtt_ms", 0),
                    o.optDouble("loss_ratio", 0.0),
                    o.optDouble("encode_fps", 0.0),
                    o.optInt("target_bitrate_kbps", 0),
                    o.optInt("congestion_level", 0));
        }

        static List<SamplePoint> parseArray(org.json.JSONArray arr) {
            if (arr == null || arr.length() == 0) {
                return Collections.emptyList();
            }
            List<SamplePoint> out = new ArrayList<>(arr.length());
            for (int i = 0; i < arr.length(); i++) {
                JSONObject o = arr.optJSONObject(i);
                if (o != null) {
                    out.add(fromJson(o));
                }
            }
            return out;
        }
    }

    public static final class ProtectionStats {
        public final String mode;
        public final boolean fecEnabled;
        public final boolean nackEnabled;
        public final long fecEncoded;
        public final long fecDecoded;
        public final long fecDecodeFailures;
        public final long nackSent;
        public final long nackReceived;
        public final long nackRetransmits;

        ProtectionStats(String mode, boolean fecEnabled, boolean nackEnabled, long fecEncoded,
                long fecDecoded, long fecDecodeFailures, long nackSent, long nackReceived,
                long nackRetransmits) {
            this.mode = mode != null ? mode : "NONE";
            this.fecEnabled = fecEnabled;
            this.nackEnabled = nackEnabled;
            this.fecEncoded = fecEncoded;
            this.fecDecoded = fecDecoded;
            this.fecDecodeFailures = fecDecodeFailures;
            this.nackSent = nackSent;
            this.nackReceived = nackReceived;
            this.nackRetransmits = nackRetransmits;
        }

        static ProtectionStats fromJson(JSONObject o) {
            if (o == null) {
                return new ProtectionStats("NONE", false, false, 0, 0, 0, 0, 0, 0);
            }
            return new ProtectionStats(
                    o.optString("mode", "NONE"),
                    o.optBoolean("fec_enabled", false),
                    o.optBoolean("nack_enabled", false),
                    o.optLong("fec_packets_encoded", 0L),
                    o.optLong("fec_packets_decoded", 0L),
                    o.optLong("fec_decode_failures", 0L),
                    o.optLong("nack_requests_sent", 0L),
                    o.optLong("nack_requests_received", 0L),
                    o.optLong("nack_retransmits", 0L));
        }
    }

    public static final class UdpStats {
        public final long datagramsTx;
        public final long datagramsRx;
        public final long datagramsInvalid;
        public final long fragmentsRx;
        public final long fragmentsDuplicate;
        public final long reassemblyOk;
        public final long reassemblyLost;
        public final long wireDropped;
        public final long malformed;
        public final long sendDroppedVideoP;
        public final long recvVideoDecodeErrors;
        public final long recvDamagedVideoFrames;

        UdpStats(long datagramsTx, long datagramsRx, long datagramsInvalid, long fragmentsRx,
                long fragmentsDuplicate, long reassemblyOk, long reassemblyLost, long wireDropped,
                long malformed, long sendDroppedVideoP, long recvVideoDecodeErrors,
                long recvDamagedVideoFrames) {
            this.datagramsTx = datagramsTx;
            this.datagramsRx = datagramsRx;
            this.datagramsInvalid = datagramsInvalid;
            this.fragmentsRx = fragmentsRx;
            this.fragmentsDuplicate = fragmentsDuplicate;
            this.reassemblyOk = reassemblyOk;
            this.reassemblyLost = reassemblyLost;
            this.wireDropped = wireDropped;
            this.malformed = malformed;
            this.sendDroppedVideoP = sendDroppedVideoP;
            this.recvVideoDecodeErrors = recvVideoDecodeErrors;
            this.recvDamagedVideoFrames = recvDamagedVideoFrames;
        }

        static UdpStats fromJson(JSONObject o) {
            if (o == null) {
                return new UdpStats(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
            }
            return new UdpStats(
                    o.optLong("datagrams_tx", 0L),
                    o.optLong("datagrams_rx", 0L),
                    o.optLong("datagrams_invalid", 0L),
                    o.optLong("fragments_rx", 0L),
                    o.optLong("fragments_duplicate", 0L),
                    o.optLong("reassembly_ok", 0L),
                    o.optLong("reassembly_lost", 0L),
                    o.optLong("wire_dropped", 0L),
                    o.optLong("malformed", 0L),
                    o.optLong("send_dropped_video_p", 0L),
                    o.optLong("recv_video_decode_errors", 0L),
                    o.optLong("recv_damaged_video_frames", 0L));
        }
    }
}
