#![allow(dead_code)]
// Create a KrakenClient pointing at a mock server.
// We can't import from a bin crate, so we inline a minimal client here.

use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha2::{Digest, Sha256, Sha512};
use std::collections::HashMap;

type HmacSha512 = Hmac<Sha512>;
type Params = HashMap<String, String>;

#[derive(Debug, Deserialize)]
pub struct KrakenResponse<T> {
    pub error: Vec<String>,
    pub result: Option<T>,
}

// Re-export types we need
pub use self::types::*;

mod types {
    use serde::{Deserialize, Serialize};
    use std::collections::HashMap;

    #[derive(Debug, Deserialize, Serialize)]
    pub struct TickerInfo {
        pub a: Vec<String>,
        pub b: Vec<String>,
        pub c: Vec<String>,
        pub v: Vec<String>,
        pub p: Vec<String>,
        pub t: Vec<u64>,
        pub l: Vec<String>,
        pub h: Vec<String>,
        pub o: String,
    }

    pub type TickerResult = HashMap<String, TickerInfo>;

    #[derive(Debug, Serialize)]
    pub struct OrderBook {
        pub asks: Vec<OrderBookLevel>,
        pub bids: Vec<OrderBookLevel>,
    }

    #[derive(Debug, Serialize)]
    pub struct OrderBookLevel {
        pub price: String,
        pub volume: String,
        pub timestamp: u64,
    }

    pub type BalanceResult = HashMap<String, String>;

    #[derive(Debug, Deserialize, Serialize)]
    pub struct TradeInfo {
        pub ordertxid: Option<String>,
        pub pair: Option<String>,
        pub time: Option<f64>,
        #[serde(rename = "type")]
        pub trade_type: Option<String>,
        pub ordertype: Option<String>,
        pub price: Option<String>,
        pub cost: Option<String>,
        pub fee: Option<String>,
        pub vol: Option<String>,
    }

    #[derive(Debug, Deserialize)]
    pub struct TradesHistoryResult {
        pub trades: Option<HashMap<String, TradeInfo>>,
        pub count: Option<u64>,
    }

    #[derive(Debug, Deserialize)]
    pub struct OpenOrdersResult {
        pub open: Option<HashMap<String, serde_json::Value>>,
    }

    #[derive(Debug, Deserialize, Serialize)]
    pub struct AddOrderResult {
        pub descr: Option<AddOrderDescr>,
        pub txid: Option<Vec<String>>,
    }

    #[derive(Debug, Deserialize, Serialize)]
    pub struct AddOrderDescr {
        pub order: Option<String>,
    }

    #[derive(Debug, Deserialize, Serialize)]
    pub struct CancelOrderResult {
        pub count: Option<u64>,
    }
}

#[derive(Debug)]
pub enum KrakenError {
    Http(reqwest::Error),
    Api(String),
    AuthRequired,
    InvalidResponse(String),
}

impl std::fmt::Display for KrakenError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            KrakenError::Http(e) => write!(f, "HTTP error: {e}"),
            KrakenError::Api(e) => write!(f, "{e}"),
            KrakenError::AuthRequired => write!(
                f,
                "Authentication required: set KRAKEN_API_KEY and KRAKEN_API_SECRET"
            ),
            KrakenError::InvalidResponse(e) => write!(f, "Invalid response: {e}"),
        }
    }
}

impl From<reqwest::Error> for KrakenError {
    fn from(e: reqwest::Error) -> Self {
        KrakenError::Http(e)
    }
}

fn classify_errors(errors: Vec<String>) -> KrakenError {
    if errors.is_empty() {
        return KrakenError::Api("Unknown error".into());
    }
    let msg = errors.join("; ");
    if msg.contains("EAPI:Rate limit") {
        KrakenError::Api("Rate limited — slow down requests".into())
    } else if msg.contains("EOrder:Insufficient funds") {
        KrakenError::Api("Insufficient funds for this order".into())
    } else if msg.contains("EQuery:Unknown asset pair") {
        KrakenError::Api(format!("Unknown asset pair: {msg}"))
    } else {
        KrakenError::Api(msg)
    }
}

#[derive(Clone)]
pub struct TestKrakenClient {
    http: reqwest::Client,
    api_key: Option<String>,
    api_secret: Option<String>,
    base_url: String,
}

pub fn mock_client(
    base_url: &str,
    api_key: Option<String>,
    api_secret: Option<String>,
) -> TestKrakenClient {
    TestKrakenClient {
        http: reqwest::Client::new(),
        api_key,
        api_secret,
        base_url: base_url.to_string(),
    }
}

impl TestKrakenClient {
    fn require_auth(&self) -> Result<(&str, &str), KrakenError> {
        match (&self.api_key, &self.api_secret) {
            (Some(k), Some(s)) => Ok((k.as_str(), s.as_str())),
            _ => Err(KrakenError::AuthRequired),
        }
    }

    fn sign(path: &str, nonce: u64, post_data: &str, secret: &str) -> String {
        let secret_bytes = BASE64.decode(secret).expect("valid base64");
        let mut sha256 = Sha256::new();
        sha256.update(format!("{nonce}{post_data}"));
        let sha256_digest = sha256.finalize();
        let mut hmac = HmacSha512::new_from_slice(&secret_bytes).expect("valid key");
        hmac.update(path.as_bytes());
        hmac.update(&sha256_digest);
        BASE64.encode(hmac.finalize().into_bytes())
    }

    async fn public<T: serde::de::DeserializeOwned>(
        &self,
        method: &str,
        params: &Params,
    ) -> Result<T, KrakenError> {
        let url = format!("{}/0/public/{method}", self.base_url);
        let resp = self.http.get(&url).query(params).send().await?;
        let body: KrakenResponse<T> = resp.json().await?;
        if !body.error.is_empty() {
            return Err(classify_errors(body.error));
        }
        body.result
            .ok_or_else(|| KrakenError::InvalidResponse("Missing result".into()))
    }

    async fn private<T: serde::de::DeserializeOwned>(
        &self,
        method: &str,
        mut params: Params,
    ) -> Result<T, KrakenError> {
        let (api_key, api_secret) = self.require_auth()?;
        let nonce = 1234567890u64; // fixed for tests
        params.insert("nonce".into(), nonce.to_string());

        let path = format!("/0/private/{method}");
        let post_data =
            serde_urlencoded::to_string(&params).map_err(|e| KrakenError::Api(e.to_string()))?;
        let signature = Self::sign(&path, nonce, &post_data, api_secret);

        let url = format!("{}{path}", self.base_url);
        let resp = self
            .http
            .post(&url)
            .header("API-Key", api_key)
            .header("API-Sign", &signature)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .body(post_data)
            .send()
            .await?;

        let body: KrakenResponse<T> = resp.json().await?;
        if !body.error.is_empty() {
            return Err(classify_errors(body.error));
        }
        body.result
            .ok_or_else(|| KrakenError::InvalidResponse("Missing result".into()))
    }

    pub async fn ticker(&self, pair: &str) -> Result<TickerResult, KrakenError> {
        let mut p: Params = HashMap::new();
        p.insert("pair".into(), pair.into());
        self.public("Ticker", &p).await
    }

    pub async fn orderbook(
        &self,
        pair: &str,
        count: Option<u32>,
    ) -> Result<OrderBook, KrakenError> {
        let mut p: Params = HashMap::new();
        p.insert("pair".into(), pair.into());
        if let Some(c) = count {
            p.insert("count".into(), c.to_string());
        }

        let url = format!("{}/0/public/Depth", self.base_url);
        let resp = self.http.get(&url).query(&p).send().await?;
        let body: KrakenResponse<serde_json::Value> = resp.json().await?;
        if !body.error.is_empty() {
            return Err(classify_errors(body.error));
        }
        let result = body
            .result
            .ok_or_else(|| KrakenError::InvalidResponse("Missing result".into()))?;

        let pair_data = result
            .as_object()
            .and_then(|m| m.values().next())
            .ok_or_else(|| KrakenError::InvalidResponse("Empty orderbook".into()))?;

        fn parse_levels(val: &serde_json::Value) -> Vec<OrderBookLevel> {
            val.as_array()
                .map(|arr| {
                    arr.iter()
                        .filter_map(|entry| {
                            let a = entry.as_array()?;
                            Some(OrderBookLevel {
                                price: a.first()?.as_str()?.to_string(),
                                volume: a.get(1)?.as_str()?.to_string(),
                                timestamp: a.get(2)?.as_u64().unwrap_or(0),
                            })
                        })
                        .collect()
                })
                .unwrap_or_default()
        }

        Ok(OrderBook {
            asks: parse_levels(&pair_data["asks"]),
            bids: parse_levels(&pair_data["bids"]),
        })
    }

    pub async fn balance(&self) -> Result<BalanceResult, KrakenError> {
        self.private("Balance", Params::new()).await
    }

    pub async fn trade_history(
        &self,
        offset: Option<u32>,
    ) -> Result<TradesHistoryResult, KrakenError> {
        let mut p = Params::new();
        if let Some(ofs) = offset {
            p.insert("ofs".into(), ofs.to_string());
        }
        self.private("TradesHistory", p).await
    }

    pub async fn open_orders(&self) -> Result<OpenOrdersResult, KrakenError> {
        self.private("OpenOrders", Params::new()).await
    }

    pub async fn add_order(
        &self,
        pair: &str,
        direction: &str,
        order_type: &str,
        volume: &str,
        price: Option<&str>,
        validate: bool,
    ) -> Result<AddOrderResult, KrakenError> {
        let mut p = Params::new();
        p.insert("pair".into(), pair.into());
        p.insert("type".into(), direction.into());
        p.insert("ordertype".into(), order_type.into());
        p.insert("volume".into(), volume.into());
        if let Some(pr) = price {
            p.insert("price".into(), pr.into());
        }
        if validate {
            p.insert("validate".into(), "true".into());
        }
        self.private("AddOrder", p).await
    }

    pub async fn cancel_order(&self, txid: &str) -> Result<CancelOrderResult, KrakenError> {
        let mut p = Params::new();
        p.insert("txid".into(), txid.into());
        self.private("CancelOrder", p).await
    }
}
