// src/corporate/openfigi.rs
use super::{types::*};
use reqwest::Client as HttpClient;
use reqwest::header::{HeaderMap, HeaderValue};
use serde_json::{json, Value};
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use std::time::Instant;
use tokio::time::{sleep, Duration};
use tokio::fs as tokio_fs;
use anyhow::{Context, anyhow};

#[derive(Clone)]
pub struct OpenFigiClient {
    client: HttpClient,
    api_key: Option<String>,
    has_key: bool,
}

impl OpenFigiClient {
    /// Creates a new OpenFIGI client, optionally with an API key.
    ///
    /// Loads the API key from the `OPENFIGI_API_KEY` environment variable if present.
    ///
    /// # Errors
    ///
    /// Returns an error if the HTTP client cannot be built or if the API key header is invalid.
    pub fn new() -> anyhow::Result<Self> {
        let api_key = dotenvy::var("OPENFIGI_API_KEY").ok();
        let has_key = api_key.is_some();

        let mut builder = HttpClient::builder()
            .user_agent("Mozilla/5.0 (compatible; OpenFIGI-Rust/1.0)")
            .timeout(Duration::from_secs(30));

        if let Some(key) = &api_key {
            let mut headers = HeaderMap::new();
            headers.insert("X-OPENFIGI-APIKEY", HeaderValue::from_str(key)?);
            builder = builder.default_headers(headers);
        }

        let client = builder.build().context("Failed to build HTTP client")?;

        println!(
            "OpenFIGI client initialized: {}",
            if has_key { "with API key" } else { "no key (limited mode)" }
        );

        Ok(Self { client, api_key, has_key })
    }

    /// Maps a batch of ISINs to FigiInfo structs, filtering for equities only.
    ///
    /// Batches requests according to rate limits (100 jobs/req with key, 5 without).
    /// Optimizes inter-request delays to approach the rate limit without exceeding it:
    /// - With key: ~240ms sleep per request (to sustain ~4 req/sec or 250 req/min).
    /// - Without key: 2.4s sleep (to sustain 25 req/min).
    /// Handles 429 rate limits with header-based backoff.
    /// Collects detailed FigiInfo from responses, using `exchCode` as proxy for `mic_code`.
    ///
    /// # Arguments
    ///
    /// * `isins` - Slice of ISIN strings to map (deduplicated internally if needed).
    ///
    /// # Returns
    ///
    /// A vector of `FigiInfo` structs for equity instruments.
    ///
    /// # Errors
    ///
    /// Returns an error on HTTP failures, JSON parsing issues, invalid API keys,
    /// or repeated rate limit violations after backoff.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use anyhow::Result;
    /// # async fn example(client: &OpenFigiClient) -> Result<()> {
    /// let isins = vec!["US0378331005".to_string(), "US5949181045".to_string()];
    /// let figis = client.map_isins_to_figi_infos(&isins).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn map_isins_to_figi_infos(&self, isins: &[String]) -> anyhow::Result<Vec<FigiInfo>> {
        if isins.is_empty() {
            return Ok(vec![]);
        }

        let mut all_figi_infos = Vec::new();
        let chunk_size = if self.has_key { 100 } else { 5 };
        let inter_sleep = if self.has_key {
            Duration::from_millis(240) // ~4.16 req/sec (250/min)
        } else {
            Duration::from_millis(2400) // 25/min
        };

        let start_time = Instant::now();
        let mut req_count = 0;

        for chunk in isins.chunks(chunk_size) {
            let jobs: Vec<Value> = chunk.iter()
                .map(|isin| json!({
                    "idType": "ID_ISIN",
                    "idValue": isin,
                    "marketSecDes": "Equity",
                }))
                .collect();

            let resp = self.client
                .post("https://api.openfigi.com/v3/mapping")
                .header("Content-Type", "application/json")
                .json(&jobs)
                .send()
                .await
                .context("Failed to send mapping request")?;

            let status = resp.status();
            let headers = resp.headers().clone();
            let body = resp.text().await.context("Failed to read response body")?;

            if status.is_client_error() || status.is_server_error() {
                if status == 429 {
                    let reset_sec = headers
                        .get("ratelimit-reset")
                        .and_then(|v| v.to_str().ok())
                        .and_then(|s| s.parse::<u64>().ok())
                        .unwrap_or(10);
                    println!("Rate limited—backing off {}s", reset_sec);
                    sleep(Duration::from_secs(reset_sec.max(10))).await;
                    continue; // Retry the same chunk
                } else if status == 401 {
                    return Err(anyhow!("Invalid OpenFIGI API key: {}", body));
                } else if status == 413 {
                    return Err(anyhow!("Payload too large—reduce chunk size: {}", body));
                }
                return Err(anyhow!("OpenFIGI error {}: {}", status, body));
            }

            let results: Vec<Value> = serde_json::from_str(&body)
                .context("Failed to parse response JSON")?;

            for (isin, result) in chunk.iter().zip(results) {
                if let Some(data) = result["data"].as_array() {
                    for item in data {
                        let sec_type = item["securityType"].as_str().unwrap_or("");
                        let market_sec = item["marketSector"].as_str().unwrap_or("");
                        if market_sec == "Equity" &&
                           (sec_type.contains("Stock") || sec_type.contains("Share") || sec_type.contains("Equity") ||
                            sec_type.contains("Common") || sec_type.contains("Preferred") || sec_type == "ADR" || sec_type == "GDR") {
                            let figi = match item["figi"].as_str() {
                                Some(f) => f.to_string(),
                                None => continue,
                            };

                            let figi_info = FigiInfo {
                                isin: isin.clone(),
                                figi,
                                name: item["name"].as_str().unwrap_or("").to_string(),
                                ticker: item["ticker"].as_str().unwrap_or("").to_string(),
                                mic_code: item["exchCode"].as_str().unwrap_or("").to_string(),
                                currency: item["currency"].as_str().unwrap_or("").to_string(),
                                compositeFIGI: item["compositeFIGI"].as_str().unwrap_or("").to_string(),
                                securityType: sec_type.to_string(),
                                marketSector: market_sec.to_string(),
                                shareClassFIGI: item["shareClassFIGI"].as_str().unwrap_or("").to_string(),
                                securityType2: item["securityType2"].as_str().unwrap_or("").to_string(),
                                securityDescription: item["securityDescription"].as_str().unwrap_or("").to_string(),
                            };

                            all_figi_infos.push(figi_info);
                        }
                    }
                }
            }

            req_count += 1;
            if req_count % 25 == 0 {
                // Optional: Enforce 6-sec window for bursts
                let elapsed = start_time.elapsed();
                if self.has_key {
                    if elapsed < Duration::from_secs(6) {
                        sleep(Duration::from_secs(6) - elapsed).await;
                    }
                } else {
                    if elapsed < Duration::from_secs(6) {
                        sleep(Duration::from_secs(60) - elapsed).await;
                    }
                }
            } else {
                sleep(inter_sleep).await;
            }
        }

        Ok(all_figi_infos)
    }

    /// Checks if the client has an API key configured.
    pub fn has_key(&self) -> bool {
        self.has_key
    }

    /// Returns a reference to the underlying HTTP client.
    pub fn get_figi_client(&self) -> &HttpClient {
        &self.client
    }
}

/// Builds a LEI-to-FigiInfo map from the LEI-ISIN mapping, filtering for equities via OpenFIGI.
///
/// Attempts to load existing entries from "data/companies_by_lei/lei_to_figi.jsonl" (JSON Lines format,
/// one LEI entry per line: {"lei": "ABC", "figis": [FigiInfo...]}). For any missing LEIs (compared to
/// `lei_to_isins`), fetches their FigiInfos and appends to the .jsonl file incrementally.
///
/// This design allows resumption after interruptions: on restart, already processed LEIs are skipped,
/// and only remaining ones are fetched. Processes LEIs in sorted order for deterministic behavior.
///
/// If no API key is present, skips building new entries and returns the loaded map (possibly partial).
///
/// # Arguments
///
/// * `lei_to_isins` - HashMap of LEI to Vec<ISIN> (used for fetching missing entries).
///
/// # Returns
///
/// The complete (or partial if interrupted) HashMap<LEI, Vec<FigiInfo>>.
///
/// # Errors
///
/// Returns an error if file I/O fails, JSON serialization/deserialization fails,
/// or if OpenFIGI queries fail during fetching.
pub async fn build_lei_to_figi_infos(lei_to_isins: &HashMap<String, Vec<String>>) -> anyhow::Result<HashMap<String, Vec<FigiInfo>>> {
    let data_dir = Path::new("data/companies_by_lei");
    tokio_fs::create_dir_all(data_dir).await.context("Failed to create data directory")?;

    let path = data_dir.join("lei_to_figi.jsonl");
    let mut lei_to_figis: HashMap<String, Vec<FigiInfo>> = load_lei_to_figi_jsonl(&path)?;

    let client = OpenFigiClient::new()?;
    if !client.has_key {
        println!("No API key—using partial LEI→FIGI map with {} entries", lei_to_figis.len());
        return Ok(lei_to_figis);
    }

    // Sort LEIs for deterministic processing order
    let mut leis: Vec<_> = lei_to_isins.keys().cloned().collect();
    leis.sort();

    let mut processed = lei_to_figis.len();
    let total = leis.len();

    for lei in leis {
        if lei_to_figis.contains_key(&lei) {
            continue; // Skip already processed
        }

        let isins = match lei_to_isins.get(&lei) {
            Some(i) => i,
            None => continue,
        };

        let unique_isins: Vec<_> = isins.iter().cloned().collect::<HashSet<_>>().into_iter().collect();
        let equity_figi_infos = client.map_isins_to_figi_infos(&unique_isins).await?;

        let mut figis = equity_figi_infos;
        if !figis.is_empty() {
            figis.sort_by_key(|f| f.figi.clone());
            figis.dedup_by_key(|f| f.figi.clone());
        }

        // Append to .jsonl incrementally
        append_lei_to_figi_jsonl(&path, &lei, &figis).context("Failed to append to JSONL")?;

        // Insert into in-memory map
        lei_to_figis.insert(lei.clone(), figis);

        processed += 1;
        if processed % 100 == 0 {
            println!("Processed {}/{} LEIs → {} total equity FIGIs", processed, total, lei_to_figis.values().map(|v| v.len()).sum::<usize>());
        }

        tokio::time::sleep(Duration::from_millis(100)).await;
    }

    println!("Completed LEI→FIGI map: {} mappings (equity-only)", lei_to_figis.len());
    Ok(lei_to_figis)
}

/// Loads LEI-to-FigiInfo map from a JSON Lines file.
///
/// Each line is expected to be a JSON object: {"lei": "ABC", "figis": [FigiInfo...]}
///
/// # Arguments
///
/// * `path` - Path to the .jsonl file.
///
/// # Returns
///
/// The loaded HashMap<LEI, Vec<FigiInfo>>.
///
/// # Errors
///
/// Returns an error if the file cannot be opened or if any line fails to parse as JSON.
fn load_lei_to_figi_jsonl(path: &Path) -> anyhow::Result<HashMap<String, Vec<FigiInfo>>> {
    let mut map = HashMap::new();

    if !path.exists() {
        return Ok(map);
    }

    let file = File::open(path).context("Failed to open JSONL file for reading")?;
    let reader = BufReader::new(file);

    for (line_num, line) in reader.lines().enumerate() {
        let line = line.context(format!("Failed to read line {}", line_num + 1))?;
        if line.trim().is_empty() {
            continue;
        }

        let entry: Value = serde_json::from_str(&line).context(format!("Failed to parse JSON on line {}", line_num + 1))?;
        let lei = entry["lei"].as_str().context("Missing 'lei' field")?.to_string();
        let figis: Vec<FigiInfo> = serde_json::from_value(entry["figis"].clone()).context("Invalid 'figis' field")?;

        map.insert(lei, figis);
    }

    println!("Loaded LEI→FIGI map with {} entries from {}", map.len(), path.display());
    Ok(map)
}

/// Appends a single LEI entry to the JSON Lines file.
///
/// # Arguments
///
/// * `path` - Path to the .jsonl file.
/// * `lei` - The LEI key.
/// * `figis` - The Vec<FigiInfo> for this LEI.
///
/// # Errors
///
/// Returns an error if the file cannot be opened for append or if serialization fails.
fn append_lei_to_figi_jsonl(path: &Path, lei: &str, figis: &[FigiInfo]) -> anyhow::Result<()> {
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .context("Failed to open JSONL file for append")?;

    let entry = json!({
        "lei": lei,
        "figis": figis,
    });

    let line = serde_json::to_string(&entry).context("Failed to serialize entry")? + "\n";
    file.write_all(line.as_bytes()).context("Failed to write to JSONL file")?;

    Ok(())
}

/// Loads or builds a HashMap of CompanyInfo objects indexed by company name.
///
/// This function:
/// 1. Attempts to load existing companies from cache
/// 2. If cache exists, updates/extends it with new data from figi_to_lei
/// 3. If no cache exists, creates a new HashMap from scratch
/// 4. Saves the result back to cache
///
/// For existing entries (matched by name):
/// - Merges securities lists (deduplicates by FIGI)
/// - Updates primary_isin if the existing one is empty or not in the securities list
///
/// For new entries:
/// - Adds them to the HashMap
///
/// Companies with no FigiInfo data are skipped.
/// The resulting HashMap is saved to `data/companies_by_name/companies.json`.
///
/// # Arguments
/// * `figi_to_lei` - HashMap mapping LEI to Vec<FigiInfo>.
///
/// # Returns
/// A HashMap mapping company name to CompanyInfo.
///
/// # Errors
/// Returns an error if file I/O fails or JSON serialization fails.
pub async fn load_or_build_companies_by_name(
    figi_to_lei: &HashMap<String, Vec<FigiInfo>>
) -> anyhow::Result<HashMap<String, CompanyInfo>> {
    // Try to load existing cache
    let mut companies_by_name = match load_companies_by_name_internal().await? {
        Some(existing) => {
            println!("Loaded {} existing companies from cache", existing.len());
            existing
        },
        None => {
            println!("No existing cache found, creating new companies HashMap");
            HashMap::new()
        }
    };
    
    let initial_count = companies_by_name.len();
    let mut added_count = 0;
    let mut updated_count = 0;
    
    println!("Processing {} LEI entries from FIGI data...", figi_to_lei.len());
    
    for (lei, figi_infos) in figi_to_lei.iter() {
        // Skip entries with no FigiInfo data
        if figi_infos.is_empty() {
            continue;
        }
        
        // Get company name from first FigiInfo entry
        let name = figi_infos[0].name.clone();
        if name.is_empty() {
            continue;
        }
        
        // Check if company already exists
        if let Some(existing_company) = companies_by_name.get_mut(&name) {
            // Update existing entry
            let merged_securities = merge_securities(&existing_company.securities, figi_infos);
            let securities_added = merged_securities.len() - existing_company.securities.len();
            
            if securities_added > 0 {
                existing_company.securities = merged_securities;
                
                // Update primary_isin if needed
                if existing_company.primary_isin.is_empty() || 
                   !existing_company.securities.iter().any(|s| s.isin == existing_company.primary_isin) {
                    existing_company.primary_isin = existing_company.securities[0].isin.clone();
                }
                
                updated_count += 1;
            }
        } else {
            // Add new entry
            let primary_isin = figi_infos[0].isin.clone();
            let securities = figi_infos.clone();
            
            let company_info = CompanyInfo {
                name: name.clone(),
                primary_isin,
                securities,
            };
            
            companies_by_name.insert(name, company_info);
            added_count += 1;
        }
    }
    
    println!("  Companies statistics:");
    println!("    - Initial: {}", initial_count);
    println!("    - Added: {}", added_count);
    println!("    - Updated: {}", updated_count);
    println!("    - Total: {}", companies_by_name.len());
    
    // Save to JSON
    save_companies_by_name(&companies_by_name).await?;
    
    Ok(companies_by_name)
}

/// Merges two lists of FigiInfo, deduplicating by FIGI.
///
/// # Arguments
/// * `existing` - Existing securities list
/// * `new_securities` - New securities to merge
///
/// # Returns
/// Merged and deduplicated list of FigiInfo
fn merge_securities(existing: &[FigiInfo], new_securities: &[FigiInfo]) -> Vec<FigiInfo> {
    let mut merged = existing.to_vec();
    let existing_figis: HashSet<String> = existing.iter()
        .map(|f| f.figi.clone())
        .collect();
    
    for new_sec in new_securities {
        if !existing_figis.contains(&new_sec.figi) {
            merged.push(new_sec.clone());
        }
    }
    
    // Sort by FIGI for consistency
    merged.sort_by(|a, b| a.figi.cmp(&b.figi));
    
    merged
}

/// Internal function to load the companies HashMap from cache.
///
/// # Returns
/// Some(HashMap) if the cache file exists and is valid, None otherwise.
///
/// # Errors
/// Returns an error if file I/O fails or JSON parsing fails.
async fn load_companies_by_name_internal() -> anyhow::Result<Option<HashMap<String, CompanyInfo>>> {
    let cache_file = Path::new("data/companies_by_name/companies.json");
    
    if !cache_file.exists() {
        return Ok(None);
    }
    
    let content = tokio_fs::read_to_string(cache_file).await
        .context("Failed to read companies.json")?;
    
    let companies: HashMap<String, CompanyInfo> = serde_json::from_str(&content)
        .context("Failed to parse companies.json")?;
    
    Ok(Some(companies))
}

/// Saves the companies HashMap to cache.
///
/// # Arguments
/// * `companies` - The companies HashMap to save
///
/// # Errors
/// Returns an error if file I/O fails or JSON serialization fails.
async fn save_companies_by_name(companies: &HashMap<String, CompanyInfo>) -> anyhow::Result<()> {
    let cache_dir = Path::new("data/companies_by_name");
    tokio_fs::create_dir_all(cache_dir).await
        .context("Failed to create data/companies_by_name directory")?;
    
    let cache_file = cache_dir.join("companies.json");
    let json_str = serde_json::to_string_pretty(&companies)
        .context("Failed to serialize companies to JSON")?;
    
    tokio_fs::write(&cache_file, json_str).await
        .context("Failed to write companies.json")?;
    
    println!("  ✓ Saved {} companies to {}", companies.len(), cache_file.display());
    
    Ok(())
}

/// Loads all OpenFIGI mapping value lists (marketSecDes, micCode, securityType).
///
/// This function fetches the available values for each mapping parameter from the OpenFIGI API
/// and caches them as JSON files in `data/openfigi/`. If the files already exist and are recent
/// (less than 30 days old), they are reused instead of re-fetching.
///
/// # Returns
/// Ok(()) on success.
///
/// # Errors
/// Returns an error if API requests fail, JSON parsing fails, or file I/O fails.
pub async fn load_figi_type_lists() -> anyhow::Result<()> {
    println!("Loading OpenFIGI mapping value lists...");
    
    let client = OpenFigiClient::new()?;
    
    // Create cache directory
    let cache_dir = Path::new("data/openfigi");
    tokio_fs::create_dir_all(cache_dir).await
        .context("Failed to create data/openfigi directory")?;
    
    // Fetch each type list
    get_figi_market_sec_des(&client, cache_dir).await?;
    get_figi_mic_code(&client, cache_dir).await?;
    get_figi_security_type(&client, cache_dir).await?;
    
    println!("OpenFIGI mapping value lists loaded successfully");
    Ok(())
}

/// Fetches and caches the list of valid marketSecDes values.
///
/// # Arguments
/// * `client` - The OpenFIGI client instance.
/// * `cache_dir` - Directory to save the cached JSON file.
///
/// # Returns
/// Ok(()) on success.
///
/// # Errors
/// Returns an error if the API request fails or file I/O fails.
async fn get_figi_market_sec_des(client: &OpenFigiClient, cache_dir: &Path) -> anyhow::Result<()> {
    let cache_file = cache_dir.join("marketSecDes.json");
    
    // Check if cache exists and is recent (< 30 days old)
    if should_use_cache(&cache_file).await? {
        println!("  Using cached marketSecDes values");
        return Ok(());
    }
    
    println!("  Fetching marketSecDes values from OpenFIGI API...");
    
    let resp = client.client
        .get("https://api.openfigi.com/v3/mapping/values/marketSecDes")
        .send()
        .await
        .context("Failed to fetch marketSecDes values")?;
    
    handle_rate_limit(&resp).await?;
    
    let values: Value = resp.json().await
        .context("Failed to parse marketSecDes response")?;
    
    // Save to cache
    let json_str = serde_json::to_string_pretty(&values)?;
    tokio_fs::write(&cache_file, json_str).await
        .context("Failed to write marketSecDes cache")?;
    
    println!("  ✓ Cached marketSecDes values");
    
    // Respect rate limits
    sleep(Duration::from_millis(if client.has_key { 240 } else { 2400 })).await;
    
    Ok(())
}

/// Fetches and caches the list of valid micCode values.
///
/// # Arguments
/// * `client` - The OpenFIGI client instance.
/// * `cache_dir` - Directory to save the cached JSON file.
///
/// # Returns
/// Ok(()) on success.
///
/// # Errors
/// Returns an error if the API request fails or file I/O fails.
async fn get_figi_mic_code(client: &OpenFigiClient, cache_dir: &Path) -> anyhow::Result<()> {
    let cache_file = cache_dir.join("micCode.json");
    
    if should_use_cache(&cache_file).await? {
        println!("  Using cached micCode values");
        return Ok(());
    }
    
    println!("  Fetching micCode values from OpenFIGI API...");
    
    let resp = client.client
        .get("https://api.openfigi.com/v3/mapping/values/micCode")
        .send()
        .await
        .context("Failed to fetch micCode values")?;
    
    handle_rate_limit(&resp).await?;
    
    let values: Value = resp.json().await
        .context("Failed to parse micCode response")?;
    
    let json_str = serde_json::to_string_pretty(&values)?;
    tokio_fs::write(&cache_file, json_str).await
        .context("Failed to write micCode cache")?;
    
    println!("  ✓ Cached micCode values");
    
    sleep(Duration::from_millis(if client.has_key { 240 } else { 2400 })).await;
    
    Ok(())
}

/// Fetches and caches the list of valid securityType values.
///
/// # Arguments
/// * `client` - The OpenFIGI client instance.
/// * `cache_dir` - Directory to save the cached JSON file.
///
/// # Returns
/// Ok(()) on success.
///
/// # Errors
/// Returns an error if the API request fails or file I/O fails.
async fn get_figi_security_type(client: &OpenFigiClient, cache_dir: &Path) -> anyhow::Result<()> {
    let cache_file = cache_dir.join("securityType.json");
    
    if should_use_cache(&cache_file).await? {
        println!("  Using cached securityType values");
        return Ok(());
    }
    
    println!("  Fetching securityType values from OpenFIGI API...");
    
    let resp = client.client
        .get("https://api.openfigi.com/v3/mapping/values/securityType")
        .send()
        .await
        .context("Failed to fetch securityType values")?;
    
    handle_rate_limit(&resp).await?;
    
    let values: Value = resp.json().await
        .context("Failed to parse securityType response")?;
    
    let json_str = serde_json::to_string_pretty(&values)?;
    tokio_fs::write(&cache_file, json_str).await
        .context("Failed to write securityType cache")?;
    
    println!("  ✓ Cached securityType values");
    
    sleep(Duration::from_millis(if client.has_key { 240 } else { 2400 })).await;
    
    Ok(())
}

/// Checks if a cache file exists and is less than 30 days old.
///
/// # Arguments
/// * `path` - Path to the cache file.
///
/// # Returns
/// True if the cache should be used, false if it needs refreshing.
async fn should_use_cache(path: &Path) -> anyhow::Result<bool> {
    if !path.exists() {
        return Ok(false);
    }
    
    let metadata = tokio_fs::metadata(path).await?;
    let modified = metadata.modified()?;
    let age = modified.elapsed().unwrap_or(std::time::Duration::from_secs(u64::MAX));
    
    // Cache is valid for 30 days
    Ok(age < std::time::Duration::from_secs(30 * 24 * 60 * 60))
}

/// Handles rate limit responses from the OpenFIGI API.
///
/// If a 429 status is received, this function sleeps for the duration specified
/// in the `ratelimit-reset` header (or 10 seconds by default).
///
/// # Arguments
/// * `resp` - The HTTP response to check.
///
/// # Returns
/// Ok(()) if no rate limit, or after waiting for the reset period.
///
/// # Errors
/// Returns an error if the response status indicates a non-rate-limit error.
async fn handle_rate_limit(resp: &reqwest::Response) -> anyhow::Result<()> {
    let status = resp.status();
    
    if status == 429 {
        let headers = resp.headers();
        let reset_sec = headers
            .get("ratelimit-reset")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u64>().ok())
            .unwrap_or(10);
        
        println!("  Rate limited—waiting {}s", reset_sec);
        sleep(std::time::Duration::from_secs(reset_sec.max(10))).await;
        
        return Err(anyhow!("Rate limited, please retry"));
    } else if status.is_client_error() || status.is_server_error() {
        return Err(anyhow!("OpenFIGI API error: {}", status));
    }
    
    Ok(())
}