//! The comment store: the rows behind a ticket's or patch proposal's
//! comments, and the lookups the webui makes of them.
//!
//! Split from [`super`], which holds the rules that do not need a database.

use std::collections::HashMap;

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

use crate::entities::{comments, users};

/// The comments on a ticket, in the order they were posted.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn list_for_ticket(
    db: &DatabaseConnection,
    ticket_id: i64,
) -> Result<Vec<comments::Model>, DbErr> {
    comments::Entity::find()
        .filter(comments::Column::TicketId.eq(ticket_id))
        .order_by_asc(comments::Column::CreatedAt)
        .all(db)
        .await
}

/// The comments on a patch proposal, in the order they were posted.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn list_for_patch(
    db: &DatabaseConnection,
    patch_proposal_id: i64,
) -> Result<Vec<comments::Model>, DbErr> {
    comments::Entity::find()
        .filter(comments::Column::PatchProposalId.eq(patch_proposal_id))
        .order_by_asc(comments::Column::CreatedAt)
        .all(db)
        .await
}

/// One comment, addressed by the UUID it was given.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn find_by_uuid(
    db: &DatabaseConnection,
    uuid: Uuid,
) -> Result<Option<comments::Model>, DbErr> {
    comments::Entity::find()
        .filter(comments::Column::Uuid.eq(uuid))
        .one(db)
        .await
}

/// A new comment on `ticket_id` by `author_id`.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn create_on_ticket(
    db: &DatabaseConnection,
    author_id: i64,
    ticket_id: i64,
    content: &str,
) -> Result<comments::Model, DbErr> {
    comments::ActiveModel {
        uuid: Set(Uuid::new_v4()),
        author_id: Set(Some(author_id)),
        remote_actor_id: Set(None),
        remote_object_id: Set(None),
        ticket_id: Set(Some(ticket_id)),
        patch_proposal_id: Set(None),
        content: Set(content.to_string()),
        content_format: Set("markdown".to_string()),
        ..Default::default()
    }
    .insert(db)
    .await
}

/// A new comment on `patch_proposal_id` by `author_id`.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn create_on_patch(
    db: &DatabaseConnection,
    author_id: i64,
    patch_proposal_id: i64,
    content: &str,
) -> Result<comments::Model, DbErr> {
    comments::ActiveModel {
        uuid: Set(Uuid::new_v4()),
        author_id: Set(Some(author_id)),
        remote_actor_id: Set(None),
        remote_object_id: Set(None),
        ticket_id: Set(None),
        patch_proposal_id: Set(Some(patch_proposal_id)),
        content: Set(content.to_string()),
        content_format: Set("markdown".to_string()),
        ..Default::default()
    }
    .insert(db)
    .await
}

/// A new comment on a ticket from a remote actor (inbound federation).
///
/// The `remote_object_id` is the Note activity's id, used for idempotency.
/// `content_format` is either "markdown" or "html" depending on the source.
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(level = "debug", skip(db))]
pub async fn create_on_ticket_from_remote(
    db: &DatabaseConnection,
    remote_actor_id: i64,
    ticket_id: i64,
    content: &str,
    content_format: &str,
    remote_object_id: &str,
) -> Result<comments::Model, DbErr> {
    comments::ActiveModel {
        uuid: Set(Uuid::new_v4()),
        author_id: Set(None),
        remote_actor_id: Set(Some(remote_actor_id)),
        remote_object_id: Set(Some(remote_object_id.to_string())),
        ticket_id: Set(Some(ticket_id)),
        patch_proposal_id: Set(None),
        content: Set(content.to_string()),
        content_format: Set(content_format.to_string()),
        ..Default::default()
    }
    .insert(db)
    .await
}

/// A new comment on a patch proposal from a remote actor (inbound federation).
///
/// The `remote_object_id` is the Note activity's id, used for idempotency.
/// `content_format` is either "markdown" or "html" depending on the source.
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(level = "debug", skip(db))]
pub async fn create_on_patch_from_remote(
    db: &DatabaseConnection,
    remote_actor_id: i64,
    patch_proposal_id: i64,
    content: &str,
    content_format: &str,
    remote_object_id: &str,
) -> Result<comments::Model, DbErr> {
    comments::ActiveModel {
        uuid: Set(Uuid::new_v4()),
        author_id: Set(None),
        remote_actor_id: Set(Some(remote_actor_id)),
        remote_object_id: Set(Some(remote_object_id.to_string())),
        ticket_id: Set(None),
        patch_proposal_id: Set(Some(patch_proposal_id)),
        content: Set(content.to_string()),
        content_format: Set(content_format.to_string()),
        ..Default::default()
    }
    .insert(db)
    .await
}

/// A comment as it was edited to be, said in the words of the columns rather
/// than in SQLite's.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn update(db: &DatabaseConnection, comment_id: i64, content: &str) -> Result<(), DbErr> {
    comments::Entity::update_many()
        .filter(comments::Column::Id.eq(comment_id))
        .set(comments::ActiveModel {
            content: Set(content.to_string()),
            updated_at: Set(Some(chrono::Utc::now().fixed_offset())),
            ..Default::default()
        })
        .exec(db)
        .await?;

    Ok(())
}

/// A comment taken down, gone without a trace.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn delete(db: &DatabaseConnection, comment_id: i64) -> Result<(), DbErr> {
    comments::Entity::delete_many()
        .filter(comments::Column::Id.eq(comment_id))
        .exec(db)
        .await?;

    Ok(())
}

/// Information about a comment author, either local or remote.
#[derive(Clone, Debug)]
pub struct CommentAuthor {
    /// Display name (local username or remote display name)
    pub name: String,
    /// Remote actor URL (None for local users)
    pub actor_url: Option<String>,
    /// Whether this is a remote author
    pub is_remote: bool,
}

/// The authors who wrote comments, looking up both local users and remote actors.
#[tracing::instrument(level = "debug", skip(db, author_ids, remote_actor_ids))]
pub async fn comment_authors(
    db: &DatabaseConnection,
    author_ids: &[i64],
    remote_actor_ids: &[i64],
) -> Result<HashMap<i64, CommentAuthor>, DbErr> {
    let mut authors = HashMap::new();

    // Look up local users
    if !author_ids.is_empty() {
        for user in users::Entity::find()
            .filter(users::Column::Id.is_in(author_ids.to_vec()))
            .all(db)
            .await?
        {
            authors.insert(
                user.id,
                CommentAuthor {
                    name: user.name,
                    actor_url: None,
                    is_remote: false,
                },
            );
        }
    }

    // Look up remote actors
    if !remote_actor_ids.is_empty() {
        use crate::entities::ap_remote_actors;
        for actor in ap_remote_actors::Entity::find()
            .filter(ap_remote_actors::Column::Id.is_in(remote_actor_ids.to_vec()))
            .all(db)
            .await?
        {
            // Use preferred_username if available, otherwise extract from actor_url
            let name = actor
                .preferred_username
                .or_else(|| actor.actor_url.rsplit('/').next().map(|s| s.to_string()))
                .unwrap_or_else(|| "unknown".to_string());

            authors.insert(
                actor.id,
                CommentAuthor {
                    name,
                    actor_url: Some(actor.actor_url),
                    is_remote: true,
                },
            );
        }
    }

    Ok(authors)
}

/// The names of the users who wrote `author_ids`, for saying on a comment who
/// wrote it. This is the legacy function for backward compatibility.
#[tracing::instrument(level = "debug", skip(db, author_ids))]
pub async fn author_names(
    db: &DatabaseConnection,
    author_ids: &[i64],
) -> Result<HashMap<i64, String>, DbErr> {
    let mut names = HashMap::new();
    if author_ids.is_empty() {
        return Ok(names);
    }

    for user in users::Entity::find()
        .filter(users::Column::Id.is_in(author_ids.to_vec()))
        .all(db)
        .await?
    {
        names.insert(user.id, user.name);
    }

    Ok(names)
}
