//! ActivityPub federation for users and repositories.
//!
//! Each user gets an ActivityPub actor at `/{base}/~{user}`. Notes can be
//! published to the outbox and delivered to followers. Reactions (Like,
//! Announce, EmojiReact) are accepted on local notes.
//!
//! Federation is gated by `FROGUE_PUBLIC_URL`: when empty, all AP endpoints
//! return 404, no delivery worker runs, and no handle is shown in the WebUI.
//! The server still works as a standalone forge.

use url::Url;

use crate::AppState;

pub mod actor;
pub mod delivery;
pub mod feed;
pub mod following;
pub mod forge;
pub mod inbox;
pub mod keys;
pub mod notes;
pub mod outbox;
pub mod remote;
pub mod signatures;
pub mod types;
pub mod webfinger;

/// Returns true if federation is enabled (public_url is non-empty).
pub fn enabled(state: &AppState) -> bool {
    !state.public_url().is_empty()
}

/// URL builders for ActivityPub endpoints.
///
/// Constructed from `state.public_url()` once per handler; every URL in every
/// AP document comes from here, never hand-formatted elsewhere.
pub struct ApUrls {
    base: String,
}

impl ApUrls {
    /// Build from the public URL (trailing slash already stripped by AppState).
    pub fn new(public_url: &str) -> Self {
        Self {
            base: public_url.to_string(),
        }
    }

    /// Actor ID: `{base}/~{user}`.
    pub fn actor(&self, user: &str) -> String {
        format!("{}/~{}", self.base, user)
    }

    /// Key ID: `{base}/~{user}#main-key`.
    pub fn key_id(&self, user: &str) -> String {
        format!("{}#main-key", self.actor(user))
    }

    /// Inbox: `{base}/~{user}/inbox`.
    pub fn inbox(&self, user: &str) -> String {
        format!("{}/inbox", self.actor(user))
    }

    /// Outbox: `{base}/~{user}/outbox`.
    pub fn outbox(&self, user: &str) -> String {
        format!("{}/outbox", self.actor(user))
    }

    /// Followers: `{base}/~{user}/followers`.
    pub fn followers(&self, user: &str) -> String {
        format!("{}/followers", self.actor(user))
    }

    /// Note: `{base}/~{user}/notes/{uuid}`.
    pub fn note(&self, user: &str, uuid: uuid::Uuid) -> String {
        format!("{}/notes/{}", self.actor(user), uuid)
    }

    /// Activity: `{base}/~{user}/activities/{uuid}`.
    pub fn activity(&self, user: &str, uuid: uuid::Uuid) -> String {
        format!("{}/activities/{}", self.actor(user), uuid)
    }

    /// Repository actor ID: `{base}/~{owner}/{slug}`.
    pub fn repo_actor(&self, owner: &str, slug: &str) -> String {
        format!("{}/~{}/{}", self.base, owner, slug)
    }

    /// Repository key ID: `{base}/~{owner}/{slug}#main-key`.
    pub fn repo_key_id(&self, owner: &str, slug: &str) -> String {
        format!("{}#main-key", self.repo_actor(owner, slug))
    }

    /// Repository inbox: `{base}/~{owner}/{slug}/inbox`.
    pub fn repo_inbox(&self, owner: &str, slug: &str) -> String {
        format!("{}/inbox", self.repo_actor(owner, slug))
    }

    /// Repository outbox: `{base}/~{owner}/{slug}/outbox`.
    pub fn repo_outbox(&self, owner: &str, slug: &str) -> String {
        format!("{}/outbox", self.repo_actor(owner, slug))
    }

    /// Repository followers: `{base}/~{owner}/{slug}/followers`.
    pub fn repo_followers(&self, owner: &str, slug: &str) -> String {
        format!("{}/followers", self.repo_actor(owner, slug))
    }

    /// Repository activity: `{base}/~{owner}/{slug}/activities/{uuid}`.
    pub fn repo_activity(&self, owner: &str, slug: &str, uuid: uuid::Uuid) -> String {
        format!("{}/activities/{}", self.repo_actor(owner, slug), uuid)
    }

    /// Repository commit document: `{base}/~{owner}/{slug}/commits/{hash}`.
    pub fn repo_commit(&self, owner: &str, slug: &str, hash: &str) -> String {
        format!("{}/commits/{}", self.repo_actor(owner, slug), hash)
    }

    /// Repository HTTP clone URI: `{base}/{owner}/{slug}.git`.
    pub fn repo_clone_uri(&self, owner: &str, slug: &str) -> String {
        format!("{}/{}/{}.git", self.base, owner, slug)
    }

    /// Repository push URIs: HTTP clone URL + SSH URL.
    /// The SSH advertise address comes from SSH settings, not public_url.
    pub fn repo_push_uris(&self, owner: &str, slug: &str, ssh_host: &str) -> Vec<String> {
        vec![
            self.repo_clone_uri(owner, slug),
            format!("ssh://git@{}/{}/{}", ssh_host, owner, slug),
        ]
    }

    /// Ticket: `{base}/~{owner}/{slug}/tickets/{number}`.
    pub fn repo_ticket(&self, owner: &str, slug: &str, number: i64) -> String {
        format!("{}/tickets/{}", self.repo_actor(owner, slug), number)
    }

    /// Patch: `{base}/~{owner}/{slug}/patches/{number}`.
    pub fn repo_patch(&self, owner: &str, slug: &str, number: i64) -> String {
        format!("{}/patches/{}", self.repo_actor(owner, slug), number)
    }

    /// Ticket comment: `{base}/~{owner}/{slug}/tickets/{number}/comments/{uuid}`.
    pub fn repo_ticket_comment(
        &self,
        owner: &str,
        slug: &str,
        number: i64,
        uuid: uuid::Uuid,
    ) -> String {
        format!(
            "{}/comments/{}",
            self.repo_ticket(owner, slug, number),
            uuid
        )
    }

    /// Patch comment: `{base}/~{owner}/{slug}/patches/{number}/comments/{uuid}`.
    pub fn repo_patch_comment(
        &self,
        owner: &str,
        slug: &str,
        number: i64,
        uuid: uuid::Uuid,
    ) -> String {
        format!("{}/comments/{}", self.repo_patch(owner, slug, number), uuid)
    }

    /// Ticket replies: `{base}/~{owner}/{slug}/tickets/{number}/replies`.
    pub fn repo_ticket_replies(&self, owner: &str, slug: &str, number: i64) -> String {
        format!("{}/replies", self.repo_ticket(owner, slug, number))
    }

    /// Patch replies: `{base}/~{owner}/{slug}/patches/{number}/replies`.
    pub fn repo_patch_replies(&self, owner: &str, slug: &str, number: i64) -> String {
        format!("{}/replies", self.repo_patch(owner, slug, number))
    }

    /// Host (authority) of the public URL, for webfinger.
    pub fn host(&self) -> String {
        Url::parse(&self.base)
            .ok()
            .and_then(|u| {
                u.host_str().map(|h| {
                    if let Some(port) = u.port() {
                        format!("{}:{}", h, port)
                    } else {
                        h.to_string()
                    }
                })
            })
            .unwrap_or_default()
    }

    /// The base URL itself.
    pub fn base(&self) -> &str {
        &self.base
    }
}
