//! ForgeFed document builders for tickets, patches, and comments.

use std::path::Path;

use sea_orm::DatabaseConnection;

use crate::activitypub::ApUrls;
use crate::activitypub::types::{
    FORGEFED_CONTEXT, OrderedCollection, Patch, PatchAttachment, PatchCollection, PatchObject,
    Ticket, TicketSource,
};
use crate::entities::patch_proposals::Model as PatchProposal;
use crate::entities::repositories::Model as Repository;
use crate::entities::tickets::Model as TicketEntity;
use crate::entities::users::Model as User;
use crate::repositories;

/// Build a ForgeFed Ticket document from a ticket entity.
pub fn build_ticket_document(
    ticket: &TicketEntity,
    owner: &str,
    slug: &str,
    creator_url: &str,
    urls: &ApUrls,
) -> Ticket {
    let ticket_url = urls.repo_ticket(owner, slug, ticket.number);
    let tracker_url = urls.repo_actor(owner, slug);
    let replies_url = urls.repo_ticket_replies(owner, slug, ticket.number);

    let content_html = crate::markdown::render(&ticket.description.clone().unwrap_or_default());
    let published = ticket.created_at.to_rfc3339();
    let updated = ticket.updated_at.to_rfc3339();

    Ticket {
        context: FORGEFED_CONTEXT.to_vec(),
        id: Some(ticket_url),
        kind: "Ticket",
        attributed_to: creator_url.to_string(),
        summary: ticket.summary.clone(),
        content: content_html,
        media_type: "text/html".to_string(),
        source: TicketSource {
            media_type: "text/markdown".to_string(),
            content: ticket.description.clone().unwrap_or_default(),
        },
        tracker: tracker_url,
        is_resolved: ticket.state == crate::tickets::State::Closed,
        resolved_by: if ticket.state == crate::tickets::State::Closed {
            // Set to repo owner as we don't track who specifically closed it
            Some(urls.repo_actor(owner, slug))
        } else {
            None
        },
        replies: replies_url,
        published,
        updated: Some(updated),
    }
}

/// Build a ForgeFed Patch (MergeRequest) document from a patch proposal entity.
pub async fn build_patch_document(
    patch: &PatchProposal,
    repo: &Repository,
    owner: &str,
    bare_path: &Path,
    creator: &User,
    urls: &ApUrls,
) -> Result<Patch, String> {
    let patch_url = urls.repo_patch(owner, &repo.slug, patch.number);
    let tracker_url = urls.repo_actor(owner, &repo.slug);
    let replies_url = urls.repo_patch_replies(owner, &repo.slug, patch.number);
    let creator_url = urls.actor(&creator.name);

    let content_html = crate::markdown::render(&patch.description.clone().unwrap_or_default());
    let published = patch.created_at.to_rfc3339();
    let updated = patch.updated_at.to_rfc3339();

    // Get the diff between target and source branches
    let source_ref = format!("refs/patches/{}", patch.number);
    let range = format!("{}...{}", patch.target_branch, source_ref);
    let diff_files = repositories::git::compare_diff(bare_path, &range)
        .await
        .map_err(|e| format!("Failed to get diff: {}", e))?
        .unwrap_or_default();

    // Convert diff files to patch text
    let patch_text = format_diff_as_patch(&diff_files);

    let patch_object = PatchObject {
        kind: "Patch",
        media_type: "text/x-diff".to_string(),
        attributed_to: creator_url.clone(),
        content: patch_text,
    };

    // Build origin and target branch URLs
    let origin_url = format!("{}/branch/{}", tracker_url, source_ref);
    let target_url = format!("{}/branch/{}", tracker_url, patch.target_branch);

    Ok(Patch {
        context: FORGEFED_CONTEXT.to_vec(),
        id: patch_url,
        kind: "Patch",
        attributed_to: creator_url,
        summary: patch.summary.clone(),
        content: content_html,
        media_type: "text/html".to_string(),
        source: TicketSource {
            media_type: "text/markdown".to_string(),
            content: patch.description.clone().unwrap_or_default(),
        },
        tracker: tracker_url,
        is_resolved: matches!(
            patch.status,
            crate::entities::patch_proposals::Status::Merged
                | crate::entities::patch_proposals::Status::Closed
        ),
        resolved_by: None, // TODO: track who merged/closed
        replies: replies_url,
        published,
        updated: Some(updated),
        attachment: PatchAttachment {
            kind: "Offer",
            origin: origin_url,
            target: target_url,
            object: PatchCollection {
                kind: "OrderedCollection",
                total_items: 1,
                ordered_items: vec![patch_object],
            },
        },
    })
}

/// Format diff files as unified diff patch text.
fn format_diff_as_patch(diff_files: &[repositories::git::DiffFile]) -> String {
    let mut output = String::new();
    for file in diff_files {
        output.push_str(&format!(
            "diff --git a/{} b/{}\n",
            file.old_path, file.new_path
        ));
        output.push_str("index 0000000..0000000 100644\n");
        output.push_str(&format!("--- a/{}\n", file.old_path));
        output.push_str(&format!("+++ b/{}\n", file.new_path));
        for hunk in &file.hunks {
            output.push_str(&hunk.header);
            output.push('\n');
            for line in &hunk.lines {
                let prefix = match line.kind {
                    repositories::git::DiffLineKind::Context => " ",
                    repositories::git::DiffLineKind::Added => "+",
                    repositories::git::DiffLineKind::Removed => "-",
                };
                output.push_str(prefix);
                output.push_str(&line.text);
                output.push('\n');
            }
        }
    }
    output
}

/// Build an OrderedCollection for ticket replies (comments).
pub async fn build_ticket_replies(
    db: &DatabaseConnection,
    ticket: &TicketEntity,
    owner: &str,
    slug: &str,
    urls: &ApUrls,
) -> Result<OrderedCollection, String> {
    let replies_url = urls.repo_ticket_replies(owner, slug, ticket.number);

    let comments = crate::comments::store::list_for_ticket(db, ticket.id)
        .await
        .map_err(|e| format!("Failed to list comments: {}", e))?;

    let comment_urls: Vec<serde_json::Value> = comments
        .iter()
        .map(|c| {
            let comment_url = urls.repo_ticket_comment(owner, slug, ticket.number, c.uuid);
            serde_json::Value::String(comment_url)
        })
        .collect();

    Ok(OrderedCollection {
        context: FORGEFED_CONTEXT.to_vec(),
        id: replies_url,
        kind: "OrderedCollection",
        total_items: comment_urls.len() as u64,
        first: None,
        ordered_items: if comment_urls.is_empty() {
            None
        } else {
            Some(comment_urls)
        },
    })
}

/// Build an OrderedCollection for patch replies (comments).
pub async fn build_patch_replies(
    db: &DatabaseConnection,
    patch: &PatchProposal,
    owner: &str,
    slug: &str,
    urls: &ApUrls,
) -> Result<OrderedCollection, String> {
    let replies_url = urls.repo_patch_replies(owner, slug, patch.number);

    let comments = crate::comments::store::list_for_patch(db, patch.id)
        .await
        .map_err(|e| format!("Failed to list comments: {}", e))?;

    let comment_urls: Vec<serde_json::Value> = comments
        .iter()
        .map(|c| {
            let comment_url = urls.repo_patch_comment(owner, slug, patch.number, c.uuid);
            serde_json::Value::String(comment_url)
        })
        .collect();

    Ok(OrderedCollection {
        context: FORGEFED_CONTEXT.to_vec(),
        id: replies_url,
        kind: "OrderedCollection",
        total_items: comment_urls.len() as u64,
        first: None,
        ordered_items: if comment_urls.is_empty() {
            None
        } else {
            Some(comment_urls)
        },
    })
}

/// Build a Note document for a comment on a ticket or patch.
pub fn build_comment_note_document(
    comment: &crate::entities::comments::Model,
    owner: &str,
    slug: &str,
    ticket_or_patch_number: i64,
    is_ticket: bool,
    author: &User,
    urls: &ApUrls,
) -> crate::activitypub::types::Note {
    let comment_url = if is_ticket {
        urls.repo_ticket_comment(owner, slug, ticket_or_patch_number, comment.uuid)
    } else {
        urls.repo_patch_comment(owner, slug, ticket_or_patch_number, comment.uuid)
    };

    let parent_url = if is_ticket {
        urls.repo_ticket(owner, slug, ticket_or_patch_number)
    } else {
        urls.repo_patch(owner, slug, ticket_or_patch_number)
    };

    let tracker_url = urls.repo_actor(owner, slug);
    let followers_url = urls.repo_followers(owner, slug);
    let author_url = urls.actor(&author.name);

    let content_html = crate::markdown::render(&comment.content);
    let published = comment.created_at.to_rfc3339();

    crate::activitypub::types::Note {
        context: FORGEFED_CONTEXT.to_vec(),
        id: comment_url.clone(),
        kind: "Note",
        attributed_to: author_url,
        content: content_html,
        media_type: Some("text/html".to_string()),
        url: comment_url.clone(),
        in_reply_to: Some(parent_url),
        source: Some(crate::activitypub::types::NoteSource {
            media_type: "text/markdown".to_string(),
            content: comment.content.clone(),
        }),
        published,
        to: vec![tracker_url],
        cc: vec![followers_url],
    }
}
