//! Typed Socket.IO events: the primary way to send and receive data.
//!
//! In Socket.IO, every message is an **event**: a named JSON array where the
//! first element is the event name and the rest are the payload fields:
//!
//! ```json
//! ["chat_message", "alice", "hello world"]
//! ```
//!
//! The [`EventType`] trait (derived with `#[derive(EventType)]`) maps a Rust
//! struct to this wire format.  [`Event`] wraps an inbound event with
//! compile-time policy markers for ack and binary handling.  The [`EventHandler`]
//! trait constructs typed events from raw parts and powers the `EventRouter`
//! derive macro for multi-event dispatch enums.
//!
//! Outbound events are sent directly via [`SocketSender::emit`](crate::client::SocketSender::emit);
//! blanket [`Emit`] impls handle serialization automatically for both
//! plain events and binary closures.
//!
//! The generic parameters `A` ([`AckMarker`]) and `B` ([`BinaryMarker`])
//! let the type system enforce whether a packet carries binary attachments
//! or expects an acknowledgement; no runtime checks needed.

use crate::ack::{AckHandle, AckType};
use crate::binary::AttachmentsBuilder;
use crate::client::Emit;
use crate::error::{EventError, PayloadError};
use crate::marker::{AckMarker, BinaryMarker, HasAck, HasBinary, NoAck, NoBinary};
use crate::packet::{Directive, DynEvent};
use crate::payload::{DeserializePayload, SerializePayload, event_from_json, event_to_json};
use bytes::Bytes;
use tokio::sync::oneshot;

/// Maps a Rust struct to a Socket.IO event name and compile-time policies.
///
/// Prefer `#[derive(EventType)]` over a manual implementation.  The derive
/// generates `NAME` and the associated types.  Add `#[derive(SerializePayload)]`
/// for emit and `#[derive(DeserializePayload)]` for recv.
pub trait EventType: Sized {
    /// The Socket.IO event name used as the first element of the wire array.
    const NAME: &'static str;

    /// Ack policy: [`NoAck`] or [`HasAck<A>`](crate::marker::HasAck).
    type Ack: AckMarker;

    /// Binary policy: [`NoBinary`] or [`HasBinary`].
    type Binary: BinaryMarker;
}

/// A typed inbound event with compile-time ack and binary policy markers.
///
/// Construct via [`TryFrom<DynEvent>`] after receiving a [`DynEvent`]
/// from the namespace channel. The ack and binary policies are determined
/// by the associated types on `E`.
pub struct Event<E>
where
    E: EventType,
{
    /// The deserialized event payload.
    pub payload: E,
    /// Ack ID (populated only when `E::Ack` = [`HasAck`]).
    pub id: <E::Ack as AckMarker>::Id,
    /// Binary attachments (populated only when `E::Binary` = [`HasBinary`]).
    pub attachments: <E::Binary as BinaryMarker>::Attachments,
}

impl<E> std::fmt::Debug for Event<E>
where
    E: std::fmt::Debug + EventType,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut map = f.debug_map();
        map.entry(&"payload", &self.payload);
        E::Ack::format(&self.id, &mut map);
        E::Binary::format(&self.attachments, &mut map);
        map.finish()
    }
}

impl<E> TryFrom<DynEvent> for Event<E>
where
    E: EventType + DeserializePayload,
{
    type Error = EventError;

    fn try_from(value: DynEvent) -> Result<Self, EventError> {
        let payload = event_from_json(&value.payload)?;
        let id = E::Ack::parse(value.id)?;
        let attachments = E::Binary::parse(value.attachments)?;
        Ok(Self {
            payload,
            id,
            attachments,
        })
    }
}

/// Constructs a typed event from raw parts; used by the `EventRouter` derive macro.
pub trait EventHandler: Sized {
    /// The event type this handler processes.
    type Payload: EventType;

    /// Builds `Self` from a deserialized payload, raw ack ID, and raw attachments.
    ///
    /// # Errors
    ///
    /// Returns an error if the ack ID or attachment policy is violated, or deserialization fails.
    fn handle(
        payload: Self::Payload,
        id: Option<u64>,
        attachments: Option<Vec<Bytes>>,
    ) -> Result<Self, EventError>;
}

/// Supertrait for enums generated by `#[derive(EventRouter)]`.
///
/// Combines `TryFrom<DynEvent>` with a `name()` method that returns the
/// Socket.IO event name of the matched variant.
pub trait EventRouter: TryFrom<DynEvent, Error = EventError> {
    /// Returns the Socket.IO event name of the matched variant.
    fn name(&self) -> &'static str;
}

impl<E> EventHandler for Event<E>
where
    E: EventType + DeserializePayload,
{
    type Payload = E;

    fn handle(
        payload: Self::Payload,
        id: Option<u64>,
        attachments: Option<Vec<Bytes>>,
    ) -> Result<Self, EventError> {
        let id = E::Ack::parse(id)?;
        let attachments = E::Binary::parse(attachments)?;
        Ok(Self {
            payload,
            id,
            attachments,
        })
    }
}

impl<E> Emit<NoAck, NoBinary> for E
where
    E: EventType<Ack = NoAck, Binary = NoBinary> + SerializePayload,
{
    type Output = ();

    fn prepare(self) -> Result<(Directive, ()), PayloadError> {
        let payload = event_to_json(&self)?;

        Ok((
            Directive::Event {
                payload: payload.into(),
                tx: None,
                attachments: None,
            },
            (),
        ))
    }
}

impl<E, A> Emit<HasAck<A>, NoBinary> for E
where
    E: EventType<Ack = HasAck<A>, Binary = NoBinary> + SerializePayload,
    A: AckType,
{
    type Output = AckHandle<A>;

    fn prepare(self) -> Result<(Directive, AckHandle<A>), PayloadError> {
        let (tx, rx) = oneshot::channel();
        let payload = event_to_json(&self)?.into();
        Ok((
            Directive::Event {
                payload,
                tx: Some(tx),
                attachments: None,
            },
            AckHandle::new(rx),
        ))
    }
}

impl<F, E> Emit<NoAck, HasBinary> for F
where
    F: FnOnce(&mut AttachmentsBuilder) -> E,
    E: EventType<Ack = NoAck, Binary = HasBinary> + SerializePayload,
{
    type Output = ();

    fn prepare(self) -> Result<(Directive, ()), PayloadError> {
        let mut builder = AttachmentsBuilder::new();
        let payload = event_to_json(&self(&mut builder))?.into();
        Ok((
            Directive::Event {
                payload,
                tx: None,
                attachments: Some(builder.finish()),
            },
            (),
        ))
    }
}

impl<F, E, A> Emit<HasAck<A>, HasBinary> for F
where
    F: FnOnce(&mut AttachmentsBuilder) -> E,
    E: EventType<Ack = HasAck<A>, Binary = HasBinary> + SerializePayload,
    A: AckType,
{
    type Output = AckHandle<A>;

    fn prepare(self) -> Result<(Directive, AckHandle<A>), PayloadError> {
        let (tx, rx) = oneshot::channel();
        let mut builder = AttachmentsBuilder::new();
        let payload = event_to_json(&self(&mut builder))?.into();
        Ok((
            Directive::Event {
                payload,
                tx: Some(tx),
                attachments: Some(builder.finish()),
            },
            AckHandle::new(rx),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::binary::AttachmentsBuilder;
    use crate::client::Emit;
    use crate::error::{AckIdError, AttachmentsError, EventError};
    use crate::marker::{HasAck, HasBinary, NoAck, NoBinary};
    use crate::packet::DynEvent;
    use crate::payload::{DeserializePayload, SerializePayload};
    use bytes::Bytes;
    use bytestring::ByteString;

    fn bss(s: &'static str) -> ByteString {
        ByteString::from_static(s)
    }

    // Proc-macro paths resolve as `::sioc::` and don't work inside this crate,
    // so test event types are defined with a declarative macro instead.
    macro_rules! test_event {
        ($name:ident, ack = $ack:ty, binary = $binary:ty) => {
            #[derive(Debug, PartialEq)]
            struct $name;
            impl EventType for $name {
                const NAME: &'static str = "ping";
                type Ack = $ack;
                type Binary = $binary;
            }
            impl DeserializePayload for $name {
                fn deserialize_payload<'de, S>(seq: &mut S) -> Result<Self, S::Error>
                where
                    S: serde::de::SeqAccess<'de>,
                {
                    while let Some(serde::de::IgnoredAny) = seq.next_element()? {}
                    Ok($name)
                }
            }
            impl SerializePayload for $name {
                fn serialize_payload<S>(&self, _seq: &mut S) -> Result<(), S::Error>
                where
                    S: serde::ser::SerializeSeq,
                {
                    Ok(())
                }
            }
        };
    }

    test_event!(Ping, ack = NoAck, binary = NoBinary);
    test_event!(PingWithAck, ack = HasAck<()>, binary = NoBinary);
    test_event!(PingWithBinary, ack = NoAck, binary = HasBinary);
    test_event!(PingWithAckAndBinary, ack = HasAck<()>, binary = HasBinary);

    fn ping_event(id: Option<u64>, attachments: Option<Vec<Bytes>>) -> DynEvent {
        DynEvent {
            payload: bss(r#"["ping"]"#),
            id,
            attachments,
        }
    }

    #[test]
    fn try_from_with_ack_and_binary_succeeds() {
        let att = vec![Bytes::from_static(b"\xFF")];
        let ev: Event<PingWithAckAndBinary> = DynEvent {
            payload: bss(r#"["ping"]"#),
            id: Some(1),
            attachments: Some(att),
        }
        .try_into()
        .unwrap();
        assert_eq!(ev.id.get(), 1);
    }

    #[test]
    fn try_from_basic_event_succeeds() {
        let ev: Event<Ping> = ping_event(None, None).try_into().unwrap();
        assert_eq!(ev.payload, Ping);
    }

    #[test]
    fn try_from_wrong_name_fails() {
        let dyn_ev = DynEvent {
            payload: bss(r#"["pong"]"#),
            id: None,
            attachments: None,
        };
        assert!(matches!(
            Event::<Ping>::try_from(dyn_ev),
            Err(EventError::Payload(_))
        ));
    }

    #[test]
    fn try_from_unexpected_ack_id_fails() {
        assert!(matches!(
            Event::<Ping>::try_from(ping_event(Some(1), None)),
            Err(EventError::AckId(AckIdError::Unexpected))
        ));
    }

    #[test]
    fn try_from_missing_ack_id_fails() {
        assert!(matches!(
            Event::<PingWithAck>::try_from(ping_event(None, None)),
            Err(EventError::AckId(AckIdError::Missing))
        ));
    }

    #[test]
    fn try_from_with_ack_id_succeeds() {
        let ev: Event<PingWithAck> = ping_event(Some(5), None).try_into().unwrap();
        assert_eq!(ev.id.get(), 5);
    }

    #[test]
    fn try_from_unexpected_attachments_fails() {
        assert!(matches!(
            Event::<Ping>::try_from(ping_event(None, Some(vec![Bytes::from_static(b"x")]))),
            Err(EventError::Attachments(AttachmentsError::Unexpected))
        ));
    }

    #[test]
    fn try_from_missing_attachments_fails() {
        assert!(matches!(
            Event::<PingWithBinary>::try_from(ping_event(None, None)),
            Err(EventError::Attachments(AttachmentsError::Missing))
        ));
    }

    #[test]
    fn try_from_with_attachments_succeeds() {
        let att = vec![Bytes::from_static(b"\xFF")];
        let ev: Event<PingWithBinary> = ping_event(None, Some(att)).try_into().unwrap();
        assert_eq!(ev.attachments.len(), 1);
    }

    #[test]
    fn event_handler_handle_succeeds() {
        let ev = Event::<Ping>::handle(Ping, None, None).unwrap();
        assert_eq!(ev.payload, Ping);
    }

    #[test]
    fn event_handler_handle_bad_ack_fails() {
        Event::<Ping>::handle(Ping, Some(1), None).unwrap_err();
    }

    #[test]
    fn emit_no_ack_no_binary_prepare() {
        let (directive, ()) = Ping.prepare().unwrap();
        let Directive::Event {
            payload,
            tx,
            attachments,
        } = directive
        else {
            panic!("expected Event directive");
        };
        assert_eq!(&payload[..], r#"["ping"]"#);
        assert!(tx.is_none());
        assert!(attachments.is_none());
    }

    #[test]
    fn emit_has_ack_no_binary_prepare() {
        let (directive, _handle) = PingWithAck.prepare().unwrap();
        let Directive::Event {
            payload,
            tx,
            attachments,
        } = directive
        else {
            panic!("expected Event directive");
        };
        assert_eq!(&payload[..], r#"["ping"]"#);
        assert!(tx.is_some());
        assert!(attachments.is_none());
    }

    #[test]
    fn emit_no_ack_has_binary_prepare() {
        let closure = |builder: &mut AttachmentsBuilder| {
            let _p = builder.attach(Bytes::from_static(b"\xFF"));
            PingWithBinary
        };
        let (directive, ()) = closure.prepare().unwrap();
        let Directive::Event {
            payload,
            tx,
            attachments,
        } = directive
        else {
            panic!("expected Event directive");
        };
        assert_eq!(&payload[..], r#"["ping"]"#);
        assert!(tx.is_none());
        let att = attachments.expect("expected attachments");
        assert_eq!(att.len(), 1);
    }

    #[test]
    fn debug_no_ack_no_binary() {
        let ev: Event<Ping> = ping_event(None, None).try_into().unwrap();
        let s = format!("{ev:?}");
        assert!(s.contains("payload"));
    }

    #[test]
    fn debug_has_ack_no_binary() {
        let ev: Event<PingWithAck> = ping_event(Some(3), None).try_into().unwrap();
        let s = format!("{ev:?}");
        assert!(s.contains("id"));
    }

    #[test]
    fn debug_no_ack_has_binary() {
        let att = vec![Bytes::from_static(b"\xFF")];
        let ev: Event<PingWithBinary> = ping_event(None, Some(att)).try_into().unwrap();
        let s = format!("{ev:?}");
        assert!(s.contains("count"));
    }

    #[test]
    fn emit_has_ack_has_binary_prepare() {
        let closure = |builder: &mut AttachmentsBuilder| {
            let _p = builder.attach(Bytes::from_static(b"\xFF"));
            PingWithAckAndBinary
        };
        let (directive, _handle) = closure.prepare().unwrap();
        let Directive::Event {
            payload,
            tx,
            attachments,
        } = directive
        else {
            panic!("expected Event directive");
        };
        assert_eq!(&payload[..], r#"["ping"]"#);
        assert!(tx.is_some());
        let att = attachments.expect("expected attachments");
        assert_eq!(att.len(), 1);
    }
}
