//! Medical data extractors.
//!
//! This module contains extractors for various medical data sources:
//! - FDA drug labels (api.fda.gov)
//! - PubMed abstracts (future)
//! - ClinicalTrials.gov (future)

pub mod fda;
mod fda_types;

pub use fda::FdaLabelExtractor;

use async_trait::async_trait;
use ed25519_dalek::{Signer, SigningKey};
use stemedb_core::signing::compute_content_hash_v2;
use stemedb_core::types::{
    Assertion, HlcTimestamp, LifecycleStage, ObjectValue, SignatureEntry, SourceClass,
};
use thiserror::Error;

/// Configuration for retry behavior with exponential backoff.
#[derive(Debug, Clone)]
pub struct RetryConfig {
    /// Maximum number of retry attempts.
    pub max_retries: u32,
    /// Initial delay between retries in milliseconds.
    pub initial_delay_ms: u64,
    /// Maximum delay between retries in milliseconds.
    pub max_delay_ms: u64,
    /// Request timeout in milliseconds.
    pub timeout_ms: u64,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self { max_retries: 3, initial_delay_ms: 1000, max_delay_ms: 30000, timeout_ms: 30000 }
    }
}

/// Errors that can occur during extraction.
#[derive(Debug, Error)]
pub enum ExtractError {
    /// HTTP request failed.
    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    /// JSON parsing failed.
    #[error("JSON parsing error: {0}")]
    Json(#[from] serde_json::Error),

    /// No data found for the query.
    #[error("No data found for: {0}")]
    NotFound(String),

    /// Rate limited by the API.
    #[error("Rate limited, retry after {0} seconds")]
    RateLimited(u64),

    /// API returned an error.
    #[error("API error: {0}")]
    ApiError(String),

    /// Extraction logic failed.
    #[error("Extraction failed: {0}")]
    ExtractionFailed(String),

    /// Request timed out.
    #[error("Request timed out after {0}ms")]
    Timeout(u64),
}

/// A claim extracted from a medical source.
///
/// This is the intermediate format between raw source data and StemeDB assertions.
/// The ontology layer converts MedicalClaims to properly-formatted assertions.
#[derive(Debug, Clone)]
pub struct MedicalClaim {
    /// The subject of the claim (built via SubjectBuilder).
    pub subject: String,

    /// The predicate name (e.g., "has_boxed_warning").
    pub predicate: String,

    /// The object value.
    pub value: ObjectValue,

    /// Confidence in the extraction (0.0 to 1.0).
    pub confidence: f32,

    /// URL of the source document.
    pub source_url: String,

    /// Section of the source document (e.g., "Boxed Warning", "Adverse Reactions").
    pub source_section: String,

    /// Direct quote from the source supporting this claim.
    pub quote: String,

    /// The source class (tier) for this claim.
    pub source_class: SourceClass,

    /// Optional metadata (e.g., date, version).
    pub metadata: Option<serde_json::Value>,
}

impl MedicalClaim {
    /// Create a new medical claim.
    pub fn new(
        subject: impl Into<String>,
        predicate: impl Into<String>,
        value: ObjectValue,
    ) -> Self {
        Self {
            subject: subject.into(),
            predicate: predicate.into(),
            value,
            confidence: 0.9,
            source_url: String::new(),
            source_section: String::new(),
            quote: String::new(),
            source_class: SourceClass::default(),
            metadata: None,
        }
    }

    /// Set the confidence level.
    pub fn with_confidence(mut self, confidence: f32) -> Self {
        self.confidence = confidence;
        self
    }

    /// Set the source URL.
    pub fn with_source_url(mut self, url: impl Into<String>) -> Self {
        self.source_url = url.into();
        self
    }

    /// Set the source section.
    pub fn with_source_section(mut self, section: impl Into<String>) -> Self {
        self.source_section = section.into();
        self
    }

    /// Set the supporting quote.
    pub fn with_quote(mut self, quote: impl Into<String>) -> Self {
        self.quote = quote.into();
        self
    }

    /// Set the source class.
    pub fn with_source_class(mut self, source_class: SourceClass) -> Self {
        self.source_class = source_class;
        self
    }

    /// Set metadata.
    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
        self.metadata = Some(metadata);
        self
    }

    /// Convert this medical claim to a signed StemeDB assertion.
    ///
    /// Uses a default lifecycle based on source class:
    /// - `Regulatory` → `Approved` (FDA labels are authoritative)
    /// - Other → `Proposed` (requires review)
    ///
    /// # Arguments
    ///
    /// * `signing_key` - Ed25519 signing key for the agent
    /// * `agent_id` - 32-byte public key of the signing agent
    /// * `hlc` - Hybrid logical clock for timestamp generation
    ///
    /// # Returns
    ///
    /// A fully constructed and signed `Assertion` ready for ingestion.
    pub fn to_assertion(
        &self,
        signing_key: &SigningKey,
        agent_id: [u8; 32],
        hlc: &uhlc::HLC,
    ) -> Assertion {
        // Regulatory sources are authoritative; others need review
        let lifecycle = if self.source_class == SourceClass::Regulatory {
            LifecycleStage::Approved
        } else {
            LifecycleStage::Proposed
        };
        self.to_assertion_with_lifecycle(signing_key, agent_id, hlc, lifecycle)
    }

    /// Convert this medical claim to a signed StemeDB assertion with explicit lifecycle.
    ///
    /// # Arguments
    ///
    /// * `signing_key` - Ed25519 signing key for the agent
    /// * `agent_id` - 32-byte public key of the signing agent
    /// * `hlc` - Hybrid logical clock for timestamp generation
    /// * `lifecycle` - The lifecycle stage for the assertion
    ///
    /// # Returns
    ///
    /// A fully constructed and signed `Assertion` ready for ingestion.
    pub fn to_assertion_with_lifecycle(
        &self,
        signing_key: &SigningKey,
        agent_id: [u8; 32],
        hlc: &uhlc::HLC,
        lifecycle: LifecycleStage,
    ) -> Assertion {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let hlc_timestamp = HlcTimestamp::now(hlc);

        // Compute source hash from provenance data
        let source_hash = self.compute_source_hash();

        // Convert metadata to bytes if present
        let source_metadata =
            self.metadata.as_ref().map(|m| serde_json::to_vec(m).unwrap_or_default());

        // Create assertion (without signature initially for signing)
        let mut assertion = Assertion {
            subject: self.subject.clone(),
            predicate: self.predicate.clone(),
            object: self.value.clone(),
            parent_hash: None,
            source_hash,
            source_class: self.source_class,
            visual_hash: None,
            epoch: None,
            source_metadata,
            lifecycle,
            signatures: Vec::new(),
            confidence: self.confidence,
            timestamp: now,
            hlc_timestamp,
            vector: None,
        };

        // Sign the assertion using enterprise v2 signing (content hash)
        // Uses shared utility from stemedb-core for consistency
        let content_hash = compute_content_hash_v2(&assertion);
        tracing::debug!(
            subject = %assertion.subject,
            predicate = %assertion.predicate,
            source_class = ?assertion.source_class,
            confidence = %assertion.confidence,
            timestamp = %assertion.timestamp,
            content_hash = %hex::encode(content_hash),
            "Computed v2 content hash for signing"
        );
        let signature = signing_key.sign(&content_hash);
        assertion.signatures.push(SignatureEntry {
            agent_id,
            signature: signature.to_bytes(),
            timestamp: now,
            version: 2, // Enterprise signing - signs content hash
        });

        assertion
    }

    /// Compute BLAKE3 hash of source provenance data.
    fn compute_source_hash(&self) -> [u8; 32] {
        let mut hasher = blake3::Hasher::new();
        hasher.update(self.source_url.as_bytes());
        hasher.update(b"|");
        hasher.update(self.source_section.as_bytes());
        hasher.update(b"|");
        hasher.update(self.quote.as_bytes());
        *hasher.finalize().as_bytes()
    }
}

/// Input for a medical extractor.
#[derive(Debug, Clone)]
pub enum SourceInput {
    /// Query by drug name (generic or brand).
    DrugName(String),

    /// Query by NDC code.
    NdcCode(String),

    /// Query by application number (NDA/ANDA).
    ApplicationNumber(String),

    /// Raw URL to fetch.
    Url(String),

    /// Raw text content to parse.
    RawText(String),
}

/// Trait for medical data extractors.
///
/// Extractors fetch data from external sources and convert them to MedicalClaims.
/// The claims are then validated against the domain ontology and converted to assertions.
#[async_trait]
pub trait MedicalExtractor: Send + Sync {
    /// Human-readable name of this extractor.
    fn name(&self) -> &str;

    /// The source class for claims from this extractor.
    fn source_class(&self) -> SourceClass;

    /// Extract claims from the source.
    ///
    /// # Arguments
    ///
    /// * `source` - The input query or content
    ///
    /// # Returns
    ///
    /// A vector of extracted claims, or an error if extraction failed.
    async fn extract(&self, source: &SourceInput) -> Result<Vec<MedicalClaim>, ExtractError>;

    /// Check if this extractor can handle the given input type.
    fn can_handle(&self, source: &SourceInput) -> bool;
}
