use std::fmt::{Display, Formatter};
use std::sync::Arc;

use futures::future::BoxFuture;

use crate::terminal::{
    CashFlowPanelData, DividendsPanelData, EarningsPanelData, FilingRef, FinancialsPanelData,
    Frequency,
};

use super::client::SecEdgarClient;
use super::facts::{
    build_cash_flow_periods, build_dividend_events, build_earnings_periods, build_source_status,
    build_statement_periods, company_name, limit_dividend_ttm, normalize_all_facts,
    select_latest_filing,
};
use super::types::CompanySubmissions;
use super::xbrl::pick_instance_document;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum EdgarLookupError {
    MissingUserAgent,
    UnknownTicker {
        ticker: String,
    },
    NoFactsAvailable,
    NoEligibleFilings {
        ticker: String,
        frequency: Frequency,
    },
    RequestFailed {
        provider: &'static str,
        detail: String,
    },
    InvalidResponse {
        provider: &'static str,
        detail: String,
    },
    XbrlParseFailed {
        detail: String,
    },
}

impl Display for EdgarLookupError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MissingUserAgent => formatter.write_str(
                "Set SEC_EDGAR_USER_AGENT to a value like `MosaicIQ admin@example.com`.",
            ),
            Self::UnknownTicker { ticker } => {
                write!(formatter, "No SEC CIK mapping found for {ticker}.")
            }
            Self::NoFactsAvailable => {
                formatter.write_str("SEC companyfacts did not contain matching disclosures.")
            }
            Self::NoEligibleFilings { ticker, frequency } => write!(
                formatter,
                "No eligible {} filings were found for {ticker}.",
                frequency.as_str()
            ),
            Self::RequestFailed { detail, .. }
            | Self::InvalidResponse { detail, .. }
            | Self::XbrlParseFailed { detail } => formatter.write_str(detail),
        }
    }
}

pub(crate) trait EdgarDataLookup: Send + Sync {
    fn financials<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<FinancialsPanelData, EdgarLookupError>>;

    fn cash_flow<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<CashFlowPanelData, EdgarLookupError>>;

    fn dividends<'a>(
        &'a self,
        ticker: &'a str,
    ) -> BoxFuture<'a, Result<DividendsPanelData, EdgarLookupError>>;

    fn earnings<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<EarningsPanelData, EdgarLookupError>>;
}

pub(crate) struct SecEdgarLookup {
    client: Arc<SecEdgarClient>,
}

impl SecEdgarLookup {
    pub(crate) fn new(client: Arc<SecEdgarClient>) -> Self {
        Self { client }
    }

    async fn context_for(
        &self,
        ticker: &str,
        frequency: Frequency,
    ) -> Result<LookupContext, EdgarLookupError> {
        let company = self.client.resolve_company(ticker).await?;
        let submissions = self.client.load_submissions(&company.cik).await?;
        let companyfacts = self.client.load_companyfacts(&company.cik).await?;
        let latest_filing = select_latest_filing(&submissions.filings.recent.rows(), frequency)
            .ok_or_else(|| EdgarLookupError::NoEligibleFilings {
                ticker: company.ticker.clone(),
                frequency,
            })?;

        Ok(LookupContext {
            company,
            submissions,
            companyfacts,
            latest_filing,
        })
    }

    async fn latest_xbrl(
        &self,
        cik: &str,
        filing: &FilingRef,
    ) -> Result<Option<super::types::ParsedXbrlDocument>, EdgarLookupError> {
        let index = self
            .client
            .load_filing_index(cik, &filing.accession_number)
            .await?;
        let filenames = index
            .directory
            .item
            .into_iter()
            .map(|item| item.name)
            .collect::<Vec<_>>();
        let Some(filename) = pick_instance_document(&filenames) else {
            return Ok(None);
        };
        let parsed = self
            .client
            .load_parsed_xbrl(cik, &filing.accession_number, &filename)
            .await?;
        Ok(Some(parsed))
    }
}

impl Default for SecEdgarLookup {
    fn default() -> Self {
        Self::new(Arc::new(SecEdgarClient::default()))
    }
}

impl EdgarDataLookup for SecEdgarLookup {
    fn financials<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<FinancialsPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let context = self.context_for(ticker, frequency).await?;
            let facts = normalize_all_facts(&context.companyfacts)?;
            let latest_xbrl = self
                .latest_xbrl(&context.company.cik, &context.latest_filing)
                .await;
            let status = build_source_status(latest_xbrl.as_ref().map(|value| value.as_ref()));
            let periods = build_statement_periods(
                &facts,
                frequency,
                4,
                latest_xbrl.as_ref().ok().and_then(|value| value.as_ref()),
            );

            Ok(FinancialsPanelData {
                symbol: context.company.ticker.clone(),
                company_name: company_name(
                    &context.company,
                    Some(&context.submissions.name),
                    Some(&context.companyfacts.entity_name),
                ),
                cik: context.company.cik.clone(),
                frequency,
                periods,
                latest_filing: Some(context.latest_filing),
                source_status: status,
            })
        })
    }

    fn cash_flow<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<CashFlowPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let context = self.context_for(ticker, frequency).await?;
            let facts = normalize_all_facts(&context.companyfacts)?;
            let latest_xbrl = self
                .latest_xbrl(&context.company.cik, &context.latest_filing)
                .await;
            let status = build_source_status(latest_xbrl.as_ref().map(|value| value.as_ref()));
            let periods = build_cash_flow_periods(
                &facts,
                frequency,
                latest_xbrl.as_ref().ok().and_then(|value| value.as_ref()),
            );

            Ok(CashFlowPanelData {
                symbol: context.company.ticker.clone(),
                company_name: company_name(
                    &context.company,
                    Some(&context.submissions.name),
                    Some(&context.companyfacts.entity_name),
                ),
                cik: context.company.cik.clone(),
                frequency,
                periods,
                latest_filing: Some(context.latest_filing),
                source_status: status,
            })
        })
    }

    fn dividends<'a>(
        &'a self,
        ticker: &'a str,
    ) -> BoxFuture<'a, Result<DividendsPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let context = self.context_for(ticker, Frequency::Quarterly).await?;
            let facts = normalize_all_facts(&context.companyfacts)?;
            let latest_xbrl = self
                .latest_xbrl(&context.company.cik, &context.latest_filing)
                .await;
            let status = build_source_status(latest_xbrl.as_ref().map(|value| value.as_ref()));
            let events = build_dividend_events(
                &facts,
                latest_xbrl.as_ref().ok().and_then(|value| value.as_ref()),
            );
            let (ttm_dividends_per_share, ttm_common_dividends_paid) = limit_dividend_ttm(&events);

            Ok(DividendsPanelData {
                symbol: context.company.ticker.clone(),
                company_name: company_name(
                    &context.company,
                    Some(&context.submissions.name),
                    Some(&context.companyfacts.entity_name),
                ),
                cik: context.company.cik.clone(),
                ttm_dividends_per_share,
                ttm_common_dividends_paid,
                latest_event: events.first().cloned(),
                events,
                latest_filing: Some(context.latest_filing),
                source_status: status,
            })
        })
    }

    fn earnings<'a>(
        &'a self,
        ticker: &'a str,
        frequency: Frequency,
    ) -> BoxFuture<'a, Result<EarningsPanelData, EdgarLookupError>> {
        Box::pin(async move {
            let context = self.context_for(ticker, frequency).await?;
            let facts = normalize_all_facts(&context.companyfacts)?;
            let latest_xbrl = self
                .latest_xbrl(&context.company.cik, &context.latest_filing)
                .await;
            let status = build_source_status(latest_xbrl.as_ref().map(|value| value.as_ref()));
            let periods = build_earnings_periods(
                &facts,
                frequency,
                latest_xbrl.as_ref().ok().and_then(|value| value.as_ref()),
            );

            Ok(EarningsPanelData {
                symbol: context.company.ticker.clone(),
                company_name: company_name(
                    &context.company,
                    Some(&context.submissions.name),
                    Some(&context.companyfacts.entity_name),
                ),
                cik: context.company.cik.clone(),
                frequency,
                periods,
                latest_filing: Some(context.latest_filing),
                source_status: status,
            })
        })
    }
}

struct LookupContext {
    company: super::types::ResolvedCompany,
    submissions: CompanySubmissions,
    companyfacts: super::types::CompanyFactsResponse,
    latest_filing: FilingRef,
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Arc;

    use futures::future::BoxFuture;

    use super::{EdgarDataLookup, EdgarLookupError, SecEdgarLookup};
    use crate::terminal::sec_edgar::client::{SecEdgarClient, SecFetch};
    use crate::terminal::Frequency;

    const TICKERS_URL: &str = "https://www.sec.gov/files/company_tickers.json";

    struct FixtureFetcher {
        text: HashMap<String, String>,
        bytes: HashMap<String, Vec<u8>>,
    }

    impl SecFetch for FixtureFetcher {
        fn get_text<'a>(&'a self, url: &'a str) -> BoxFuture<'a, Result<String, EdgarLookupError>> {
            Box::pin(async move {
                self.text
                    .get(url)
                    .cloned()
                    .ok_or_else(|| EdgarLookupError::RequestFailed {
                        provider: "SEC EDGAR",
                        detail: format!("missing fixture for {url}"),
                    })
            })
        }

        fn get_bytes<'a>(
            &'a self,
            url: &'a str,
        ) -> BoxFuture<'a, Result<Vec<u8>, EdgarLookupError>> {
            Box::pin(async move {
                self.bytes
                    .get(url)
                    .cloned()
                    .ok_or_else(|| EdgarLookupError::RequestFailed {
                        provider: "SEC EDGAR",
                        detail: format!("missing fixture for {url}"),
                    })
            })
        }
    }

    fn lookup() -> SecEdgarLookup {
        let mut text = HashMap::new();
        let mut bytes = HashMap::new();

        text.insert(
            TICKERS_URL.to_string(),
            include_str!("../../../tests/fixtures/sec/company_tickers.json").to_string(),
        );

        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0000320193",
                accession_number: "0000320193-24-000123",
                submissions: include_str!("../../../tests/fixtures/sec/aapl/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/aapl/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/aapl/index_annual.json"),
                xml_name: "aapl-20240928_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/aapl/instance_annual.xml"),
            },
        );
        text.insert(
            "https://www.sec.gov/Archives/edgar/data/320193/000032019325000010/index.json"
                .to_string(),
            include_str!("../../../tests/fixtures/sec/aapl/index_quarterly.json").to_string(),
        );
        bytes.insert(
            "https://www.sec.gov/Archives/edgar/data/320193/000032019325000010/aapl-20241228_htm.xml".to_string(),
            include_bytes!("../../../tests/fixtures/sec/aapl/instance_quarterly.xml").to_vec(),
        );

        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0000789019",
                accession_number: "0000950170-24-087843",
                submissions: include_str!("../../../tests/fixtures/sec/msft/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/msft/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/msft/index.json"),
                xml_name: "msft-20240630_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/msft/instance.xml"),
            },
        );
        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0000021344",
                accession_number: "0000021344-25-000010",
                submissions: include_str!("../../../tests/fixtures/sec/ko/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/ko/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/ko/index.json"),
                xml_name: "ko-20250328_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/ko/instance.xml"),
            },
        );
        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0001045810",
                accession_number: "0001045810-25-000020",
                submissions: include_str!("../../../tests/fixtures/sec/nvda/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/nvda/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/nvda/index.json"),
                xml_name: "nvda-20250427_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/nvda/instance.xml"),
            },
        );
        add_company(
            &mut text,
            &mut bytes,
            CompanyFixture {
                cik: "0001000184",
                accession_number: "0001000184-24-000001",
                submissions: include_str!("../../../tests/fixtures/sec/sap/submissions.json"),
                companyfacts: include_str!("../../../tests/fixtures/sec/sap/companyfacts.json"),
                index: include_str!("../../../tests/fixtures/sec/sap/index.json"),
                xml_name: "sap-20231231_htm.xml",
                xml: include_str!("../../../tests/fixtures/sec/sap/instance.xml"),
            },
        );

        SecEdgarLookup::new(Arc::new(SecEdgarClient::new(Box::new(FixtureFetcher {
            text,
            bytes,
        }))))
    }

    struct CompanyFixture<'a> {
        cik: &'a str,
        accession_number: &'a str,
        submissions: &'a str,
        companyfacts: &'a str,
        index: &'a str,
        xml_name: &'a str,
        xml: &'a str,
    }

    fn add_company(
        text: &mut HashMap<String, String>,
        bytes: &mut HashMap<String, Vec<u8>>,
        fixture: CompanyFixture<'_>,
    ) {
        text.insert(
            format!("https://data.sec.gov/submissions/CIK{}.json", fixture.cik),
            fixture.submissions.to_string(),
        );
        text.insert(
            format!(
                "https://data.sec.gov/api/xbrl/companyfacts/CIK{}.json",
                fixture.cik
            ),
            fixture.companyfacts.to_string(),
        );
        text.insert(
            format!(
                "https://www.sec.gov/Archives/edgar/data/{}/{}/index.json",
                fixture.cik.trim_start_matches('0'),
                fixture.accession_number.replace('-', "")
            ),
            fixture.index.to_string(),
        );
        bytes.insert(
            format!(
                "https://www.sec.gov/Archives/edgar/data/{}/{}/{}",
                fixture.cik.trim_start_matches('0'),
                fixture.accession_number.replace('-', ""),
                fixture.xml_name
            ),
            fixture.xml.as_bytes().to_vec(),
        );
    }

    #[test]
    fn financials_returns_four_annual_periods() {
        let data = futures::executor::block_on(lookup().financials("AAPL", Frequency::Annual))
            .expect("financials should load");
        assert_eq!(data.periods.len(), 4);
        assert_eq!(data.periods[0].revenue, Some(395000000000.0));
        assert!(data.source_status.latest_xbrl_parsed);
    }

    #[test]
    fn financials_supports_quarterly_periods() {
        let data = futures::executor::block_on(lookup().financials("AAPL", Frequency::Quarterly))
            .expect("quarterly financials should load");
        assert_eq!(data.periods.len(), 4);
        assert_eq!(data.periods[0].revenue, Some(125000000000.0));
    }

    #[test]
    fn cash_flow_computes_free_cash_flow() {
        let data = futures::executor::block_on(lookup().cash_flow("MSFT", Frequency::Annual))
            .expect("cash flow should load");
        assert_eq!(data.periods[0].operating_cash_flow, Some(120000000000.0));
        assert_eq!(data.periods[0].free_cash_flow, Some(75523000000.0));
    }

    #[test]
    fn dividends_returns_history_and_ttm_summary() {
        let data =
            futures::executor::block_on(lookup().dividends("KO")).expect("dividends should load");
        assert_eq!(data.events.len(), 8);
        let ttm_dividends_per_share = data.ttm_dividends_per_share.unwrap_or_default();
        assert!((ttm_dividends_per_share - 1.985).abs() < 0.000_1);
        assert_eq!(
            data.latest_event
                .as_ref()
                .map(|value| value.frequency_guess.as_str()),
            Some("quarterly")
        );
    }

    #[test]
    fn earnings_returns_yoy_deltas() {
        let data = futures::executor::block_on(lookup().earnings("NVDA", Frequency::Quarterly))
            .expect("earnings should load");
        assert_eq!(data.periods.len(), 8);
        assert_eq!(data.periods[0].revenue, Some(26100000000.0));
        assert!(
            data.periods[0]
                .revenue_yoy_change_percent
                .unwrap_or_default()
                > 250.0
        );
    }

    #[test]
    fn ifrs_financials_fallback_works() {
        let data = futures::executor::block_on(lookup().financials("SAP", Frequency::Annual))
            .expect("ifrs annual financials should load");
        assert_eq!(data.periods[0].revenue, Some(35200000000.0));
        assert_eq!(data.periods[0].net_income, Some(6200000000.0));
    }
}
