//! Remote note storage and management.
//!
//! Handles incoming Create, Update, and Delete activities for notes from
//! remote actors. Only stores notes from actors we follow with accepted state.

use chrono::{DateTime, FixedOffset, Utc};
use sea_orm::{
    ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, Set,
};
use tracing::{debug, info, instrument, warn};

use crate::activitypub::types::{
    CreateActivity, CreateEmbedded, CreateObject, DeleteActivity, DeleteObject, UpdateActivity,
    UpdateEmbedded, UpdateObject,
};
use crate::entities::prelude::{ApFollowing, ApRemoteActors, ApRemoteNotes};
use crate::entities::{ap_following, ap_remote_actors, ap_remote_notes};

/// Store a remote note in the database.
///
/// This is a shared function used by both Create and Announce handlers.
/// Only stores notes from actors we follow with accepted state.
/// The content is sanitized to prevent XSS attacks.
/// Returns the ID of the stored note.
#[instrument(skip(db))]
pub async fn store_note(
    db: &DatabaseConnection,
    verified_actor_url: &str,
    object_id: String,
    content: String,
    published: DateTime<FixedOffset>,
    object_type: &str,
) -> Result<i64, String> {
    // Only handle Note objects
    if object_type != "Note" {
        debug!(object_type, "ignoring non-Note object");
        return Err("not a Note object".to_string());
    }

    // Verify the actor is in our following list with accepted state
    let remote_actor = ApRemoteActors::find()
        .filter(ap_remote_actors::Column::ActorUrl.eq(verified_actor_url))
        .one(db)
        .await
        .map_err(|e| format!("failed to find remote actor: {}", e))?
        .ok_or("remote actor not found")?;

    let following = ApFollowing::find()
        .filter(ap_following::Column::RemoteActorId.eq(remote_actor.id))
        .filter(ap_following::Column::State.eq("accepted"))
        .one(db)
        .await
        .map_err(|e| format!("failed to check following status: {}", e))?;

    if following.is_none() {
        debug!(
            actor_url = %verified_actor_url,
            "dropping note from non-followed actor"
        );
        return Err("actor not followed".to_string());
    }

    // Sanitize the content
    let sanitized_content = crate::markdown::sanitize_html(&content);

    // Insert the note (idempotent via unique constraint on object_url)
    let note_model = ap_remote_notes::ActiveModel {
        remote_actor_id: Set(remote_actor.id),
        object_url: Set(object_id.clone()),
        content_html: Set(sanitized_content.clone()),
        published: Set(published),
        ..Default::default()
    };

    match note_model.insert(db).await {
        Ok(result) => {
            info!(
                object_url = %object_id,
                actor_url = %verified_actor_url,
                "stored remote note"
            );
            Ok(result.id)
        }
        Err(e) if e.to_string().contains("UNIQUE constraint failed") => {
            debug!(
                object_url = %object_id,
                "note already stored (duplicate)"
            );
            // Return the existing note's ID
            let existing = ApRemoteNotes::find()
                .filter(ap_remote_notes::Column::ObjectUrl.eq(object_id))
                .one(db)
                .await
                .map_err(|e| format!("failed to find existing note: {}", e))?
                .ok_or("existing note not found")?;
            Ok(existing.id)
        }
        Err(e) => Err(format!("failed to insert remote note: {}", e)),
    }
}

/// Handle an incoming Create activity for a Note.
///
/// Only stores notes from actors we follow with accepted state. The content is
/// sanitized to prevent XSS attacks. Also stores any attachments as remote
/// URLs.
#[instrument(skip(db, http_client, create, public_url, signer))]
pub async fn handle_create(
    db: &DatabaseConnection,
    http_client: &reqwest_middleware::ClientWithMiddleware,
    verified_actor_url: &str,
    public_url: &str,
    user_name: &str,
    signer: &crate::entities::ap_actors::Model,
    create: &CreateActivity,
) -> Result<(), String> {
    // Extract the object
    let (object_id, content, published, object_type, attachments) = match &create.object {
        CreateObject::Url(url) => {
            // Fetch the object with signed GET
            debug!(url, "fetching URL-only object");
            let json = match crate::activitypub::remote::signed_get(
                http_client,
                url,
                public_url,
                signer,
                user_name,
            )
            .await
            {
                Ok(json) => json,
                Err(e) => {
                    warn!(error = %e, url, "failed to fetch URL-only object");
                    return Err(format!("failed to fetch object: {}", e));
                }
            };

            // Parse the fetched object
            let parsed: CreateEmbedded = match serde_json::from_value(json) {
                Ok(obj) => obj,
                Err(e) => {
                    warn!(error = %e, url, "failed to parse fetched object");
                    return Err(format!("failed to parse object: {}", e));
                }
            };

            let published = if let Some(pub_str) = &parsed.published {
                pub_str
                    .parse::<DateTime<Utc>>()
                    .map_err(|e| format!("failed to parse published timestamp: {}", e))?
                    .into()
            } else {
                Utc::now().into()
            };

            (
                parsed.id.clone(),
                parsed.content.clone().unwrap_or_default(),
                published,
                parsed.kind.clone(),
                parsed.attachment.clone(),
            )
        }
        CreateObject::Embedded(embedded) => {
            let published = if let Some(pub_str) = &embedded.published {
                pub_str
                    .parse::<DateTime<Utc>>()
                    .map_err(|e| format!("failed to parse published timestamp: {}", e))?
                    .into()
            } else {
                Utc::now().into()
            };
            (
                embedded.id.clone(),
                embedded.content.clone().unwrap_or_default(),
                published,
                embedded.kind.clone(),
                embedded.attachment.clone(),
            )
        }
    };

    let note_id = store_note(
        db,
        verified_actor_url,
        object_id,
        content,
        published,
        &object_type,
    )
    .await?;

    // Store attachments as remote URLs (never fetch the bytes)
    if let Some(attachment_list) = attachments {
        for attachment in attachment_list {
            // Only store Document attachments with URLs
            if attachment.kind != "Document" {
                continue;
            }

            let filename = attachment.name.unwrap_or_else(|| "attachment".to_string());
            let content_type = attachment
                .media_type
                .unwrap_or_else(|| "application/octet-stream".to_string());

            match crate::attachments::store::create_remote(
                db,
                crate::attachments::store::Owner::ApNote(note_id),
                filename,
                content_type,
                attachment.url,
            )
            .await
            {
                Ok(_) => {
                    debug!("stored remote attachment for note");
                }
                Err(e) => {
                    warn!(error = %e, "failed to store remote attachment");
                    // Continue processing other attachments
                }
            }
        }
    }

    Ok(())
}

/// Handle an incoming Update activity for a Note.
///
/// Only updates notes that we've previously stored. The content is
/// re-sanitized.
#[instrument(skip(db, http_client, update, public_url, signer))]
pub async fn handle_update(
    db: &DatabaseConnection,
    http_client: &reqwest_middleware::ClientWithMiddleware,
    verified_actor_url: &str,
    public_url: &str,
    user_name: &str,
    signer: &crate::entities::ap_actors::Model,
    update: &UpdateActivity,
) -> Result<(), String> {
    // Extract the object
    let (object_id, content, published, object_type, attachments) = match &update.object {
        UpdateObject::Url(url) => {
            // Fetch the object with signed GET (best-effort)
            debug!(url, "fetching URL-only object for Update");
            let json = match crate::activitypub::remote::signed_get(
                http_client,
                url,
                public_url,
                signer,
                user_name,
            )
            .await
            {
                Ok(json) => json,
                Err(e) => {
                    warn!(error = %e, url, "failed to fetch URL-only object for Update");
                    return Ok(()); // Best-effort, skip with log
                }
            };

            // Parse the fetched object
            let parsed: UpdateEmbedded = match serde_json::from_value(json) {
                Ok(obj) => obj,
                Err(e) => {
                    warn!(error = %e, url, "failed to parse fetched object");
                    return Ok(()); // Best-effort, skip with log
                }
            };

            let published = if let Some(pub_str) = &parsed.published {
                pub_str
                    .parse::<DateTime<Utc>>()
                    .map_err(|e| format!("failed to parse published timestamp: {}", e))?
                    .into()
            } else {
                Utc::now().into()
            };

            (
                parsed.id.clone(),
                parsed.content.clone().unwrap_or_default(),
                published,
                parsed.kind.clone(),
                parsed.attachment.clone(),
            )
        }
        UpdateObject::Embedded(embedded) => {
            let published = if let Some(pub_str) = &embedded.published {
                pub_str
                    .parse::<DateTime<Utc>>()
                    .map_err(|e| format!("failed to parse published timestamp: {}", e))?
                    .into()
            } else {
                Utc::now().into()
            };
            (
                embedded.id.clone(),
                embedded.content.clone().unwrap_or_default(),
                published,
                embedded.kind.clone(),
                embedded.attachment.clone(),
            )
        }
    };

    // Only handle Note objects
    if object_type != "Note" {
        debug!(object_type, "ignoring Update of non-Note object");
        return Ok(());
    }

    // Find the existing note
    let existing_note = ApRemoteNotes::find()
        .filter(ap_remote_notes::Column::ObjectUrl.eq(&object_id))
        .one(db)
        .await
        .map_err(|e| format!("failed to find remote note: {}", e))?
        .ok_or_else(|| {
            debug!(object_url = %object_id, "Update for unknown note");
            "note not found".to_string()
        })?;

    // Verify the actor owns the note
    if existing_note.remote_actor_id != {
        let remote_actor = ApRemoteActors::find()
            .filter(ap_remote_actors::Column::ActorUrl.eq(verified_actor_url))
            .one(db)
            .await
            .map_err(|e| format!("failed to find remote actor: {}", e))?
            .ok_or("remote actor not found")?;
        remote_actor.id
    } {
        warn!(
            object_url = %object_id,
            actor_url = %verified_actor_url,
            "Update from non-owner actor"
        );
        return Ok(());
    }

    // Sanitize the updated content
    let sanitized_content = crate::markdown::sanitize_html(&content);

    // Save the note ID before moving existing_note
    let existing_note_id = existing_note.id;

    // Update the note
    let mut active_model: ap_remote_notes::ActiveModel = existing_note.into();
    active_model.content_html = Set(sanitized_content);
    active_model.published = Set(published);

    active_model
        .update(db)
        .await
        .map_err(|e| format!("failed to update remote note: {}", e))?;

    // Handle attachments: delete existing and add new ones
    if let Some(attachment_list) = attachments {
        // Delete existing attachments for this note
        let existing_attachments = crate::attachments::store::list_for(
            db,
            &crate::attachments::store::Owner::ApNote(existing_note_id),
        )
        .await
        .map_err(|e| format!("failed to list existing attachments: {}", e))?;

        for att in existing_attachments {
            crate::attachments::store::delete_remote(db, att.id)
                .await
                .map_err(|e| format!("failed to delete old attachment: {}", e))?;
        }

        // Add new attachments
        for attachment in attachment_list {
            // Only store Document attachments with URLs
            if attachment.kind != "Document" {
                continue;
            }

            let filename = attachment.name.unwrap_or_else(|| "attachment".to_string());
            let content_type = attachment
                .media_type
                .unwrap_or_else(|| "application/octet-stream".to_string());

            match crate::attachments::store::create_remote(
                db,
                crate::attachments::store::Owner::ApNote(existing_note_id),
                filename,
                content_type,
                attachment.url,
            )
            .await
            {
                Ok(_) => {
                    debug!("stored remote attachment for updated note");
                }
                Err(e) => {
                    warn!(error = %e, "failed to store remote attachment on update");
                    // Continue processing other attachments
                }
            }
        }
    }

    info!(
        object_url = %object_id,
        actor_url = %verified_actor_url,
        "updated remote note"
    );

    Ok(())
}

/// Handle an incoming Delete activity for a Note.
///
/// Removes the note from storage if it exists and the actor owns it.
#[instrument(skip(db, delete))]
pub async fn handle_delete(
    db: &DatabaseConnection,
    verified_actor_url: &str,
    delete: &DeleteActivity,
) -> Result<(), String> {
    // Extract the object ID
    let object_id = match &delete.object {
        DeleteObject::Url(url) => url.clone(),
        DeleteObject::Embedded(embedded) => embedded.id.clone(),
    };

    // Find the existing note
    let existing_note = ApRemoteNotes::find()
        .filter(ap_remote_notes::Column::ObjectUrl.eq(&object_id))
        .one(db)
        .await
        .map_err(|e| format!("failed to find remote note: {}", e))?
        .ok_or_else(|| {
            debug!(object_url = %object_id, "Delete for unknown note");
            "note not found".to_string()
        })?;

    // Verify the actor owns the note
    let remote_actor = ApRemoteActors::find()
        .filter(ap_remote_actors::Column::ActorUrl.eq(verified_actor_url))
        .one(db)
        .await
        .map_err(|e| format!("failed to find remote actor: {}", e))?
        .ok_or("remote actor not found")?;

    if existing_note.remote_actor_id != remote_actor.id {
        warn!(
            object_url = %object_id,
            actor_url = %verified_actor_url,
            "Delete from non-owner actor"
        );
        return Ok(());
    }

    // Delete the note
    existing_note
        .delete(db)
        .await
        .map_err(|e| format!("failed to delete remote note: {}", e))?;

    info!(
        object_url = %object_id,
        actor_url = %verified_actor_url,
        "deleted remote note"
    );

    Ok(())
}
