//! The dev-facing extension point: [`OracleHandler`].
//!
//! Implementing this trait is the **only** way a developer customizes their
//! oracle. The runtime calls [`OracleHandler::observe`] once for each
//! observation request the oracle is eligible to answer; the handler returns
//! a [`ProofClaimEvidence`] (or `None` to skip) and the runtime takes it
//! from there — populating identity, chain_id, nonce, timestamp, request_id,
//! and signing.
//!
//! Note what is **not** on this trait:
//! - No `sign`. The runtime owns signing.
//! - No `submit`. The runtime owns HTTP.
//! - No `oracle_id`. The runtime fills it from the configured identity.
//! - No `chain_id`. The runtime learns it from `GET /chain-info` at start.
//!
//! That intentional narrowness is what guarantees every oracle in the wild
//! emits byte-identical wire bytes for the same observation + claim.

use async_trait::async_trait;
use fialucci_types::{ProofClaim, ProofEvidence};

/// Re-export of the chain's observation-request struct.
pub type ObservationRequest = fialucci_types::OracleObservationRequest;

/// The pair an [`OracleHandler`] returns from [`observe`](OracleHandler::observe).
///
/// Devs decide *what* the oracle asserts and *what evidence* it attaches.
/// Everything else — identity, chain_id, nonce, timestamp, request_id binding,
/// signing — is the runtime's job.
#[derive(Debug, Clone)]
pub struct ProofClaimEvidence {
    /// What the oracle asserts about the observation request's subject.
    ///
    /// Must conform to the request's `required_claim` if the chain is going
    /// to accept it; the runtime does not enforce this — it's the dev's job
    /// to assert a claim the chain considers valid.
    pub claim: ProofClaim,
    /// Evidence backing the claim. Construct via
    /// [`fialucci_types::hash_evidence_bytes`](crate::types::hash_evidence_bytes)
    /// for the `evidence_hash` field.
    pub evidence: ProofEvidence,
}

/// The dev-facing extension point.
///
/// Implementors observe the world (via whatever off-chain data source the
/// oracle wraps) and decide what claim to submit. The runtime calls
/// [`observe`](Self::observe) for each request the configured
/// [`OracleIdentity`](crate::OracleIdentity) is eligible to answer.
///
/// Returning `Ok(None)` skips the request (the runtime will retry on the
/// next poll cycle). Returning `Err(HandlerError)` is logged but does not
/// stop the runtime — transient errors (network, source unreachable) are
/// expected.
#[async_trait]
pub trait OracleHandler: Send + Sync + 'static {
    /// Decide whether to respond to an observation request and, if so,
    /// with what claim and evidence.
    async fn observe(
        &self,
        request: &ObservationRequest,
    ) -> Result<Option<ProofClaimEvidence>, HandlerError>;
}

/// Generic error type for handler implementations.
///
/// Handlers should map their domain-specific errors into this via the
/// boxed-error variant. The SDK does not interpret variants — it logs and
/// continues — so the specific error type does not affect runtime behavior.
#[derive(Debug, thiserror::Error)]
pub enum HandlerError {
    /// Any handler-side failure. Boxed to keep the trait dyn-compatible.
    #[error("handler error: {0}")]
    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}

impl HandlerError {
    /// Wrap any string into a [`HandlerError::Other`].
    pub fn msg(s: impl Into<String>) -> Self {
        // Box a synthesised io::Error so we satisfy the Other variant's
        // boxed-error constraint without bringing in a heavier error crate.
        Self::Other(Box::new(std::io::Error::other(s.into())))
    }
}

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

    struct AlwaysSkip;

    #[async_trait]
    impl OracleHandler for AlwaysSkip {
        async fn observe(
            &self,
            _request: &ObservationRequest,
        ) -> Result<Option<ProofClaimEvidence>, HandlerError> {
            Ok(None)
        }
    }

    #[tokio::test]
    async fn trait_is_dyn_compatible() {
        // If this compiles, the trait can be used as `Arc<dyn OracleHandler>`
        // (required by the runtime).
        let _: std::sync::Arc<dyn OracleHandler> = std::sync::Arc::new(AlwaysSkip);
    }

    #[test]
    fn handler_error_msg_constructs() {
        let e = HandlerError::msg("kaboom");
        assert!(format!("{e}").contains("kaboom"));
    }
}
