use std::fmt;
use std::sync::Arc;

use alpaca_http::RequestParts;
use reqwest::Method;
use serde::de::DeserializeOwned;

use crate::{Error, client::ClientInner, pagination};

use super::response::merge_snapshot_page;
use super::{
    BarsRequest, BarsResponse, ChainRequest, ChainResponse, ConditionCodesRequest,
    ConditionCodesResponse, ExchangeCodesResponse, LatestQuotesRequest, LatestQuotesResponse,
    LatestTradesRequest, LatestTradesResponse, SnapshotsRequest, SnapshotsResponse, TradesRequest,
    TradesResponse,
};

const MAX_SNAPSHOT_SYMBOLS_PER_REQUEST: usize = 100;

#[derive(Clone)]
pub struct OptionsClient {
    inner: Arc<ClientInner>,
}

impl OptionsClient {
    pub(crate) fn new(inner: Arc<ClientInner>) -> Self {
        Self { inner }
    }

    pub async fn bars(&self, request: BarsRequest) -> Result<BarsResponse, Error> {
        request.validate()?;
        self.get_json(
            "options.bars",
            "/v1beta1/options/bars",
            request.into_query(),
        )
        .await
    }

    pub async fn bars_all(&self, request: BarsRequest) -> Result<BarsResponse, Error> {
        let client = self.clone();
        pagination::collect_all(request, move |request| {
            let client = client.clone();
            async move { client.bars(request).await }
        })
        .await
    }

    pub async fn trades(&self, request: TradesRequest) -> Result<TradesResponse, Error> {
        request.validate()?;
        self.get_json(
            "options.trades",
            "/v1beta1/options/trades",
            request.into_query(),
        )
        .await
    }

    pub async fn trades_all(&self, request: TradesRequest) -> Result<TradesResponse, Error> {
        let client = self.clone();
        pagination::collect_all(request, move |request| {
            let client = client.clone();
            async move { client.trades(request).await }
        })
        .await
    }

    pub async fn latest_quotes(
        &self,
        request: LatestQuotesRequest,
    ) -> Result<LatestQuotesResponse, Error> {
        request.validate()?;
        self.get_json(
            "options.latest_quotes",
            "/v1beta1/options/quotes/latest",
            request.into_query(),
        )
        .await
    }

    pub async fn latest_trades(
        &self,
        request: LatestTradesRequest,
    ) -> Result<LatestTradesResponse, Error> {
        request.validate()?;
        self.get_json(
            "options.latest_trades",
            "/v1beta1/options/trades/latest",
            request.into_query(),
        )
        .await
    }

    pub async fn snapshots(&self, request: SnapshotsRequest) -> Result<SnapshotsResponse, Error> {
        request.validate()?;
        self.get_json(
            "options.snapshots",
            "/v1beta1/options/snapshots",
            request.into_query(),
        )
        .await
    }

    pub async fn snapshots_all(
        &self,
        request: SnapshotsRequest,
    ) -> Result<SnapshotsResponse, Error> {
        request.validate_all()?;

        let mut combined = SnapshotsResponse::default();
        for batch in request.batches(MAX_SNAPSHOT_SYMBOLS_PER_REQUEST) {
            let client = self.clone();
            let next = pagination::collect_all(batch, move |request| {
                let client = client.clone();
                async move { client.snapshots(request).await }
            })
            .await?;

            merge_snapshot_page(
                "options.snapshots_all",
                &mut combined.snapshots,
                next.snapshots,
            )?;
        }

        Ok(combined)
    }

    pub async fn chain(&self, request: ChainRequest) -> Result<ChainResponse, Error> {
        request.validate()?;
        let path = format!("/v1beta1/options/snapshots/{}", request.path_symbol());
        self.get_json("options.chain", path, request.into_query())
            .await
    }

    pub async fn chain_all(&self, request: ChainRequest) -> Result<ChainResponse, Error> {
        let client = self.clone();
        pagination::collect_all(request, move |request| {
            let client = client.clone();
            async move { client.chain(request).await }
        })
        .await
    }

    pub async fn condition_codes(
        &self,
        request: ConditionCodesRequest,
    ) -> Result<ConditionCodesResponse, Error> {
        let path = format!(
            "/v1beta1/options/meta/conditions/{}",
            request.ticktype.as_str()
        );
        self.get_json("options.condition_codes", path, Vec::new())
            .await
    }

    pub async fn exchange_codes(&self) -> Result<ExchangeCodesResponse, Error> {
        self.get_json(
            "options.exchange_codes",
            "/v1beta1/options/meta/exchanges",
            Vec::new(),
        )
        .await
    }

    #[allow(dead_code)]
    #[must_use]
    pub(crate) fn inner(&self) -> &Arc<ClientInner> {
        &self.inner
    }

    async fn get_json<Response>(
        &self,
        operation: &'static str,
        path: impl Into<String>,
        query: Vec<(String, String)>,
    ) -> Result<Response, Error>
    where
        Response: DeserializeOwned,
    {
        let request = RequestParts::new(Method::GET, path.into())
            .with_operation(operation)
            .with_query(query);

        self.inner
            .send_json::<Response>(request)
            .await
            .map(|response| response.into_body())
    }
}

impl fmt::Debug for OptionsClient {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OptionsClient").finish()
    }
}
