use futures::future::BoxFuture;
use reqwest::Client;
use serde::Deserialize;
use yfinance_rs::{CacheMode, SearchBuilder, YfClient};

use crate::terminal::Company;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SecurityKind {
    Equity,
    Fund,
    Other(String),
}

impl SecurityKind {
    #[must_use]
    pub(crate) const fn is_supported(&self) -> bool {
        matches!(self, Self::Equity | Self::Fund)
    }

    #[must_use]
    pub(crate) fn label(&self) -> &str {
        match self {
            Self::Equity => "Equity",
            Self::Fund => "Fund",
            Self::Other(label) => label.as_str(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SecurityMatch {
    pub symbol: String,
    pub name: Option<String>,
    pub exchange: Option<String>,
    pub kind: SecurityKind,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SecurityLookupError {
    SearchUnavailable,
    DetailUnavailable { symbol: String },
}

pub(crate) trait SecurityLookup: Send + Sync {
    fn search<'a>(
        &'a self,
        query: &'a str,
    ) -> BoxFuture<'a, Result<Vec<SecurityMatch>, SecurityLookupError>>;

    fn load_company<'a>(
        &'a self,
        security_match: &'a SecurityMatch,
    ) -> BoxFuture<'a, Result<Company, SecurityLookupError>>;
}

pub(crate) struct YahooFinanceLookup {
    client: YfClient,
    http_client: Client,
}

impl Default for YahooFinanceLookup {
    fn default() -> Self {
        Self {
            client: YfClient::default(),
            http_client: Client::new(),
        }
    }
}

impl SecurityLookup for YahooFinanceLookup {
    fn search<'a>(
        &'a self,
        query: &'a str,
    ) -> BoxFuture<'a, Result<Vec<SecurityMatch>, SecurityLookupError>> {
        Box::pin(async move {
            let response = SearchBuilder::new(&self.client, query)
                .quotes_count(10)
                .news_count(0)
                .lists_count(0)
                .lang("en-US")
                .region("US")
                .cache_mode(CacheMode::Bypass)
                .fetch()
                .await
                .map_err(|_| SecurityLookupError::SearchUnavailable)?;

            Ok(response
                .results
                .into_iter()
                .map(|result| {
                    let kind = result.kind.to_string();

                    SecurityMatch {
                        symbol: result.symbol.to_string(),
                        name: result.name,
                        exchange: result.exchange.map(|exchange| exchange.to_string()),
                        kind: match kind.as_str() {
                            "EQUITY" => SecurityKind::Equity,
                            "FUND" => SecurityKind::Fund,
                            _ => SecurityKind::Other(kind),
                        },
                    }
                })
                .collect())
        })
    }

    fn load_company<'a>(
        &'a self,
        security_match: &'a SecurityMatch,
    ) -> BoxFuture<'a, Result<Company, SecurityLookupError>> {
        Box::pin(async move {
            let detail_error = || SecurityLookupError::DetailUnavailable {
                symbol: security_match.symbol.clone(),
            };

            let quote = self
                .fetch_live_quote(&security_match.symbol)
                .await
                .map_err(|_| detail_error())?;

            map_live_quote_to_company(security_match, quote).ok_or_else(detail_error)
        })
    }
}

impl YahooFinanceLookup {
    async fn fetch_live_quote(
        &self,
        symbol: &str,
    ) -> Result<YahooQuoteResult, SecurityLookupError> {
        let response = self
            .http_client
            .get("https://query1.finance.yahoo.com/v7/finance/quote")
            .query(&[("symbols", symbol)])
            .send()
            .await
            .map_err(|_| SecurityLookupError::DetailUnavailable {
                symbol: symbol.to_string(),
            })?
            .error_for_status()
            .map_err(|_| SecurityLookupError::DetailUnavailable {
                symbol: symbol.to_string(),
            })?;

        let envelope = response.json::<YahooQuoteEnvelope>().await.map_err(|_| {
            SecurityLookupError::DetailUnavailable {
                symbol: symbol.to_string(),
            }
        })?;

        envelope
            .quote_response
            .result
            .into_iter()
            .find(|quote| quote.symbol.eq_ignore_ascii_case(symbol))
            .ok_or_else(|| SecurityLookupError::DetailUnavailable {
                symbol: symbol.to_string(),
            })
    }
}

fn map_live_quote_to_company(
    security_match: &SecurityMatch,
    quote: YahooQuoteResult,
) -> Option<Company> {
    let price = quote.regular_market_price?;
    let previous_close = quote.regular_market_previous_close.unwrap_or(price);
    let change = price - previous_close;
    let change_percent = if previous_close > 0.0 {
        (change / previous_close) * 100.0
    } else {
        0.0
    };

    let name = quote
        .long_name
        .or(quote.short_name)
        .or_else(|| security_match.name.clone())
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| security_match.symbol.clone());

    Some(Company {
        symbol: security_match.symbol.clone(),
        name,
        price,
        change,
        change_percent,
        market_cap: quote.market_cap.unwrap_or(0.0),
        volume: quote.regular_market_volume.unwrap_or(0),
        pe: quote.trailing_pe,
        eps: quote.eps_trailing_twelve_months,
        high52_week: quote.fifty_two_week_high,
        low52_week: quote.fifty_two_week_low,
    })
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct YahooQuoteEnvelope {
    quote_response: YahooQuoteResponse,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct YahooQuoteResponse {
    result: Vec<YahooQuoteResult>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct YahooQuoteResult {
    symbol: String,
    short_name: Option<String>,
    long_name: Option<String>,
    regular_market_price: Option<f64>,
    regular_market_previous_close: Option<f64>,
    regular_market_volume: Option<u64>,
    market_cap: Option<f64>,
    trailing_pe: Option<f64>,
    eps_trailing_twelve_months: Option<f64>,
    fifty_two_week_high: Option<f64>,
    fifty_two_week_low: Option<f64>,
}

#[cfg(test)]
mod tests {
    use super::{map_live_quote_to_company, SecurityKind, SecurityMatch, YahooQuoteResult};

    #[test]
    fn maps_live_quote_fields_into_company_panel_shape() {
        let security_match = SecurityMatch {
            symbol: "CASEY".to_string(),
            name: Some("Fallback Name".to_string()),
            exchange: Some("NASDAQ".to_string()),
            kind: SecurityKind::Equity,
        };

        let company = map_live_quote_to_company(
            &security_match,
            YahooQuoteResult {
                symbol: "CASEY".to_string(),
                short_name: Some("Casey's".to_string()),
                long_name: Some("Casey's General Stores, Inc.".to_string()),
                regular_market_price: Some(743.42),
                regular_market_previous_close: Some(737.16),
                regular_market_volume: Some(363_594),
                market_cap: Some(27_650_000_000.0),
                trailing_pe: Some(33.4),
                eps_trailing_twelve_months: Some(22.28),
                fifty_two_week_high: Some(751.24),
                fifty_two_week_low: Some(313.89),
            },
        )
        .unwrap();

        assert_eq!(company.symbol, "CASEY");
        assert_eq!(company.name, "Casey's General Stores, Inc.");
        assert_eq!(company.price, 743.42);
        assert!((company.change - 6.26).abs() < 0.0001);
        assert!((company.change_percent - 0.849205).abs() < 0.0001);
        assert_eq!(company.market_cap, 27_650_000_000.0);
        assert_eq!(company.volume, 363_594);
        assert_eq!(company.pe, Some(33.4));
        assert_eq!(company.eps, Some(22.28));
        assert_eq!(company.high52_week, Some(751.24));
        assert_eq!(company.low52_week, Some(313.89));
    }

    #[test]
    fn falls_back_to_security_match_name_when_quote_name_is_missing() {
        let security_match = SecurityMatch {
            symbol: "ABC".to_string(),
            name: Some("Fallback Name".to_string()),
            exchange: Some("NYSE".to_string()),
            kind: SecurityKind::Equity,
        };

        let company = map_live_quote_to_company(
            &security_match,
            YahooQuoteResult {
                symbol: "ABC".to_string(),
                short_name: None,
                long_name: None,
                regular_market_price: Some(100.0),
                regular_market_previous_close: None,
                regular_market_volume: None,
                market_cap: None,
                trailing_pe: None,
                eps_trailing_twelve_months: None,
                fifty_two_week_high: None,
                fifty_two_week_low: None,
            },
        )
        .unwrap();

        assert_eq!(company.name, "Fallback Name");
        assert_eq!(company.change, 0.0);
        assert_eq!(company.change_percent, 0.0);
        assert_eq!(company.market_cap, 0.0);
    }
}
