//! Control posts: signed protocol operations carried as public posts that
//! receivers apply to local state (delete, update visibility) without
//! rendering in feeds.
//!
//! Wire flow:
//! 1. Author creates a `Post { author, content = ControlOp JSON, ... }` with
//!    `VisibilityIntent::Control`.
//! 2. Post propagates via CDN like any other post (header-diffs on neighbor
//!    posts ship the reference; receivers pull the control post).
//! 3. On receive, callers invoke `apply_control_post_if_applicable` to
//!    decode, verify the ControlOp's signature against the post's author,
//!    confirm the target post's author matches, and apply.
//!
//! Control posts themselves are stored with `VisibilityIntent::Control`; feed
//! queries exclude them. They remain in storage as tombstones so we can
//! re-propagate them to peers and so future arrivals of the target post are
//! rejected via the delete tombstone.

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

/// Parse the post's content as a `ControlOp`, verify its signature against
/// the post's author, verify target ownership, and apply to local storage.
/// No-op (returns Ok) if the post is not a control post. Returns an error
/// on a control post with an invalid signature or mismatched target author.
/// Callers pass an existing storage guard so the apply happens under the
/// same lock as the post-store that triggered the call.
pub fn apply_control_post_if_applicable(
    s: &Storage,
    post: &Post,
    intent: Option<&VisibilityIntent>,
) -> anyhow::Result<()> {
    if !matches!(intent, Some(VisibilityIntent::Control)) {
        return Ok(());
    }
    let op: ControlOp = serde_json::from_str(&post.content)
        .map_err(|e| anyhow::anyhow!("control post content is not a valid ControlOp: {}", e))?;
    match op {
        ControlOp::DeletePost { post_id, timestamp_ms, signature } => {
            if !crypto::verify_control_delete(&post.author, &post_id, timestamp_ms, &signature) {
                anyhow::bail!("invalid control-delete signature");
            }
            if let Some(target) = s.get_post(&post_id)? {
                if target.author != post.author {
                    anyhow::bail!("control-delete author does not match target post's author");
                }
            }
            let record = DeleteRecord {
                post_id,
                author: post.author,
                timestamp_ms,
                signature: signature.clone(),
            };
            let _ = s.store_delete(&record);
            let _ = s.apply_delete(&record);
            Ok(())
        }
        ControlOp::UpdateVisibility { post_id, new_visibility, timestamp_ms, signature } => {
            if !crypto::verify_control_visibility(&post.author, &post_id, &new_visibility, timestamp_ms, &signature) {
                anyhow::bail!("invalid control-visibility signature");
            }
            if let Some(target) = s.get_post(&post_id)? {
                if target.author != post.author {
                    anyhow::bail!("control-visibility author does not match target post's author");
                }
                let _ = s.update_post_visibility(&post_id, &new_visibility);
            }
            let _ = (timestamp_ms, new_visibility);
            Ok(())
        }
    }
}

/// Unified receive path: for every incoming post, call this instead of
/// `store_post_with_visibility` / `store_post_with_intent`. If the post is a
/// control post, the op is verified and applied atomically under the same
/// storage guard; if verification fails the post is NOT stored (so we don't
/// propagate bogus controls to other peers via neighbor-manifest diffs).
///
/// Returns Ok(true) if the post was newly stored, Ok(false) if already known,
/// and an error for control posts with invalid signatures or mismatched
/// target authors.
pub fn receive_post(
    s: &Storage,
    id: &PostId,
    post: &Post,
    visibility: &PostVisibility,
    intent: Option<&VisibilityIntent>,
) -> anyhow::Result<bool> {
    // Verify signed intent posts BEFORE storing. Bogus signed posts must
    // never enter storage and get re-propagated via neighbor-manifest diffs.
    match intent {
        Some(VisibilityIntent::Control) => {
            let op: ControlOp = serde_json::from_str(&post.content).map_err(|e| {
                anyhow::anyhow!("control post content is not a valid ControlOp: {}", e)
            })?;
            match &op {
                ControlOp::DeletePost { post_id, timestamp_ms, signature } => {
                    if !crypto::verify_control_delete(&post.author, post_id, *timestamp_ms, signature) {
                        anyhow::bail!("invalid control-delete signature");
                    }
                }
                ControlOp::UpdateVisibility { post_id, new_visibility, timestamp_ms, signature } => {
                    if !crypto::verify_control_visibility(&post.author, post_id, new_visibility, *timestamp_ms, signature) {
                        anyhow::bail!("invalid control-visibility signature");
                    }
                }
            }
        }
        Some(VisibilityIntent::Profile) => {
            crate::profile::verify_profile_post(post)?;
        }
        Some(VisibilityIntent::Announcement) => {
            // Only the hardcoded anchor posting identity may author
            // announcements. verify_announcement_post checks both the
            // author match and the signature.
            crate::announcement::verify_announcement_post(post)?;
        }
        _ => {}
    }

    // FoF Layer 2/3 hardening (pre-deploy audit): reject malformed
    // FoF gating blocks BEFORE storage. Bounds wrap_slots count,
    // enforces 1:1 wrap_slots/pub_post_set parity, validates
    // ciphertext field sizes, caps revocation_list. Also enforces
    // the FoFClosed-implies-gating invariant. Rejection prevents
    // bad data from being re-propagated via neighbor-manifest diffs.
    crate::fof::validate_fof_closed_has_gating(post, visibility)?;
    crate::fof::validate_fof_gating_on_receive(post)?;

    let stored = if let Some(intent) = intent {
        s.store_post_with_intent(id, post, visibility, intent)?
    } else {
        s.store_post_with_visibility(id, post, visibility)?
    };
    if stored {
        apply_control_post_if_applicable(s, post, intent)?;
        crate::profile::apply_profile_post_if_applicable(s, post, intent)?;
        crate::announcement::apply_announcement_if_applicable(s, post, intent)?;
    }
    Ok(stored)
}

/// Build a Post representing a control-delete operation. Caller is
/// responsible for storing and propagating it.
pub fn build_delete_control_post(
    author: &NodeId,
    author_secret: &[u8; 32],
    target_post_id: &crate::types::PostId,
) -> 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_control_delete(author_secret, target_post_id, timestamp_ms);
    let op = ControlOp::DeletePost {
        post_id: *target_post_id,
        timestamp_ms,
        signature,
    };
    Post {
        author: *author,
        content: serde_json::to_string(&op).unwrap_or_default(),
        attachments: vec![],
        timestamp_ms,
    fof_gating: None,
    supersedes_post_id: None,
    }
}

/// Build a Post representing a control-update-visibility operation. Caller
/// is responsible for storing and propagating it.
pub fn build_visibility_control_post(
    author: &NodeId,
    author_secret: &[u8; 32],
    target_post_id: &crate::types::PostId,
    new_visibility: &PostVisibility,
) -> 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_control_visibility(author_secret, target_post_id, new_visibility, timestamp_ms);
    let op = ControlOp::UpdateVisibility {
        post_id: *target_post_id,
        new_visibility: new_visibility.clone(),
        timestamp_ms,
        signature,
    };
    Post {
        author: *author,
        content: serde_json::to_string(&op).unwrap_or_default(),
        attachments: vec![],
        timestamp_ms,
    fof_gating: None,
    supersedes_post_id: None,
    }
}

#[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 control_delete_roundtrip_verifies_and_applies() {
        let s = temp_storage();
        let (author_sec, author_pub) = make_keypair(7);

        let post = Post {
            author: author_pub,
            content: "hello".to_string(),
            attachments: vec![],
            timestamp_ms: 1000,
        fof_gating: None,
        supersedes_post_id: None,
        };
        let post_id = crate::content::compute_post_id(&post);
        s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();

        let control = build_delete_control_post(&author_pub, &author_sec, &post_id);
        let control_id = crate::content::compute_post_id(&control);
        let stored = receive_post(
            &s,
            &control_id,
            &control,
            &PostVisibility::Public,
            Some(&VisibilityIntent::Control),
        ).unwrap();
        assert!(stored);
        assert!(s.is_deleted(&post_id).unwrap());
    }

    #[test]
    fn control_delete_rejects_wrong_author() {
        let s = temp_storage();
        let (_author_sec, author_pub) = make_keypair(7);
        let (other_sec, _other_pub) = make_keypair(9);

        let post = Post {
            author: author_pub,
            content: "hello".to_string(),
            attachments: vec![],
            timestamp_ms: 1000,
        fof_gating: None,
        supersedes_post_id: None,
        };
        let post_id = crate::content::compute_post_id(&post);
        s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap();

        // Sign with wrong secret → invalid signature for `author_pub`.
        let control = build_delete_control_post(&author_pub, &other_sec, &post_id);
        let control_id = crate::content::compute_post_id(&control);
        let res = receive_post(
            &s,
            &control_id,
            &control,
            &PostVisibility::Public,
            Some(&VisibilityIntent::Control),
        );
        assert!(res.is_err());
        assert!(s.get_post(&control_id).unwrap().is_none());
        assert!(!s.is_deleted(&post_id).unwrap());
    }
}
