//! CSV → Parquet importer.
//!
//! Auto-detects three CSV formats:
//!
//! ## 1. Standard format (with header)
//! ```csv
//! timestamp,open,high,low,close,volume
//! 1704067200000,100.0,102.0,99.5,101.0,50000.0
//! ```
//!
//! ## 2. MetaTrader 4 export (no header, comma-separated, 7 columns)
//! ```text
//! 2024.01.01,00:00,1.10024,1.10136,1.10024,1.10070,18
//! ```
//!
//! ## 3. MetaTrader 5 export (no header, tab-separated, 9 columns)
//! ```text
//! 2024.01.01	00:01:00	1.10024	1.10136	1.10024	1.10070	18	54000000	44
//! ```

use std::io::{BufRead, BufReader};
use std::path::Path;
use std::sync::Arc;

use arrow::array::{ArrayRef, Float64Array, Int64Array};
use arrow::csv::ReaderBuilder as CsvReaderBuilder;
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use bt_kernel::{BtError, Result};

/// Detected CSV format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CsvFormat {
    /// Header row, columns: timestamp, open, high, low, close, volume
    Standard,
    /// MT4: no header, comma-delimited, 7 cols: date,time,o,h,l,c,vol
    MetaTrader4,
    /// MT5: no header, tab-delimited, 9 cols: date,time,o,h,l,c,tickvol,vol,spread
    MetaTrader5,
}

/// Parse a CSV file and return raw RecordBatches with schema:
/// `(timestamp: Int64 [ms], open: f64, high: f64, low: f64, close: f64, volume: f64)`.
///
/// Auto-detects standard, MT4, and MT5 formats.
pub fn import_csv(csv_path: &Path) -> Result<Vec<RecordBatch>> {
    let raw = std::fs::read_to_string(csv_path)
        .map_err(|e| BtError::Data(format!("reading {}: {e}", csv_path.display())))?;

    let format = detect_format(&raw)?;
    tracing::info!(?format, csv = %csv_path.display(), "detected CSV format");

    let batches = match format {
        CsvFormat::Standard => parse_standard(&raw, csv_path)?,
        CsvFormat::MetaTrader4 => parse_metatrader(&raw, b',', false)?,
        CsvFormat::MetaTrader5 => parse_metatrader(&raw, b'\t', true)?,
    };

    if batches.is_empty() {
        return Err(BtError::Data(format!(
            "CSV file {} contains no data rows",
            csv_path.display()
        )));
    }

    let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
    tracing::info!(rows = total_rows, "parsed CSV");

    Ok(batches)
}

// ── Format detection ────────────────────────────────────────────────────────

fn detect_format(content: &str) -> Result<CsvFormat> {
    let first_line = content
        .lines()
        .find(|l| {
            let t = l.trim();
            !t.is_empty() && !t.starts_with('<')
        })
        .ok_or_else(|| BtError::Data("CSV file is empty".to_string()))?;

    // Standard format: first line starts with a letter (header row)
    if first_line
        .chars()
        .next()
        .is_some_and(|c| c.is_ascii_alphabetic())
    {
        return Ok(CsvFormat::Standard);
    }

    // MT4/MT5: first line starts with a digit (date like "2024.01.01")
    // MT5 uses tabs, MT4 uses commas
    if first_line.contains('\t') {
        Ok(CsvFormat::MetaTrader5)
    } else {
        Ok(CsvFormat::MetaTrader4)
    }
}

// ── Standard parser ─────────────────────────────────────────────────────────

fn parse_standard(content: &str, csv_path: &Path) -> Result<Vec<RecordBatch>> {
    let schema = Arc::new(standard_schema());
    let cursor = std::io::Cursor::new(content.as_bytes());
    let reader = CsvReaderBuilder::new(Arc::clone(&schema))
        .with_header(true)
        .build(cursor)
        .map_err(|e| BtError::Data(format!("reading CSV {}: {e}", csv_path.display())))?;

    let batches: Vec<RecordBatch> = reader
        .into_iter()
        .collect::<std::result::Result<Vec<_>, _>>()
        .map_err(|e| BtError::Data(format!("parsing CSV {}: {e}", csv_path.display())))?;

    Ok(batches)
}

fn standard_schema() -> Schema {
    Schema::new(vec![
        Field::new("timestamp", DataType::Int64, false),
        Field::new("open", DataType::Float64, false),
        Field::new("high", DataType::Float64, false),
        Field::new("low", DataType::Float64, false),
        Field::new("close", DataType::Float64, false),
        Field::new("volume", DataType::Float64, false),
    ])
}

// ── MetaTrader parser ───────────────────────────────────────────────────────

/// Parse MT4 or MT5 CSV.
///
/// MT4 (comma, 7 cols): date,time,open,high,low,close,tickvol
/// MT5 (tab,   9 cols): date,time,open,high,low,close,tickvol,vol,spread
fn parse_metatrader(
    content: &str,
    delimiter: u8,
    has_real_volume: bool,
) -> Result<Vec<RecordBatch>> {
    let reader = BufReader::new(content.as_bytes());

    let mut timestamps = Vec::new();
    let mut open = Vec::new();
    let mut high = Vec::new();
    let mut low = Vec::new();
    let mut close = Vec::new();
    let mut volume = Vec::new();

    for (line_num, line) in reader.lines().enumerate() {
        let line = line.map_err(|e| BtError::Data(format!("reading line {line_num}: {e}")))?;
        let line = line.trim();

        // Skip empty lines and angle-bracket headers like <DATE>\t<TIME>...
        if line.is_empty() || line.starts_with('<') {
            continue;
        }

        let delim = delimiter as char;
        let fields: Vec<&str> = line.split(delim).collect();

        // MT4: 7 fields, MT5: 9 fields — we need at least 7
        if fields.len() < 7 {
            return Err(BtError::Data(format!(
                "line {}: expected at least 7 fields, got {}",
                line_num + 1,
                fields.len()
            )));
        }

        let date_str = fields[0].trim();
        let time_str = fields[1].trim();
        let ts_ms = parse_mt_datetime(date_str, time_str)
            .map_err(|e| BtError::Data(format!("line {}: {e}", line_num + 1)))?;

        let o = parse_f64(fields[2], "open", line_num)?;
        let h = parse_f64(fields[3], "high", line_num)?;
        let l = parse_f64(fields[4], "low", line_num)?;
        let c = parse_f64(fields[5], "close", line_num)?;

        // Use real volume (col 7) for MT5 if available, else tick volume (col 6)
        let v = if has_real_volume && fields.len() >= 8 {
            let real_vol = parse_f64(fields[7], "volume", line_num)?;
            if real_vol > 0.0 {
                real_vol
            } else {
                parse_f64(fields[6], "tick_volume", line_num)?
            }
        } else {
            parse_f64(fields[6], "tick_volume", line_num)?
        };

        timestamps.push(ts_ms);
        open.push(o);
        high.push(h);
        low.push(l);
        close.push(c);
        volume.push(v);
    }

    if timestamps.is_empty() {
        return Ok(Vec::new());
    }

    let schema = Arc::new(standard_schema());
    let arrays: Vec<ArrayRef> = vec![
        Arc::new(Int64Array::from(timestamps)),
        Arc::new(Float64Array::from(open)),
        Arc::new(Float64Array::from(high)),
        Arc::new(Float64Array::from(low)),
        Arc::new(Float64Array::from(close)),
        Arc::new(Float64Array::from(volume)),
    ];

    let batch = RecordBatch::try_new(schema, arrays)?;
    Ok(vec![batch])
}

/// Parse MetaTrader datetime ("2024.01.15", "00:01:00") → Unix milliseconds (UTC).
fn parse_mt_datetime(date: &str, time: &str) -> std::result::Result<i64, String> {
    // Date: "YYYY.MM.DD" or "YYYY/MM/DD" or "YYYY-MM-DD"
    let date = date.replace(['.', '/'], "-");
    let parts: Vec<&str> = date.split('-').collect();
    if parts.len() != 3 {
        return Err(format!("invalid date '{date}', expected YYYY.MM.DD"));
    }

    let year: i32 = parts[0]
        .parse()
        .map_err(|_| format!("invalid year '{}'", parts[0]))?;
    let month: u32 = parts[1]
        .parse()
        .map_err(|_| format!("invalid month '{}'", parts[1]))?;
    let day: u32 = parts[2]
        .parse()
        .map_err(|_| format!("invalid day '{}'", parts[2]))?;

    // Time: "HH:MM" or "HH:MM:SS" or "HH:MM:SS.mmm"
    let time_parts: Vec<&str> = time.split(':').collect();
    if time_parts.len() < 2 {
        return Err(format!("invalid time '{time}', expected HH:MM[:SS]"));
    }

    let hour: u32 = time_parts[0]
        .parse()
        .map_err(|_| format!("invalid hour '{}'", time_parts[0]))?;
    let minute: u32 = time_parts[1]
        .parse()
        .map_err(|_| format!("invalid minute '{}'", time_parts[1]))?;
    let second: u32 = if time_parts.len() >= 3 {
        // Handle "SS.mmm" — take only the integer part
        let sec_str = time_parts[2].split('.').next().unwrap_or("0");
        sec_str
            .parse()
            .map_err(|_| format!("invalid second '{sec_str}'"))?
    } else {
        0
    };

    // Convert to Unix ms: days since epoch × 86400000 + time of day
    let days = days_from_civil(year, month, day);
    let ms = (days as i64) * 86_400_000
        + (hour as i64) * 3_600_000
        + (minute as i64) * 60_000
        + (second as i64) * 1_000;

    Ok(ms)
}

/// Convert civil date to days since Unix epoch (1970-01-01).
/// Algorithm from Howard Hinnant.
fn days_from_civil(y: i32, m: u32, d: u32) -> i32 {
    let y = if m <= 2 { y - 1 } else { y };
    let era = if y >= 0 { y } else { y - 399 } / 400;
    let yoe = (y - era * 400) as u32;
    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146097 + doe as i32 - 719468
}

fn parse_f64(s: &str, field: &str, line: usize) -> Result<f64> {
    s.trim().parse::<f64>().map_err(|e| {
        BtError::Data(format!(
            "line {}: invalid {field} '{}': {e}",
            line + 1,
            s.trim()
        ))
    })
}

#[cfg(test)]
mod tests {
    use std::io::Write;
    use std::path::PathBuf;

    use arrow::array::{Float64Array, Int64Array};
    use tempfile::TempDir;

    use super::*;

    fn write_standard_csv(dir: &Path) -> PathBuf {
        let path = dir.join("standard.csv");
        let mut f = std::fs::File::create(&path).unwrap();
        writeln!(f, "timestamp,open,high,low,close,volume").unwrap();
        writeln!(f, "1704067200000,100.0,102.0,99.5,101.0,50000.0").unwrap();
        writeln!(f, "1704067260000,101.0,103.0,100.5,102.5,35000.0").unwrap();
        path
    }

    fn write_mt4_csv(dir: &Path) -> PathBuf {
        let path = dir.join("mt4_export.csv");
        let mut f = std::fs::File::create(&path).unwrap();
        writeln!(f, "2024.01.01,00:00,1.10024,1.10136,1.10024,1.10070,18").unwrap();
        writeln!(f, "2024.01.01,00:01,1.10070,1.10165,1.10070,1.10165,32").unwrap();
        path
    }

    fn write_mt5_csv(dir: &Path) -> PathBuf {
        let path = dir.join("mt5_export.csv");
        let mut f = std::fs::File::create(&path).unwrap();
        writeln!(
            f,
            "2024.01.01\t00:01:00\t1.10024\t1.10136\t1.10024\t1.10070\t18\t54000000\t44"
        )
        .unwrap();
        writeln!(
            f,
            "2024.01.01\t00:02:00\t1.10070\t1.10165\t1.10070\t1.10165\t32\t55575000\t46"
        )
        .unwrap();
        path
    }

    #[test]
    fn import_standard_csv() {
        let tmp = TempDir::new().unwrap();
        let csv_path = write_standard_csv(tmp.path());
        let batches = import_csv(&csv_path).unwrap();
        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 2);

        let batch = &batches[0];
        let ts = batch
            .column_by_name("timestamp")
            .unwrap()
            .as_any()
            .downcast_ref::<Int64Array>()
            .unwrap();
        assert_eq!(ts.value(0), 1_704_067_200_000);

        let close = batch
            .column_by_name("close")
            .unwrap()
            .as_any()
            .downcast_ref::<Float64Array>()
            .unwrap();
        assert_eq!(close.value(0), 101.0);
    }

    #[test]
    fn import_mt4_csv() {
        let tmp = TempDir::new().unwrap();
        let csv_path = write_mt4_csv(tmp.path());
        let batches = import_csv(&csv_path).unwrap();
        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 2);

        let batch = &batches[0];
        let ts = batch
            .column_by_name("timestamp")
            .unwrap()
            .as_any()
            .downcast_ref::<Int64Array>()
            .unwrap();
        assert_eq!(ts.value(0), 1_704_067_200_000);

        let open = batch
            .column_by_name("open")
            .unwrap()
            .as_any()
            .downcast_ref::<Float64Array>()
            .unwrap();
        assert!((open.value(0) - 1.10024).abs() < 0.00001);

        let volume = batch
            .column_by_name("volume")
            .unwrap()
            .as_any()
            .downcast_ref::<Float64Array>()
            .unwrap();
        assert_eq!(volume.value(0), 18.0);
    }

    #[test]
    fn import_mt5_csv() {
        let tmp = TempDir::new().unwrap();
        let csv_path = write_mt5_csv(tmp.path());
        let batches = import_csv(&csv_path).unwrap();
        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 2);

        let batch = &batches[0];
        let ts = batch
            .column_by_name("timestamp")
            .unwrap()
            .as_any()
            .downcast_ref::<Int64Array>()
            .unwrap();
        assert_eq!(ts.value(0), 1_704_067_260_000);

        // MT5: uses real volume (col 7) over tick volume (col 6)
        let volume = batch
            .column_by_name("volume")
            .unwrap()
            .as_any()
            .downcast_ref::<Float64Array>()
            .unwrap();
        assert_eq!(volume.value(0), 54000000.0);
    }

    #[test]
    fn detect_standard_format() {
        let content =
            "timestamp,open,high,low,close,volume\n1704067200000,100.0,102.0,99.5,101.0,50000.0\n";
        assert_eq!(detect_format(content).unwrap(), CsvFormat::Standard);
    }

    #[test]
    fn detect_mt4_format() {
        let content = "2024.01.01,00:00,1.10024,1.10136,1.10024,1.10070,18\n";
        assert_eq!(detect_format(content).unwrap(), CsvFormat::MetaTrader4);
    }

    #[test]
    fn detect_mt5_format() {
        let content =
            "2024.01.01\t00:01:00\t1.10024\t1.10136\t1.10024\t1.10070\t18\t54000000\t44\n";
        assert_eq!(detect_format(content).unwrap(), CsvFormat::MetaTrader5);
    }

    #[test]
    fn mt_datetime_parsing() {
        assert_eq!(
            parse_mt_datetime("2024.01.01", "00:00").unwrap(),
            1_704_067_200_000
        );
        assert_eq!(
            parse_mt_datetime("2024.01.01", "00:01:00").unwrap(),
            1_704_067_260_000
        );
        assert_eq!(
            parse_mt_datetime("2024/01/01", "00:00").unwrap(),
            1_704_067_200_000
        );
        assert_eq!(
            parse_mt_datetime("2024-01-01", "12:30:45").unwrap(),
            1_704_112_245_000
        );
    }

    #[test]
    fn import_empty_csv_errors() {
        let tmp = TempDir::new().unwrap();
        let path = tmp.path().join("empty.csv");
        let mut f = std::fs::File::create(&path).unwrap();
        writeln!(f, "timestamp,open,high,low,close,volume").unwrap();
        assert!(import_csv(&path).is_err());
    }

    #[test]
    fn import_mt5_with_angle_bracket_header() {
        let tmp = TempDir::new().unwrap();
        let path = tmp.path().join("mt5_with_header.csv");
        let mut f = std::fs::File::create(&path).unwrap();
        writeln!(
            f,
            "<DATE>\t<TIME>\t<OPEN>\t<HIGH>\t<LOW>\t<CLOSE>\t<TICKVOL>\t<VOL>\t<SPREAD>"
        )
        .unwrap();
        writeln!(
            f,
            "2024.01.01\t00:01:00\t1.10024\t1.10136\t1.10024\t1.10070\t18\t54000000\t44"
        )
        .unwrap();
        drop(f);
        let batches = import_csv(&path).unwrap();
        assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 1);
    }
}
