//! Opinionated runtime — the part that makes this SDK "not deployable" in
//! the dev's hands.
//!
//! Devs configure an [`Oracle`] via [`OracleBuilder`] (identity, node URL,
//! handler, poll cadence) and call [`Oracle::run`]; the runtime owns
//! everything after that:
//!
//! 1. On `build()` — `GET /chain-info` to discover `chain_id`. The dev
//!    cannot override this (the chain rejects mismatched chain_ids anyway,
//!    so allowing override only invites misconfiguration).
//! 2. On `run()` — poll `GET /oracle/observation-requests/matching` at the
//!    configured cadence.
//! 3. For each returned request, call [`OracleHandler::observe`].
//! 4. If the handler returned `Some(ProofClaimEvidence)`, the runtime fills
//!    in identity, chain_id, nonce, timestamp, and the observation-request
//!    binding, signs via the identity's crate-private signing call, and
//!    `POST`s to `/oracle/observation-requests/{id}/submissions`.
//! 5. On any error short of an explicit shutdown signal, log and continue.
//!
//! Notably absent: a way for the dev to skip signing, batch-sign, sign with
//! a different key per-request, or post anywhere other than the configured
//! node. The trade-off is intentional — every oracle in the wild has
//! identical wire behavior.

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;

use fialucci_types::{OracleDataSubmissionSigningError, ProofDomain};

use crate::client::{ChainClient, ClientError, SubmissionAck};
use crate::handler::{HandlerError, ObservationRequest, OracleHandler, ProofClaimEvidence};
use crate::identity::OracleIdentity;
use crate::proof_body::{ProofBodyBuildError, ProofBodyBuilder};

/// The configured, running oracle.
///
/// Build via [`Oracle::builder`].
pub struct Oracle {
    identity: OracleIdentity,
    client: ChainClient,
    handler: Arc<dyn OracleHandler>,
    chain_id: String,
    poll_interval: Duration,
    /// Per-instance monotonic nonce. Persisting across restarts is the dev's
    /// concern (they hold the identity); for v0.0.1 the SDK assumes the dev
    /// is comfortable with restart-resetting to 0, which the chain accepts
    /// (submissions are uniquified by `proof_id`, not `nonce`).
    nonce: AtomicU64,
}

// Manual Debug — `Arc<dyn OracleHandler>` doesn't impl Debug (the trait
// would have to require it, which would push the requirement onto every
// dev's handler impl). The other fields can print themselves; we render
// the handler as a placeholder.
impl std::fmt::Debug for Oracle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Oracle")
            .field("identity", &self.identity)
            .field("client", &self.client)
            .field("handler", &"<dyn OracleHandler>")
            .field("chain_id", &self.chain_id)
            .field("poll_interval", &self.poll_interval)
            .field("nonce", &self.nonce.load(Ordering::SeqCst))
            .finish()
    }
}

impl Oracle {
    /// Start configuring a new oracle.
    pub fn builder() -> OracleBuilder {
        OracleBuilder::default()
    }

    /// This oracle's [`OracleId`](fialucci_types::OracleId).
    pub fn oracle_id(&self) -> fialucci_types::OracleId {
        self.identity.oracle_id()
    }

    /// The chain ID discovered at `build()` time.
    pub fn chain_id(&self) -> &str {
        &self.chain_id
    }

    /// Run the event loop forever. Returns only on an unrecoverable error
    /// (e.g. the node URL is permanently unreachable for longer than the
    /// caller's wrapping retry policy tolerates — the SDK itself logs and
    /// continues on transient failures).
    ///
    /// The loop:
    /// 1. polls for matching observation requests,
    /// 2. dispatches each to [`OracleHandler::observe`],
    /// 3. signs and submits any `Some` results,
    /// 4. sleeps for `poll_interval`,
    /// 5. repeats.
    pub async fn run(self) -> Result<(), RuntimeError> {
        loop {
            match self.tick().await {
                Ok(n) => {
                    eprintln!(
                        "fialucci-oracle: tick processed {n} request(s); sleeping {:?}",
                        self.poll_interval
                    );
                }
                Err(e) => {
                    eprintln!("fialucci-oracle: tick error: {e}");
                }
            }
            tokio::time::sleep(self.poll_interval).await;
        }
    }

    /// One iteration of the event loop. Exposed for tests and for devs who
    /// want to drive ticks manually (e.g. from a cron-like external trigger).
    ///
    /// Returns the number of submissions produced this tick.
    pub async fn tick(&self) -> Result<usize, RuntimeError> {
        let requests = self
            .client
            .matching_observation_requests(self.identity.oracle_id())
            .await?;

        let mut produced = 0;
        for request in requests {
            match self.handler.observe(&request).await {
                Ok(Some(claim_evidence)) => match self.submit(&request, claim_evidence).await {
                    Ok(ack) => {
                        eprintln!(
                            "fialucci-oracle: submitted for request {} → status={}",
                            hex::encode(request.request_id.0),
                            ack.status
                        );
                        produced += 1;
                    }
                    Err(e) => {
                        eprintln!(
                            "fialucci-oracle: submit failed for request {}: {e}",
                            hex::encode(request.request_id.0)
                        );
                    }
                },
                Ok(None) => {
                    // Handler chose not to respond; nothing to do.
                }
                Err(e) => {
                    eprintln!(
                        "fialucci-oracle: handler error on request {}: {e}",
                        hex::encode(request.request_id.0)
                    );
                }
            }
        }
        Ok(produced)
    }

    /// Build a body from `request` + `claim_evidence`, sign it, and POST it
    /// to the observation-request-specific submission endpoint.
    ///
    /// Exposed for tests and one-shot use. The runtime's [`tick`](Self::tick)
    /// calls this internally.
    pub async fn submit(
        &self,
        request: &ObservationRequest,
        claim_evidence: ProofClaimEvidence,
    ) -> Result<SubmissionAck, RuntimeError> {
        let body = ProofBodyBuilder::new()
            .chain_id(self.chain_id.clone())
            .domain(map_domain(&request.domain))
            .subject(request.subject.clone())
            .claim(claim_evidence.claim)
            .evidence(claim_evidence.evidence)
            .oracle_id(self.identity.oracle_id())
            .nonce(self.nonce.fetch_add(1, Ordering::SeqCst))
            .timestamp_now()
            .observation_request_id(request.request_id)
            .build()?;

        let signed = self.identity.sign_body(body)?;

        let ack = self
            .client
            .submit_observation_request_response(request.request_id, &signed)
            .await?;
        Ok(ack)
    }

    /// **Build + sign** a proactive (free-form) oracle data submission —
    /// without dispatching it to the chain. Returns the
    /// [`SignedOracleDataSubmission`] ready to POST to
    /// `/oracle/submissions`.
    ///
    /// Use this when the caller owns the dispatch — e.g. an Allora
    /// pipeline whose `http-outbound-adapter` POSTs the signed body to a
    /// declarative chain URL, or any other transport the SDK doesn't
    /// know about. The runtime still fills in `chain_id`, `oracle_id`,
    /// `nonce`, `timestamp_ms`; signs via the identity's crate-private
    /// path. `observation_request_id` is `None` (free-form).
    ///
    /// [`submit_proactive`](Self::submit_proactive) is the convenience
    /// wrapper that combines this with the SDK's `ChainClient::submit_submission`.
    pub async fn sign_proactive(
        &self,
        domain: ProofDomain,
        subject: fialucci_types::ProofSubject,
        claim: fialucci_types::ProofClaim,
        evidence: fialucci_types::ProofEvidence,
    ) -> Result<fialucci_types::SignedOracleDataSubmission, RuntimeError> {
        let body = ProofBodyBuilder::new()
            .chain_id(self.chain_id.clone())
            .domain(domain)
            .subject(subject)
            .claim(claim)
            .evidence(evidence)
            .oracle_id(self.identity.oracle_id())
            .nonce(self.nonce.fetch_add(1, Ordering::SeqCst))
            .timestamp_now()
            .build()?;

        let signed = self.identity.sign_body(body)?;
        Ok(signed)
    }

    /// Submit a **proactive** (free-form) oracle data submission — one not
    /// bound to any observation request.
    ///
    /// Use this when the oracle observes the world on its own schedule and
    /// asserts claims unprompted (e.g. a POC that polls GitHub and submits
    /// when a PR is merged). The runtime still owns identity, chain_id,
    /// nonce, timestamp, signing, and HTTP — the dev only supplies what
    /// the assertion is about (`domain`, `subject`, `claim`, `evidence`).
    ///
    /// The signed submission lands at `POST /oracle/submissions`. The
    /// body's `observation_request_id` is `None`, marking the submission
    /// as free-form on the wire.
    ///
    /// Implemented in terms of [`sign_proactive`](Self::sign_proactive) +
    /// [`ChainClient::submit_submission`](crate::ChainClient::submit_submission).
    pub async fn submit_proactive(
        &self,
        domain: ProofDomain,
        subject: fialucci_types::ProofSubject,
        claim: fialucci_types::ProofClaim,
        evidence: fialucci_types::ProofEvidence,
    ) -> Result<SubmissionAck, RuntimeError> {
        let signed = self
            .sign_proactive(domain, subject, claim, evidence)
            .await?;
        let ack = self.client.submit_submission(&signed).await?;
        Ok(ack)
    }
}

/// Map a chain-side `OracleDataDomain` (from the observation request) onto
/// the `ProofDomain` the body expects.
///
/// `fialucci-types` already provides the canonical mapping — extra domain
/// variants (`WorkCompletion`, `PropertyRecords`, `IdentityCredential`,
/// `Weather`) round-trip into `ProofDomain::Custom("…")` via that
/// implementation; we forward to it rather than re-implementing the table
/// (which would drift the next time a domain is added upstream).
fn map_domain(d: &fialucci_types::OracleDataDomain) -> ProofDomain {
    d.to_proof_domain()
}

/// Fluent configurator for [`Oracle`].
#[derive(Default)]
pub struct OracleBuilder {
    identity: Option<OracleIdentity>,
    node_url: Option<String>,
    handler: Option<Arc<dyn OracleHandler>>,
    poll_interval: Option<Duration>,
}

impl OracleBuilder {
    /// Use the supplied identity for all signing.
    pub fn identity(mut self, identity: OracleIdentity) -> Self {
        self.identity = Some(identity);
        self
    }

    /// Point at a chain node (e.g. `"http://127.0.0.1:8080"`).
    pub fn node_url(mut self, url: impl Into<String>) -> Self {
        self.node_url = Some(url.into());
        self
    }

    /// Install the handler that decides what to claim for each observation
    /// request.
    pub fn handler<H: OracleHandler>(mut self, handler: H) -> Self {
        self.handler = Some(Arc::new(handler));
        self
    }

    /// How often [`Oracle::run`] polls for matching observation requests.
    /// Defaults to 5 seconds when not set.
    pub fn poll_interval(mut self, interval: Duration) -> Self {
        self.poll_interval = Some(interval);
        self
    }

    /// Finalize the configuration and discover `chain_id` from the node.
    pub async fn build(self) -> Result<Oracle, BuildError> {
        let identity = self.identity.ok_or(BuildError::MissingField("identity"))?;
        let node_url = self.node_url.ok_or(BuildError::MissingField("node_url"))?;
        let handler = self.handler.ok_or(BuildError::MissingField("handler"))?;
        let poll_interval = self.poll_interval.unwrap_or(Duration::from_secs(5));

        let client =
            ChainClient::new(node_url).map_err(|e| BuildError::ClientInit(e.to_string()))?;
        let info = client
            .chain_info()
            .await
            .map_err(|e| BuildError::ChainInfo(e.to_string()))?;

        Ok(Oracle {
            identity,
            client,
            handler,
            chain_id: info.chain_id,
            poll_interval,
            nonce: AtomicU64::new(0),
        })
    }
}

/// Errors surfaced by [`OracleBuilder::build`].
#[derive(Debug, thiserror::Error)]
pub enum BuildError {
    /// A required builder field was not set.
    #[error("required field `{0}` not set on OracleBuilder")]
    MissingField(&'static str),
    /// Constructing the underlying [`ChainClient`] failed.
    #[error("client init failed: {0}")]
    ClientInit(String),
    /// `GET /chain-info` failed at build time.
    #[error("chain-info discovery failed: {0}")]
    ChainInfo(String),
}

/// Errors surfaced by [`Oracle::run`] / [`Oracle::tick`] / [`Oracle::submit`].
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
    /// HTTP client failure (poll or submit).
    #[error("HTTP client error: {0}")]
    Client(#[from] ClientError),
    /// Body builder rejected the runtime-constructed body — should not happen
    /// in practice (the runtime supplies every required field).
    #[error("proof body construction failed: {0}")]
    Body(#[from] ProofBodyBuildError),
    /// Authoritative signing call (`sign_oracle_data_submission`) failed.
    /// Conversion `From<OracleDataSubmissionSigningError>` is hand-rolled
    /// below — see comment on that impl.
    #[error("signing failed: {0}")]
    Signing(OracleDataSubmissionSigningError),
    /// Handler-side failure surfaced explicitly.
    #[error("handler error: {0}")]
    Handler(#[from] HandlerError),
}

// `OracleDataSubmissionSigningError` does not implement `std::error::Error`
// upstream (only `Display` + `Debug`), so `#[from]` on the variant above
// cannot derive the conversion automatically. We hand-roll it; the typed
// variant is preserved so callers can pattern-match if they ever need to.
impl From<OracleDataSubmissionSigningError> for RuntimeError {
    fn from(e: OracleDataSubmissionSigningError) -> Self {
        RuntimeError::Signing(e)
    }
}

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

    struct NeverObserve;

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

    #[tokio::test]
    async fn build_requires_identity() {
        let err = Oracle::builder()
            .node_url("http://localhost:8080")
            .handler(NeverObserve)
            .build()
            .await
            .unwrap_err();
        assert!(matches!(err, BuildError::MissingField("identity")));
    }

    #[tokio::test]
    async fn build_requires_node_url() {
        let err = Oracle::builder()
            .identity(OracleIdentity::generate())
            .handler(NeverObserve)
            .build()
            .await
            .unwrap_err();
        assert!(matches!(err, BuildError::MissingField("node_url")));
    }

    #[tokio::test]
    async fn build_requires_handler() {
        let err = Oracle::builder()
            .identity(OracleIdentity::generate())
            .node_url("http://localhost:8080")
            .build()
            .await
            .unwrap_err();
        assert!(matches!(err, BuildError::MissingField("handler")));
    }

    /// Spot-check that `submit_proactive`'s body-construction path matches
    /// what the chain expects when no observation request is involved.
    ///
    /// We can't drive `Oracle::submit_proactive` end-to-end without a live
    /// node, so we exercise the same builder + identity composition here
    /// and assert: (a) the signed submission's `body.observation_request_id`
    /// is `None`; (b) the signature verifies against the identity's own
    /// public key. Together these are the wire invariants `submit_proactive`
    /// relies on.
    #[test]
    fn proactive_body_omits_observation_request_id_and_signs_correctly() {
        use fialucci_types::{
            hash_evidence_bytes, oracle_data_submission_signing_bytes, OracleInfo, OracleRegistry,
            OracleStatus, ProofClaim, ProofDomain, ProofEvidence, ProofSubject,
        };

        let identity = OracleIdentity::generate();
        let oracle_id = identity.oracle_id();

        let body = ProofBodyBuilder::new()
            .chain_id("fialucci-poc-test")
            .domain(ProofDomain::Github)
            .subject(ProofSubject::ExternalId("github:foo/bar#42".into()))
            .claim(ProofClaim::WorkCompleted)
            .evidence(ProofEvidence {
                evidence_hash: hash_evidence_bytes(b"{\"merged\":true}"),
                evidence_uri: None,
                content_type: Some("application/json".into()),
                metadata: None,
            })
            .oracle_id(oracle_id)
            .nonce(0)
            .timestamp_now()
            .build()
            .unwrap();

        // Invariant (a): no observation-request binding on the wire.
        assert!(body.observation_request_id.is_none());

        // Invariant (b): signature verifies against the identity's pubkey.
        // The chain uses the same `verify_oracle_data_submission_signature`
        // function, so a pass here means the chain accepts the signature.
        let signed = identity.sign_body(body).unwrap();
        let registry = OracleRegistry {
            oracles: vec![OracleInfo {
                oracle_id,
                public_key: identity.public_key(),
                node_peer_id: None,
                status: OracleStatus::Active,
                capabilities: vec![],
                display_name: Some("test".into()),
                metadata: None,
                operator_name: None,
                operator_type: None,
                trust_tier: None,
                developer_name: None,
                evidence_schemas: vec![],
                registration_reference: None,
            }],
        };
        fialucci_types::verify_oracle_data_submission_signature(&signed, &registry)
            .expect("signature must verify");

        // Belt-and-suspenders: prove we signed exactly what the chain re-derives.
        let canonical_bytes = oracle_data_submission_signing_bytes(&signed.body).unwrap();
        assert!(canonical_bytes.starts_with(fialucci_types::ORACLE_PROOF_DOMAIN));
    }
}
