//! Response views returned by [`crate::ChatApp`] and serialized by HTTP `/chat/*` handlers.

use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::ids::{MessageId, RoomId, UserId};

/// File attachment metadata embedded in [`MessageView`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentView {
    pub file_id: String,
    pub ordinal: i32,
    pub file_name: Option<String>,
    pub original_name: Option<String>,
    pub content_type: Option<String>,
    pub mime_type: Option<String>,
    pub extension: Option<String>,
    pub size_bytes: Option<i64>,
    pub status: Option<String>,
    pub visibility: Option<String>,
    pub file_url: Option<String>,
    pub bucket: Option<String>,
    pub storage_key: Option<String>,
    pub authorized_url_path: String,
    pub proxy_url_path: String,
    pub public_url_path: String,
}

/// Per-emoji reaction aggregate for a message.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReactionCount {
    pub emoji: String,
    pub count: i64,
    pub reacted: bool,
}

/// Reaction totals for a message after add/remove mutations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReactionSummary {
    pub message_id: MessageId,
    pub reactions: Vec<ReactionCount>,
}

/// UI grouping hints for consecutive messages from the same sender within 24 hours.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageGroupView {
    pub starts: bool,
    pub ends: bool,
}

/// Durable chat message with attachments, reactions, and optional grouping metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageView {
    pub id: MessageId,
    pub room_id: RoomId,
    pub room_seq: i64,
    pub sender_id: UserId,
    pub client_message_id: Option<String>,
    pub body_text: Option<String>,
    pub body_json: Option<Value>,
    pub reply_to_message_id: Option<MessageId>,
    pub created_at: DateTime<Utc>,
    pub edited_at: Option<DateTime<Utc>>,
    pub deleted_at: Option<DateTime<Utc>>,
    pub metadata_json: Option<Value>,
    pub attachments: Vec<AttachmentView>,
    pub reactions: Vec<ReactionCount>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message_group: Option<MessageGroupView>,
}

/// Paginated message list with an optional `before_seq` cursor for the next page.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessagePage {
    pub items: Vec<MessageView>,
    pub next_before_seq: Option<i64>,
}

/// Room summary including last-message cursor fields used for WSS resume checks.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoomView {
    pub id: RoomId,
    pub organization_id: String,
    pub kind: String,
    pub title: Option<String>,
    pub created_by: UserId,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub archived_at: Option<DateTime<Utc>>,
    pub last_message_id: Option<MessageId>,
    pub last_message_seq: i64,
    pub last_message_at: Option<DateTime<Utc>>,
    pub version: i64,
}

/// Paginated room list returned by `GET /chat/rooms`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoomPage {
    pub items: Vec<RoomView>,
}

/// Room membership row including read cursor and notification preferences.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemberView {
    pub room_id: RoomId,
    pub user_id: UserId,
    pub role: String,
    pub joined_at: DateTime<Utc>,
    pub last_read_seq: i64,
    pub last_read_message_id: Option<MessageId>,
    pub muted: bool,
    pub notification_mode: Option<String>,
    pub hidden_at: Option<DateTime<Utc>>,
}

/// Read receipt cursor after `POST /chat/rooms/{room_id}/read-cursor`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadCursorView {
    pub room_id: RoomId,
    pub user_id: UserId,
    pub last_read_seq: i64,
    pub last_read_message_id: Option<MessageId>,
}

/// Acknowledgement for a successful message deletion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteResult {
    pub ok: bool,
    pub message_id: MessageId,
}

/// Acknowledgement for a successful member removal.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveResult {
    pub ok: bool,
    pub user_id: UserId,
}

/// Single full-text search match with room context.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchHit {
    pub room_id: RoomId,
    pub message: MessageView,
}

/// Search results page from `POST /chat/messages/search`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchPage {
    pub items: Vec<SearchHit>,
}

const MESSAGE_GROUP_WINDOW: Duration = Duration::hours(24);

/// Fills `message_group` on each item using same-sender proximity within a 24-hour window.
///
/// `previous_boundary` and `next_boundary` let paginated slices connect to messages outside
/// the current page.
pub fn annotate_message_groups(
    items: &mut [MessageView],
    previous_boundary: Option<(&str, DateTime<Utc>)>,
    next_boundary: Option<(&str, DateTime<Utc>)>,
) {
    let groups: Vec<MessageGroupView> = items
        .iter()
        .enumerate()
        .map(|(index, current)| {
            let previous = if index == 0 {
                previous_boundary
            } else {
                let previous = &items[index - 1];
                Some((previous.sender_id.as_str(), previous.created_at))
            };
            let next = if index + 1 == items.len() {
                next_boundary
            } else {
                let next = &items[index + 1];
                Some((next.sender_id.as_str(), next.created_at))
            };

            MessageGroupView {
                starts: !matches!(
                    previous,
                    Some((sender_id, created_at))
                        if messages_share_group(
                            sender_id,
                            created_at,
                            current.sender_id.as_str(),
                            current.created_at,
                        )
                ),
                ends: !matches!(
                    next,
                    Some((sender_id, created_at))
                        if messages_share_group(
                            current.sender_id.as_str(),
                            current.created_at,
                            sender_id,
                            created_at,
                        )
                ),
            }
        })
        .collect();

    for (item, group) in items.iter_mut().zip(groups) {
        item.message_group = Some(group);
    }
}

fn messages_share_group(
    previous_sender_id: &str,
    previous_created_at: DateTime<Utc>,
    current_sender_id: &str,
    current_created_at: DateTime<Utc>,
) -> bool {
    previous_sender_id == current_sender_id
        && current_created_at >= previous_created_at
        && (current_created_at - previous_created_at) <= MESSAGE_GROUP_WINDOW
}

#[cfg(test)]
mod tests {
    use chrono::TimeZone;
    use serde_json::json;
    use uuid::Uuid;

    use super::{MessageView, annotate_message_groups};

    fn message(sender_id: &str, room_seq: i64, hour: u32) -> MessageView {
        MessageView {
            id: Uuid::new_v4(),
            room_id: Uuid::new_v4(),
            room_seq,
            sender_id: sender_id.to_string(),
            client_message_id: None,
            body_text: Some(format!("message-{room_seq}")),
            body_json: None,
            reply_to_message_id: None,
            created_at: chrono::Utc
                .with_ymd_and_hms(2026, 1, 1, hour, 0, 0)
                .unwrap(),
            edited_at: None,
            deleted_at: None,
            metadata_json: Some(json!({ "room_seq": room_seq })),
            attachments: Vec::new(),
            reactions: Vec::new(),
            message_group: None,
        }
    }

    #[test]
    fn annotates_uninterrupted_messages_from_same_sender_as_one_group() {
        let mut items = vec![
            message("user-1", 1, 10),
            message("user-2", 2, 11),
            message("user-2", 3, 12),
            message("user-1", 4, 13),
        ];

        annotate_message_groups(&mut items, None, None);

        assert_eq!(items[0].message_group.as_ref().unwrap().starts, true);
        assert_eq!(items[0].message_group.as_ref().unwrap().ends, true);
        assert_eq!(items[1].message_group.as_ref().unwrap().starts, true);
        assert_eq!(items[1].message_group.as_ref().unwrap().ends, false);
        assert_eq!(items[2].message_group.as_ref().unwrap().starts, false);
        assert_eq!(items[2].message_group.as_ref().unwrap().ends, true);
        assert_eq!(items[3].message_group.as_ref().unwrap().starts, true);
        assert_eq!(items[3].message_group.as_ref().unwrap().ends, true);
    }

    #[test]
    fn splits_groups_when_gap_exceeds_twenty_four_hours() {
        let mut items = vec![message("user-1", 1, 10), message("user-1", 2, 11)];
        let next_day = chrono::Utc.with_ymd_and_hms(2026, 1, 2, 12, 1, 0).unwrap();
        items[1].created_at = next_day;

        annotate_message_groups(&mut items, None, None);

        assert_eq!(items[0].message_group.as_ref().unwrap().starts, true);
        assert_eq!(items[0].message_group.as_ref().unwrap().ends, true);
        assert_eq!(items[1].message_group.as_ref().unwrap().starts, true);
        assert_eq!(items[1].message_group.as_ref().unwrap().ends, true);
    }

    #[test]
    fn uses_page_boundaries_when_previous_or_next_messages_are_outside_the_slice() {
        let created_at = chrono::Utc.with_ymd_and_hms(2026, 1, 1, 10, 0, 0).unwrap();
        let mut items = vec![message("user-1", 2, 11), message("user-1", 3, 12)];

        annotate_message_groups(
            &mut items,
            Some(("user-1", created_at)),
            Some((
                "user-2",
                chrono::Utc.with_ymd_and_hms(2026, 1, 1, 13, 0, 0).unwrap(),
            )),
        );

        assert_eq!(items[0].message_group.as_ref().unwrap().starts, false);
        assert_eq!(items[0].message_group.as_ref().unwrap().ends, false);
        assert_eq!(items[1].message_group.as_ref().unwrap().starts, false);
        assert_eq!(items[1].message_group.as_ref().unwrap().ends, true);
    }
}
