//! Experiment tracking via SQLite.
//!
//! Reuses the existing `runs` table from `bt-data`'s metadata SQLite.
//! Saves run manifests, metrics, and parameters for later querying.

use std::collections::HashMap;

use bt_analytics::PerformanceMetrics;
use bt_core::orchestrator::BacktestConfig;
use bt_core::{BatchResultLite, RunManifest};
use bt_expr::ScalarValue;
use bt_kernel::{BtError, Result};
use chrono::Utc;
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Lightweight experiment store backed by SQLite.
///
/// Uses the same database as `SqliteMetadataStore` — the `runs` table
/// is already created by migration `0001_initial`.
pub struct ExperimentStore {
    conn: Connection,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunRecord {
    pub id: String,
    pub strategy_name: String,
    pub strategy_hash: String,
    pub params: HashMap<String, ScalarValue>,
    pub metrics: PerformanceMetrics,
    pub config: BacktestConfig,
    pub manifest: RunManifest,
    pub created_at: String,
    pub duration_ms: u64,
    pub experiment_tag: Option<String>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RunFilter {
    pub strategy_name: Option<String>,
    pub experiment_tag: Option<String>,
    pub min_sharpe: Option<f64>,
    pub limit: Option<usize>,
}

impl ExperimentStore {
    /// Open an experiment store using an existing metadata SQLite database.
    pub fn open(path: &str) -> Result<Self> {
        let conn = Connection::open(path).map_err(BtError::Sqlite)?;

        // Ensure experiment_tag column exists (idempotent migration).
        conn.execute_batch("ALTER TABLE runs ADD COLUMN experiment_tag TEXT;")
            .ok(); // Ignore error if column already exists.

        // Ensure index exists.
        conn.execute_batch(
            "CREATE INDEX IF NOT EXISTS idx_runs_experiment_tag ON runs(experiment_tag);
             CREATE INDEX IF NOT EXISTS idx_runs_strategy_name ON runs(strategy_name);",
        )
        .map_err(BtError::Sqlite)?;

        Ok(Self { conn })
    }

    /// Save a full backtest run (from `bt.run()` with manifest).
    pub fn save_run(
        &self,
        manifest: &RunManifest,
        metrics: &PerformanceMetrics,
        duration_ms: u64,
        experiment_tag: Option<&str>,
    ) -> Result<String> {
        let id = manifest.run_id.clone();
        let created_at = Utc::now().to_rfc3339();
        let config_json = serde_json::to_string(&manifest.config)?;
        let params_json = serde_json::to_string(&manifest.parameters)?;
        let data_versions = serde_json::to_string(&manifest.data_versions)?;
        let metrics_json = serde_json::to_string(metrics)?;
        let manifest_json = serde_json::to_string(manifest)?;

        self.conn
            .execute(
                "INSERT INTO runs (
                    id, created_at, strategy_name, strategy_hash, config_json,
                    params_json, data_versions, rng_seed, engine_version,
                    status, duration_ms, metrics_json, manifest_json, experiment_tag
                ) VALUES (
                    ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14
                )",
                params![
                    id,
                    created_at,
                    manifest.strategy_name,
                    manifest.strategy_code_hash,
                    config_json,
                    params_json,
                    data_versions,
                    manifest.rng_seed as i64,
                    manifest.engine_version,
                    "completed",
                    duration_ms as i64,
                    metrics_json,
                    manifest_json,
                    experiment_tag,
                ],
            )
            .map_err(BtError::Sqlite)?;

        Ok(id)
    }

    /// Save batch lite results (from sweeps/walk-forward).
    pub fn save_batch_lite(
        &self,
        results: &[BatchResultLite],
        config: &BacktestConfig,
        experiment_tag: Option<&str>,
    ) -> Result<Vec<String>> {
        let config_json = serde_json::to_string(config)?;
        let created_at = Utc::now().to_rfc3339();
        let engine_version = env!("CARGO_PKG_VERSION");

        let tx = self.conn.unchecked_transaction().map_err(BtError::Sqlite)?;

        let mut ids = Vec::with_capacity(results.len());
        for result in results {
            let id = Uuid::new_v4().to_string();
            let metrics_json = serde_json::to_string(&result.metrics)?;

            tx.execute(
                "INSERT INTO runs (
                    id, created_at, strategy_name, strategy_hash, config_json,
                    params_json, data_versions, rng_seed, engine_version,
                    status, duration_ms, metrics_json, manifest_json, experiment_tag
                ) VALUES (
                    ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14
                )",
                params![
                    id,
                    created_at,
                    result.strategy_name,
                    "",
                    config_json,
                    "{}",
                    "{}",
                    0_i64,
                    engine_version,
                    "completed",
                    0_i64,
                    metrics_json,
                    "{}",
                    experiment_tag,
                ],
            )
            .map_err(BtError::Sqlite)?;

            ids.push(id);
        }

        tx.commit().map_err(BtError::Sqlite)?;
        Ok(ids)
    }

    /// Query runs with optional filters.
    pub fn query_runs(&self, filter: &RunFilter) -> Result<Vec<RunRecord>> {
        let mut sql = String::from(
            "SELECT id, strategy_name, strategy_hash, params_json, metrics_json,
                    config_json, manifest_json, created_at, duration_ms, experiment_tag
             FROM runs WHERE 1=1",
        );
        let mut params_vec: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();

        if let Some(ref name) = filter.strategy_name {
            sql.push_str(&format!(" AND strategy_name = ?{}", params_vec.len() + 1));
            params_vec.push(Box::new(name.clone()));
        }
        if let Some(ref tag) = filter.experiment_tag {
            sql.push_str(&format!(" AND experiment_tag = ?{}", params_vec.len() + 1));
            params_vec.push(Box::new(tag.clone()));
        }

        sql.push_str(" ORDER BY created_at DESC");

        if let Some(limit) = filter.limit {
            sql.push_str(&format!(" LIMIT {limit}"));
        }

        let param_refs: Vec<&dyn rusqlite::types::ToSql> =
            params_vec.iter().map(|b| b.as_ref()).collect();

        let mut stmt = self.conn.prepare(&sql).map_err(BtError::Sqlite)?;

        let rows = stmt
            .query_map(param_refs.as_slice(), |row| {
                let params_json: String = row.get(3)?;
                let metrics_json: String = row.get(4)?;
                let config_json: String = row.get(5)?;
                let manifest_json: String = row.get(6)?;

                Ok(RawRunRow {
                    id: row.get(0)?,
                    strategy_name: row.get(1)?,
                    strategy_hash: row.get(2)?,
                    params_json,
                    metrics_json,
                    config_json,
                    manifest_json,
                    created_at: row.get(7)?,
                    duration_ms: row.get::<_, i64>(8)? as u64,
                    experiment_tag: row.get(9)?,
                })
            })
            .map_err(BtError::Sqlite)?;

        let mut records = Vec::new();
        for row in rows {
            let raw = row.map_err(BtError::Sqlite)?;
            let params: HashMap<String, ScalarValue> =
                serde_json::from_str(&raw.params_json).unwrap_or_default();
            let metrics: PerformanceMetrics =
                serde_json::from_str(&raw.metrics_json).unwrap_or_else(|_| default_metrics());
            let config: BacktestConfig =
                serde_json::from_str(&raw.config_json).unwrap_or_else(|_| default_config());
            let manifest: RunManifest =
                serde_json::from_str(&raw.manifest_json).unwrap_or_else(|_| default_manifest());

            // Apply min_sharpe filter in Rust (simpler than SQL for JSON fields).
            if let Some(min_sharpe) = filter.min_sharpe {
                if metrics.sharpe < min_sharpe {
                    continue;
                }
            }

            records.push(RunRecord {
                id: raw.id,
                strategy_name: raw.strategy_name,
                strategy_hash: raw.strategy_hash,
                params,
                metrics,
                config,
                manifest,
                created_at: raw.created_at,
                duration_ms: raw.duration_ms,
                experiment_tag: raw.experiment_tag,
            });
        }

        Ok(records)
    }
}

struct RawRunRow {
    id: String,
    strategy_name: String,
    strategy_hash: String,
    params_json: String,
    metrics_json: String,
    config_json: String,
    manifest_json: String,
    created_at: String,
    duration_ms: u64,
    experiment_tag: Option<String>,
}

fn default_metrics() -> PerformanceMetrics {
    PerformanceMetrics {
        total_return: 0.0,
        cagr: 0.0,
        volatility: 0.0,
        sharpe: 0.0,
        sortino: 0.0,
        calmar: 0.0,
        max_drawdown: 0.0,
        skewness: 0.0,
        kurtosis: 0.0,
        tail_ratio: 0.0,
        omega_ratio: 0.0,
        ulcer_index: 0.0,
        best_day: 0.0,
        worst_day: 0.0,
        avg_daily_return: 0.0,
        pct_positive_days: 0.0,
        max_drawdown_duration_days: 0.0,
        tstat_sharpe: 0.0,
        alpha: 0.0,
        beta: 0.0,
        tstat_alpha: 0.0,
        trade_stats: None,
    }
}

fn default_config() -> BacktestConfig {
    BacktestConfig::default()
}

fn default_manifest() -> RunManifest {
    RunManifest {
        run_id: String::new(),
        timestamp: bt_kernel::Timestamp::from_nanos(0),
        strategy_name: String::new(),
        strategy_code_hash: String::new(),
        parameters: HashMap::new(),
        config: BacktestConfig::default(),
        data_versions: HashMap::new(),
        rng_seed: 0,
        engine_version: String::new(),
        platform: String::new(),
    }
}
