//! Profile posts: persona display metadata (display_name, bio, avatar_cid)
//! carried as a signed public post with `VisibilityIntent::Profile`.
//!
//! The post's `author` is the posting identity; the signature inside
//! `ProfilePostContent` is by that identity's secret. Profile posts propagate
//! via the normal CDN path (pull + header-diff). Receivers verify the
//! signature, then upsert a row in the `profiles` table keyed by the post's
//! author (= posting identity) with the new display fields.
//!
//! Profile posts are never rendered in feeds — the feed filter excludes
//! `VisibilityIntent::Profile` posts (see `Storage::get_feed*`).

use crate::crypto;
use crate::storage::Storage;
use crate::types::{NodeId, Post, PostId, PostVisibility, ProfilePostContent, PublicProfile, VisibilityIntent};

/// Verify a profile-post signature without any other side effects. Used by
/// receive paths before storing, so bogus profile posts with invalid
/// signatures never enter storage and can't be re-propagated.
pub fn verify_profile_post(post: &Post) -> anyhow::Result<ProfilePostContent> {
    let content: ProfilePostContent = serde_json::from_str(&post.content)
        .map_err(|e| anyhow::anyhow!("profile post content is not a valid ProfilePostContent: {}", e))?;
    if !crypto::verify_profile(
        &post.author,
        &content.display_name,
        &content.bio,
        &content.avatar_cid,
        content.timestamp_ms,
        &content.signature,
    ) {
        anyhow::bail!("invalid profile-post signature");
    }
    Ok(content)
}

/// If the post is a Profile post, verify + apply by upserting the
/// `profiles` row keyed by the post's author (= posting identity). Only
/// applied if newer than the existing row's `updated_at`.
pub fn apply_profile_post_if_applicable(
    s: &Storage,
    post: &Post,
    intent: Option<&VisibilityIntent>,
) -> anyhow::Result<()> {
    if !matches!(intent, Some(VisibilityIntent::Profile)) {
        return Ok(());
    }
    let content = verify_profile_post(post)?;

    // Only apply if newer than the stored row (last-writer-wins by timestamp).
    if let Some(existing) = s.get_profile(&post.author)? {
        if existing.updated_at >= content.timestamp_ms {
            return Ok(());
        }
    }

    let profile = PublicProfile {
        node_id: post.author,
        display_name: content.display_name,
        bio: content.bio,
        updated_at: content.timestamp_ms,
        anchors: vec![],
        recent_peers: vec![],
        preferred_peers: vec![],
        public_visible: true,
        avatar_cid: content.avatar_cid,
    };
    s.store_profile(&profile)?;
    Ok(())
}

/// Build a Profile post signed by the posting identity. Caller is
/// responsible for storing and propagating it.
pub fn build_profile_post(
    author: &NodeId,
    author_secret: &[u8; 32],
    display_name: &str,
    bio: &str,
    avatar_cid: Option<[u8; 32]>,
) -> Post {
    let timestamp_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0);
    let signature = crypto::sign_profile(author_secret, display_name, bio, &avatar_cid, timestamp_ms);
    let content = ProfilePostContent {
        display_name: display_name.to_string(),
        bio: bio.to_string(),
        avatar_cid,
        timestamp_ms,
        signature,
    };
    Post {
        author: *author,
        content: serde_json::to_string(&content).unwrap_or_default(),
        attachments: vec![],
        timestamp_ms,
    }
}

/// Profile-post visibility is always Public on the wire: the signature binds
/// the content to the posting identity and no recipient targeting is needed.
pub fn profile_post_visibility() -> PostVisibility {
    PostVisibility::Public
}

/// Compute the `PostId` for a freshly-built profile post.
pub fn profile_post_id(post: &Post) -> PostId {
    crate::content::compute_post_id(post)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::storage::Storage;
    use ed25519_dalek::SigningKey;

    fn temp_storage() -> Storage {
        Storage::open(":memory:").unwrap()
    }

    fn make_keypair(seed_byte: u8) -> ([u8; 32], NodeId) {
        let seed = [seed_byte; 32];
        let signing_key = SigningKey::from_bytes(&seed);
        let public = signing_key.verifying_key();
        (seed, *public.as_bytes())
    }

    #[test]
    fn profile_roundtrip_verifies_and_stores() {
        let s = temp_storage();
        let (sec, pub_id) = make_keypair(11);

        let post = build_profile_post(&pub_id, &sec, "Alice", "hello world", None);
        apply_profile_post_if_applicable(&s, &post, Some(&VisibilityIntent::Profile)).unwrap();

        let stored = s.get_profile(&pub_id).unwrap().expect("profile stored");
        assert_eq!(stored.display_name, "Alice");
        assert_eq!(stored.bio, "hello world");
    }

    #[test]
    fn profile_rejects_wrong_author_signature() {
        let s = temp_storage();
        let (_sec_a, pub_a) = make_keypair(1);
        let (sec_b, _pub_b) = make_keypair(2);

        // Build a post claiming `pub_a` but signing with `sec_b`.
        let post = build_profile_post(&pub_a, &sec_b, "Impostor", "", None);
        let res = apply_profile_post_if_applicable(&s, &post, Some(&VisibilityIntent::Profile));
        assert!(res.is_err());
        assert!(s.get_profile(&pub_a).unwrap().is_none());
    }

    #[test]
    fn profile_ignores_older_timestamp() {
        let s = temp_storage();
        let (sec, pub_id) = make_keypair(3);

        // Seed with a newer profile.
        let mut newer = build_profile_post(&pub_id, &sec, "NewName", "", None);
        // Hack the timestamp to make it clearly newer.
        let mut content: ProfilePostContent = serde_json::from_str(&newer.content).unwrap();
        content.timestamp_ms = 10_000;
        content.signature = crypto::sign_profile(&sec, &content.display_name, &content.bio, &content.avatar_cid, content.timestamp_ms);
        newer.content = serde_json::to_string(&content).unwrap();
        newer.timestamp_ms = 10_000;
        apply_profile_post_if_applicable(&s, &newer, Some(&VisibilityIntent::Profile)).unwrap();

        // Apply an older profile — should be ignored.
        let mut older = build_profile_post(&pub_id, &sec, "OldName", "", None);
        let mut content_o: ProfilePostContent = serde_json::from_str(&older.content).unwrap();
        content_o.timestamp_ms = 5_000;
        content_o.signature = crypto::sign_profile(&sec, &content_o.display_name, &content_o.bio, &content_o.avatar_cid, content_o.timestamp_ms);
        older.content = serde_json::to_string(&content_o).unwrap();
        older.timestamp_ms = 5_000;
        apply_profile_post_if_applicable(&s, &older, Some(&VisibilityIntent::Profile)).unwrap();

        let stored = s.get_profile(&pub_id).unwrap().unwrap();
        assert_eq!(stored.display_name, "NewName");
    }
}
