use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};

use arrow::array::{Array, Float64Array, UInt32Array, UInt8Array};
use arrow::record_batch::RecordBatch;
use bt_analytics::PerformanceMetrics;
use bt_core::{
    BacktestConfig, BacktestRunner, BarInterval, ExecutionConfig, ExecutionPrice, FeeConfig,
    FillModel, PositionSizingMode, SimpleBacktestRunner, SlippageConfig, StrategyMetadata,
    StrategyPlan,
};
use bt_data::{DataStore, DatasetVersion, WriteOptions};
use bt_expr::{compile_expr, Expr, ScalarValue};
use bt_kernel::{concat_batches, BtError, Result, SymbolId, TimeRange, Timestamp};
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;

struct GoldenStore {
    bars: RecordBatch,
}

impl GoldenStore {
    fn new() -> Result<Self> {
        let bars = load_fixture_parquet(&fixture_root().join("bars_1m.parquet"))?;
        Ok(Self { bars })
    }
}

impl DataStore for GoldenStore {
    fn load_bars(
        &self,
        _symbol: SymbolId,
        _range: TimeRange,
        _version: Option<&str>,
    ) -> Result<RecordBatch> {
        Ok(self.bars.clone())
    }

    fn load_features(
        &self,
        _feature_set: &str,
        _symbol: SymbolId,
        _range: TimeRange,
        _columns: Option<&[&str]>,
    ) -> Result<RecordBatch> {
        Err(BtError::Unsupported("golden store".to_string()))
    }

    fn load_exo(&self, _name: &str, _range: TimeRange) -> Result<RecordBatch> {
        Err(BtError::Unsupported("golden store".to_string()))
    }

    fn list_versions(&self, _dataset: &str) -> Result<Vec<DatasetVersion>> {
        Ok(vec![golden_version()])
    }

    fn active_version(&self, _dataset: &str) -> Result<DatasetVersion> {
        Ok(golden_version())
    }

    fn write_bars(
        &self,
        _symbol: SymbolId,
        _batch: &RecordBatch,
        _opts: WriteOptions,
    ) -> Result<DatasetVersion> {
        Err(BtError::Unsupported("golden store".to_string()))
    }
}

#[test]
fn golden_buy_and_hold_equity_trade_metrics_and_manifest_match_fixture() {
    let strategy = StrategyPlan {
        name: "golden_buy_and_hold".to_string(),
        signals: HashMap::from([(
            "signal".to_string(),
            compile_expr(Expr::Literal(ScalarValue::Float64(1.0))).expect("compile"),
        )]),
        position_sizing: compile_expr(Expr::Column("signal".to_string())).expect("compile sizing"),
        required_columns: vec!["close".to_string()],
        required_features: Vec::new(),
        parameters: HashMap::new(),
        metadata: StrategyMetadata::default(),
        raw_signals: None,
        raw_position_sizing: None,
        has_dynamic_periods: false,
    };

    let config = BacktestConfig {
        universe: vec![SymbolId(1)],
        time_range: TimeRange::new(
            Timestamp::from_nanos(0),
            Timestamp::from_nanos(240_000_000_000),
        )
        .expect("range"),
        bar_interval: BarInterval::Minutes(1),
        initial_capital: 1000.0,
        currency: "USD".to_string(),
        risk_free_rate: 0.025,
        execution: ExecutionConfig {
            signal_delay: 1,
            execution_price: ExecutionPrice::AtClose,
            max_position_pct: 1.0,
            allow_short: false,
            allow_fractional: true,
            skip_gap_bars: false,
            position_sizing_mode: PositionSizingMode::Units,
            fill_model: FillModel::default(),
            ..Default::default()
        },
        fees: FeeConfig::default(),
        slippage: SlippageConfig::FixedBps { bps: 0.0 },
        data_version: Some("golden_v1".to_string()),
        rng_seed: Some(7),
        trading_days_per_year: 365.25,
        output_resolution: Some(BarInterval::Minutes(1)),
        resample_to: None,
        feature_sets: vec![],
        symbol_names: HashMap::new(),
        warmup_bars: 0,
    };

    let result = SimpleBacktestRunner::new()
        .run(
            &strategy,
            &config,
            &GoldenStore::new().expect("golden store"),
        )
        .expect("run golden");

    let expected_equity: Vec<f64> =
        read_json(&fixture_root().join("expected_equity.json")).expect("read equity fixture");
    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .expect("equity");
    let actual_equity = (0..equity.len())
        .map(|idx| equity.value(idx))
        .collect::<Vec<_>>();
    assert_eq!(actual_equity, expected_equity);

    let expected_trades: serde_json::Value =
        read_json(&fixture_root().join("expected_trades.json")).expect("read trade fixture");
    assert_eq!(
        trade_snapshot(&result.trades).expect("snapshot"),
        expected_trades
    );

    let expected_metrics: PerformanceMetrics =
        read_json(&fixture_root().join("expected_metrics.json")).expect("read metrics fixture");
    assert_metrics_close(&result.metrics, &expected_metrics);

    let expected_manifest: serde_json::Value =
        read_json(&fixture_root().join("expected_manifest_snapshot.json"))
            .expect("read manifest fixture");
    let actual_manifest = serde_json::json!({
        "strategy_name": result.manifest.strategy_name,
        "data_version": result.manifest.data_versions.get("bars_1m").cloned().unwrap_or_default(),
        "config": result.manifest.config,
        "engine_version": result.manifest.engine_version,
    });
    assert_eq!(actual_manifest, expected_manifest);
}

fn load_fixture_parquet(path: &Path) -> Result<RecordBatch> {
    let file = File::open(path)?;
    let reader = ParquetRecordBatchReaderBuilder::try_new(file)?.build()?;

    let mut batches = Vec::new();
    for batch in reader {
        batches.push(batch?);
    }

    if batches.is_empty() {
        return Err(BtError::Data(format!(
            "golden fixture parquet is empty: {}",
            path.display()
        )));
    }

    if batches.len() == 1 {
        return Ok(batches.remove(0));
    }

    let schema = batches[0].schema();
    concat_batches(schema, &batches)
}

fn trade_snapshot(trades: &RecordBatch) -> Result<serde_json::Value> {
    let quantity = f64_column(trades, "quantity")?;
    let fill_price = f64_column(trades, "fill_price")?;
    let side = u8_column(trades, "side")?;
    let symbol_id = u32_column(trades, "symbol_id")?;

    let mut out = Vec::with_capacity(trades.num_rows());
    for idx in 0..trades.num_rows() {
        out.push(serde_json::json!({
            "symbol_id": symbol_id.value(idx),
            "side": side.value(idx),
            "quantity": quantity.value(idx),
            "fill_price": fill_price.value(idx),
        }));
    }

    Ok(serde_json::json!(out))
}

fn assert_metrics_close(actual: &PerformanceMetrics, expected: &PerformanceMetrics) {
    assert_close(actual.total_return, expected.total_return, "total_return");
    assert_close(actual.max_drawdown, expected.max_drawdown, "max_drawdown");
    // Annualized metrics (CAGR, volatility, sharpe, sortino, calmar) are not
    // compared because the golden fixture uses 4 synthetic 1-second bars, making
    // annualization numerically extreme. The critical checks are equity curve,
    // total_return, max_drawdown, trades, and manifest.
}

fn assert_close(actual: f64, expected: f64, label: &str) {
    let delta = (actual - expected).abs();
    assert!(
        delta <= 1e-12,
        "metric {label} mismatch: actual={actual}, expected={expected}, delta={delta}"
    );
}

fn fixture_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("golden")
        .join("buy_and_hold")
        .join("v1")
}

fn read_json<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
    let payload = std::fs::read_to_string(path)?;
    Ok(serde_json::from_str(&payload)?)
}

fn golden_version() -> DatasetVersion {
    DatasetVersion {
        id: "golden_v1".to_string(),
        dataset: "bars_1m".to_string(),
        created_at: Timestamp::from_nanos(1),
        schema_hash: "schema".to_string(),
        row_count: 4,
        time_range: TimeRange::new(
            Timestamp::from_nanos(0),
            Timestamp::from_nanos(240_000_000_000),
        )
        .expect("range"),
        symbols: vec![SymbolId(1)],
        source_hash: "golden".to_string(),
        metadata: serde_json::json!({}),
    }
}

fn f64_column<'a>(batch: &'a RecordBatch, name: &str) -> Result<&'a Float64Array> {
    batch
        .column_by_name(name)
        .ok_or_else(|| BtError::Data(format!("missing column '{name}'")))?
        .as_any()
        .downcast_ref::<Float64Array>()
        .ok_or_else(|| BtError::Data(format!("column '{name}' must be Float64")))
}

fn u8_column<'a>(batch: &'a RecordBatch, name: &str) -> Result<&'a UInt8Array> {
    batch
        .column_by_name(name)
        .ok_or_else(|| BtError::Data(format!("missing column '{name}'")))?
        .as_any()
        .downcast_ref::<UInt8Array>()
        .ok_or_else(|| BtError::Data(format!("column '{name}' must be UInt8")))
}

fn u32_column<'a>(batch: &'a RecordBatch, name: &str) -> Result<&'a UInt32Array> {
    batch
        .column_by_name(name)
        .ok_or_else(|| BtError::Data(format!("missing column '{name}'")))?
        .as_any()
        .downcast_ref::<UInt32Array>()
        .ok_or_else(|| BtError::Data(format!("column '{name}' must be UInt32")))
}
