use chrono::{DateTime, Utc};
use std::io::{Cursor, Read, Seek};
use swym_dal::repo::market_event::{InsertCandle, InsertTrade};

/// Parse a Binance spot trades zip archive.
///
/// CSV format (no header): trade_id, price, qty, quote_qty, time, is_buyer_maker, is_best_match
///
/// Timestamps are milliseconds (pre-2025 files) or microseconds (2025+ files).
/// The threshold `1_000_000_000_000_000` (10^15) safely distinguishes the two:
/// a millisecond value cannot exceed this until the year 33658.
///
/// `is_buyer_maker=true` means the buyer was the maker, so the taker was a seller → side "Sell".
pub fn parse_zip<R: Read + Seek>(
    reader: R,
    instrument_id: i32,
    time_received: DateTime<Utc>,
) -> Result<impl Iterator<Item = Result<InsertTrade, ParseError>>, ParseError> {
    let mut archive = zip::ZipArchive::new(reader)?;
    let file = archive.by_index(0)?;

    // Read the entire zip entry into memory so we can return a non-borrowing iterator.
    // Individual daily files are small (<50 MB compressed); monthly files are larger
    // but still manageable when buffered through the streaming CSV reader below.
    let mut buf = Vec::new();
    let mut file = file;
    file.read_to_end(&mut buf)?;

    let records: Vec<InsertTrade> = {
        let mut rdr = csv::ReaderBuilder::new()
            .has_headers(false)
            .from_reader(Cursor::new(buf));

        let mut trades = Vec::new();
        for result in rdr.records() {
            let rec = result?;
            // Binance 2025+ files include a header row. Detect it by checking whether
            // the first field (trade_id) is non-numeric and skip if so.
            if rec[0].parse::<i64>().is_err() {
                continue;
            }
            let trade_id = rec[0].to_string();
            let price: f64 = rec[1].parse().map_err(|_| ParseError::Field("price"))?;
            let qty: f64 = rec[2].parse().map_err(|_| ParseError::Field("qty"))?;
            // rec[3] is quote_qty — not stored
            let time_raw: i64 = rec[4].parse().map_err(|_| ParseError::Field("time"))?;
            let is_buyer_maker: bool = match rec[5].trim() {
                "True" | "true" => true,
                "False" | "false" => false,
                _ => return Err(ParseError::Field("is_buyer_maker")),
            };
            // rec[6] is is_best_match — not stored

            let time_exchange = if time_raw > 1_000_000_000_000_000 {
                DateTime::from_timestamp_micros(time_raw)
                    .ok_or(ParseError::Field("time (microseconds out of range)"))?
            } else {
                DateTime::from_timestamp_millis(time_raw)
                    .ok_or(ParseError::Field("time (milliseconds out of range)"))?
            };

            // is_buyer_maker=true → buyer was maker, taker was seller → side "Sell"
            let side = if is_buyer_maker { "Sell" } else { "Buy" };

            trades.push(InsertTrade {
                instrument_id,
                time_exchange,
                time_received,
                trade_id,
                price,
                amount: qty,
                side: side.to_string(),
            });
        }
        trades
    };

    Ok(records.into_iter().map(Ok))
}

/// Parse a Binance spot klines zip archive.
///
/// CSV format (no header, 12 columns):
///   open_time_ms, open, high, low, close, volume,
///   close_time_ms, quote_volume, trade_count,
///   taker_buy_base_vol, taker_buy_quote_vol, ignore
///
/// Timestamps are always milliseconds in kline files.
pub fn parse_klines_zip<R: Read + Seek>(
    reader: R,
    instrument_id: i32,
    interval: &str,
) -> Result<impl Iterator<Item = Result<InsertCandle, ParseError>>, ParseError> {
    let mut archive = zip::ZipArchive::new(reader)?;
    let file = archive.by_index(0)?;

    let mut buf = Vec::new();
    let mut file = file;
    file.read_to_end(&mut buf)?;

    let interval = interval.to_string();
    let records: Vec<InsertCandle> = {
        let mut rdr = csv::ReaderBuilder::new()
            .has_headers(false)
            .from_reader(Cursor::new(buf));

        let mut candles = Vec::new();
        for result in rdr.records() {
            let rec = result?;
            let open_time_raw: i64 = rec[0].parse().map_err(|_| ParseError::Field("open_time"))?;
            let open: f64 = rec[1].parse().map_err(|_| ParseError::Field("open"))?;
            let high: f64 = rec[2].parse().map_err(|_| ParseError::Field("high"))?;
            let low: f64 = rec[3].parse().map_err(|_| ParseError::Field("low"))?;
            let close: f64 = rec[4].parse().map_err(|_| ParseError::Field("close"))?;
            let volume: f64 = rec[5].parse().map_err(|_| ParseError::Field("volume"))?;
            let close_time_raw: i64 = rec[6].parse().map_err(|_| ParseError::Field("close_time"))?;
            // rec[7] is quote_volume — not stored
            let trade_count: i32 = rec[8].parse().map_err(|_| ParseError::Field("trade_count"))?;
            // rec[9], rec[10], rec[11] ignored

            // Apply same ms/μs detection as trades, though kline files are always ms.
            let open_time = if open_time_raw > 1_000_000_000_000_000 {
                DateTime::from_timestamp_micros(open_time_raw)
                    .ok_or(ParseError::Field("open_time (microseconds out of range)"))?
            } else {
                DateTime::from_timestamp_millis(open_time_raw)
                    .ok_or(ParseError::Field("open_time (milliseconds out of range)"))?
            };
            let close_time = if close_time_raw > 1_000_000_000_000_000 {
                DateTime::from_timestamp_micros(close_time_raw)
                    .ok_or(ParseError::Field("close_time (microseconds out of range)"))?
            } else {
                DateTime::from_timestamp_millis(close_time_raw)
                    .ok_or(ParseError::Field("close_time (milliseconds out of range)"))?
            };

            candles.push(InsertCandle {
                instrument_id,
                interval: interval.clone(),
                open_time,
                close_time,
                open,
                high,
                low,
                close,
                volume,
                trade_count,
            });
        }
        candles
    };

    Ok(records.into_iter().map(Ok))
}

#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    #[error("zip error: {0}")]
    Zip(#[from] zip::result::ZipError),
    #[error("csv error: {0}")]
    Csv(#[from] csv::Error),
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("field parse error: {0}")]
    Field(&'static str),
}
