//! JSON-lines control protocol between PHP (`RustMediaEngine`) and this sidecar.
//!
//! Requests carry an `id` the reply echoes back; events have no `id` and are
//! pushed to every connected control client.

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
pub struct Request {
    pub id: u64,
    #[serde(flatten)]
    pub command: Command,
}

#[derive(Debug, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum Command {
    /// Liveness probe (also used by PHP to detect an already-running sidecar).
    Ping,
    /// Accept a browser SDP offer for `peer` in `room`; reply carries the answer.
    /// Further renegotiation (new tracks) rides the WebRTC data channel directly.
    AddPeer {
        room: String,
        peer: String,
        offer: String,
        #[serde(default)]
        record: Option<String>,
    },
    /// Kick a peer (disconnects its transport; emits `peer_left`).
    RemovePeer { room: String, peer: String },
    /// Server-side mute: stop forwarding the peer's audio to the room.
    MutePeer {
        room: String,
        peer: String,
        muted: bool,
    },
    /// Start recording a peer's inbound audio to `path` (Ogg/Opus).
    RecordStart {
        room: String,
        peer: String,
        path: String,
    },
    /// Stop and finalize a peer's recording.
    RecordStop { room: String, peer: String },
    /// Snapshot of rooms/peers (connection state, mute, connected seconds).
    Stats,
    /// Graceful stop.
    Shutdown,
}

#[derive(Debug, Serialize)]
pub struct Reply {
    pub id: u64,
    pub ok: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub answer: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rooms: Option<Vec<RoomStats>>,
}

impl Reply {
    pub fn ok(id: u64) -> Self {
        Reply {
            id,
            ok: true,
            answer: None,
            error: None,
            rooms: None,
        }
    }

    pub fn answer(id: u64, sdp: String) -> Self {
        Reply {
            answer: Some(sdp),
            ..Reply::ok(id)
        }
    }

    pub fn error(id: u64, error: impl Into<String>) -> Self {
        Reply {
            ok: false,
            error: Some(error.into()),
            ..Reply::ok(id)
        }
    }

    pub fn stats(id: u64, rooms: Vec<RoomStats>) -> Self {
        Reply {
            rooms: Some(rooms),
            ..Reply::ok(id)
        }
    }
}

#[derive(Debug, Serialize)]
pub struct RoomStats {
    pub room: String,
    pub peers: Vec<PeerStats>,
}

#[derive(Debug, Serialize)]
pub struct PeerStats {
    pub peer: String,
    pub connected: bool,
    pub muted: bool,
    pub seconds: f64,
}

/// Server-pushed notifications (no `id`).
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum Notice {
    PeerConnected { room: String, peer: String },
    PeerLeft { room: String, peer: String, seconds: f64 },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_add_peer() {
        let req: Request = serde_json::from_str(
            r#"{"id":7,"cmd":"add_peer","room":"r1","peer":"p1","offer":"v=0...","record":"/tmp/rec/p1.ogg"}"#,
        )
        .unwrap();
        assert_eq!(req.id, 7);
        match req.command {
            Command::AddPeer {
                room,
                peer,
                offer,
                record,
            } => {
                assert_eq!(room, "r1");
                assert_eq!(peer, "p1");
                assert_eq!(offer, "v=0...");
                assert_eq!(record.as_deref(), Some("/tmp/rec/p1.ogg"));
            }
            other => panic!("wrong command: {other:?}"),
        }
    }

    #[test]
    fn record_defaults_to_none() {
        let req: Request =
            serde_json::from_str(r#"{"id":1,"cmd":"add_peer","room":"r","peer":"p","offer":"x"}"#)
                .unwrap();
        match req.command {
            Command::AddPeer { record, .. } => assert!(record.is_none()),
            other => panic!("wrong command: {other:?}"),
        }
    }

    #[test]
    fn reply_serializes_compactly() {
        let json = serde_json::to_string(&Reply::answer(3, "v=0".into())).unwrap();
        assert_eq!(json, r#"{"id":3,"ok":true,"answer":"v=0"}"#);

        let json = serde_json::to_string(&Reply::error(4, "no such peer")).unwrap();
        assert_eq!(json, r#"{"id":4,"ok":false,"error":"no such peer"}"#);
    }

    #[test]
    fn notice_has_event_tag() {
        let json = serde_json::to_string(&Notice::PeerLeft {
            room: "r".into(),
            peer: "p".into(),
            seconds: 1.5,
        })
        .unwrap();
        assert_eq!(
            json,
            r#"{"event":"peer_left","room":"r","peer":"p","seconds":1.5}"#
        );
    }
}
