use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};

use futures::future::BoxFuture;
use reqwest::Client;
use tokio::sync::Mutex as AsyncMutex;

use super::service::EdgarLookupError;
use super::types::{
    CompanyFactsResponse, CompanySubmissions, FilingIndex, ParsedXbrlDocument, RecentFilings,
    ResolvedCompany, TickerDirectoryEntry,
};
use super::xbrl::parse_xbrl_instance;

const TICKERS_URL: &str = "https://www.sec.gov/files/company_tickers.json";
const SUBMISSIONS_URL_PREFIX: &str = "https://data.sec.gov/submissions/CIK";
const SUBMISSIONS_FILE_URL_PREFIX: &str = "https://data.sec.gov/submissions";
const COMPANYFACTS_URL_PREFIX: &str = "https://data.sec.gov/api/xbrl/companyfacts/CIK";
const SEC_ARCHIVE_PREFIX: &str = "https://www.sec.gov/Archives/edgar/data";
const TICKER_CACHE_TTL: Duration = Duration::from_secs(24 * 60 * 60);
const SHORT_CACHE_TTL: Duration = Duration::from_secs(15 * 60);
const REQUEST_SPACING: Duration = Duration::from_millis(125);

pub(crate) trait SecFetch: Send + Sync {
    fn get_text<'a>(&'a self, url: &'a str) -> BoxFuture<'a, Result<String, EdgarLookupError>>;
    fn get_bytes<'a>(&'a self, url: &'a str) -> BoxFuture<'a, Result<Vec<u8>, EdgarLookupError>>;
}

pub(crate) trait SecUserAgentProvider: Send + Sync {
    fn user_agent(&self) -> Option<String>;
}

struct EnvSecUserAgentProvider;

impl SecUserAgentProvider for EnvSecUserAgentProvider {
    fn user_agent(&self) -> Option<String> {
        std::env::var("SEC_EDGAR_USER_AGENT").ok()
    }
}

pub(crate) struct LiveSecFetcher {
    client: Client,
    user_agent_provider: std::sync::Arc<dyn SecUserAgentProvider>,
    last_request_at: AsyncMutex<Option<Instant>>,
}

impl LiveSecFetcher {
    pub(crate) fn new(user_agent_provider: std::sync::Arc<dyn SecUserAgentProvider>) -> Self {
        Self {
            client: Client::new(),
            user_agent_provider,
            last_request_at: AsyncMutex::new(None),
        }
    }

    async fn throttle(&self) {
        let mut guard = self.last_request_at.lock().await;
        if let Some(last_request_at) = *guard {
            let elapsed = last_request_at.elapsed();
            if elapsed < REQUEST_SPACING {
                tokio::time::sleep(REQUEST_SPACING - elapsed).await;
            }
        }
        *guard = Some(Instant::now());
    }

    fn user_agent(&self) -> Result<String, EdgarLookupError> {
        self.user_agent_provider
            .user_agent()
            .filter(|value| !value.trim().is_empty())
            .ok_or(EdgarLookupError::MissingUserAgent)
    }
}

impl Default for LiveSecFetcher {
    fn default() -> Self {
        Self::new(std::sync::Arc::new(EnvSecUserAgentProvider))
    }
}

impl SecFetch for LiveSecFetcher {
    fn get_text<'a>(&'a self, url: &'a str) -> BoxFuture<'a, Result<String, EdgarLookupError>> {
        Box::pin(async move {
            self.throttle().await;
            let response = self
                .client
                .get(url)
                .header("User-Agent", self.user_agent()?)
                .header("Accept-Encoding", "gzip, deflate")
                .send()
                .await
                .map_err(|source| EdgarLookupError::RequestFailed {
                    provider: "SEC EDGAR",
                    detail: source.to_string(),
                })?
                .error_for_status()
                .map_err(|source| EdgarLookupError::RequestFailed {
                    provider: "SEC EDGAR",
                    detail: source.to_string(),
                })?;

            response
                .text()
                .await
                .map_err(|source| EdgarLookupError::RequestFailed {
                    provider: "SEC EDGAR",
                    detail: source.to_string(),
                })
        })
    }

    fn get_bytes<'a>(&'a self, url: &'a str) -> BoxFuture<'a, Result<Vec<u8>, EdgarLookupError>> {
        Box::pin(async move {
            self.throttle().await;
            let response = self
                .client
                .get(url)
                .header("User-Agent", self.user_agent()?)
                .header("Accept-Encoding", "gzip, deflate")
                .send()
                .await
                .map_err(|source| EdgarLookupError::RequestFailed {
                    provider: "SEC EDGAR",
                    detail: source.to_string(),
                })?
                .error_for_status()
                .map_err(|source| EdgarLookupError::RequestFailed {
                    provider: "SEC EDGAR",
                    detail: source.to_string(),
                })?;

            response
                .bytes()
                .await
                .map(|value| value.to_vec())
                .map_err(|source| EdgarLookupError::RequestFailed {
                    provider: "SEC EDGAR",
                    detail: source.to_string(),
                })
        })
    }
}

#[derive(Debug, Clone)]
struct CacheEntry<T> {
    cached_at: Instant,
    value: T,
}

impl<T> CacheEntry<T> {
    fn new(value: T) -> Self {
        Self {
            cached_at: Instant::now(),
            value,
        }
    }

    fn is_fresh(&self, ttl: Duration) -> bool {
        self.cached_at.elapsed() <= ttl
    }
}

pub(crate) struct SecEdgarClient {
    fetcher: Box<dyn SecFetch>,
    tickers_cache: Mutex<Option<CacheEntry<HashMap<String, ResolvedCompany>>>>,
    submissions_cache: Mutex<HashMap<String, CacheEntry<CompanySubmissions>>>,
    submissions_file_cache: Mutex<HashMap<String, CacheEntry<RecentFilings>>>,
    companyfacts_cache: Mutex<HashMap<String, CacheEntry<CompanyFactsResponse>>>,
    filing_index_cache: Mutex<HashMap<String, CacheEntry<FilingIndex>>>,
    instance_xml_cache: Mutex<HashMap<String, CacheEntry<Vec<u8>>>>,
    parsed_xbrl_cache: Mutex<HashMap<String, CacheEntry<ParsedXbrlDocument>>>,
}

impl SecEdgarClient {
    pub(crate) fn new(fetcher: Box<dyn SecFetch>) -> Self {
        Self {
            fetcher,
            tickers_cache: Mutex::new(None),
            submissions_cache: Mutex::new(HashMap::new()),
            submissions_file_cache: Mutex::new(HashMap::new()),
            companyfacts_cache: Mutex::new(HashMap::new()),
            filing_index_cache: Mutex::new(HashMap::new()),
            instance_xml_cache: Mutex::new(HashMap::new()),
            parsed_xbrl_cache: Mutex::new(HashMap::new()),
        }
    }

    pub(crate) async fn resolve_company(
        &self,
        ticker: &str,
    ) -> Result<ResolvedCompany, EdgarLookupError> {
        let normalized = normalize_ticker(ticker);
        let directory = self.load_tickers().await?;

        if let Some(company) = directory.get(&normalized) {
            return Ok(company.clone());
        }

        let fallback = normalized.replace('.', "-");
        if let Some(company) = directory.get(&fallback) {
            return Ok(company.clone());
        }

        let alternate = normalized.replace('-', ".");
        if let Some(company) = directory.get(&alternate) {
            return Ok(company.clone());
        }

        Err(EdgarLookupError::UnknownTicker {
            ticker: ticker.to_string(),
        })
    }

    pub(crate) async fn load_submissions(
        &self,
        cik: &str,
    ) -> Result<CompanySubmissions, EdgarLookupError> {
        if let Some(cached) = get_cached_value(&self.submissions_cache, cik, SHORT_CACHE_TTL) {
            return Ok(cached);
        }

        let url = format!("{SUBMISSIONS_URL_PREFIX}{cik}.json");
        let payload = self.fetcher.get_text(&url).await?;
        let decoded = serde_json::from_str::<CompanySubmissions>(&payload).map_err(|source| {
            EdgarLookupError::InvalidResponse {
                provider: "SEC EDGAR",
                detail: source.to_string(),
            }
        })?;
        store_cached_value(&self.submissions_cache, cik.to_string(), decoded.clone());
        Ok(decoded)
    }

    pub(crate) async fn load_companyfacts(
        &self,
        cik: &str,
    ) -> Result<CompanyFactsResponse, EdgarLookupError> {
        if let Some(cached) = get_cached_value(&self.companyfacts_cache, cik, SHORT_CACHE_TTL) {
            return Ok(cached);
        }

        let url = format!("{COMPANYFACTS_URL_PREFIX}{cik}.json");
        let payload = self.fetcher.get_text(&url).await?;
        let decoded = serde_json::from_str::<CompanyFactsResponse>(&payload).map_err(|source| {
            EdgarLookupError::InvalidResponse {
                provider: "SEC EDGAR",
                detail: source.to_string(),
            }
        })?;
        store_cached_value(&self.companyfacts_cache, cik.to_string(), decoded.clone());
        Ok(decoded)
    }

    pub(crate) async fn load_submissions_file(
        &self,
        filename: &str,
    ) -> Result<RecentFilings, EdgarLookupError> {
        if let Some(cached) =
            get_cached_value(&self.submissions_file_cache, filename, SHORT_CACHE_TTL)
        {
            return Ok(cached);
        }

        let url = format!("{SUBMISSIONS_FILE_URL_PREFIX}/{filename}");
        let payload = self.fetcher.get_text(&url).await?;
        let decoded = serde_json::from_str::<RecentFilings>(&payload).map_err(|source| {
            EdgarLookupError::InvalidResponse {
                provider: "SEC EDGAR",
                detail: source.to_string(),
            }
        })?;
        store_cached_value(
            &self.submissions_file_cache,
            filename.to_string(),
            decoded.clone(),
        );
        Ok(decoded)
    }

    pub(crate) async fn load_filing_index(
        &self,
        cik: &str,
        accession_number: &str,
    ) -> Result<FilingIndex, EdgarLookupError> {
        let cache_key = format!("{cik}:{accession_number}");
        if let Some(cached) =
            get_cached_value(&self.filing_index_cache, &cache_key, SHORT_CACHE_TTL)
        {
            return Ok(cached);
        }

        let accession_number_no_dashes = accession_number.replace('-', "");
        let cik_no_zeroes = cik.trim_start_matches('0');
        let url =
            format!("{SEC_ARCHIVE_PREFIX}/{cik_no_zeroes}/{accession_number_no_dashes}/index.json");
        let payload = self.fetcher.get_text(&url).await?;
        let decoded = serde_json::from_str::<FilingIndex>(&payload).map_err(|source| {
            EdgarLookupError::InvalidResponse {
                provider: "SEC EDGAR",
                detail: source.to_string(),
            }
        })?;
        store_cached_value(&self.filing_index_cache, cache_key, decoded.clone());
        Ok(decoded)
    }

    pub(crate) async fn load_instance_xml(
        &self,
        cik: &str,
        accession_number: &str,
        filename: &str,
    ) -> Result<Vec<u8>, EdgarLookupError> {
        let cache_key = format!("{cik}:{accession_number}:{filename}");
        if let Some(cached) =
            get_cached_value(&self.instance_xml_cache, &cache_key, SHORT_CACHE_TTL)
        {
            return Ok(cached);
        }

        let accession_number_no_dashes = accession_number.replace('-', "");
        let cik_no_zeroes = cik.trim_start_matches('0');
        let url =
            format!("{SEC_ARCHIVE_PREFIX}/{cik_no_zeroes}/{accession_number_no_dashes}/{filename}");
        let payload = self.fetcher.get_bytes(&url).await?;
        store_cached_value(&self.instance_xml_cache, cache_key, payload.clone());
        Ok(payload)
    }

    pub(crate) async fn load_parsed_xbrl(
        &self,
        cik: &str,
        accession_number: &str,
        filename: &str,
    ) -> Result<ParsedXbrlDocument, EdgarLookupError> {
        let cache_key = format!("{cik}:{accession_number}:{filename}");
        if let Some(cached) = get_cached_value(&self.parsed_xbrl_cache, &cache_key, SHORT_CACHE_TTL)
        {
            return Ok(cached);
        }

        let bytes = self
            .load_instance_xml(cik, accession_number, filename)
            .await?;
        let parsed = parse_xbrl_instance(&bytes)?;
        store_cached_value(&self.parsed_xbrl_cache, cache_key, parsed.clone());
        Ok(parsed)
    }

    async fn load_tickers(&self) -> Result<HashMap<String, ResolvedCompany>, EdgarLookupError> {
        if let Ok(guard) = self.tickers_cache.lock() {
            if let Some(entry) = guard
                .as_ref()
                .filter(|entry| entry.is_fresh(TICKER_CACHE_TTL))
            {
                return Ok(entry.value.clone());
            }
        }

        let payload = self.fetcher.get_text(TICKERS_URL).await?;
        let decoded = serde_json::from_str::<HashMap<String, TickerDirectoryEntry>>(&payload)
            .map_err(|source| EdgarLookupError::InvalidResponse {
                provider: "SEC EDGAR",
                detail: source.to_string(),
            })?;

        let directory = decoded
            .into_values()
            .map(|entry| {
                (
                    normalize_ticker(&entry.ticker),
                    ResolvedCompany {
                        ticker: normalize_ticker(&entry.ticker),
                        company_name: entry.title,
                        cik: format!("{:010}", entry.cik_str),
                    },
                )
            })
            .collect::<HashMap<_, _>>();

        if let Ok(mut guard) = self.tickers_cache.lock() {
            *guard = Some(CacheEntry::new(directory.clone()));
        }

        Ok(directory)
    }
}

impl Default for SecEdgarClient {
    fn default() -> Self {
        Self::new(Box::new(LiveSecFetcher::default()))
    }
}

fn normalize_ticker(value: &str) -> String {
    value.trim().to_ascii_uppercase()
}

fn get_cached_value<T: Clone>(
    cache: &Mutex<HashMap<String, CacheEntry<T>>>,
    key: &str,
    ttl: Duration,
) -> Option<T> {
    let mut guard = cache.lock().ok()?;
    match guard.get(key) {
        Some(entry) if entry.is_fresh(ttl) => Some(entry.value.clone()),
        Some(_) => {
            guard.remove(key);
            None
        }
        None => None,
    }
}

fn store_cached_value<T>(cache: &Mutex<HashMap<String, CacheEntry<T>>>, key: String, value: T) {
    if let Ok(mut guard) = cache.lock() {
        guard.insert(key, CacheEntry::new(value));
    }
}
