//! ActivityPub note publishing.
//!
//! Handles creating notes from markdown input, rendering them to HTML,
//! and creating the corresponding Create activities.

use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
use uuid::Uuid;

use crate::activitypub::types::{CONTEXT, Create, Note};
use crate::entities::prelude::{ApActors, ApNotes};
use crate::entities::{ap_activities, ap_actors, ap_notes};

/// Publishes a new note from markdown content.
///
/// Renders the markdown to HTML, stores the note, and creates a Create activity.
pub async fn publish(
    db: &DatabaseConnection,
    actor_id: i64,
    actor_url: &str,
    _public_url: &str,
    content_md: &str,
) -> Result<ap_notes::Model, sea_orm::DbErr> {
    // Render markdown to HTML
    let content_html = crate::markdown::render(content_md);

    // Generate UUID for the note
    let note_uuid = Uuid::new_v4();
    let note_url = format!("{}/notes/{}", actor_url, note_uuid);

    // Insert the note
    let note = ap_notes::ActiveModel {
        uuid: Set(note_uuid),
        actor_id: Set(actor_id),
        content_md: Set(content_md.to_string()),
        content_html: Set(content_html.clone()),
        ..Default::default()
    };
    let note = note.insert(db).await?;

    // Build the Note object
    let note_obj = Note {
        context: CONTEXT.to_vec(),
        id: note_url.clone(),
        kind: "Note",
        attributed_to: actor_url.to_string(),
        content: content_html,
        media_type: None,
        url: note_url.clone(),
        in_reply_to: None,
        source: None,
        published: chrono::Utc::now().to_rfc3339(),
        to: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
        cc: vec![],
    };

    // Build the Create activity
    let activity_uuid = Uuid::new_v4();
    let activity_url = format!("{}/activities/{}", actor_url, activity_uuid);

    let create_activity = Create {
        context: CONTEXT.to_vec(),
        id: activity_url,
        kind: "Create",
        actor: actor_url.to_string(),
        object: note_obj,
        published: chrono::Utc::now().to_rfc3339(),
        to: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
        cc: vec![],
    };

    // Serialize the activity
    let activity_json = serde_json::to_string(&create_activity)
        .map_err(|e| sea_orm::DbErr::Custom(e.to_string()))?;

    // Insert the activity
    let activity = ap_activities::ActiveModel {
        uuid: Set(activity_uuid),
        actor_id: Set(actor_id),
        activity_json: Set(activity_json),
        object_url: Set(note_url),
        ..Default::default()
    };
    let activity = activity.insert(db).await?;

    // Fan out to followers
    deliver_to_followers(db, actor_id, activity.id).await?;

    Ok(note)
}

/// Retrieves a note by its URL.
///
/// Returns the note as an ActivityPub Note object, or None if not found.
pub async fn get_note(
    db: &DatabaseConnection,
    note_url: &str,
    actor_url: &str,
) -> Result<Option<serde_json::Value>, sea_orm::DbErr> {
    // Extract the UUID from the URL
    // URL format: {base}/~{user}/notes/{uuid}
    let uuid_str = note_url.rsplit('/').next().unwrap_or("");
    let uuid = match Uuid::parse_str(uuid_str) {
        Ok(uuid) => uuid,
        Err(_) => return Ok(None),
    };

    // Find the note
    let note = ApNotes::find()
        .filter(ap_notes::Column::Uuid.eq(uuid))
        .one(db)
        .await?;

    let Some(note) = note else {
        return Ok(None);
    };

    // Find the actor to get the user name
    let actor = ApActors::find()
        .filter(ap_actors::Column::Id.eq(note.actor_id))
        .one(db)
        .await?;

    let Some(actor) = actor else {
        return Ok(None);
    };

    // Get the user to build the actor URL
    let user = match actor.user_id {
        Some(user_id) => {
            use crate::entities::prelude::Users;
            use crate::entities::users;
            use sea_orm::QueryFilter;

            Users::find()
                .filter(users::Column::Id.eq(user_id))
                .one(db)
                .await?
        }
        None => None,
    };

    let Some(_user) = user else {
        return Ok(None);
    };

    // Build the Note object
    let note_obj = Note {
        context: CONTEXT.to_vec(),
        id: note_url.to_string(),
        kind: "Note",
        attributed_to: actor_url.to_string(),
        content: note.content_html,
        media_type: None,
        url: note_url.to_string(),
        in_reply_to: None,
        source: None,
        published: note.created_at.to_rfc3339(),
        to: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
        cc: vec![],
    };

    Ok(Some(serde_json::to_value(note_obj).unwrap()))
}

/// Lists notes for an actor, newest first.
pub async fn list(
    db: &DatabaseConnection,
    actor_id: i64,
) -> Result<Vec<ap_notes::Model>, sea_orm::DbErr> {
    use sea_orm::QueryOrder;

    ApNotes::find()
        .filter(ap_notes::Column::ActorId.eq(actor_id))
        .order_by_desc(ap_notes::Column::CreatedAt)
        .all(db)
        .await
}

/// Reaction counts for a note.
pub struct NoteReactions {
    pub likes: u64,
    pub announces: u64,
    pub emojis: Vec<(String, u64)>,
}

impl NoteReactions {
    pub fn is_empty(&self) -> bool {
        self.likes == 0 && self.announces == 0 && self.emojis.is_empty()
    }
}

/// Loads reaction counts for a note.
pub async fn get_reactions(
    db: &DatabaseConnection,
    note_id: i64,
) -> Result<NoteReactions, sea_orm::DbErr> {
    use crate::entities::ap_reactions;
    use crate::entities::prelude::ApReactions;

    let reactions = ApReactions::find()
        .filter(ap_reactions::Column::NoteId.eq(note_id))
        .all(db)
        .await?;

    let mut likes = 0u64;
    let mut announces = 0u64;
    let mut emoji_counts = std::collections::HashMap::new();

    for reaction in reactions {
        match reaction.kind.as_str() {
            "like" => likes += 1,
            "announce" => announces += 1,
            "emoji_react" => {
                if let Some(emoji) = reaction.emoji {
                    *emoji_counts.entry(emoji).or_insert(0u64) += 1;
                }
            }
            _ => {}
        }
    }

    let mut emojis: Vec<(String, u64)> = emoji_counts.into_iter().collect();
    emojis.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by count descending

    Ok(NoteReactions {
        likes,
        announces,
        emojis,
    })
}

/// Deliver an activity to all followers of an actor.
///
/// Groups followers by sharedInbox when available to avoid duplicate deliveries.
async fn deliver_to_followers(
    db: &DatabaseConnection,
    actor_id: i64,
    activity_id: i64,
) -> Result<(), sea_orm::DbErr> {
    use crate::entities::prelude::{ApFollowers, ApRemoteActors};
    use crate::entities::{ap_deliveries, ap_followers};
    use sea_orm::QueryFilter;
    use std::collections::HashSet;

    // Find all followers of this actor
    let followers = ApFollowers::find()
        .filter(ap_followers::Column::LocalActorId.eq(actor_id))
        .all(db)
        .await?;

    // Collect unique inbox URLs, preferring sharedInbox when available
    let mut inbox_urls = HashSet::new();
    for follower in followers {
        let remote_actor = ApRemoteActors::find_by_id(follower.remote_actor_id)
            .one(db)
            .await?;

        if let Some(remote_actor) = remote_actor {
            // Use sharedInbox if available, otherwise use individual inbox
            let inbox_url = remote_actor
                .shared_inbox_url
                .unwrap_or(remote_actor.inbox_url);
            inbox_urls.insert(inbox_url);
        }
    }

    // Create a delivery row for each unique inbox
    for inbox_url in inbox_urls {
        let delivery = ap_deliveries::ActiveModel {
            activity_id: Set(activity_id),
            signer_actor_id: Set(actor_id),
            inbox_url: Set(inbox_url),
            status: Set("pending".to_string()),
            attempts: Set(0),
            next_attempt_at: Set(chrono::Utc::now().into()),
            last_error: Set(None),
            ..Default::default()
        };
        delivery.insert(db).await?;
    }

    Ok(())
}
