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

use arrow::array::{
    ArrayRef, BooleanArray, Float64Array, TimestampNanosecondArray, UInt32Array, UInt8Array,
};
use arrow::record_batch::RecordBatch;
use bt_core::{
    BacktestConfig, BacktestRunner, BarInterval, ExecutionConfig, ExecutionPrice, FeeConfig,
    FillModel, OrderConfig, PositionSizingMode, SimpleBacktestRunner, SlippageConfig,
    StrategyMetadata, StrategyPlan,
};
use bt_data::{DataStore, DatasetVersion, WriteOptions};
use bt_expr::{compile_expr, Expr, ScalarValue};
use bt_kernel::{BtError, Result, SymbolId, TimeRange, Timestamp};
use parquet::arrow::ArrowWriter;
use parquet::basic::Compression;
use parquet::file::properties::WriterProperties;

fn main() -> Result<()> {
    let fixture_root = fixture_root();
    std::fs::create_dir_all(&fixture_root)?;

    let bars = golden_bars();
    write_bars_parquet(&fixture_root.join("bars_1m.parquet"), &bars)?;

    let strategy = StrategyPlan {
        name: "golden_buy_and_hold".to_string(),
        signals: HashMap::from([(
            "signal".to_string(),
            compile_expr(Expr::Literal(ScalarValue::Float64(1.0)))
                .map_err(|err| BtError::Data(format!("compile signal: {err}")))?,
        )]),
        position_sizing: compile_expr(Expr::Column("signal".to_string()))
            .map_err(|err| BtError::Data(format!("compile sizing: {err}")))?,
        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 = golden_config();
    let store = GoldenStore::new(bars.clone());
    let result = SimpleBacktestRunner::new().run(&strategy, &config, &store)?;

    let equity = result
        .equity_curve
        .as_any()
        .downcast_ref::<Float64Array>()
        .ok_or_else(|| BtError::Data("equity curve must be Float64".to_string()))?;
    let equity_values = (0..equity.len())
        .map(|idx| equity.value(idx))
        .collect::<Vec<_>>();
    write_json(&fixture_root.join("expected_equity.json"), &equity_values)?;

    let trades_json = trade_snapshot(&result.trades)?;
    write_json(&fixture_root.join("expected_trades.json"), &trades_json)?;

    write_json(&fixture_root.join("expected_metrics.json"), &result.metrics)?;

    let manifest_snapshot = 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,
    });
    write_json(
        &fixture_root.join("expected_manifest_snapshot.json"),
        &manifest_snapshot,
    )?;

    println!("Golden fixtures generated in {}", fixture_root.display());
    Ok(())
}

struct GoldenStore {
    bars: RecordBatch,
}

impl GoldenStore {
    fn new(bars: RecordBatch) -> Self {
        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 fixture generator".to_string()))
    }

    fn load_exo(&self, _name: &str, _range: TimeRange) -> Result<RecordBatch> {
        Err(BtError::Unsupported("golden fixture generator".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 fixture generator".to_string()))
    }
}

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 write_bars_parquet(path: &Path, batch: &RecordBatch) -> Result<()> {
    let file = File::create(path)?;
    let props = WriterProperties::builder()
        .set_compression(Compression::ZSTD(Default::default()))
        .build();

    let mut writer = ArrowWriter::try_new(file, batch.schema(), Some(props))?;
    writer.write(batch)?;
    writer.close()?;
    Ok(())
}

fn write_json<T: serde::Serialize>(path: &Path, value: &T) -> Result<()> {
    let payload = serde_json::to_string_pretty(value)?;
    std::fs::write(path, payload)?;
    Ok(())
}

fn golden_config() -> BacktestConfig {
    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(),
            orders: OrderConfig::default(),
            pyramiding: false,
        },
        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,
    }
}

fn golden_bars() -> RecordBatch {
    let schema = bt_data::schema::canonical_bars_1m_schema();
    let arrays: Vec<ArrayRef> = vec![
        Arc::new(
            TimestampNanosecondArray::from(vec![
                0_i64,
                60_000_000_000,
                120_000_000_000,
                180_000_000_000,
            ])
            .with_timezone("UTC"),
        ),
        Arc::new(UInt32Array::from(vec![1_u32, 1, 1, 1])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![100.0, 101.0, 102.0, 103.0])),
        Arc::new(Float64Array::from(vec![10.0, 10.0, 10.0, 10.0])),
        Arc::new(Float64Array::from(vec![
            Some(5.0),
            Some(5.0),
            Some(5.0),
            Some(5.0),
        ])),
        Arc::new(Float64Array::from(vec![
            Some(5.0),
            Some(5.0),
            Some(5.0),
            Some(5.0),
        ])),
        Arc::new(UInt32Array::from(vec![1_u32, 1, 1, 1])),
        Arc::new(Float64Array::from(vec![
            Some(99.9),
            Some(100.9),
            Some(101.9),
            Some(102.9),
        ])),
        Arc::new(Float64Array::from(vec![
            Some(100.1),
            Some(101.1),
            Some(102.1),
            Some(103.1),
        ])),
        Arc::new(Float64Array::from(vec![
            Some(0.2),
            Some(0.2),
            Some(0.2),
            Some(0.2),
        ])),
        Arc::new(BooleanArray::from(vec![false, false, false, false])),
        Arc::new(UInt8Array::from(vec![0_u8, 0, 0, 0])),
    ];
    RecordBatch::try_new(schema, arrays).expect("valid batch")
}

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 fixture_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("golden")
        .join("buy_and_hold")
        .join("v1")
}

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")))
}
