//! FDA Drug Label Extractor.
//!
//! Fetches drug label information from api.fda.gov and extracts structured claims.
//!
//! # API Documentation
//!
//! https://open.fda.gov/apis/drug/label/
//!
//! # Example
//!
//! ```ignore
//! use stemedb_ontology::pharma::extractors::{FdaLabelExtractor, SourceInput, MedicalExtractor};
//!
//! let extractor = FdaLabelExtractor::new();
//! let claims = extractor.extract(&SourceInput::DrugName("semaglutide".to_string())).await?;
//! ```

use std::time::Duration;

use async_trait::async_trait;
use backoff::ExponentialBackoff;
use regex::Regex;
use stemedb_core::types::{ObjectValue, SourceClass};
use tracing::{info, instrument, warn};

use super::fda_types::{FdaLabel, FdaLabelResponse};
use super::{ExtractError, MedicalClaim, MedicalExtractor, RetryConfig, SourceInput};

/// FDA Open API base URL.
const FDA_API_BASE: &str = "https://api.fda.gov/drug/label.json";

/// Extractor for FDA drug labels.
#[derive(Debug, Clone)]
pub struct FdaLabelExtractor {
    client: reqwest::Client,
    /// Optional API key for higher rate limits.
    api_key: Option<String>,
    /// Configuration for retry behavior.
    retry_config: RetryConfig,
}

impl Default for FdaLabelExtractor {
    fn default() -> Self {
        Self::new()
    }
}

impl FdaLabelExtractor {
    /// Create a new FDA label extractor.
    pub fn new() -> Self {
        Self { client: reqwest::Client::new(), api_key: None, retry_config: RetryConfig::default() }
    }

    /// Create with an API key for higher rate limits.
    pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
        self.api_key = Some(key.into());
        self
    }

    /// Configure retry behavior for API requests.
    pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
        self.retry_config = config;
        self
    }

    /// Get the current retry configuration.
    pub fn retry_config(&self) -> &RetryConfig {
        &self.retry_config
    }

    /// Build the API URL for a drug query.
    fn build_url(&self, drug_name: &str) -> String {
        let search = format!(
            "openfda.generic_name:\"{}\" OR openfda.brand_name:\"{}\"",
            drug_name.to_lowercase(),
            drug_name.to_lowercase()
        );
        let encoded_search = urlencoding::encode(&search);

        let mut url = format!("{}?search={}&limit=1", FDA_API_BASE, encoded_search);

        if let Some(ref key) = self.api_key {
            url.push_str(&format!("&api_key={}", key));
        }

        url
    }

    /// Fetch label data from FDA API with exponential backoff retry.
    #[instrument(skip(self), fields(drug_name = %drug_name))]
    async fn fetch_label(&self, drug_name: &str) -> Result<FdaLabelResponse, ExtractError> {
        let url = self.build_url(drug_name);
        info!(url = %url, "Fetching FDA label");

        let backoff_config = ExponentialBackoff {
            initial_interval: Duration::from_millis(self.retry_config.initial_delay_ms),
            max_interval: Duration::from_millis(self.retry_config.max_delay_ms),
            // Use timeout as max elapsed time (not multiplied by retries)
            max_elapsed_time: Some(Duration::from_millis(self.retry_config.timeout_ms)),
            ..Default::default()
        };

        let timeout_ms = self.retry_config.timeout_ms;
        let max_retries = self.retry_config.max_retries;
        let drug_name_owned = drug_name.to_string();

        // Track retry count with atomic counter
        let attempt_count = std::sync::atomic::AtomicU32::new(0);

        let operation = || async {
            // Check retry count BEFORE attempting
            let attempt = attempt_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
            if attempt > max_retries {
                return Err(backoff::Error::permanent(ExtractError::ExtractionFailed(format!(
                    "Max retries ({}) exceeded",
                    max_retries
                ))));
            }

            let response = self
                .client
                .get(&url)
                .timeout(Duration::from_millis(timeout_ms))
                .send()
                .await
                .map_err(|e| {
                    if e.is_timeout() {
                        backoff::Error::permanent(ExtractError::Timeout(timeout_ms))
                    } else if e.is_connect() {
                        // Connection errors are transient - retry
                        warn!(error = %e, attempt = attempt, "Connection error, will retry");
                        backoff::Error::transient(ExtractError::Http(e))
                    } else {
                        backoff::Error::permanent(ExtractError::Http(e))
                    }
                })?;

            if response.status() == reqwest::StatusCode::NOT_FOUND {
                return Err(backoff::Error::permanent(ExtractError::NotFound(
                    drug_name_owned.clone(),
                )));
            }

            if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
                let retry_after = response
                    .headers()
                    .get("retry-after")
                    .and_then(|v| v.to_str().ok())
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(60);
                warn!(retry_after = retry_after, attempt = attempt, "Rate limited, will retry");
                return Err(backoff::Error::transient(ExtractError::RateLimited(retry_after)));
            }

            if response.status().is_server_error() {
                let status = response.status();
                let body = response.text().await.unwrap_or_default();
                warn!(status = %status, attempt = attempt, "Server error, will retry");
                return Err(backoff::Error::transient(ExtractError::ApiError(format!(
                    "HTTP {}: {}",
                    status, body
                ))));
            }

            if response.status().is_client_error() {
                return Err(backoff::Error::permanent(ExtractError::ApiError(format!(
                    "HTTP {}: {}",
                    response.status(),
                    response.text().await.unwrap_or_default()
                ))));
            }

            let text = response
                .text()
                .await
                .map_err(|e| backoff::Error::permanent(ExtractError::Http(e)))?;

            // Check for "no matches found" response
            if text.contains("No matches found") {
                return Err(backoff::Error::permanent(ExtractError::NotFound(
                    drug_name_owned.clone(),
                )));
            }

            let data: FdaLabelResponse = serde_json::from_str(&text)
                .map_err(|e| backoff::Error::permanent(ExtractError::Json(e)))?;
            Ok(data)
        };

        backoff::future::retry(backoff_config, operation).await
    }

    /// Extract claims from a label result.
    fn extract_claims_from_label(&self, drug_name: &str, label: &FdaLabel) -> Vec<MedicalClaim> {
        let mut claims = Vec::new();
        let generic_name = label
            .openfda
            .as_ref()
            .and_then(|o| o.generic_name.as_ref())
            .and_then(|names| names.first())
            .map(String::as_str)
            .unwrap_or(drug_name);

        // Normalize to canonical drug name
        let subject = self.normalize_drug_name(generic_name);

        // Extract boxed warning
        if let Some(ref warnings) = label.boxed_warning {
            let has_warning = !warnings.is_empty() && warnings.iter().any(|w| !w.trim().is_empty());
            claims.push(
                MedicalClaim::new(&subject, "has_boxed_warning", ObjectValue::Boolean(has_warning))
                    .with_confidence(1.0)
                    .with_source_class(SourceClass::Regulatory)
                    .with_source_section("Boxed Warning")
                    .with_quote(warnings.join(" ").chars().take(500).collect::<String>()),
            );

            if has_warning {
                claims.push(
                    MedicalClaim::new(
                        &subject,
                        "boxed_warning_text",
                        ObjectValue::Text(warnings.join(" ")),
                    )
                    .with_confidence(1.0)
                    .with_source_class(SourceClass::Regulatory)
                    .with_source_section("Boxed Warning"),
                );
            }
        } else {
            claims.push(
                MedicalClaim::new(&subject, "has_boxed_warning", ObjectValue::Boolean(false))
                    .with_confidence(0.9) // Slightly lower confidence since absence doesn't mean never
                    .with_source_class(SourceClass::Regulatory)
                    .with_source_section("Label Review"),
            );
        }

        // Extract adverse reactions rates
        if let Some(ref adverse) = label.adverse_reactions {
            let text = adverse.join(" ");
            claims.extend(self.extract_adverse_reaction_rates(&subject, &text));
        }

        // Extract indications
        if let Some(ref indications) = label.indications_and_usage {
            let text = indications.join(" ");
            claims.extend(self.extract_indications(&subject, &text));
        }

        // Extract dosage information
        if let Some(ref dosage) = label.dosage_and_administration {
            let text = dosage.join(" ");
            claims.extend(self.extract_dosage_info(&subject, &text));
        }

        // Add source URL to all claims
        let source_url = format!(
            "https://api.fda.gov/drug/label.json?search=openfda.generic_name:\"{}\"",
            generic_name.to_lowercase()
        );
        for claim in &mut claims {
            claim.source_url = source_url.clone();
        }

        claims
    }

    /// Normalize drug name to canonical form.
    fn normalize_drug_name(&self, name: &str) -> String {
        // Simple normalization - capitalize first letter of each word
        let normalized = name
            .split_whitespace()
            .map(|word| {
                let mut chars = word.chars();
                match chars.next() {
                    None => String::new(),
                    Some(c) => {
                        c.to_uppercase().chain(chars.flat_map(|c| c.to_lowercase())).collect()
                    }
                }
            })
            .collect::<Vec<_>>()
            .join("");

        // Check known aliases and normalize formulation variants
        let lower = normalized.to_lowercase();
        match lower.as_str() {
            "ozempic" | "wegovy" | "rybelsus" => "Semaglutide".to_string(),
            "mounjaro" | "zepbound" => "Tirzepatide".to_string(),
            "victoza" | "saxenda" => "Liraglutide".to_string(),
            "trulicity" => "Dulaglutide".to_string(),
            _ => {
                // Handle formulation variants (e.g., "oral semaglutide" -> "Semaglutide")
                if lower.contains("semaglutide") {
                    "Semaglutide".to_string()
                } else if lower.contains("tirzepatide") {
                    "Tirzepatide".to_string()
                } else if lower.contains("liraglutide") {
                    "Liraglutide".to_string()
                } else if lower.contains("dulaglutide") {
                    "Dulaglutide".to_string()
                } else {
                    normalized
                }
            }
        }
    }

    /// Extract adverse reaction rates from text.
    fn extract_adverse_reaction_rates(&self, subject: &str, text: &str) -> Vec<MedicalClaim> {
        let mut claims = Vec::new();

        // Pattern: "X% of patients" or "X percent" or "occurred in X%"
        let rate_pattern =
            Regex::new(r"(?i)(nausea|vomiting|diarrhea|constipation|injection site|hypoglycemia)\s*(?:\([^)]*\))?\s*(?:occurred in|was reported in|was seen in|:)?\s*(\d+\.?\d*)\s*%")
                .ok();

        if let Some(ref pattern) = rate_pattern {
            for cap in pattern.captures_iter(text) {
                let event = cap.get(1).map(|m| m.as_str().to_lowercase()).unwrap_or_default();
                let rate_str = cap.get(2).map(|m| m.as_str()).unwrap_or("0");
                let rate: f64 = rate_str.parse().unwrap_or(0.0);

                let predicate = match event.as_str() {
                    "nausea" => "nausea_rate",
                    "vomiting" => "vomiting_rate",
                    "diarrhea" => "diarrhea_rate",
                    "constipation" => "constipation_rate",
                    "injection site" => "injection_site_reaction_rate",
                    "hypoglycemia" => "hypoglycemia_risk",
                    _ => continue,
                };

                // Find context around the match for quote
                let match_pos = cap.get(0).map(|m| m.start()).unwrap_or(0);
                let start = match_pos.saturating_sub(100);
                let end = (match_pos + 200).min(text.len());
                let quote = text[start..end].to_string();

                claims.push(
                    MedicalClaim::new(subject, predicate, ObjectValue::Number(rate / 100.0))
                        .with_confidence(0.95)
                        .with_source_class(SourceClass::Regulatory)
                        .with_source_section("Adverse Reactions")
                        .with_quote(quote),
                );
            }
        }

        claims
    }

    /// Extract approved indications.
    fn extract_indications(&self, subject: &str, text: &str) -> Vec<MedicalClaim> {
        let mut claims = Vec::new();

        // Look for common indication patterns
        let indication_patterns = [
            (r"(?i)type 2 diabetes", "Type2Diabetes"),
            (r"(?i)chronic weight management", "ChronicWeightManagement"),
            (r"(?i)cardiovascular risk reduction", "CardiovascularRiskReduction"),
            (r"(?i)reduce the risk of major adverse cardiovascular events", "MACE_Reduction"),
        ];

        for (pattern, indication) in indication_patterns {
            if let Ok(re) = Regex::new(pattern) {
                if re.is_match(text) {
                    claims.push(
                        MedicalClaim::new(
                            subject,
                            "approved_indications",
                            ObjectValue::Text(indication.to_string()),
                        )
                        .with_confidence(0.98)
                        .with_source_class(SourceClass::Regulatory)
                        .with_source_section("Indications and Usage"),
                    );
                }
            }
        }

        claims
    }

    /// Extract dosage information.
    fn extract_dosage_info(&self, subject: &str, text: &str) -> Vec<MedicalClaim> {
        let mut claims = Vec::new();

        // Look for max dose patterns
        let max_dose_pattern =
            Regex::new(r"(?i)(?:maximum|max)\s*(?:recommended)?\s*dose[:\s]*(\d+\.?\d*)\s*mg").ok();

        if let Some(ref pattern) = max_dose_pattern {
            if let Some(cap) = pattern.captures(text) {
                if let Some(dose_str) = cap.get(1).map(|m| m.as_str()) {
                    if let Ok(dose) = dose_str.parse::<f64>() {
                        claims.push(
                            MedicalClaim::new(
                                subject,
                                "max_approved_dose_mg",
                                ObjectValue::Number(dose),
                            )
                            .with_confidence(0.95)
                            .with_source_class(SourceClass::Regulatory)
                            .with_source_section("Dosage and Administration"),
                        );
                    }
                }
            }
        }

        claims
    }
}

#[async_trait]
impl MedicalExtractor for FdaLabelExtractor {
    fn name(&self) -> &str {
        "FDA Drug Label"
    }

    fn source_class(&self) -> SourceClass {
        SourceClass::Regulatory
    }

    fn can_handle(&self, source: &SourceInput) -> bool {
        matches!(
            source,
            SourceInput::DrugName(_) | SourceInput::ApplicationNumber(_) | SourceInput::NdcCode(_)
        )
    }

    #[instrument(skip(self), fields(extractor = "FDA"))]
    async fn extract(&self, source: &SourceInput) -> Result<Vec<MedicalClaim>, ExtractError> {
        let drug_name = match source {
            SourceInput::DrugName(name) => name.clone(),
            SourceInput::ApplicationNumber(num) => num.clone(),
            SourceInput::NdcCode(code) => code.clone(),
            _ => {
                return Err(ExtractError::ExtractionFailed(
                    "FDA extractor only handles drug names, application numbers, and NDC codes"
                        .to_string(),
                ))
            }
        };

        let response = self.fetch_label(&drug_name).await?;

        let label = response
            .results
            .into_iter()
            .next()
            .ok_or_else(|| ExtractError::NotFound(drug_name.clone()))?;

        let claims = self.extract_claims_from_label(&drug_name, &label);

        info!(
            drug = %drug_name,
            claims_count = claims.len(),
            "Extracted claims from FDA label"
        );

        Ok(claims)
    }
}

#[cfg(test)]
#[path = "fda_tests.rs"]
mod tests;
