//! FinMind (Taiwan + global stock data) endpoints — 54 datasets.
//!
//! FinMind rows are passed through as `serde_json::Value` (the server serializes
//! them the same way). Every method returns `Rows` = `FinanceResult<Vec<Value>>`;
//! callers who want typed rows can `serde_json::from_value` each element.
//!
//! No API token is required (rate-limited); a token raises the quota (configured
//! server-side, not in the SDK).

use serde::Serialize;
use serde_json::Value;

use crate::client::FinProxy;
use crate::error::SdkError;

/// FinMind result: the standard envelope with untyped rows.
pub type Rows = motosan_finance::FinanceResult<Vec<Value>>;

/// `?symbol=&start=&end=` — symbol-keyed datasets (start/end optional).
#[derive(Debug, Clone, Serialize, Default)]
pub struct SymbolRange {
    pub symbol: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end: Option<String>,
}

impl SymbolRange {
    pub fn new(symbol: impl Into<String>) -> Self {
        Self { symbol: symbol.into(), ..Default::default() }
    }
    pub fn start(mut self, d: impl Into<String>) -> Self {
        self.start = Some(d.into());
        self
    }
    pub fn end(mut self, d: impl Into<String>) -> Self {
        self.end = Some(d.into());
        self
    }
}

/// `?start=&end=` — market-wide datasets (no symbol).
#[derive(Debug, Clone, Serialize, Default)]
pub struct Range {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end: Option<String>,
}

impl Range {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn start(mut self, d: impl Into<String>) -> Self {
        self.start = Some(d.into());
        self
    }
    pub fn end(mut self, d: impl Into<String>) -> Self {
        self.end = Some(d.into());
        self
    }
}

/// `?data_id=&start=&end=` — derivatives / convertible-bond datasets.
#[derive(Debug, Clone, Serialize, Default)]
pub struct DataIdRange {
    pub data_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end: Option<String>,
}

impl DataIdRange {
    pub fn new(data_id: impl Into<String>) -> Self {
        Self { data_id: data_id.into(), ..Default::default() }
    }
    pub fn start(mut self, d: impl Into<String>) -> Self {
        self.start = Some(d.into());
        self
    }
    pub fn end(mut self, d: impl Into<String>) -> Self {
        self.end = Some(d.into());
        self
    }
}

/// `?securities_trader_id=&start=&end=` — broker/trader datasets.
#[derive(Debug, Clone, Serialize, Default)]
pub struct TraderRange {
    pub securities_trader_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end: Option<String>,
}

impl TraderRange {
    pub fn new(securities_trader_id: impl Into<String>) -> Self {
        Self { securities_trader_id: securities_trader_id.into(), ..Default::default() }
    }
    pub fn start(mut self, d: impl Into<String>) -> Self {
        self.start = Some(d.into());
        self
    }
    pub fn end(mut self, d: impl Into<String>) -> Self {
        self.end = Some(d.into());
        self
    }
}

/// `?currency=&start=&end=` — exchange-rate datasets.
#[derive(Debug, Clone, Serialize, Default)]
pub struct CurrencyRange {
    pub currency: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end: Option<String>,
}

impl CurrencyRange {
    pub fn new(currency: impl Into<String>) -> Self {
        Self { currency: currency.into(), ..Default::default() }
    }
    pub fn start(mut self, d: impl Into<String>) -> Self {
        self.start = Some(d.into());
        self
    }
    pub fn end(mut self, d: impl Into<String>) -> Self {
        self.end = Some(d.into());
        self
    }
}

/// Empty query for the one parameterless endpoint (serializes to no query string).
#[derive(Serialize)]
struct NoParams {}

/// FinMind sub-client, obtained via [`FinProxy::finmind`].
pub struct FinMind<'a> {
    pub(crate) client: &'a FinProxy,
}

impl FinProxy {
    /// Access the FinMind (Taiwan + global) endpoints.
    pub fn finmind(&self) -> FinMind<'_> {
        FinMind { client: self }
    }
}

// --- Prices + valuation ---
impl FinMind<'_> {
    /// GET /v1/finmind/taiwan-stock-price-adj
    pub async fn price_adj(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-price-adj", p).await
    }
    /// GET /v1/finmind/taiwan-stock-price-tick
    pub async fn price_tick(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-price-tick", p).await
    }
    /// GET /v1/finmind/taiwan-stock-kbar
    pub async fn kbar(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-kbar", p).await
    }
    /// GET /v1/finmind/taiwan-stock-week-price
    pub async fn week_price(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-week-price", p).await
    }
    /// GET /v1/finmind/taiwan-stock-month-price
    pub async fn month_price(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-month-price", p).await
    }
    /// GET /v1/finmind/taiwan-stock-10year
    pub async fn ten_year(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-10year", p).await
    }
    /// GET /v1/finmind/taiwan-stock-price-limit
    pub async fn price_limit(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-price-limit", p).await
    }
    /// GET /v1/finmind/taiwan-stock-per
    pub async fn per(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-per", p).await
    }
    /// GET /v1/finmind/taiwan-stock-day-trading
    pub async fn day_trading(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-day-trading", p).await
    }
    /// GET /v1/finmind/taiwan-stock-info (no params)
    pub async fn info(&self) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/taiwan-stock-info", &NoParams {}).await
    }
}

// --- Chips + chip-extended ---
impl FinMind<'_> {
    /// GET /v1/finmind/institutional-trading
    pub async fn institutional_trading(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/institutional-trading", p).await
    }
    /// GET /v1/finmind/total-institutional-trading
    pub async fn total_institutional_trading(&self, p: &Range) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/total-institutional-trading", p).await
    }
    /// GET /v1/finmind/margin-balance
    pub async fn margin_balance(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/margin-balance", p).await
    }
    /// GET /v1/finmind/total-margin-balance
    pub async fn total_margin_balance(&self, p: &Range) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/total-margin-balance", p).await
    }
    /// GET /v1/finmind/foreign-ownership
    pub async fn foreign_ownership(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/foreign-ownership", p).await
    }
    /// GET /v1/finmind/holding-distribution
    pub async fn holding_distribution(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/holding-distribution", p).await
    }
    /// GET /v1/finmind/securities-lending
    pub async fn securities_lending(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/securities-lending", p).await
    }
    /// GET /v1/finmind/government-bank-buy-sell
    pub async fn government_bank_buy_sell(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/government-bank-buy-sell", p).await
    }
    /// GET /v1/finmind/daily-short-sale-balances
    pub async fn daily_short_sale_balances(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/daily-short-sale-balances", p).await
    }
    /// GET /v1/finmind/margin-short-sale-suspension
    pub async fn margin_short_sale_suspension(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/margin-short-sale-suspension", p).await
    }
    /// GET /v1/finmind/total-exchange-margin-maintenance
    pub async fn total_exchange_margin_maintenance(&self, p: &Range) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/total-exchange-margin-maintenance", p).await
    }
    /// GET /v1/finmind/day-trading-suspension
    pub async fn day_trading_suspension(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/day-trading-suspension", p).await
    }
    /// GET /v1/finmind/disposition-securities
    pub async fn disposition_securities(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/disposition-securities", p).await
    }
    /// GET /v1/finmind/securities-trader-info
    pub async fn securities_trader_info(&self, p: &TraderRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/securities-trader-info", p).await
    }
    /// GET /v1/finmind/trading-daily-report
    pub async fn trading_daily_report(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/trading-daily-report", p).await
    }
    /// GET /v1/finmind/trading-daily-report-agg
    pub async fn trading_daily_report_agg(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/trading-daily-report-agg", p).await
    }
    /// GET /v1/finmind/warrant-trading-daily-report
    pub async fn warrant_trading_daily_report(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/warrant-trading-daily-report", p).await
    }
}

// --- Macro / corporate actions ---
impl FinMind<'_> {
    /// GET /v1/finmind/stock-news
    pub async fn stock_news(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/stock-news", p).await
    }
    /// GET /v1/finmind/business-indicator
    pub async fn business_indicator(&self, p: &Range) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/business-indicator", p).await
    }
    /// GET /v1/finmind/industry-chain
    pub async fn industry_chain(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/industry-chain", p).await
    }
    /// GET /v1/finmind/trading-date
    pub async fn trading_date(&self, p: &Range) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/trading-date", p).await
    }
    /// GET /v1/finmind/stock-suspended
    pub async fn stock_suspended(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/stock-suspended", p).await
    }
    /// GET /v1/finmind/stock-split-price
    pub async fn stock_split_price(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/stock-split-price", p).await
    }
    /// GET /v1/finmind/par-value-change
    pub async fn par_value_change(&self, p: &Range) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/par-value-change", p).await
    }
    /// GET /v1/finmind/capital-reduction-price
    pub async fn capital_reduction_price(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/capital-reduction-price", p).await
    }
}

// --- Derivatives + convertible bonds + global ---
impl FinMind<'_> {
    /// GET /v1/finmind/futures-daily
    pub async fn futures_daily(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/futures-daily", p).await
    }
    /// GET /v1/finmind/options-daily
    pub async fn options_daily(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/options-daily", p).await
    }
    /// GET /v1/finmind/futures-institutional
    pub async fn futures_institutional(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/futures-institutional", p).await
    }
    /// GET /v1/finmind/options-institutional
    pub async fn options_institutional(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/options-institutional", p).await
    }
    /// GET /v1/finmind/futures-large-traders
    pub async fn futures_large_traders(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/futures-large-traders", p).await
    }
    /// GET /v1/finmind/options-large-traders
    pub async fn options_large_traders(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/options-large-traders", p).await
    }
    /// GET /v1/finmind/futures-tick
    pub async fn futures_tick(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/futures-tick", p).await
    }
    /// GET /v1/finmind/options-tick
    pub async fn options_tick(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/options-tick", p).await
    }
    /// GET /v1/finmind/futures-dealer-volume
    pub async fn futures_dealer_volume(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/futures-dealer-volume", p).await
    }
    /// GET /v1/finmind/options-dealer-volume
    pub async fn options_dealer_volume(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/options-dealer-volume", p).await
    }
    /// GET /v1/finmind/convertible-bond-info
    pub async fn convertible_bond_info(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/convertible-bond-info", p).await
    }
    /// GET /v1/finmind/convertible-bond-daily
    pub async fn convertible_bond_daily(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/convertible-bond-daily", p).await
    }
    /// GET /v1/finmind/convertible-bond-institutional
    pub async fn convertible_bond_institutional(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/convertible-bond-institutional", p).await
    }
    /// GET /v1/finmind/convertible-bond-overview
    pub async fn convertible_bond_overview(&self, p: &DataIdRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/convertible-bond-overview", p).await
    }
    /// GET /v1/finmind/us-stock-info
    pub async fn us_stock_info(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/us-stock-info", p).await
    }
    /// GET /v1/finmind/europe-stock-info
    pub async fn europe_stock_info(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/europe-stock-info", p).await
    }
    /// GET /v1/finmind/japan-stock-info
    pub async fn japan_stock_info(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/japan-stock-info", p).await
    }
    /// GET /v1/finmind/uk-stock-info
    pub async fn uk_stock_info(&self, p: &SymbolRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/uk-stock-info", p).await
    }
    /// GET /v1/finmind/exchange-rate
    pub async fn exchange_rate(&self, p: &CurrencyRange) -> Result<Rows, SdkError> {
        self.client.get("/v1/finmind/exchange-rate", p).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use wiremock::matchers::{method, path, query_param};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    // Body helper: a FinMind FinanceResult<Vec<Value>> with one untyped row.
    fn body() -> &'static str {
        r#"{"success":true,"data":{"results":[{"x":1}],"provider":"finmind",
            "warnings":[],"extra":{}},"meta":{}}"#
    }

    #[tokio::test]
    async fn kbar_encodes_symbol_range_and_unwraps_value_rows() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/v1/finmind/taiwan-stock-kbar"))
            .and(query_param("symbol", "2330"))
            .and(query_param("start", "2024-01-01"))
            .and(query_param("end", "2024-02-01"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body()))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let res = client
            .finmind()
            .kbar(&SymbolRange::new("2330").start("2024-01-01").end("2024-02-01"))
            .await
            .unwrap();
        assert_eq!(res.provider.as_deref(), Some("finmind"));
        assert_eq!(res.results.unwrap().len(), 1);
    }

    #[tokio::test]
    async fn info_sends_no_query_params() {
        let server = MockServer::start().await;
        // Match only requests with NO query string by mounting on the bare path.
        Mock::given(method("GET"))
            .and(path("/v1/finmind/taiwan-stock-info"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body()))
            .mount(&server)
            .await;

        let client = FinProxy::new(server.uri()).unwrap();
        let res = client.finmind().info().await.unwrap();
        assert_eq!(res.provider.as_deref(), Some("finmind"));
    }

    #[tokio::test]
    async fn total_margin_balance_encodes_range() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/v1/finmind/total-margin-balance"))
            .and(query_param("start", "2024-01-01"))
            .and(query_param("end", "2024-01-31"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body()))
            .mount(&server)
            .await;
        let client = FinProxy::new(server.uri()).unwrap();
        let res = client
            .finmind()
            .total_margin_balance(&Range::new().start("2024-01-01").end("2024-01-31"))
            .await
            .unwrap();
        assert_eq!(res.provider.as_deref(), Some("finmind"));
    }

    #[tokio::test]
    async fn securities_trader_info_encodes_trader_id() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/v1/finmind/securities-trader-info"))
            .and(query_param("securities_trader_id", "1234"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body()))
            .mount(&server)
            .await;
        let client = FinProxy::new(server.uri()).unwrap();
        let res = client
            .finmind()
            .securities_trader_info(&TraderRange::new("1234"))
            .await
            .unwrap();
        assert_eq!(res.provider.as_deref(), Some("finmind"));
    }

    #[tokio::test]
    async fn trading_date_hits_path() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/v1/finmind/trading-date"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body()))
            .mount(&server)
            .await;
        let client = FinProxy::new(server.uri()).unwrap();
        let res = client.finmind().trading_date(&Range::new()).await.unwrap();
        assert_eq!(res.provider.as_deref(), Some("finmind"));
    }

    #[tokio::test]
    async fn futures_daily_encodes_data_id() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/v1/finmind/futures-daily"))
            .and(query_param("data_id", "TX"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body()))
            .mount(&server)
            .await;
        let client = FinProxy::new(server.uri()).unwrap();
        let res = client
            .finmind()
            .futures_daily(&DataIdRange::new("TX").start("2024-01-02"))
            .await
            .unwrap();
        assert_eq!(res.provider.as_deref(), Some("finmind"));
    }

    #[tokio::test]
    async fn exchange_rate_encodes_currency() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/v1/finmind/exchange-rate"))
            .and(query_param("currency", "USD"))
            .respond_with(ResponseTemplate::new(200).set_body_string(body()))
            .mount(&server)
            .await;
        let client = FinProxy::new(server.uri()).unwrap();
        let res = client.finmind().exchange_rate(&CurrencyRange::new("USD")).await.unwrap();
        assert_eq!(res.provider.as_deref(), Some("finmind"));
    }
}
