package com.foxx.androidcast.network;

/*********************************************************************
 * CastSession.java
 * Created at: Fri 15 May 2026 16:49:02 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CastSession
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (9 commits, 207 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	dbf1f38a83f982eeae65310c6c91e22b86fcee043fdee0b33260a400e8331025
**********************************************************************/
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.CodecNegotiator;
import com.foxx.androidcast.media.CodecPriorityCatalog;
import com.foxx.androidcast.media.codec.PassthroughCodecPolicy;
import com.foxx.androidcast.sender.CodecCatalog;
import com.foxx.androidcast.stats.SessionStatsContext;

import java.io.IOException;
import java.util.List;

/** Auth handshake and message I/O over a {@link CastTransport}. */
public class CastSession {
    public static final class HandshakeResult {
        public final String senderName;
        /** Sender cast settings from handshake. */
        public final CastSettings settings;
        /** Receiver advertisement ({@code AUTO} = auto-detected FEC capability). */
        public final CastSettings receiverAdvertisement;
        public final String negotiatedVideoMime;

        public HandshakeResult(String senderName, CastSettings settings,
                CastSettings receiverAdvertisement, String negotiatedVideoMime) {
            this.senderName = senderName;
            this.settings = settings;
            this.receiverAdvertisement = receiverAdvertisement;
            this.negotiatedVideoMime = negotiatedVideoMime;
        }
    }

    private final CastTransport transport;

    public CastSession(CastTransport transport) {
        this.transport = transport;
    }

    public CastTransport getTransport() {
        return transport;
    }

    public void listen(int port) throws IOException {
        transport.listen(port);
    }

    public void prepareListen(int port) throws IOException {
        transport.prepareListen(port);
    }

    public void acceptClient() throws IOException {
        transport.acceptClient();
    }

    public void connect(String host, int port) throws IOException {
        transport.connect(host, port);
    }

    public HandshakeResult clientHandshake(String deviceName, String pin, CastSettings settings) throws IOException {
        byte[] hello = CastProtocol.helloPayload(deviceName);
        transport.send(CastProtocol.MSG_HELLO, hello);
        recordOutbound(CastProtocol.MSG_HELLO, hello);
        byte[] auth = CastProtocol.authPayload(pin);
        transport.send(CastProtocol.MSG_AUTH, auth);
        recordOutbound(CastProtocol.MSG_AUTH, auth);
        CastProtocol.Message reply = receive(10_000);
        if (reply == null || reply.type != CastProtocol.MSG_AUTH_OK) {
            throw new IOException("Authentication failed");
        }
        byte[] castSettings = CastProtocol.castSettingsPayload(settings);
        transport.send(CastProtocol.MSG_CAST_SETTINGS, castSettings);
        recordOutbound(CastProtocol.MSG_CAST_SETTINGS, castSettings);
        byte[] codecCaps = CastProtocol.codecCapsPayload(CodecCatalog.localEncoderMimes());
        transport.send(CastProtocol.MSG_CODEC_CAPS, codecCaps);
        recordOutbound(CastProtocol.MSG_CODEC_CAPS, codecCaps);

        String negotiated = null;
        CastSettings receiverAdvertisement = null;
        long deadline = System.currentTimeMillis() + 15_000;
        while (System.currentTimeMillis() < deadline) {
            CastProtocol.Message msg = receive(2_000);
            if (msg == null) {
                continue;
            }
            if (msg.type == CastProtocol.MSG_RECEIVER_CAST_SETTINGS) {
                receiverAdvertisement = CastProtocol.parseCastSettings(msg.payload);
            } else if (msg.type == CastProtocol.MSG_CODEC_SELECTED) {
                negotiated = CastProtocol.parseCodecSelected(msg.payload);
                break;
            }
        }
        if (negotiated == null) {
            throw new IOException("Codec negotiation timeout");
        }
        settings.setNegotiatedVideoMime(negotiated);
        CodecPriorityCatalog.logReceivedCodec("sender", negotiated, settings);
        return new HandshakeResult(deviceName, settings, receiverAdvertisement, negotiated);
    }

    public HandshakeResult serverHandshake(String expectedPin, CastSettings localSettings) throws IOException {
        String senderName = "Sender";
        boolean authed = false;
        CastSettings remote = null;
        List<String> senderCaps = null;
        long deadline = System.currentTimeMillis() + 30_000;
        while (System.currentTimeMillis() < deadline) {
            CastProtocol.Message msg = receive(2_000);
            if (msg == null) {
                continue;
            }
            switch (msg.type) {
                case CastProtocol.MSG_HELLO:
                    senderName = CastProtocol.payloadAsUtf8(msg.payload);
                    break;
                case CastProtocol.MSG_AUTH:
                    if (CastProtocol.pinMatches(msg.payload, expectedPin)) {
                        authed = true;
                        transport.send(CastProtocol.MSG_AUTH_OK, new byte[0]);
                        recordOutbound(CastProtocol.MSG_AUTH_OK, new byte[0]);
                    } else {
                        transport.send(CastProtocol.MSG_AUTH_FAIL, new byte[0]);
                        recordOutbound(CastProtocol.MSG_AUTH_FAIL, new byte[0]);
                        throw new IOException("Bad PIN");
                    }
                    break;
                case CastProtocol.MSG_CAST_SETTINGS:
                    remote = CastProtocol.parseCastSettings(msg.payload);
                    if (!CastProtocol.transportMatches(localSettings, remote)) {
                        throw new IOException("Transport mismatch: receiver="
                                + localSettings.getTransport() + " sender=" + remote.getTransport());
                    }
                    break;
                case CastProtocol.MSG_CODEC_CAPS:
                    senderCaps = CastProtocol.parseCodecCaps(msg.payload);
                    break;
                default:
                    break;
            }
            if (authed && remote != null && senderCaps != null) {
                CastSettings receiverAdvert = new CastSettings();
                receiverAdvert.copyFrom(localSettings);
                receiverAdvert.setStreamProtection(
                        com.foxx.androidcast.network.transport.StreamProtectionCapability
                                .handshakeAdvertisement());
                byte[] recvSettings = CastProtocol.castSettingsPayload(receiverAdvert);
                transport.send(CastProtocol.MSG_RECEIVER_CAST_SETTINGS, recvSettings);
                recordOutbound(CastProtocol.MSG_RECEIVER_CAST_SETTINGS, recvSettings);
                List<String> receiverCaps = CodecCatalog.localDecoderMimes();
                byte[] recvCaps = CastProtocol.codecCapsPayload(receiverCaps);
                transport.send(CastProtocol.MSG_CODEC_CAPS, recvCaps);
                recordOutbound(CastProtocol.MSG_CODEC_CAPS, recvCaps);
                String mime = CodecNegotiator.negotiate(senderCaps, receiverCaps,
                        PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote, "receiver");
                byte[] selected = CastProtocol.codecSelectedPayload(mime);
                transport.send(CastProtocol.MSG_CODEC_SELECTED, selected);
                recordOutbound(CastProtocol.MSG_CODEC_SELECTED, selected);
                remote.setNegotiatedVideoMime(mime);
                return new HandshakeResult(senderName, remote, receiverAdvert, mime);
            }
        }
        throw new IOException("Handshake timeout");
    }

    public void send(byte type, byte[] payload) throws IOException {
        transport.send(type, payload);
    }

    private static void recordOutbound(byte type, byte[] payload) {
        int size = payload != null ? payload.length : 0;
        SessionStatsContext.recordOutboundMessage(type, false, size);
    }

    private static void recordInbound(CastProtocol.Message msg) {
        if (msg == null) {
            return;
        }
        boolean keyFrame = false;
        if (msg.type == CastProtocol.MSG_VIDEO_FRAME) {
            try {
                keyFrame = CastProtocol.parseVideoFrame(msg.payload).keyFrame;
            } catch (IOException ignored) {
            }
        }
        int size = msg.payload != null ? msg.payload.length : 0;
        SessionStatsContext.recordInboundMessage(msg.type, keyFrame, size);
    }

    public CastProtocol.Message receive(int timeoutMs) throws IOException {
        CastProtocol.Message msg = transport.receive(timeoutMs);
        recordInbound(msg);
        return msg;
    }

    public void releaseConnection() {
        transport.releaseConnection();
    }

    public void close() {
        transport.close();
    }
}
