use std::sync::Arc;

use gtk::{gio, glib, glib::closure_local, prelude::*, subclass::prelude::*};
use indexmap::IndexMap;
use matrix_sdk_ui::timeline::{
    AnyOtherFullStateEventContent, Error as TimelineError, EventSendState, EventTimelineItem,
    RepliedToEvent, TimelineDetails, TimelineEventItemId, TimelineItemContent,
};
use ruma::{
    events::{receipt::Receipt, AnySyncTimelineEvent, TimelineEventType},
    serde::Raw,
    MatrixToUri, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, OwnedUserId,
};
use serde::{de::IgnoredAny, Deserialize};
use tracing::{debug, error};

mod reaction_group;
mod reaction_list;

pub use self::{
    reaction_group::{ReactionData, ReactionGroup},
    reaction_list::ReactionList,
};
use super::{
    timeline::{TimelineItem, TimelineItemImpl},
    Member, Room,
};
use crate::{
    prelude::*,
    spawn_tokio,
    utils::matrix::{raw_eq, timestamp_to_date, MediaMessage, VisualMediaMessage},
};

/// The possible states of a message.
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[enum_type(name = "MessageState")]
pub enum MessageState {
    /// The message has no particular state.
    #[default]
    None,
    /// The message is being sent.
    Sending,
    /// A transient error occurred when sending the message.
    ///
    /// The user can try to send it again.
    RecoverableError,
    /// A permanent error occurred when sending the message.
    ///
    /// The message can only be cancelled.
    PermanentError,
    /// The message was edited.
    Edited,
}

/// The read receipt of a user.
#[derive(Clone, Debug)]
pub struct UserReadReceipt {
    /// The ID of the user.
    pub user_id: OwnedUserId,
    /// The data of the receipt.
    pub receipt: Receipt,
}

mod imp {
    use std::{
        cell::{Cell, OnceCell, RefCell},
        marker::PhantomData,
        sync::LazyLock,
    };

    use glib::subclass::Signal;

    use super::*;

    #[derive(Debug, glib::Properties)]
    #[properties(wrapper_type = super::Event)]
    pub struct Event {
        /// The room containing this event.
        #[property(get, set = Self::set_room, construct_only)]
        room: OnceCell<Room>,
        /// The underlying SDK timeline item.
        item: RefCell<Option<Arc<EventTimelineItem>>>,
        /// The global permanent ID of this event, if it has been received from
        /// the server, as a string.
        #[property(get = Self::event_id_string)]
        event_id_string: PhantomData<Option<String>>,
        /// The ID of the sender of this event, as a string.
        #[property(get = Self::sender_id_string)]
        sender_id_string: PhantomData<String>,
        /// The timestamp of this event, as a `GDateTime`.
        #[property(get = Self::timestamp)]
        timestamp: PhantomData<glib::DateTime>,
        /// The formatted timestamp of this event.
        #[property(get = Self::formatted_timestamp)]
        formatted_timestamp: PhantomData<String>,
        /// The pretty-formatted JSON source, if it has been echoed back by the
        /// server.
        #[property(get = Self::source)]
        source: PhantomData<Option<String>>,
        /// Whether we have the JSON source of this event.
        #[property(get = Self::has_source)]
        has_source: PhantomData<bool>,
        /// The state of this event.
        #[property(get, builder(MessageState::default()))]
        state: Cell<MessageState>,
        /// Whether this event was edited.
        #[property(get = Self::is_edited)]
        is_edited: PhantomData<bool>,
        /// The pretty-formatted JSON source for the latest edit of this
        /// event.
        ///
        /// This string is empty if the event is not edited.
        #[property(get = Self::latest_edit_source)]
        latest_edit_source: PhantomData<String>,
        /// The ID for the latest edit of this event, as a string.
        ///
        /// This string is empty if the event is not edited.
        #[property(get = Self::latest_edit_event_id_string)]
        latest_edit_event_id_string: PhantomData<String>,
        /// The timestamp for the latest edit of this event, as a `GDateTime`,
        /// if any.
        #[property(get = Self::latest_edit_timestamp)]
        latest_edit_timestamp: PhantomData<Option<glib::DateTime>>,
        /// The formatted timestamp for the latest edit of this event.
        ///
        /// This string is empty if the event is not edited.
        #[property(get = Self::latest_edit_formatted_timestamp)]
        latest_edit_formatted_timestamp: PhantomData<String>,
        /// Whether this event should be highlighted.
        #[property(get = Self::is_highlighted)]
        is_highlighted: PhantomData<bool>,
        /// The reactions on this event.
        #[property(get)]
        reactions: ReactionList,
        /// The read receipts on this event.
        #[property(get)]
        read_receipts: gio::ListStore,
        /// Whether this event has any read receipt.
        #[property(get = Self::has_read_receipts)]
        has_read_receipts: PhantomData<bool>,
    }

    impl Default for Event {
        fn default() -> Self {
            Self {
                room: Default::default(),
                item: Default::default(),
                event_id_string: Default::default(),
                sender_id_string: Default::default(),
                timestamp: Default::default(),
                formatted_timestamp: Default::default(),
                source: Default::default(),
                has_source: Default::default(),
                state: Default::default(),
                is_edited: Default::default(),
                latest_edit_source: Default::default(),
                latest_edit_event_id_string: Default::default(),
                latest_edit_timestamp: Default::default(),
                latest_edit_formatted_timestamp: Default::default(),
                is_highlighted: Default::default(),
                reactions: Default::default(),
                read_receipts: gio::ListStore::new::<glib::BoxedAnyObject>(),
                has_read_receipts: Default::default(),
            }
        }
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Event {
        const NAME: &'static str = "RoomEvent";
        type Type = super::Event;
        type ParentType = TimelineItem;
    }

    #[glib::derived_properties]
    impl ObjectImpl for Event {
        fn signals() -> &'static [Signal] {
            static SIGNALS: LazyLock<Vec<Signal>> =
                LazyLock::new(|| vec![Signal::builder("item-changed").build()]);
            SIGNALS.as_ref()
        }
    }

    impl TimelineItemImpl for Event {
        fn can_hide_header(&self) -> bool {
            self.item().content().can_show_header()
        }

        fn event_sender_id(&self) -> Option<OwnedUserId> {
            Some(self.sender_id())
        }

        fn selectable(&self) -> bool {
            true
        }
    }

    impl Event {
        /// Set the room that contains this event.
        fn set_room(&self, room: Room) {
            let room = self.room.get_or_init(|| room);

            if let Some(session) = room.session() {
                self.reactions.set_user(session.user().clone());
            }
        }

        /// Set the underlying SDK timeline item.
        pub(super) fn set_item(&self, item: EventTimelineItem) {
            let obj = self.obj();

            let item = Arc::new(item);
            let prev_item = self.item.replace(Some(item.clone()));

            self.reactions.update(item.reactions());
            self.update_read_receipts(item.read_receipts());

            let prev_source = prev_item.as_ref().and_then(|i| i.original_json());
            let source = item.original_json();
            if !raw_eq(prev_source, source) {
                obj.notify_source();
            }
            if prev_source.is_some() != source.is_some() {
                obj.notify_has_source();
            }

            if prev_item.as_ref().and_then(|i| i.event_id()) != item.event_id() {
                obj.notify_event_id_string();
            }
            if prev_item
                .as_ref()
                .is_some_and(|i| i.content().is_edited() != item.content().is_edited())
            {
                obj.notify_is_edited();
            }
            if prev_item
                .as_ref()
                .is_some_and(|i| i.is_highlighted() != item.is_highlighted())
            {
                obj.notify_is_highlighted();
            }
            if !raw_eq(
                prev_item
                    .as_ref()
                    .and_then(|i| i.latest_edit_raw())
                    .as_ref(),
                item.latest_edit_raw().as_ref(),
            ) {
                obj.notify_latest_edit_source();
                obj.notify_latest_edit_event_id_string();
                obj.notify_latest_edit_timestamp();
                obj.notify_latest_edit_formatted_timestamp();
            }

            self.update_state();
            obj.emit_by_name::<()>("item-changed", &[]);
        }

        /// The underlying SDK timeline item.
        pub(super) fn item(&self) -> Arc<EventTimelineItem> {
            self.item
                .borrow()
                .clone()
                .expect("event should have timeline item after construction")
        }

        /// The global permanent or temporary identifier of this event.
        pub(super) fn identifier(&self) -> TimelineEventItemId {
            self.item().identifier()
        }

        /// The global permanent ID of this event, if it has been received from
        /// the server.
        pub(super) fn event_id(&self) -> Option<OwnedEventId> {
            self.item().event_id().map(ToOwned::to_owned)
        }

        /// The global permanent ID of this event, if it has been received from
        /// the server, as a string.
        fn event_id_string(&self) -> Option<String> {
            self.item().event_id().map(ToString::to_string)
        }

        /// The temporary ID of this event, if it has been sent with this
        /// session.
        pub(crate) fn transaction_id(&self) -> Option<OwnedTransactionId> {
            self.item().transaction_id().map(ToOwned::to_owned)
        }

        /// The ID of the sender of this event.
        pub(super) fn sender_id(&self) -> OwnedUserId {
            self.item().sender().to_owned()
        }

        /// The ID of the sender of this event, as a string.
        fn sender_id_string(&self) -> String {
            self.item().sender().to_string()
        }

        /// The timestamp of this event, as the number of milliseconds
        /// since Unix Epoch.
        pub(super) fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
            self.item().timestamp()
        }

        /// The timestamp of this event, as a `GDateTime`.
        fn timestamp(&self) -> glib::DateTime {
            timestamp_to_date(self.origin_server_ts())
        }

        /// The formatted timestamp of this event.
        fn formatted_timestamp(&self) -> String {
            self.timestamp()
                .format("%c")
                .map(Into::into)
                .unwrap_or_default()
        }

        /// The raw JSON source, if it has been echoed back by the server.
        pub(super) fn raw(&self) -> Option<Raw<AnySyncTimelineEvent>> {
            self.item().original_json().cloned()
        }

        /// The pretty-formatted JSON source, if it has been echoed back by the
        /// server.
        fn source(&self) -> Option<String> {
            self.item().original_json().map(raw_to_pretty_string)
        }

        /// Whether we have the JSON source.
        fn has_source(&self) -> bool {
            self.item().original_json().is_some()
        }

        /// Compute the current state of this event.
        fn compute_state(&self) -> MessageState {
            let item = self.item();

            if let Some(send_state) = item.send_state() {
                match send_state {
                    EventSendState::NotSentYet => return MessageState::Sending,
                    EventSendState::SendingFailed {
                        error,
                        is_recoverable,
                    } => {
                        if !matches!(
                            self.state.get(),
                            MessageState::PermanentError | MessageState::RecoverableError,
                        ) {
                            error!("Could not send message: {error}");
                        }

                        let new_state = if *is_recoverable {
                            MessageState::RecoverableError
                        } else {
                            MessageState::PermanentError
                        };

                        return new_state;
                    }
                    EventSendState::Sent { .. } => {}
                }
            }

            match item.content() {
                TimelineItemContent::Message(msg) if msg.is_edited() => MessageState::Edited,
                _ => MessageState::None,
            }
        }

        /// Update the state of this event.
        fn update_state(&self) {
            let state = self.compute_state();

            if self.state.get() == state {
                return;
            }

            self.state.set(state);
            self.obj().notify_state();
        }

        /// Whether this event was edited.
        fn is_edited(&self) -> bool {
            self.item().content().is_edited()
        }

        /// The JSON source for the latest edit of this event, if any.
        fn latest_edit_raw(&self) -> Option<Raw<AnySyncTimelineEvent>> {
            self.item().latest_edit_raw()
        }

        /// The pretty-formatted JSON source for the latest edit of this event.
        ///
        /// This string is empty if the event is not edited.
        fn latest_edit_source(&self) -> String {
            self.latest_edit_raw()
                .as_ref()
                .map(raw_to_pretty_string)
                .unwrap_or_default()
        }

        /// The ID of the latest edit of this `Event`.
        ///
        /// This string is empty if the event is not edited.
        fn latest_edit_event_id_string(&self) -> String {
            self.latest_edit_raw()
                .as_ref()
                .and_then(|r| r.get_field::<String>("event_id").ok().flatten())
                .unwrap_or_default()
        }

        /// The timestamp of the latest edit of this `Event`, as a `GDateTime`,
        /// if any.
        fn latest_edit_timestamp(&self) -> Option<glib::DateTime> {
            self.latest_edit_raw()
                .as_ref()
                .and_then(|r| {
                    r.get_field::<MilliSecondsSinceUnixEpoch>("origin_server_ts")
                        .ok()
                        .flatten()
                })
                .map(timestamp_to_date)
        }

        /// The formatted timestamp of the latest edit of this `Event`.
        fn latest_edit_formatted_timestamp(&self) -> String {
            self.latest_edit_timestamp()
                .and_then(|d| d.format("%c").ok())
                .map(Into::into)
                .unwrap_or_default()
        }

        /// Whether this `Event` should be highlighted.
        fn is_highlighted(&self) -> bool {
            self.item().is_highlighted()
        }

        /// Update the read receipts list with the given receipts.
        fn update_read_receipts(&self, new_read_receipts: &IndexMap<OwnedUserId, Receipt>) {
            let old_count = self.read_receipts.n_items();
            let new_count = new_read_receipts.len() as u32;

            if old_count == new_count {
                let mut is_all_same = true;
                for (i, new_user_id) in new_read_receipts.keys().enumerate() {
                    let Some(old_receipt) = self
                        .read_receipts
                        .item(i as u32)
                        .and_downcast::<glib::BoxedAnyObject>()
                    else {
                        is_all_same = false;
                        break;
                    };

                    if old_receipt.borrow::<UserReadReceipt>().user_id != *new_user_id {
                        is_all_same = false;
                        break;
                    }
                }

                if is_all_same {
                    return;
                }
            }

            let new_read_receipts = new_read_receipts
                .into_iter()
                .map(|(user_id, receipt)| {
                    glib::BoxedAnyObject::new(UserReadReceipt {
                        user_id: user_id.clone(),
                        receipt: receipt.clone(),
                    })
                })
                .collect::<Vec<_>>();
            self.read_receipts.splice(0, old_count, &new_read_receipts);

            let prev_has_read_receipts = old_count > 0;
            let has_read_receipts = new_count > 0;

            if prev_has_read_receipts != has_read_receipts {
                self.obj().notify_has_read_receipts();
            }
        }

        /// Whether this event has any read receipt.
        fn has_read_receipts(&self) -> bool {
            self.read_receipts.n_items() > 0
        }
    }
}

glib::wrapper! {
    /// A Matrix room event.
    pub struct Event(ObjectSubclass<imp::Event>) @extends TimelineItem;
}

impl Event {
    /// Create a new `Event` in the given room with the given SDK timeline item.
    pub fn new(item: EventTimelineItem, room: &Room, timeline_id: &str) -> Self {
        let obj = glib::Object::builder::<Self>()
            .property("room", room)
            .property("timeline-id", timeline_id)
            .build();

        obj.imp().set_item(item);

        obj
    }

    /// Update this event with the given SDK timeline item.
    pub(crate) fn update_with(&self, item: EventTimelineItem) {
        self.imp().set_item(item);
    }

    /// The underlying SDK timeline item.
    pub(crate) fn item(&self) -> Arc<EventTimelineItem> {
        self.imp().item()
    }

    /// The global permanent or temporary identifier of this event.
    pub(crate) fn identifier(&self) -> TimelineEventItemId {
        self.imp().identifier()
    }

    /// Whether the given identifier matches this event.
    ///
    /// The result can be different from comparing two [`TimelineEventItemId`]s
    /// because an event can have a transaction ID and an event ID.
    pub(crate) fn matches_identifier(&self, identifier: &TimelineEventItemId) -> bool {
        let item = self.item();
        match identifier {
            TimelineEventItemId::TransactionId(txn_id) => {
                item.transaction_id().is_some_and(|id| id == txn_id)
            }
            TimelineEventItemId::EventId(event_id) => {
                item.event_id().is_some_and(|id| id == event_id)
            }
        }
    }

    /// The permanent global ID of this event, if it has been received from the
    /// server.
    pub(crate) fn event_id(&self) -> Option<OwnedEventId> {
        self.imp().event_id()
    }

    /// The temporary ID of this event, if it has been sent with this session.
    pub(crate) fn transaction_id(&self) -> Option<OwnedTransactionId> {
        self.imp().transaction_id()
    }

    /// The ID of the sender of this event.
    pub(crate) fn sender_id(&self) -> OwnedUserId {
        self.imp().sender_id()
    }

    /// The sender of this event.
    ///
    /// This should only be called when the event's room members list is
    /// available, otherwise it will be created on every call.
    pub(crate) fn sender(&self) -> Member {
        self.room()
            .get_or_create_members()
            .get_or_create(self.sender_id())
    }

    /// The timestamp of this event, as the number of milliseconds
    /// since Unix Epoch.
    pub(crate) fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
        self.imp().origin_server_ts()
    }

    /// The raw JSON source for this event, if it has been echoed back
    /// by the server.
    pub(crate) fn raw(&self) -> Option<Raw<AnySyncTimelineEvent>> {
        self.imp().raw()
    }

    /// The content of this event.
    pub(crate) fn content(&self) -> TimelineItemContent {
        self.item().content().clone()
    }

    /// Whether this event contains a message.
    ///
    /// Message events include the following variants of
    /// [`TimelineItemContent`]:
    ///
    /// - `Message`
    /// - `Sticker`
    ///
    /// Note that this differs from the SDK/Matrix definition that only includes
    /// `m.room.message` events, and from the Ruma definition (e.g. used for
    /// `AnySyncMessageLikeEvent`) which includes all non-state events.
    pub(crate) fn is_message(&self) -> bool {
        matches!(
            self.content(),
            TimelineItemContent::Message(_) | TimelineItemContent::Sticker(_)
        )
    }

    /// The media message of this event, if any.
    pub(crate) fn media_message(&self) -> Option<MediaMessage> {
        match self.item().content() {
            TimelineItemContent::Message(msg) => MediaMessage::from_message(msg.msgtype()),
            _ => None,
        }
    }

    /// The visual media message of this event, if any.
    pub(crate) fn visual_media_message(&self) -> Option<VisualMediaMessage> {
        match self.item().content() {
            TimelineItemContent::Message(msg) => VisualMediaMessage::from_message(msg.msgtype()),
            _ => None,
        }
    }

    /// Whether this event might contain an `@room` mention.
    ///
    /// This means that either it does not have intentional mentions, or it has
    /// intentional mentions and `room` is set to `true`.
    pub(crate) fn can_contain_at_room(&self) -> bool {
        self.item().content().can_contain_at_room()
    }

    /// Whether this is an `m.room.create` event.
    pub(crate) fn is_room_create_event(&self) -> bool {
        match self.item().content() {
            TimelineItemContent::OtherState(other_state) => matches!(
                other_state.content(),
                AnyOtherFullStateEventContent::RoomCreate(_)
            ),
            _ => false,
        }
    }

    /// Get the ID of the event this event replies to, if any.
    pub(crate) fn reply_to_id(&self) -> Option<OwnedEventId> {
        match self.item().content() {
            TimelineItemContent::Message(message) => {
                message.in_reply_to().map(|d| d.event_id.clone())
            }
            _ => None,
        }
    }

    /// Get the details of the event this event replies to, if any.
    ///
    /// Returns `None(_)` if this event is not a reply.
    pub(crate) fn reply_to_event_content(&self) -> Option<TimelineDetails<Box<RepliedToEvent>>> {
        match self.item().content() {
            TimelineItemContent::Message(message) => message.in_reply_to().map(|d| d.event.clone()),
            _ => None,
        }
    }

    /// Fetch missing details for this event.
    ///
    /// This is a no-op if called for a local event.
    pub(crate) async fn fetch_missing_details(&self) -> Result<(), TimelineError> {
        let Some(event_id) = self.event_id() else {
            return Ok(());
        };

        let timeline = self.room().timeline().matrix_timeline();
        spawn_tokio!(async move { timeline.fetch_details_for_event(&event_id).await })
            .await
            .expect("task was not aborted")
    }

    /// Whether this event can be replied to.
    pub(crate) fn can_be_replied_to(&self) -> bool {
        // We only allow to reply to messages.
        if !self.is_message() {
            return false;
        }

        // The SDK API has its own rules.
        if !self.item().can_be_replied_to() {
            return false;
        }

        // Finally, check that the current permissions allow us to send messages.
        self.room().permissions().can_send_message()
    }

    /// Whether this event can be reacted to.
    pub(crate) fn can_be_reacted_to(&self) -> bool {
        // We only allow to react to messages.
        if !self.is_message() {
            return false;
        }

        // We cannot react to an event that is being sent.
        if self.event_id().is_none() {
            return false;
        }

        // Finally, check that the current permissions allow us to send messages.
        self.room().permissions().can_send_reaction()
    }

    /// Whether this event can be redacted.
    ///
    /// This uses the raw JSON to be able to redact even events that failed to
    /// deserialize.
    pub(crate) fn can_be_redacted(&self) -> bool {
        let Some(raw) = self.raw() else {
            // Events without raw JSON are already redacted events, and events that are not
            // sent yet, we can ignore them.
            return false;
        };

        let is_redacted = match raw.get_field::<UnsignedRedactedDeHelper>("unsigned") {
            Ok(Some(unsigned)) => unsigned.redacted_because.is_some(),
            Ok(None) => {
                debug!("Missing unsigned field in event");
                false
            }
            Err(error) => {
                error!("Could not deserialize unsigned field in event: {error}");
                false
            }
        };
        if is_redacted {
            // There is no point in redacting it twice.
            return false;
        }

        match raw.get_field::<TimelineEventType>("type") {
            Ok(Some(t)) => !NON_REDACTABLE_EVENTS.contains(&t),
            Ok(None) => {
                debug!("Missing type field in event");
                true
            }
            Err(error) => {
                error!("Could not deserialize type field in event: {error}");
                true
            }
        }
    }

    /// Whether this `Event` can count as an unread message.
    ///
    /// This follows the algorithm in [MSC2654], excluding events that we don't
    /// show in the timeline.
    ///
    /// [MSC2654]: https://github.com/matrix-org/matrix-spec-proposals/pull/2654
    pub(crate) fn counts_as_unread(&self) -> bool {
        self.item().content().counts_as_unread()
    }

    /// The `matrix.to` URI representation for this event.
    ///
    /// Returns `None` if we don't have the ID of the event.
    pub(crate) async fn matrix_to_uri(&self) -> Option<MatrixToUri> {
        Some(self.room().matrix_to_event_uri(self.event_id()?).await)
    }

    /// Listen to the signal emitted when the SDK item changed.
    pub(crate) fn connect_item_changed<F: Fn(&Self) + 'static>(
        &self,
        f: F,
    ) -> glib::SignalHandlerId {
        self.connect_closure(
            "item-changed",
            true,
            closure_local!(move |obj: Self| {
                f(&obj);
            }),
        )
    }
}

/// Convert raw JSON to a pretty-formatted JSON string.
fn raw_to_pretty_string<T>(raw: &Raw<T>) -> String {
    // We have to convert it to a Value, because a RawValue cannot be
    // pretty-printed.
    let json = serde_json::to_value(raw).unwrap();

    serde_json::to_string_pretty(&json).unwrap()
}

/// List of events that should not be redacted to avoid bricking a room.
const NON_REDACTABLE_EVENTS: &[TimelineEventType] = &[
    TimelineEventType::RoomCreate,
    TimelineEventType::RoomEncryption,
    TimelineEventType::RoomServerAcl,
];

/// A helper type to know whether an event was redacted.
#[derive(Deserialize)]
struct UnsignedRedactedDeHelper {
    redacted_because: Option<IgnoredAny>,
}
