use std::path::Path;
use std::sync::{Arc, Mutex};

use bt_kernel::{AssetClass, BtError, Result, SymbolId, SymbolInfo, TimeRange, Timestamp};
use chrono::{DateTime, SecondsFormat, Utc};
use rusqlite::{params, Connection, OptionalExtension};
use uuid::Uuid;

use crate::partition::{datetime_to_timestamp, timestamp_to_datetime};
use crate::schema::canonical_bars_1m_schema_json;
use crate::version::DatasetVersion;

const MIGRATION_0001_ID: &str = "0001_initial";
const MIGRATION_0001_SQL: &str = include_str!("../migrations/0001_initial.sql");

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SymbolMapping {
    pub provider: String,
    pub raw_symbol: String,
    pub symbol_id: SymbolId,
    pub valid_from: Timestamp,
    pub valid_to: Option<Timestamp>,
}

#[derive(Clone)]
pub struct SqliteMetadataStore {
    conn: Arc<Mutex<Connection>>,
}

impl SqliteMetadataStore {
    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
        tracing::info!(path = %path.as_ref().display(), "opening sqlite metadata store");
        if let Some(parent) = path.as_ref().parent() {
            std::fs::create_dir_all(parent)?;
        }

        let conn = Connection::open(path)?;
        let store = Self {
            conn: Arc::new(Mutex::new(conn)),
        };

        store.initialize()?;
        Ok(store)
    }

    pub fn in_memory() -> Result<Self> {
        let conn = Connection::open_in_memory()?;
        let store = Self {
            conn: Arc::new(Mutex::new(conn)),
        };

        store.initialize()?;
        Ok(store)
    }

    pub fn upsert_symbol(
        &self,
        info: &SymbolInfo,
        active_from: Option<Timestamp>,
        active_to: Option<Timestamp>,
        metadata: serde_json::Value,
    ) -> Result<()> {
        let active_from = active_from.map(timestamp_to_rfc3339).transpose()?;
        let active_to = active_to.map(timestamp_to_rfc3339).transpose()?;
        let metadata_json = serde_json::to_string(&metadata)?;

        self.with_conn(|conn| {
            conn.execute(
                "INSERT INTO symbols (
                    id, ticker, asset_class, exchange, base_currency, quote_currency,
                    tick_size, lot_size, margin_required, active_from, active_to, metadata
                ) VALUES (
                    ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12
                )
                ON CONFLICT(id) DO UPDATE SET
                    ticker = excluded.ticker,
                    asset_class = excluded.asset_class,
                    exchange = excluded.exchange,
                    base_currency = excluded.base_currency,
                    quote_currency = excluded.quote_currency,
                    tick_size = excluded.tick_size,
                    lot_size = excluded.lot_size,
                    margin_required = excluded.margin_required,
                    active_from = excluded.active_from,
                    active_to = excluded.active_to,
                    metadata = excluded.metadata",
                params![
                    info.id.0,
                    info.ticker.as_str(),
                    asset_class_to_db(&info.asset_class),
                    info.exchange.as_str(),
                    info.base_currency.as_str(),
                    info.quote_currency.as_str(),
                    info.tick_size,
                    info.lot_size,
                    info.margin_required,
                    active_from,
                    active_to,
                    metadata_json,
                ],
            )?;
            Ok(())
        })
    }

    pub fn list_symbol_infos(&self) -> Result<Vec<SymbolInfo>> {
        self.with_conn(|conn| {
            let mut stmt = conn.prepare(
                "SELECT
                    id, ticker, asset_class, exchange, base_currency, quote_currency,
                    tick_size, lot_size, margin_required
                 FROM symbols
                 ORDER BY id",
            )?;

            let mut rows = stmt.query([])?;
            let mut out = Vec::new();
            while let Some(row) = rows.next()? {
                let asset_class: String = row.get(2)?;
                out.push(SymbolInfo {
                    id: SymbolId(row.get::<_, u32>(0)?),
                    ticker: row.get(1)?,
                    asset_class: asset_class_from_db(&asset_class)?,
                    exchange: row.get(3)?,
                    base_currency: row.get(4)?,
                    quote_currency: row.get(5)?,
                    tick_size: row.get(6)?,
                    lot_size: row.get(7)?,
                    margin_required: row.get(8)?,
                });
            }

            Ok(out)
        })
    }

    pub fn symbol_info(&self, symbol_id: SymbolId) -> Result<SymbolInfo> {
        self.with_conn(|conn| {
            let mut stmt = conn.prepare(
                "SELECT
                    id, ticker, asset_class, exchange, base_currency, quote_currency,
                    tick_size, lot_size, margin_required
                 FROM symbols
                 WHERE id = ?1
                 LIMIT 1",
            )?;

            let mut rows = stmt.query(params![symbol_id.0])?;
            let row = rows.next()?.ok_or_else(|| {
                BtError::SymbolNotFound(format!("symbol_id {} not found", symbol_id.0))
            })?;

            let asset_class: String = row.get(2)?;
            Ok(SymbolInfo {
                id: SymbolId(row.get::<_, u32>(0)?),
                ticker: row.get(1)?,
                asset_class: asset_class_from_db(&asset_class)?,
                exchange: row.get(3)?,
                base_currency: row.get(4)?,
                quote_currency: row.get(5)?,
                tick_size: row.get(6)?,
                lot_size: row.get(7)?,
                margin_required: row.get(8)?,
            })
        })
    }

    pub fn add_symbol_mapping(&self, mapping: &SymbolMapping) -> Result<()> {
        let valid_from = timestamp_to_rfc3339(mapping.valid_from)?;
        let valid_to = mapping.valid_to.map(timestamp_to_rfc3339).transpose()?;

        self.with_conn(|conn| {
            conn.execute(
                "INSERT INTO symbol_mappings (
                    provider, raw_symbol, symbol_id, valid_from, valid_to
                ) VALUES (
                    ?1, ?2, ?3, ?4, ?5
                )
                ON CONFLICT(provider, raw_symbol, valid_from) DO UPDATE SET
                    symbol_id = excluded.symbol_id,
                    valid_to = excluded.valid_to",
                params![
                    mapping.provider.as_str(),
                    mapping.raw_symbol.as_str(),
                    mapping.symbol_id.0,
                    valid_from,
                    valid_to,
                ],
            )?;
            Ok(())
        })
    }

    pub fn resolve_symbol_mapping(
        &self,
        provider: &str,
        raw_symbol: &str,
        at: Timestamp,
    ) -> Result<SymbolId> {
        let at = timestamp_to_rfc3339(at)?;
        self.with_conn(|conn| {
            let symbol_id = conn
                .query_row(
                    "SELECT symbol_id
                     FROM symbol_mappings
                     WHERE provider = ?1
                       AND raw_symbol = ?2
                       AND valid_from <= ?3
                       AND (valid_to IS NULL OR valid_to > ?3)
                     ORDER BY valid_from DESC
                     LIMIT 1",
                    params![provider, raw_symbol, at],
                    |row| row.get::<_, u32>(0),
                )
                .optional()?
                .ok_or_else(|| {
                    BtError::SymbolNotFound(format!(
                        "mapping not found for provider='{provider}', raw_symbol='{raw_symbol}'"
                    ))
                })?;

            Ok(SymbolId(symbol_id))
        })
    }

    pub fn register_dataset_version(
        &self,
        dataset_name: &str,
        version: &DatasetVersion,
        parquet_root: &Path,
        set_active: bool,
    ) -> Result<()> {
        self.with_conn(|conn| {
            let tx = conn.transaction()?;

            let dataset_id = match tx
                .query_row(
                    "SELECT id FROM datasets WHERE name = ?1",
                    params![dataset_name],
                    |row| row.get::<_, String>(0),
                )
                .optional()?
            {
                Some(id) => id,
                None => {
                    let id = Uuid::new_v4().to_string();
                    let schema_json = serde_json::to_string(&canonical_bars_1m_schema_json())?;
                    tx.execute(
                        "INSERT INTO datasets (id, name, schema_json, created_at, description)
                         VALUES (?1, ?2, ?3, ?4, ?5)",
                        params![
                            id,
                            dataset_name,
                            schema_json,
                            now_rfc3339(),
                            "Canonical bars dataset"
                        ],
                    )?;
                    id
                }
            };

            if set_active {
                tx.execute(
                    "UPDATE dataset_versions SET is_active = FALSE WHERE dataset_id = ?1",
                    params![dataset_id],
                )?;
            }

            let symbols = version.symbols.iter().map(|id| id.0).collect::<Vec<_>>();
            let symbols_json = serde_json::to_string(&symbols)?;
            let metadata_json = serde_json::to_string(&version.metadata)?;

            tx.execute(
                "INSERT INTO dataset_versions (
                    id,
                    dataset_id,
                    version_tag,
                    created_at,
                    schema_hash,
                    row_count,
                    time_start,
                    time_end,
                    symbols_json,
                    source_hash,
                    parquet_root,
                    is_active,
                    metadata
                ) VALUES (
                    ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13
                )",
                params![
                    version.id.as_str(),
                    dataset_id,
                    Option::<String>::None,
                    timestamp_to_rfc3339(version.created_at)?,
                    version.schema_hash.as_str(),
                    version.row_count as i64,
                    timestamp_to_rfc3339(version.time_range.start)?,
                    timestamp_to_rfc3339(version.time_range.end)?,
                    symbols_json,
                    version.source_hash.as_str(),
                    parquet_root.to_string_lossy().to_string(),
                    set_active,
                    metadata_json,
                ],
            )?;

            tx.commit()?;
            tracing::info!(dataset = %dataset_name, symbol_id = version.symbols.first().map(|s| s.0).unwrap_or(0), "registered dataset version");
            Ok(())
        })
    }

    pub fn list_versions(&self, dataset_name: &str) -> Result<Vec<DatasetVersion>> {
        self.with_conn(|conn| {
            let mut stmt = conn.prepare(
                "SELECT
                    dv.id,
                    d.name,
                    dv.created_at,
                    dv.schema_hash,
                    dv.row_count,
                    dv.time_start,
                    dv.time_end,
                    dv.symbols_json,
                    dv.source_hash,
                    dv.metadata
                 FROM dataset_versions dv
                 JOIN datasets d ON d.id = dv.dataset_id
                 WHERE d.name = ?1
                 ORDER BY dv.created_at DESC",
            )?;

            let mut rows = stmt.query(params![dataset_name])?;
            let mut out = Vec::new();
            while let Some(row) = rows.next()? {
                out.push(row_to_dataset_version(row)?);
            }

            Ok(out)
        })
    }

    pub fn active_version(&self, dataset_name: &str) -> Result<DatasetVersion> {
        self.with_conn(|conn| {
            let mut stmt = conn.prepare(
                "SELECT
                    dv.id,
                    d.name,
                    dv.created_at,
                    dv.schema_hash,
                    dv.row_count,
                    dv.time_start,
                    dv.time_end,
                    dv.symbols_json,
                    dv.source_hash,
                    dv.metadata
                 FROM dataset_versions dv
                 JOIN datasets d ON d.id = dv.dataset_id
                 WHERE d.name = ?1 AND dv.is_active = TRUE
                 ORDER BY dv.created_at DESC
                 LIMIT 1",
            )?;

            let mut rows = stmt.query(params![dataset_name])?;
            let row = rows.next()?.ok_or_else(|| {
                BtError::Data(format!(
                    "no active version found for dataset '{dataset_name}'"
                ))
            })?;
            let version = row_to_dataset_version(row)?;

            Ok(version)
        })
    }

    pub fn version_exists(&self, dataset_name: &str, version_id: &str) -> Result<bool> {
        self.with_conn(|conn| {
            let exists = conn
                .query_row(
                    "SELECT 1
                     FROM dataset_versions dv
                     JOIN datasets d ON d.id = dv.dataset_id
                     WHERE d.name = ?1 AND dv.id = ?2
                     LIMIT 1",
                    params![dataset_name, version_id],
                    |_| Ok(true),
                )
                .optional()?
                .unwrap_or(false);

            Ok(exists)
        })
    }

    fn initialize(&self) -> Result<()> {
        self.with_conn(|conn| {
            conn.execute_batch(
                "PRAGMA foreign_keys = ON;
                 CREATE TABLE IF NOT EXISTS schema_migrations (
                    id TEXT PRIMARY KEY,
                    applied_at TEXT NOT NULL
                 );",
            )?;

            let already_applied = conn
                .query_row(
                    "SELECT 1 FROM schema_migrations WHERE id = ?1",
                    params![MIGRATION_0001_ID],
                    |_| Ok(true),
                )
                .optional()?
                .unwrap_or(false);

            if !already_applied {
                conn.execute_batch(MIGRATION_0001_SQL)?;
                conn.execute(
                    "INSERT INTO schema_migrations (id, applied_at) VALUES (?1, ?2)",
                    params![MIGRATION_0001_ID, now_rfc3339()],
                )?;
            }

            Ok(())
        })
    }

    fn with_conn<T, F>(&self, f: F) -> Result<T>
    where
        F: FnOnce(&mut Connection) -> Result<T>,
    {
        let mut guard = self
            .conn
            .lock()
            .map_err(|_| BtError::Data("sqlite connection mutex poisoned".to_string()))?;

        f(&mut guard)
    }
}

fn row_to_dataset_version(row: &rusqlite::Row<'_>) -> Result<DatasetVersion> {
    let created_at_str: String = row.get(2)?;
    let start_str: String = row.get(5)?;
    let end_str: String = row.get(6)?;
    let symbols_json: String = row.get(7)?;
    let metadata_json: String = row.get(9)?;

    let symbol_ids = serde_json::from_str::<Vec<u32>>(&symbols_json)?;

    Ok(DatasetVersion {
        id: row.get(0)?,
        dataset: row.get(1)?,
        created_at: rfc3339_to_timestamp(&created_at_str)?,
        schema_hash: row.get(3)?,
        row_count: row.get::<_, i64>(4)? as u64,
        time_range: TimeRange {
            start: rfc3339_to_timestamp(&start_str)?,
            end: rfc3339_to_timestamp(&end_str)?,
        },
        symbols: symbol_ids.into_iter().map(SymbolId).collect(),
        source_hash: row.get(8)?,
        metadata: serde_json::from_str(&metadata_json)?,
    })
}

fn now_rfc3339() -> String {
    Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true)
}

fn timestamp_to_rfc3339(ts: Timestamp) -> Result<String> {
    Ok(timestamp_to_datetime(ts)?.to_rfc3339_opts(SecondsFormat::Nanos, true))
}

fn rfc3339_to_timestamp(value: &str) -> Result<Timestamp> {
    let dt = DateTime::parse_from_rfc3339(value)
        .map_err(|err| BtError::Data(format!("invalid RFC3339 timestamp '{value}': {err}")))?
        .with_timezone(&Utc);

    datetime_to_timestamp(dt)
        .map_err(|_| BtError::Data(format!("invalid parsed timestamp '{value}'")))
}

fn asset_class_to_db(asset_class: &AssetClass) -> &'static str {
    match asset_class {
        AssetClass::CryptoSpot => "CryptoSpot",
        AssetClass::CryptoPerpetual => "CryptoPerpetual",
        AssetClass::CryptoFuture => "CryptoFuture",
        AssetClass::Equity => "Equity",
        AssetClass::EquityOption => "EquityOption",
        AssetClass::Future => "Future",
        AssetClass::Forex => "Forex",
        AssetClass::Index => "Index",
        AssetClass::Custom => "Custom",
    }
}

fn asset_class_from_db(value: &str) -> Result<AssetClass> {
    match value {
        "CryptoSpot" => Ok(AssetClass::CryptoSpot),
        "CryptoPerpetual" => Ok(AssetClass::CryptoPerpetual),
        "CryptoFuture" => Ok(AssetClass::CryptoFuture),
        "Equity" => Ok(AssetClass::Equity),
        "EquityOption" => Ok(AssetClass::EquityOption),
        "Future" => Ok(AssetClass::Future),
        "Forex" => Ok(AssetClass::Forex),
        "Index" => Ok(AssetClass::Index),
        "Custom" => Ok(AssetClass::Custom),
        _ => Err(BtError::Data(format!(
            "unknown asset class in metadata: '{value}'"
        ))),
    }
}
