//! `Offer` — sender-initiated proximity offer (= offer-first mode).
//!
//! Spec reference: `draft-vandemeent-iddrop-00` §5.1, §6.3, Appendix A.

use serde::{Deserialize, Serialize};

/// Spec §5.1 + §6.3: sender-initiated proximity offer.
///
/// An offer is broadcast over a proximity channel (NFC tap, QR scan,
/// BLE beacon) so that nearby receivers can choose to accept,
/// challenge, or reject. Offers MUST be temporary (TTL-bound), MUST
/// be non-discoverable by default, and MUST NOT be auto-accepted.
///
/// Required fields per the draft: `offer_id`, `expires_at`,
/// `sender_pubkey`, `claimed_aint`, `claim_class`, `semantic_type`,
/// `display_name`, `preview_hash`, `ssm_class`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Offer {
    /// Protocol version, always [`crate::PROTOCOL_VERSION`].
    pub protocol: String,

    /// Unique offer id — used by receiver to bind acceptance + sealed transfer.
    pub offer_id: String,

    /// Unix-seconds when this offer expires. MUST be finite + short
    /// (typical: 30-120s for in-person handoff).
    pub expires_at: u64,

    /// Ed25519 pubkey of the sender (hex, 64 chars). Receiver checks
    /// this against AINS for `claimed_aint` (Rule 4).
    pub sender_pubkey: String,

    /// AINS handle being offered (e.g. `"jasper.aint"`).
    pub claimed_aint: String,

    /// Kind of claim — typically `"identity"`.
    pub claim_class: String,

    /// Semantic class on the wire — matches TBZ v2 `PayloadClass::Identity`.
    pub semantic_type: String,

    /// Human-readable display name (= shown in the receiver's accept dialog).
    pub display_name: String,

    /// SHA-256 hash of a preview manifest the receiver can fetch before
    /// committing. Optional — when present, receiver can inspect what
    /// is being offered before accepting.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub preview_hash: Option<String>,

    /// SSM (Semantic Surface Manifest) class hint for routing/triage.
    /// Matches the SSM byte's `payload_class` semantics.
    pub ssm_class: String,
}

impl Offer {
    /// Build a fresh offer for an identity drop.
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        sender_pubkey: impl Into<String>,
        claimed_aint: impl Into<String>,
        display_name: impl Into<String>,
        ttl_seconds: u64,
    ) -> Self {
        use rand::RngCore;
        let mut id = [0u8; 16];
        rand::thread_rng().fill_bytes(&mut id);
        let now = crate::claim::current_unix_seconds();
        Self {
            protocol: crate::PROTOCOL_VERSION.to_string(),
            offer_id: hex::encode(id),
            expires_at: now + ttl_seconds,
            sender_pubkey: sender_pubkey.into(),
            claimed_aint: claimed_aint.into(),
            claim_class: "identity".to_string(),
            semantic_type: "identity".to_string(),
            display_name: display_name.into(),
            preview_hash: None,
            ssm_class: "identity".to_string(),
        }
    }
}
