//! Symbol name-to-ID resolution and metadata cache.
//!
//! Loads the symbol list from the cTrader server and provides cached lookups.
//! Note: `ProtoOaLightSymbol` only contains basic info (name, ID, enabled).
//! Full symbol details (digits, pip position, volumes) require `ProtoOASymbolByIdReq`.

use std::collections::HashMap;
use std::time::Duration;

use prost::Message;
use tracing::{debug, info};

use crate::dispatch::MessageDispatcher;
use crate::error::{CTraderError, Result};
use crate::proto::{self, ProtoMessage, ProtoOaLightSymbol};

/// Per-symbol metadata from the light symbol list.
#[derive(Debug, Clone)]
pub struct SymbolInfo {
    /// cTrader symbol ID.
    pub symbol_id: i64,
    /// Human-readable symbol name (e.g. "EURUSD").
    pub symbol_name: String,
    /// Whether the symbol is enabled for trading.
    pub enabled: bool,
    /// Base asset ID.
    pub base_asset_id: Option<i64>,
    /// Quote asset ID.
    pub quote_asset_id: Option<i64>,
}

/// Cached symbol mapper loaded from the server.
pub struct SymbolMapper {
    /// Name → symbol info.
    by_name: HashMap<String, SymbolInfo>,
    /// ID → symbol info.
    by_id: HashMap<i64, SymbolInfo>,
}

impl SymbolMapper {
    /// Load the full symbol list from the server.
    pub async fn load(
        dispatcher: &MessageDispatcher,
        account_id: i64,
        timeout: Duration,
    ) -> Result<Self> {
        info!("loading symbol list");

        let req = proto::ProtoOaSymbolsListReq {
            payload_type: Some(proto::PT_SYMBOLS_LIST_REQ as i32),
            ctid_trader_account_id: account_id,
            include_archived_symbols: Some(false),
        };

        let envelope = ProtoMessage {
            payload_type: proto::PT_SYMBOLS_LIST_REQ,
            payload: Some(req.encode_to_vec()),
            client_msg_id: None,
        };

        let resp = dispatcher.send_request(envelope, timeout).await?;

        if resp.payload_type != proto::PT_SYMBOLS_LIST_RES {
            return Err(CTraderError::ProtocolError(format!(
                "expected SymbolsListRes ({}), got {}",
                proto::PT_SYMBOLS_LIST_RES,
                resp.payload_type
            )));
        }

        let list = proto::ProtoOaSymbolsListRes::decode(
            resp.payload
                .as_deref()
                .ok_or_else(|| CTraderError::ProtocolError("empty SymbolsListRes".into()))?,
        )?;

        let mut by_name = HashMap::new();
        let mut by_id = HashMap::new();

        for sym in &list.symbol {
            let info = symbol_info_from_light(sym);
            if let Some(ref name) = sym.symbol_name {
                by_name.insert(name.clone(), info.clone());
            }
            by_id.insert(info.symbol_id, info);
        }

        info!(count = by_name.len(), "symbol list loaded");
        debug!(
            sample = ?by_name.keys().take(5).collect::<Vec<_>>(),
            "sample symbols"
        );

        Ok(Self { by_name, by_id })
    }

    /// Resolve a symbol name to its info.
    pub fn resolve(&self, name: &str) -> Option<&SymbolInfo> {
        self.by_name.get(name)
    }

    /// Resolve a symbol ID to its info.
    pub fn resolve_id(&self, id: i64) -> Option<&SymbolInfo> {
        self.by_id.get(&id)
    }

    /// Get the symbol ID for a given name.
    pub fn symbol_id(&self, name: &str) -> Result<i64> {
        self.by_name
            .get(name)
            .map(|s| s.symbol_id)
            .ok_or_else(|| CTraderError::UnknownSymbol(name.into()))
    }

    /// Number of cached symbols.
    pub fn len(&self) -> usize {
        self.by_name.len()
    }

    /// Whether the cache is empty.
    pub fn is_empty(&self) -> bool {
        self.by_name.is_empty()
    }

    /// Resolve a symbol ID to its name.
    pub fn symbol_name(&self, id: i64) -> Result<String> {
        self.by_id
            .get(&id)
            .map(|s| s.symbol_name.clone())
            .ok_or_else(|| CTraderError::UnknownSymbol(format!("id={id}")))
    }
}

fn symbol_info_from_light(sym: &ProtoOaLightSymbol) -> SymbolInfo {
    SymbolInfo {
        symbol_id: sym.symbol_id,
        symbol_name: sym.symbol_name.clone().unwrap_or_default(),
        enabled: sym.enabled.unwrap_or(true),
        base_asset_id: sym.base_asset_id,
        quote_asset_id: sym.quote_asset_id,
    }
}

// ── Volume helpers ─────────────────────────────────────────────────

/// Convert lots (e.g. 0.01) to cTrader volume in cents.
///
/// cTrader volumes are in units of 0.01 lot.
/// 1.00 lot = 100_000 volume units for standard forex.
pub fn lots_to_volume(lots: f64) -> i64 {
    (lots * 100_000.0) as i64
}

/// Convert cTrader volume (cents) back to lots.
pub fn volume_to_lots(volume: i64) -> f64 {
    volume as f64 / 100_000.0
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lots_roundtrip() {
        let lots = 1.5;
        let volume = lots_to_volume(lots);
        assert_eq!(volume, 150_000);
        let back = volume_to_lots(volume);
        assert!((back - lots).abs() < 1e-10);
    }

    #[test]
    fn micro_lot() {
        let volume = lots_to_volume(0.01);
        assert_eq!(volume, 1000);
    }

    #[test]
    fn zero_lots() {
        assert_eq!(lots_to_volume(0.0), 0);
        assert!((volume_to_lots(0) - 0.0).abs() < 1e-10);
    }
}
