use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use uuid::Uuid;

/// Response from the /api/feed/v2 and /api/feed/v3 endpoints
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedResponse {
    pub clips: Vec<Clip>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub has_more: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub num_total_results: Option<u32>, // Only in v2
}

/// Request body for the /api/feed/v3 endpoint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cursor: Option<String>,
    pub limit: u32,
    pub filters: FeedFilters,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedFilters {
    pub disliked: String,
    pub trashed: String,
    #[serde(rename = "fromStudioProject")]
    pub from_studio_project: PresenceFilter,
    pub stem: PresenceFilter,
    pub workspace: WorkspaceFilter,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PresenceFilter {
    pub presence: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceFilter {
    pub presence: String,
    #[serde(rename = "workspaceId")]
    pub workspace_id: Uuid,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Clip {
    pub status: String,
    pub title: String,
    pub play_count: u32,
    pub upvote_count: u32,
    pub allow_comments: bool,
    pub id: Uuid,
    pub entity_type: String,
    #[serde(default)]
    pub video_url: String,
    #[serde(default)]
    pub audio_url: String,
    #[serde(default)]
    pub image_url: String,
    #[serde(default)]
    pub image_large_url: String,
    pub major_model_version: String,
    pub model_name: String,
    pub metadata: ClipMetadata,
    pub is_liked: bool,
    pub user_id: Uuid,
    pub display_name: String,
    pub handle: String,
    pub is_handle_updated: bool,
    pub avatar_image_url: String,
    pub is_trashed: bool,
    pub created_at: DateTime<Utc>,
    pub is_public: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reaction: Option<Reaction>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project: Option<Project>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ownership: Option<Ownership>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub explicit: Option<bool>,
    pub comment_count: u32,
    pub flag_count: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_tags: Option<String>,
    pub is_contest_clip: bool,
    pub has_hook: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipMetadata {
    #[serde(default)]
    pub tags: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub negative_tags: Option<String>,
    pub prompt: String,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(default)]
    pub duration: f64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub refund_credits: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub control_sliders: Option<ControlSliders>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub configurations: Option<BTreeMap<String, serde_json::Value>>,
    pub can_remix: bool,
    pub is_remix: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub priority: Option<u32>,
    pub has_stem: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub has_vocal: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub can_publish_with_vocal: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_bpm: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub min_bpm: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub avg_bpm: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub gpt_description_prompt: Option<String>,
    pub uses_latest_model: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_badges: Option<ModelBadges>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ControlSliders {
    pub style_weight: f64,
    pub weirdness_constraint: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ModelBadges {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub songcard: Option<Badge>,
    pub songrow: Badge,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Badge {
    pub display_name: String,
    pub light: BadgeColors,
    pub dark: BadgeColors,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BadgeColors {
    pub text_color: String,
    pub background_color: String,
    pub border_color: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Reaction {
    pub play_count: u32,
    pub skip_count: u32,
    pub flagged: bool,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Project {
    pub id: Uuid,
    pub name: String,
    pub description: String,
    pub is_trashed: bool,
    pub is_public: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Ownership {
    pub ownership_reason: String,
}

/// Batch feed request for efficient API calls
/// Multiple feed requests are collected and sent as a single batched request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchedFeedRequest {
    pub requests: Vec<FeedRequest>,
}

impl BatchedFeedRequest {
    pub fn new(requests: Vec<FeedRequest>) -> Self {
        Self { requests }
    }

    pub fn add_request(mut self, request: FeedRequest) -> Self {
        self.requests.push(request);
        self
    }

    pub fn is_empty(&self) -> bool {
        self.requests.is_empty()
    }

    pub fn len(&self) -> usize {
        self.requests.len()
    }
}

/// Batch feed response combining results from multiple feed requests
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchedFeedResponse {
    pub responses: Vec<FeedResponse>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total_clips_fetched: Option<u32>,
}

impl BatchedFeedResponse {
    pub fn new(responses: Vec<FeedResponse>) -> Self {
        let total_clips_fetched = Some(responses.iter().map(|r| r.clips.len() as u32).sum());
        Self {
            responses,
            total_clips_fetched,
        }
    }

    pub fn all_clips(&self) -> Vec<&Clip> {
        self.responses
            .iter()
            .flat_map(|r| r.clips.iter())
            .collect()
    }

    pub fn combined_clips(&self) -> Vec<Clip> {
        self.responses
            .iter()
            .flat_map(|r| r.clips.clone())
            .collect()
    }
}

/// Request to mark feed items as watched/read
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarkWatchedRequest {
    pub clip_ids: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub skip_count: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub play_count: Option<u32>,
}

impl MarkWatchedRequest {
    pub fn new(clip_ids: Vec<String>) -> Self {
        Self {
            clip_ids,
            skip_count: None,
            play_count: None,
        }
    }

    pub fn with_skip_count(mut self, count: u32) -> Self {
        self.skip_count = Some(count);
        self
    }

    pub fn with_play_count(mut self, count: u32) -> Self {
        self.play_count = Some(count);
        self
    }
}

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

    #[test]
    fn test_batched_feed_request_new() {
        let req1 = FeedRequest {
            cursor: None,
            limit: 10,
            filters: FeedFilters {
                disliked: "dislike".to_string(),
                trashed: "trash".to_string(),
                from_studio_project: PresenceFilter {
                    presence: "not".to_string(),
                },
                stem: PresenceFilter {
                    presence: "not".to_string(),
                },
                workspace: WorkspaceFilter {
                    presence: "yes".to_string(),
                    workspace_id: Uuid::new_v4(),
                },
            },
        };

        let batch = BatchedFeedRequest::new(vec![req1.clone()]);
        assert_eq!(batch.len(), 1);
        assert!(!batch.is_empty());
    }

    #[test]
    fn test_batched_feed_request_add() {
        let req1 = FeedRequest {
            cursor: None,
            limit: 10,
            filters: FeedFilters {
                disliked: "dislike".to_string(),
                trashed: "trash".to_string(),
                from_studio_project: PresenceFilter {
                    presence: "not".to_string(),
                },
                stem: PresenceFilter {
                    presence: "not".to_string(),
                },
                workspace: WorkspaceFilter {
                    presence: "yes".to_string(),
                    workspace_id: Uuid::new_v4(),
                },
            },
        };

        let batch = BatchedFeedRequest::new(vec![])
            .add_request(req1.clone())
            .add_request(req1.clone());
        assert_eq!(batch.len(), 2);
    }

    #[test]
    fn test_mark_watched_request_builder() {
        let req = MarkWatchedRequest::new(vec!["clip1".to_string(), "clip2".to_string()])
            .with_skip_count(2)
            .with_play_count(5);

        assert_eq!(req.clip_ids.len(), 2);
        assert_eq!(req.skip_count, Some(2));
        assert_eq!(req.play_count, Some(5));
    }
}
