//! Types for the [`m.room.redaction`] event.
//!
//! [`m.room.redaction`]: https://spec.matrix.org/latest/client-server-api/#mroomredaction

use as_variant::as_variant;
use js_int::Int;
#[cfg(feature = "canonical-json")]
use ruma_common::canonical_json::RedactionEvent;
use ruma_common::{
    serde::CanBeEmpty, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
    OwnedTransactionId, OwnedUserId, RoomId, RoomVersionId, UserId,
};
use ruma_macros::{Event, EventContent};
use serde::{Deserialize, Serialize};
use tracing::error;

use crate::{
    BundledMessageLikeRelations, EventContent, MessageLikeEventType, RedactContent,
    RedactedMessageLikeEventContent, RedactedUnsigned, StaticEventContent,
};

mod event_serde;

/// A possibly-redacted redaction event.
#[allow(clippy::exhaustive_enums)]
#[derive(Clone, Debug)]
pub enum RoomRedactionEvent {
    /// Original, unredacted form of the event.
    Original(OriginalRoomRedactionEvent),

    /// Redacted form of the event with minimal fields.
    Redacted(RedactedRoomRedactionEvent),
}

/// A possibly-redacted redaction event without a `room_id`.
#[allow(clippy::exhaustive_enums)]
#[derive(Clone, Debug)]
pub enum SyncRoomRedactionEvent {
    /// Original, unredacted form of the event.
    Original(OriginalSyncRoomRedactionEvent),

    /// Redacted form of the event with minimal fields.
    Redacted(RedactedSyncRoomRedactionEvent),
}

/// Redaction event.
#[derive(Clone, Debug)]
#[allow(clippy::exhaustive_structs)]
pub struct OriginalRoomRedactionEvent {
    /// Data specific to the event type.
    pub content: RoomRedactionEventContent,

    /// The ID of the event that was redacted.
    ///
    /// This field is required in room versions prior to 11.
    pub redacts: Option<OwnedEventId>,

    /// The globally unique event identifier for the user who sent the event.
    pub event_id: OwnedEventId,

    /// The fully-qualified ID of the user who sent this event.
    pub sender: OwnedUserId,

    /// Timestamp in milliseconds on originating homeserver when this event was sent.
    pub origin_server_ts: MilliSecondsSinceUnixEpoch,

    /// The ID of the room associated with this event.
    pub room_id: OwnedRoomId,

    /// Additional key-value pairs not signed by the homeserver.
    pub unsigned: RoomRedactionUnsigned,
}

impl From<OriginalRoomRedactionEvent> for OriginalSyncRoomRedactionEvent {
    fn from(value: OriginalRoomRedactionEvent) -> Self {
        let OriginalRoomRedactionEvent {
            content,
            redacts,
            event_id,
            sender,
            origin_server_ts,
            unsigned,
            ..
        } = value;

        Self { content, redacts, event_id, sender, origin_server_ts, unsigned }
    }
}

/// Redacted redaction event.
#[derive(Clone, Debug, Event)]
#[allow(clippy::exhaustive_structs)]
pub struct RedactedRoomRedactionEvent {
    /// Data specific to the event type.
    pub content: RedactedRoomRedactionEventContent,

    /// The globally unique event identifier for the user who sent the event.
    pub event_id: OwnedEventId,

    /// The fully-qualified ID of the user who sent this event.
    pub sender: OwnedUserId,

    /// Timestamp in milliseconds on originating homeserver when this event was sent.
    pub origin_server_ts: MilliSecondsSinceUnixEpoch,

    /// The ID of the room associated with this event.
    pub room_id: OwnedRoomId,

    /// Additional key-value pairs not signed by the homeserver.
    pub unsigned: RedactedUnsigned,
}

/// Redaction event without a `room_id`.
#[derive(Clone, Debug)]
#[allow(clippy::exhaustive_structs)]
pub struct OriginalSyncRoomRedactionEvent {
    /// Data specific to the event type.
    pub content: RoomRedactionEventContent,

    /// The ID of the event that was redacted.
    ///
    /// This field is required in room versions prior to 11.
    pub redacts: Option<OwnedEventId>,

    /// The globally unique event identifier for the user who sent the event.
    pub event_id: OwnedEventId,

    /// The fully-qualified ID of the user who sent this event.
    pub sender: OwnedUserId,

    /// Timestamp in milliseconds on originating homeserver when this event was sent.
    pub origin_server_ts: MilliSecondsSinceUnixEpoch,

    /// Additional key-value pairs not signed by the homeserver.
    pub unsigned: RoomRedactionUnsigned,
}

impl OriginalSyncRoomRedactionEvent {
    /// Convert this sync event into a full event, one with a `room_id` field.
    pub fn into_full_event(self, room_id: OwnedRoomId) -> OriginalRoomRedactionEvent {
        let Self { content, redacts, event_id, sender, origin_server_ts, unsigned } = self;

        OriginalRoomRedactionEvent {
            content,
            redacts,
            event_id,
            sender,
            origin_server_ts,
            room_id,
            unsigned,
        }
    }

    pub(crate) fn into_maybe_redacted(self) -> SyncRoomRedactionEvent {
        SyncRoomRedactionEvent::Original(self)
    }
}

/// Redacted redaction event without a `room_id`.
#[derive(Clone, Debug, Event)]
#[allow(clippy::exhaustive_structs)]
pub struct RedactedSyncRoomRedactionEvent {
    /// Data specific to the event type.
    pub content: RedactedRoomRedactionEventContent,

    /// The globally unique event identifier for the user who sent the event.
    pub event_id: OwnedEventId,

    /// The fully-qualified ID of the user who sent this event.
    pub sender: OwnedUserId,

    /// Timestamp in milliseconds on originating homeserver when this event was sent.
    pub origin_server_ts: MilliSecondsSinceUnixEpoch,

    /// Additional key-value pairs not signed by the homeserver.
    pub unsigned: RedactedUnsigned,
}

/// A redaction of an event.
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[ruma_event(type = "m.room.redaction", kind = MessageLike, custom_redacted)]
pub struct RoomRedactionEventContent {
    /// The ID of the event that was redacted.
    ///
    /// This field is required starting from room version 11.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redacts: Option<OwnedEventId>,

    /// The reason for the redaction, if any.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

impl RoomRedactionEventContent {
    /// Creates an empty `RoomRedactionEventContent` according to room versions 1 through 10.
    pub fn new_v1() -> Self {
        Self::default()
    }

    /// Creates a `RoomRedactionEventContent` with the required `redacts` field introduced in room
    /// version 11.
    pub fn new_v11(redacts: OwnedEventId) -> Self {
        Self { redacts: Some(redacts), ..Default::default() }
    }

    /// Add the given reason to this `RoomRedactionEventContent`.
    pub fn with_reason(mut self, reason: String) -> Self {
        self.reason = Some(reason);
        self
    }
}

impl RedactContent for RoomRedactionEventContent {
    type Redacted = RedactedRoomRedactionEventContent;

    fn redact(self, version: &RoomVersionId) -> Self::Redacted {
        let redacts = match version {
            RoomVersionId::V1
            | RoomVersionId::V2
            | RoomVersionId::V3
            | RoomVersionId::V4
            | RoomVersionId::V5
            | RoomVersionId::V6
            | RoomVersionId::V7
            | RoomVersionId::V8
            | RoomVersionId::V9
            | RoomVersionId::V10 => None,
            _ => self.redacts,
        };

        RedactedRoomRedactionEventContent { redacts }
    }
}

/// A redacted redaction event.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct RedactedRoomRedactionEventContent {
    /// The ID of the event that was redacted.
    ///
    /// This field is required starting from room version 11.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redacts: Option<OwnedEventId>,
}

impl EventContent for RedactedRoomRedactionEventContent {
    type EventType = MessageLikeEventType;

    fn event_type(&self) -> Self::EventType {
        MessageLikeEventType::RoomRedaction
    }
}

impl StaticEventContent for RedactedRoomRedactionEventContent {
    const TYPE: &'static str = "m.room.redaction";
}

impl RedactedMessageLikeEventContent for RedactedRoomRedactionEventContent {}

impl RoomRedactionEvent {
    /// Returns the `type` of this event.
    pub fn event_type(&self) -> MessageLikeEventType {
        match self {
            Self::Original(ev) => ev.content.event_type(),
            Self::Redacted(ev) => ev.content.event_type(),
        }
    }

    /// Returns this event's `event_id` field.
    pub fn event_id(&self) -> &EventId {
        match self {
            Self::Original(ev) => &ev.event_id,
            Self::Redacted(ev) => &ev.event_id,
        }
    }

    /// Returns this event's `sender` field.
    pub fn sender(&self) -> &UserId {
        match self {
            Self::Original(ev) => &ev.sender,
            Self::Redacted(ev) => &ev.sender,
        }
    }

    /// Returns this event's `origin_server_ts` field.
    pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
        match self {
            Self::Original(ev) => ev.origin_server_ts,
            Self::Redacted(ev) => ev.origin_server_ts,
        }
    }

    /// Returns this event's `room_id` field.
    pub fn room_id(&self) -> &RoomId {
        match self {
            Self::Original(ev) => &ev.room_id,
            Self::Redacted(ev) => &ev.room_id,
        }
    }

    /// Returns the ID of the event that this event redacts, according to the given room version.
    ///
    /// # Panics
    ///
    /// Panics if this is a non-redacted event and both `redacts` field are `None`, which is only
    /// possible if the event was modified after being deserialized.
    pub fn redacts(&self, room_version: &RoomVersionId) -> Option<&EventId> {
        match self {
            Self::Original(ev) => Some(ev.redacts(room_version)),
            Self::Redacted(ev) => ev.content.redacts.as_deref(),
        }
    }

    /// Get the inner `RoomRedactionEvent` if this is an unredacted event.
    pub fn as_original(&self) -> Option<&OriginalRoomRedactionEvent> {
        as_variant!(self, Self::Original)
    }
}

impl SyncRoomRedactionEvent {
    /// Returns the `type` of this event.
    pub fn event_type(&self) -> MessageLikeEventType {
        match self {
            Self::Original(ev) => ev.content.event_type(),
            Self::Redacted(ev) => ev.content.event_type(),
        }
    }

    /// Returns this event's `event_id` field.
    pub fn event_id(&self) -> &EventId {
        match self {
            Self::Original(ev) => &ev.event_id,
            Self::Redacted(ev) => &ev.event_id,
        }
    }

    /// Returns this event's `sender` field.
    pub fn sender(&self) -> &UserId {
        match self {
            Self::Original(ev) => &ev.sender,
            Self::Redacted(ev) => &ev.sender,
        }
    }

    /// Returns this event's `origin_server_ts` field.
    pub fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
        match self {
            Self::Original(ev) => ev.origin_server_ts,
            Self::Redacted(ev) => ev.origin_server_ts,
        }
    }

    /// Returns the ID of the event that this event redacts, according to the given room version.
    ///
    /// # Panics
    ///
    /// Panics if this is a non-redacted event and both `redacts` field are `None`, which is only
    /// possible if the event was modified after being deserialized.
    pub fn redacts(&self, room_version: &RoomVersionId) -> Option<&EventId> {
        match self {
            Self::Original(ev) => Some(ev.redacts(room_version)),
            Self::Redacted(ev) => ev.content.redacts.as_deref(),
        }
    }

    /// Get the inner `SyncRoomRedactionEvent` if this is an unredacted event.
    pub fn as_original(&self) -> Option<&OriginalSyncRoomRedactionEvent> {
        as_variant!(self, Self::Original)
    }

    /// Convert this sync event into a full event (one with a `room_id` field).
    pub fn into_full_event(self, room_id: OwnedRoomId) -> RoomRedactionEvent {
        match self {
            Self::Original(ev) => RoomRedactionEvent::Original(ev.into_full_event(room_id)),
            Self::Redacted(ev) => RoomRedactionEvent::Redacted(ev.into_full_event(room_id)),
        }
    }
}

impl From<RoomRedactionEvent> for SyncRoomRedactionEvent {
    fn from(full: RoomRedactionEvent) -> Self {
        match full {
            RoomRedactionEvent::Original(ev) => Self::Original(ev.into()),
            RoomRedactionEvent::Redacted(ev) => Self::Redacted(ev.into()),
        }
    }
}

impl OriginalRoomRedactionEvent {
    /// Returns the ID of the event that this event redacts, according to the proper `redacts` field
    /// for the given room version.
    ///
    /// If the `redacts` field is not the proper one for the given room version, this falls back to
    /// the one that is available.
    ///
    /// # Panics
    ///
    /// Panics if both `redacts` field are `None`, which is only possible if the event was modified
    /// after being deserialized.
    pub fn redacts(&self, room_version: &RoomVersionId) -> &EventId {
        redacts(room_version, self.redacts.as_deref(), self.content.redacts.as_deref())
    }
}

impl OriginalSyncRoomRedactionEvent {
    /// Returns the ID of the event that this event redacts, according to the proper `redacts` field
    /// for the given room version.
    ///
    /// If the `redacts` field is not the proper one for the given room version, this falls back to
    /// the one that is available.
    ///
    /// # Panics
    ///
    /// Panics if both `redacts` field are `None`, which is only possible if the event was modified
    /// after being deserialized.
    pub fn redacts(&self, room_version: &RoomVersionId) -> &EventId {
        redacts(room_version, self.redacts.as_deref(), self.content.redacts.as_deref())
    }
}

#[cfg(feature = "canonical-json")]
impl RedactionEvent for OriginalRoomRedactionEvent {}
#[cfg(feature = "canonical-json")]
impl RedactionEvent for OriginalSyncRoomRedactionEvent {}
#[cfg(feature = "canonical-json")]
impl RedactionEvent for RoomRedactionEvent {}
#[cfg(feature = "canonical-json")]
impl RedactionEvent for SyncRoomRedactionEvent {}

/// Extra information about a redaction that is not incorporated into the event's hash.
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct RoomRedactionUnsigned {
    /// The time in milliseconds that has elapsed since the event was sent.
    ///
    /// This field is generated by the local homeserver, and may be incorrect if the local time on
    /// at least one of the two servers is out of sync, which can cause the age to either be
    /// negative or greater than it actually is.
    pub age: Option<Int>,

    /// The client-supplied transaction ID, if the client being given the event is the same one
    /// which sent it.
    pub transaction_id: Option<OwnedTransactionId>,

    /// [Bundled aggregations] of related child events.
    ///
    /// [Bundled aggregations]: https://spec.matrix.org/latest/client-server-api/#aggregations-of-child-events
    #[serde(rename = "m.relations", default)]
    pub relations: BundledMessageLikeRelations<OriginalSyncRoomRedactionEvent>,
}

impl RoomRedactionUnsigned {
    /// Create a new `Unsigned` with fields set to `None`.
    pub fn new() -> Self {
        Self { age: None, transaction_id: None, relations: BundledMessageLikeRelations::default() }
    }
}

impl Default for RoomRedactionUnsigned {
    fn default() -> Self {
        Self::new()
    }
}

impl CanBeEmpty for RoomRedactionUnsigned {
    /// Whether this unsigned data is empty (all fields are `None`).
    ///
    /// This method is used to determine whether to skip serializing the `unsigned` field in room
    /// events. Do not use it to determine whether an incoming `unsigned` field was present - it
    /// could still have been present but contained none of the known fields.
    fn is_empty(&self) -> bool {
        self.age.is_none() && self.transaction_id.is_none() && self.relations.is_empty()
    }
}

/// Returns the value of the proper `redacts` field for the given room version.
///
/// If the `redacts` field is not the proper one for the given room version, this falls back to
/// the one that is available.
///
/// # Panics
///
/// Panics if both `redacts` and `content_redacts` are `None`.
fn redacts<'a>(
    room_version: &'_ RoomVersionId,
    redacts: Option<&'a EventId>,
    content_redacts: Option<&'a EventId>,
) -> &'a EventId {
    match room_version {
            RoomVersionId::V1
            | RoomVersionId::V2
            | RoomVersionId::V3
            | RoomVersionId::V4
            | RoomVersionId::V5
            | RoomVersionId::V6
            | RoomVersionId::V7
            | RoomVersionId::V8
            | RoomVersionId::V9
            | RoomVersionId::V10 => redacts
                .or_else(|| {
                    error!("Redacts field at event level not available, falling back to the one inside content");
                    content_redacts
        })
                .expect("At least one redacts field is set"),
            _ => content_redacts
                .or_else(|| {
                    error!("Redacts field inside content not available, falling back to the one at the event level");
                    redacts
        })
                .expect("At least one redacts field is set"),
        }
}
