use reqwest::{Client, Response, StatusCode};
use serde::de::DeserializeOwned;
use serde::Deserialize;

use crate::error::{Error, Result};
use crate::models::{Account, Balance, Holdings, Transaction};

const DEFAULT_BASE_URL: &str = "https://lunchflow.app/api/v1";

pub struct LunchFlowClient {
    http: Client,
    api_key: String,
    base_url: String,
}

/// Parameters for the get_transactions request.
#[derive(Debug, Default)]
pub struct TransactionParams {
    /// Include pending transactions (default: false)
    pub include_pending: bool,
    /// Inclusive start date in `YYYY-MM-DD` format
    pub from: Option<String>,
    /// Inclusive end date in `YYYY-MM-DD` format
    pub to: Option<String>,
}

#[derive(Debug, Deserialize)]
struct ApiError {
    message: String,
}

impl LunchFlowClient {
    /// Create a client with the given API key.
    pub fn new(api_key: impl Into<String>) -> Result<Self> {
        let http = Client::builder()
            .https_only(true)
            .build()
            .map_err(Error::Http)?;

        Ok(Self {
            http,
            api_key: api_key.into(),
            base_url: DEFAULT_BASE_URL.to_string(),
        })
    }

    /// Create a client with a custom base URL (useful for testing).
    pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Result<Self> {
        let http = Client::builder().build().map_err(Error::Http)?;

        Ok(Self {
            http,
            api_key: api_key.into(),
            base_url: base_url.into(),
        })
    }

    /// List all accounts.
    pub async fn list_accounts(&self) -> Result<Vec<Account>> {
        let url = format!("{}/accounts", self.base_url);
        let resp = self.send(self.http.get(&url)).await?;
        let body: crate::models::AccountList = decode(resp).await?;
        Ok(body.accounts)
    }

    /// Get the balance for a specific account.
    pub async fn get_balance(&self, account_id: u64) -> Result<Balance> {
        let url = format!("{}/accounts/{}/balance", self.base_url, account_id);
        let resp = self.send(self.http.get(&url)).await?;
        let body: crate::models::BalanceResponse = decode(resp).await?;
        Ok(body.balance)
    }

    /// Get transactions for a specific account.
    pub async fn get_transactions(
        &self,
        account_id: u64,
        params: TransactionParams,
    ) -> Result<Vec<Transaction>> {
        let url = format!("{}/accounts/{}/transactions", self.base_url, account_id);
        let mut query: Vec<(&str, String)> = Vec::new();

        if params.include_pending {
            query.push(("include_pending", "true".to_string()));
        }
        if let Some(from) = params.from {
            query.push(("from", from));
        }
        if let Some(to) = params.to {
            query.push(("to", to));
        }

        let resp = self.send(self.http.get(&url).query(&query)).await?;
        let body: crate::models::TransactionList = decode(resp).await?;
        Ok(body.transactions)
    }

    /// Get holdings for a specific account.
    ///
    /// Only supported for brokerage accounts from providers: snaptrade, quiltt, pluggy.
    /// Returns [`Error::HoldingsNotSupported`] for other account types.
    pub async fn get_holdings(&self, account_id: u64) -> Result<Holdings> {
        let url = format!("{}/accounts/{}/holdings", self.base_url, account_id);
        let resp = self.send(self.http.get(&url)).await?;
        decode(resp).await
    }

    /// Attach the API key header and dispatch the request, mapping HTTP error
    /// status codes to typed [`Error`] variants.
    async fn send(&self, req: reqwest::RequestBuilder) -> Result<reqwest::Response> {
        let resp = req
            .header("x-api-key", &self.api_key)
            .send()
            .await
            .map_err(Error::Http)?;

        match resp.status() {
            StatusCode::OK => Ok(resp),
            StatusCode::UNAUTHORIZED => Err(Error::Unauthorized),
            StatusCode::FORBIDDEN => Err(Error::Forbidden),
            StatusCode::NOT_FOUND => {
                let msg = extract_message(resp).await;
                Err(Error::NotFound(msg))
            }
            StatusCode::NOT_IMPLEMENTED => Err(Error::HoldingsNotSupported),
            s => {
                let msg = extract_message(resp).await;
                Err(Error::ServerError(format!("{}: {}", s.as_u16(), msg)))
            }
        }
    }
}

/// Deserialize a successful response, including the raw body in the error if
/// parsing fails so mismatches are easy to diagnose.
async fn decode<T: DeserializeOwned>(resp: Response) -> Result<T> {
    let text = resp.text().await.map_err(Error::Http)?;
    serde_json::from_str::<T>(&text).map_err(|e| Error::Decode(format!("{e}\n  body: {text}")))
}

/// Pull the `message` field out of an error response body, falling back to
/// the raw text if JSON parsing fails.
async fn extract_message(resp: reqwest::Response) -> String {
    let text = resp.text().await.unwrap_or_default();
    serde_json::from_str::<ApiError>(&text)
        .map(|e| e.message)
        .unwrap_or(text)
}
