//! Ergonomic builder for [`OracleDataSubmissionBody`].
//!
//! Provides a fluent API on top of `fialucci-types`'s raw struct. The builder
//! does **not** sign — signing is the runtime's job, gated by the identity's
//! crate-private `sign_body`. The dev sees this surface only via
//! [`OracleHandler::observe`](crate::OracleHandler::observe), which returns
//! a [`ProofClaimEvidence`](crate::ProofClaimEvidence) — the runtime then
//! fills in `chain_id`, `oracle_id`, `nonce`, `timestamp_ms`, and
//! `observation_request_id` itself before signing.

use std::time::{SystemTime, UNIX_EPOCH};

use fialucci_types::{
    OracleDataSubmissionBody, OracleId, OracleObservationRequestId, ProofClaim, ProofDomain,
    ProofEvidence, ProofSubject,
};

/// Fluent builder for [`OracleDataSubmissionBody`].
///
/// Used by the runtime when composing a body from the request context + the
/// dev's [`ProofClaimEvidence`](crate::ProofClaimEvidence). Also useful to
/// developers writing one-off submissions in `examples/` or tests.
#[derive(Debug, Default, Clone)]
pub struct ProofBodyBuilder {
    chain_id: Option<String>,
    domain: Option<ProofDomain>,
    subject: Option<ProofSubject>,
    claim: Option<ProofClaim>,
    evidence: Option<ProofEvidence>,
    oracle_id: Option<OracleId>,
    timestamp_ms: Option<u128>,
    nonce: Option<u64>,
    observation_request_id: Option<OracleObservationRequestId>,
}

impl ProofBodyBuilder {
    /// Start a new empty builder.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the chain ID the proof is bound to. **Must** match the verifying
    /// node's `FIALUCCI_CHAIN_ID`; the chain rejects any mismatch.
    pub fn chain_id(mut self, chain_id: impl Into<String>) -> Self {
        self.chain_id = Some(chain_id.into());
        self
    }

    /// Set the data domain — what kind of evidence this proof carries.
    pub fn domain(mut self, domain: ProofDomain) -> Self {
        self.domain = Some(domain);
        self
    }

    /// Set the subject — what entity the claim refers to.
    pub fn subject(mut self, subject: ProofSubject) -> Self {
        self.subject = Some(subject);
        self
    }

    /// Set the claim — what the oracle asserts about the subject.
    pub fn claim(mut self, claim: ProofClaim) -> Self {
        self.claim = Some(claim);
        self
    }

    /// Set the evidence package directly.
    pub fn evidence(mut self, evidence: ProofEvidence) -> Self {
        self.evidence = Some(evidence);
        self
    }

    /// Set the oracle identity that will sign this proof.
    ///
    /// Filled in by the runtime from the configured [`OracleIdentity`](crate::OracleIdentity)
    /// — devs constructing a body manually for one-off use also need to
    /// supply it here.
    pub fn oracle_id(mut self, oracle_id: OracleId) -> Self {
        self.oracle_id = Some(oracle_id);
        self
    }

    /// Set the timestamp explicitly.
    pub fn timestamp_ms(mut self, ts: u128) -> Self {
        self.timestamp_ms = Some(ts);
        self
    }

    /// Set the timestamp to the current Unix-epoch milliseconds.
    pub fn timestamp_now(mut self) -> Self {
        self.timestamp_ms = Some(unix_ms_now());
        self
    }

    /// Set the per-oracle sequential nonce. Lets the same oracle submit
    /// multiple distinct proofs for the same subject.
    pub fn nonce(mut self, nonce: u64) -> Self {
        self.nonce = Some(nonce);
        self
    }

    /// Link this submission to an `OracleObservationRequest`. When `Some`,
    /// the field is included in canonical serialization (and so in the
    /// submission ID and signature).
    pub fn observation_request_id(mut self, id: OracleObservationRequestId) -> Self {
        self.observation_request_id = Some(id);
        self
    }

    /// Finalize the builder into an unsigned [`OracleDataSubmissionBody`].
    ///
    /// Required fields: `chain_id`, `domain`, `subject`, `claim`, `evidence`,
    /// `oracle_id`. `timestamp_ms` defaults to `now`; `nonce` defaults to
    /// `0`; `observation_request_id` defaults to `None`.
    pub fn build(self) -> Result<OracleDataSubmissionBody, ProofBodyBuildError> {
        Ok(OracleDataSubmissionBody {
            chain_id: self
                .chain_id
                .ok_or(ProofBodyBuildError::MissingField("chain_id"))?,
            domain: self
                .domain
                .ok_or(ProofBodyBuildError::MissingField("domain"))?,
            subject: self
                .subject
                .ok_or(ProofBodyBuildError::MissingField("subject"))?,
            claim: self
                .claim
                .ok_or(ProofBodyBuildError::MissingField("claim"))?,
            evidence: self
                .evidence
                .ok_or(ProofBodyBuildError::MissingField("evidence"))?,
            oracle_id: self
                .oracle_id
                .ok_or(ProofBodyBuildError::MissingField("oracle_id"))?,
            timestamp_ms: self.timestamp_ms.unwrap_or_else(unix_ms_now),
            nonce: self.nonce.unwrap_or(0),
            observation_request_id: self.observation_request_id,
        })
    }
}

/// Errors surfaced by [`ProofBodyBuilder::build`].
#[derive(Debug, thiserror::Error)]
pub enum ProofBodyBuildError {
    /// A required field was not supplied.
    #[error("required field `{0}` not set on ProofBodyBuilder")]
    MissingField(&'static str),
}

fn unix_ms_now() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_millis())
        .unwrap_or(0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use fialucci_types::hash_evidence_bytes;

    fn dummy_evidence() -> ProofEvidence {
        ProofEvidence {
            evidence_hash: hash_evidence_bytes(b"hello"),
            evidence_uri: None,
            content_type: None,
            metadata: None,
        }
    }

    #[test]
    fn missing_chain_id_errors() {
        let err = ProofBodyBuilder::new()
            .domain(ProofDomain::Manual)
            .subject(ProofSubject::ExternalId("x".into()))
            .claim(ProofClaim::WorkCompleted)
            .evidence(dummy_evidence())
            .oracle_id(OracleId([0u8; 32]))
            .build()
            .unwrap_err();
        assert!(matches!(err, ProofBodyBuildError::MissingField("chain_id")));
    }

    #[test]
    fn timestamp_defaults_to_now_when_unset() {
        let body = ProofBodyBuilder::new()
            .chain_id("fialucci-test")
            .domain(ProofDomain::Manual)
            .subject(ProofSubject::ExternalId("x".into()))
            .claim(ProofClaim::WorkCompleted)
            .evidence(dummy_evidence())
            .oracle_id(OracleId([0u8; 32]))
            .build()
            .unwrap();
        // Hard to assert "now" exactly; just check non-zero (since we ran
        // after 1970-01-01).
        assert!(body.timestamp_ms > 0);
    }

    #[test]
    fn observation_request_id_round_trips() {
        let req = OracleObservationRequestId([7u8; 32]);
        let body = ProofBodyBuilder::new()
            .chain_id("fialucci-test")
            .domain(ProofDomain::Manual)
            .subject(ProofSubject::ExternalId("x".into()))
            .claim(ProofClaim::WorkCompleted)
            .evidence(dummy_evidence())
            .oracle_id(OracleId([0u8; 32]))
            .observation_request_id(req)
            .build()
            .unwrap();
        assert_eq!(body.observation_request_id, Some(req));
    }
}
