//! SQLite persistence layer for crash recovery and audit trail.
//!
//! Design:
//! - WAL mode for concurrent read/write without blocking
//! - Write-ahead journaling: fills persisted before position update
//! - Prepared statement caching via `prepare_cached` for hot paths
//! - All timestamps as f64 UNIX epoch seconds (consistent with codebase)
//! - INSERT OR IGNORE on fills for idempotent dedup

use rusqlite::{params, Connection, Result as SqlResult};
use serde_json;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, info};

use crate::types::*;

/// SQLite persistence layer.
pub struct Database {
    conn: Connection,
    run_id: String,
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn now_f64() -> f64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs_f64()
}

fn side_to_str(side: Side) -> &'static str {
    match side {
        Side::Yes => "yes",
        Side::No => "no",
    }
}

fn str_to_side(s: &str) -> Side {
    match s {
        "no" => Side::No,
        _ => Side::Yes,
    }
}

fn order_side_to_str(os: OrderSide) -> &'static str {
    match os {
        OrderSide::Buy => "buy",
        OrderSide::Sell => "sell",
    }
}

fn str_to_order_side(s: &str) -> OrderSide {
    match s {
        "sell" => OrderSide::Sell,
        _ => OrderSide::Buy,
    }
}

fn order_type_to_str(ot: OrderType) -> &'static str {
    match ot {
        OrderType::Limit => "limit",
        OrderType::Market => "market",
    }
}

fn str_to_order_type(s: &str) -> OrderType {
    match s {
        "market" => OrderType::Market,
        _ => OrderType::Limit,
    }
}

fn tif_to_str(tif: TimeInForce) -> &'static str {
    match tif {
        TimeInForce::GTC => "gtc",
        TimeInForce::GTD => "gtd",
        TimeInForce::FOK => "fok",
        TimeInForce::FAK => "fak",
    }
}

fn str_to_tif(s: &str) -> TimeInForce {
    match s {
        "gtd" => TimeInForce::GTD,
        "fok" => TimeInForce::FOK,
        "fak" => TimeInForce::FAK,
        _ => TimeInForce::GTC,
    }
}

fn status_to_str(st: OrderStatus) -> &'static str {
    match st {
        OrderStatus::New => "new",
        OrderStatus::Submitted => "submitted",
        OrderStatus::Accepted => "accepted",
        OrderStatus::PartiallyFilled => "partially_filled",
        OrderStatus::Filled => "filled",
        OrderStatus::Canceled => "canceled",
        OrderStatus::Rejected => "rejected",
    }
}

fn str_to_status(s: &str) -> OrderStatus {
    match s {
        "submitted" => OrderStatus::Submitted,
        "accepted" => OrderStatus::Accepted,
        "partially_filled" => OrderStatus::PartiallyFilled,
        "filled" => OrderStatus::Filled,
        "canceled" => OrderStatus::Canceled,
        "rejected" => OrderStatus::Rejected,
        _ => OrderStatus::New,
    }
}

// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------

const SCHEMA: &str = "
    -- Append-only fill journal (source of truth for position reconstruction)
    CREATE TABLE IF NOT EXISTS fills (
        fill_id TEXT PRIMARY KEY,
        order_id TEXT NOT NULL,
        market_id TEXT NOT NULL,
        side TEXT NOT NULL,
        order_side TEXT NOT NULL,
        price REAL NOT NULL,
        size REAL NOT NULL,
        fee REAL NOT NULL DEFAULT 0.0,
        timestamp REAL NOT NULL,
        token_id TEXT,
        exchange TEXT NOT NULL DEFAULT '',
        inserted_at REAL NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_fills_market_ts ON fills(market_id, timestamp);
    CREATE INDEX IF NOT EXISTS idx_fills_order ON fills(order_id);
    CREATE INDEX IF NOT EXISTS idx_fills_inserted ON fills(inserted_at);
    CREATE INDEX IF NOT EXISTS idx_fills_exchange ON fills(exchange);

    -- Order lifecycle events (append-only, one row per status change)
    CREATE TABLE IF NOT EXISTS order_events (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        order_id TEXT NOT NULL,
        market_id TEXT NOT NULL,
        side TEXT NOT NULL,
        order_side TEXT NOT NULL,
        price REAL NOT NULL,
        size REAL NOT NULL,
        filled_size REAL NOT NULL DEFAULT 0.0,
        remaining_size REAL NOT NULL,
        order_type TEXT NOT NULL,
        time_in_force TEXT NOT NULL,
        status TEXT NOT NULL,
        status_reason TEXT,
        exchange TEXT NOT NULL DEFAULT '',
        created_at REAL NOT NULL,
        recorded_at REAL NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_order_events_order ON order_events(order_id);
    CREATE INDEX IF NOT EXISTS idx_order_events_market_status ON order_events(market_id, status);
    CREATE INDEX IF NOT EXISTS idx_order_events_recorded ON order_events(recorded_at);

    -- Position snapshots (periodic, for fast recovery)
    CREATE TABLE IF NOT EXISTS position_snapshots (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        snapshot_batch REAL NOT NULL,
        market_id TEXT NOT NULL,
        side TEXT NOT NULL,
        size REAL NOT NULL,
        avg_entry_price REAL NOT NULL,
        realized_pnl REAL NOT NULL,
        unrealized_pnl REAL NOT NULL,
        token_id TEXT,
        exchange TEXT NOT NULL DEFAULT ''
    );
    CREATE INDEX IF NOT EXISTS idx_pos_snap_batch ON position_snapshots(snapshot_batch DESC);

    -- Risk events (kill switch, violations, alerts)
    CREATE TABLE IF NOT EXISTS risk_events (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        event_type TEXT NOT NULL,
        details TEXT,
        timestamp REAL NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_risk_events_ts ON risk_events(timestamp DESC);

    -- Historical feed tick data (for replay and analysis)
    CREATE TABLE IF NOT EXISTS feed_ticks (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        feed_name TEXT NOT NULL,
        market_id TEXT NOT NULL DEFAULT '',
        price REAL NOT NULL,
        bid REAL NOT NULL DEFAULT 0.0,
        ask REAL NOT NULL DEFAULT 0.0,
        volume_24h REAL NOT NULL DEFAULT 0.0,
        source TEXT NOT NULL DEFAULT '',
        timestamp REAL NOT NULL,
        recorded_at REAL NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_ticks_feed_ts ON feed_ticks(feed_name, timestamp);
    CREATE INDEX IF NOT EXISTS idx_ticks_market_ts ON feed_ticks(market_id, timestamp);
    CREATE INDEX IF NOT EXISTS idx_ticks_recorded ON feed_ticks(recorded_at);

    -- Strategy runs (audit trail)
    CREATE TABLE IF NOT EXISTS strategy_runs (
        run_id TEXT PRIMARY KEY,
        strategy_name TEXT NOT NULL,
        exchange TEXT NOT NULL,
        started_at REAL NOT NULL,
        ended_at REAL,
        config TEXT
    );

    -- Full L2 orderbook snapshots (collector)
    CREATE TABLE IF NOT EXISTS orderbook_snapshots (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        market_id TEXT NOT NULL,
        exchange TEXT NOT NULL,
        bids_json TEXT NOT NULL,
        asks_json TEXT NOT NULL,
        bid_count INTEGER NOT NULL DEFAULT 0,
        ask_count INTEGER NOT NULL DEFAULT 0,
        best_bid REAL NOT NULL DEFAULT 0.0,
        best_ask REAL NOT NULL DEFAULT 0.0,
        mid_price REAL NOT NULL DEFAULT 0.0,
        spread REAL NOT NULL DEFAULT 0.0,
        timestamp REAL NOT NULL,
        recorded_at REAL NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_ob_snap_market_ts ON orderbook_snapshots(market_id, timestamp);
    CREATE INDEX IF NOT EXISTS idx_ob_snap_recorded ON orderbook_snapshots(recorded_at);

    -- Individual trade records (collector)
    CREATE TABLE IF NOT EXISTS market_trades (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        trade_id TEXT NOT NULL DEFAULT '',
        market_id TEXT NOT NULL,
        exchange TEXT NOT NULL,
        price REAL NOT NULL,
        size REAL NOT NULL,
        side TEXT NOT NULL DEFAULT '',
        maker TEXT NOT NULL DEFAULT '',
        taker TEXT NOT NULL DEFAULT '',
        timestamp REAL NOT NULL,
        recorded_at REAL NOT NULL
    );
    CREATE UNIQUE INDEX IF NOT EXISTS idx_market_trades_dedup
        ON market_trades(market_id, trade_id) WHERE trade_id != '';
    CREATE INDEX IF NOT EXISTS idx_market_trades_market_ts ON market_trades(market_id, timestamp);

    -- Market metadata (collector, upsert)
    CREATE TABLE IF NOT EXISTS market_metadata (
        market_id TEXT NOT NULL,
        exchange TEXT NOT NULL,
        name TEXT NOT NULL DEFAULT '',
        question TEXT NOT NULL DEFAULT '',
        description TEXT NOT NULL DEFAULT '',
        slug TEXT NOT NULL DEFAULT '',
        yes_token_id TEXT NOT NULL DEFAULT '',
        no_token_id TEXT NOT NULL DEFAULT '',
        condition_id TEXT NOT NULL DEFAULT '',
        neg_risk INTEGER NOT NULL DEFAULT 0,
        status TEXT NOT NULL DEFAULT '',
        volume REAL NOT NULL DEFAULT 0.0,
        liquidity REAL NOT NULL DEFAULT 0.0,
        expiry TEXT NOT NULL DEFAULT '',
        rewards_json TEXT NOT NULL DEFAULT '',
        created_at TEXT NOT NULL DEFAULT '',
        updated_at REAL NOT NULL DEFAULT 0.0,
        PRIMARY KEY (market_id, exchange)
    );

    -- Event-level metadata (collector, upsert)
    CREATE TABLE IF NOT EXISTS event_metadata (
        event_id TEXT NOT NULL,
        exchange TEXT NOT NULL,
        name TEXT NOT NULL DEFAULT '',
        market_ids_json TEXT NOT NULL DEFAULT '[]',
        neg_risk INTEGER NOT NULL DEFAULT 0,
        condition_id TEXT NOT NULL DEFAULT '',
        expiry TEXT NOT NULL DEFAULT '',
        status TEXT NOT NULL DEFAULT '',
        total_volume REAL NOT NULL DEFAULT 0.0,
        updated_at REAL NOT NULL DEFAULT 0.0,
        PRIMARY KEY (event_id, exchange)
    );

    -- Collector run audit trail
    CREATE TABLE IF NOT EXISTS collector_runs (
        run_id TEXT PRIMARY KEY,
        started_at REAL NOT NULL,
        ended_at REAL,
        config_json TEXT NOT NULL DEFAULT '',
        market_count INTEGER NOT NULL DEFAULT 0,
        ob_snapshot_count INTEGER NOT NULL DEFAULT 0,
        trade_count INTEGER NOT NULL DEFAULT 0
    );
";

// ---------------------------------------------------------------------------
// Database implementation
// ---------------------------------------------------------------------------

impl Database {
    /// Open or create a database at the given path.
    /// Configures WAL mode, creates tables if absent.
    pub fn open(path: &str) -> SqlResult<Self> {
        // Create parent directories for file-based databases
        if path != ":memory:" {
            if let Some(parent) = Path::new(path).parent() {
                if !parent.as_os_str().is_empty() {
                    let _ = std::fs::create_dir_all(parent);
                }
            }
        }

        let conn = Connection::open(path)?;

        // Performance pragmas for low-latency trading workloads
        conn.execute_batch(
            "PRAGMA journal_mode = WAL;
             PRAGMA synchronous = FULL;
             PRAGMA cache_size = -8000;
             PRAGMA busy_timeout = 5000;
             PRAGMA temp_store = MEMORY;
             PRAGMA mmap_size = 268435456;",
        )?;

        conn.execute_batch(SCHEMA)?;

        // Migration: add exchange column to tables created by Phase 2 schema.
        // ALTER TABLE ADD COLUMN fails silently if column already exists (new databases).
        for alter in &[
            "ALTER TABLE fills ADD COLUMN exchange TEXT NOT NULL DEFAULT ''",
            "ALTER TABLE order_events ADD COLUMN exchange TEXT NOT NULL DEFAULT ''",
            "ALTER TABLE position_snapshots ADD COLUMN exchange TEXT NOT NULL DEFAULT ''",
        ] {
            let _ = conn.execute(alter, []);
        }

        let run_id = uuid::Uuid::new_v4().to_string();
        info!(path = %path, run_id = %run_id, "database opened");

        Ok(Self { conn, run_id })
    }

    /// Get the run ID for this session.
    pub fn run_id(&self) -> &str {
        &self.run_id
    }

    // -------------------------------------------------------------------
    // Fill journaling
    // -------------------------------------------------------------------

    /// Insert a fill record (write-ahead, before position update).
    /// Uses INSERT OR IGNORE for idempotent dedup on fill_id.
    pub fn insert_fill(&self, fill: &Fill) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT OR IGNORE INTO fills
                 (fill_id, order_id, market_id, side, order_side,
                  price, size, fee, timestamp, token_id, exchange, inserted_at)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
            )?
            .execute(params![
                fill.fill_id,
                fill.order_id,
                fill.market_id,
                side_to_str(fill.side),
                order_side_to_str(fill.order_side),
                fill.price,
                fill.size,
                fill.fee,
                fill.timestamp,
                fill.token_id,
                fill.exchange,
                now_f64(),
            ])?;
        Ok(())
    }

    /// Query fills since a timestamp (for recovery replay).
    pub fn fills_since(&self, since: f64) -> SqlResult<Vec<Fill>> {
        let mut stmt = self.conn.prepare_cached(
            "SELECT fill_id, order_id, market_id, side, order_side,
                    price, size, fee, timestamp, token_id, exchange
             FROM fills WHERE timestamp >= ?1 ORDER BY timestamp ASC",
        )?;

        let fills = stmt
            .query_map(params![since], Self::row_to_fill)?
            .collect::<SqlResult<Vec<_>>>()?;

        Ok(fills)
    }

    /// Query fills with optional limit and market filter (for CLI).
    pub fn query_fills(&self, limit: u32, market_id: Option<&str>) -> SqlResult<Vec<Fill>> {
        if let Some(mid) = market_id {
            let mut stmt = self.conn.prepare_cached(
                "SELECT fill_id, order_id, market_id, side, order_side,
                        price, size, fee, timestamp, token_id, exchange
                 FROM fills WHERE market_id = ?1
                 ORDER BY timestamp DESC LIMIT ?2",
            )?;
            let result = stmt
                .query_map(params![mid, limit], Self::row_to_fill)?
                .collect();
            result
        } else {
            let mut stmt = self.conn.prepare_cached(
                "SELECT fill_id, order_id, market_id, side, order_side,
                        price, size, fee, timestamp, token_id, exchange
                 FROM fills ORDER BY timestamp DESC LIMIT ?1",
            )?;
            let result = stmt
                .query_map(params![limit], Self::row_to_fill)?
                .collect();
            result
        }
    }

    /// Total fill count.
    pub fn fill_count(&self) -> SqlResult<u64> {
        self.conn
            .query_row("SELECT COUNT(*) FROM fills", [], |row| row.get(0))
    }

    fn row_to_fill(row: &rusqlite::Row<'_>) -> rusqlite::Result<Fill> {
        Ok(Fill {
            fill_id: row.get(0)?,
            order_id: row.get(1)?,
            market_id: row.get(2)?,
            side: str_to_side(&row.get::<_, String>(3)?),
            order_side: str_to_order_side(&row.get::<_, String>(4)?),
            price: row.get(5)?,
            size: row.get(6)?,
            fee: row.get(7)?,
            timestamp: row.get(8)?,
            token_id: row.get(9)?,
            exchange: row.get::<_, String>(10).unwrap_or_default(),
            is_maker: false,
        })
    }

    // -------------------------------------------------------------------
    // Order lifecycle logging
    // -------------------------------------------------------------------

    /// Record an order event (status transition).
    pub fn insert_order_event(&self, order: &Order) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT INTO order_events
                 (order_id, market_id, side, order_side, price, size,
                  filled_size, remaining_size, order_type, time_in_force,
                  status, status_reason, exchange, created_at, recorded_at)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15)",
            )?
            .execute(params![
                order.id,
                order.market_id,
                side_to_str(order.side),
                order_side_to_str(order.order_side),
                order.price,
                order.size,
                order.filled_size,
                order.remaining_size,
                order_type_to_str(order.order_type),
                tif_to_str(order.time_in_force),
                status_to_str(order.status),
                order.status_reason,
                order.exchange,
                order.created_at,
                now_f64(),
            ])?;
        Ok(())
    }

    /// Get the latest status for each open order (for orphaned order detection).
    pub fn latest_open_order_ids(&self) -> SqlResult<Vec<String>> {
        let mut stmt = self.conn.prepare(
            "SELECT order_id FROM order_events
             WHERE id IN (SELECT MAX(id) FROM order_events GROUP BY order_id)
             AND status IN ('new','submitted','accepted','partially_filled')",
        )?;

        let ids = stmt
            .query_map([], |row| row.get::<_, String>(0))?
            .collect::<SqlResult<Vec<_>>>()?;

        Ok(ids)
    }

    // -------------------------------------------------------------------
    // Position snapshots
    // -------------------------------------------------------------------

    /// Snapshot all positions in a single transaction.
    pub fn snapshot_positions(&self, positions: &[Position]) -> SqlResult<()> {
        let batch_ts = now_f64();
        let tx = self.conn.unchecked_transaction()?;

        {
            let mut stmt = tx.prepare_cached(
                "INSERT INTO position_snapshots
                 (snapshot_batch, market_id, side, size, avg_entry_price,
                  realized_pnl, unrealized_pnl, token_id, exchange)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
            )?;

            for pos in positions {
                stmt.execute(params![
                    batch_ts,
                    pos.market_id,
                    side_to_str(pos.side),
                    pos.size,
                    pos.avg_entry_price,
                    pos.realized_pnl,
                    pos.unrealized_pnl,
                    pos.token_id,
                    pos.exchange,
                ])?;
            }
        }

        tx.commit()?;
        debug!(count = positions.len(), batch = batch_ts, "position snapshot saved");
        Ok(())
    }

    /// Load the latest position snapshot.
    /// Returns (snapshot_timestamp, positions).
    pub fn latest_position_snapshot(&self) -> SqlResult<(f64, Vec<Position>)> {
        let batch_ts: Option<f64> = self.conn.query_row(
            "SELECT MAX(snapshot_batch) FROM position_snapshots",
            [],
            |row| row.get(0),
        )?;

        let batch_ts = match batch_ts {
            Some(ts) if ts > 0.0 => ts,
            _ => return Ok((0.0, Vec::new())),
        };

        let mut stmt = self.conn.prepare(
            "SELECT market_id, side, size, avg_entry_price,
                    realized_pnl, unrealized_pnl, token_id, exchange
             FROM position_snapshots WHERE snapshot_batch = ?1",
        )?;

        let positions = stmt
            .query_map(params![batch_ts], |row| {
                Ok(Position {
                    market_id: row.get(0)?,
                    side: str_to_side(&row.get::<_, String>(1)?),
                    size: row.get(2)?,
                    avg_entry_price: row.get(3)?,
                    realized_pnl: row.get(4)?,
                    unrealized_pnl: row.get(5)?,
                    token_id: row.get(6)?,
                    exchange: row.get::<_, String>(7).unwrap_or_default(),
                })
            })?
            .collect::<SqlResult<Vec<_>>>()?;

        Ok((batch_ts, positions))
    }

    // -------------------------------------------------------------------
    // Risk events
    // -------------------------------------------------------------------

    /// Record a risk event (kill switch, violation, alert).
    pub fn insert_risk_event(&self, event_type: &str, details: Option<&str>) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT INTO risk_events (event_type, details, timestamp)
                 VALUES (?1, ?2, ?3)",
            )?
            .execute(params![event_type, details, now_f64()])?;
        Ok(())
    }

    // -------------------------------------------------------------------
    // Feed tick history
    // -------------------------------------------------------------------

    /// Record a feed tick snapshot to the database.
    pub fn insert_tick(
        &self,
        feed_name: &str,
        market_id: &str,
        price: f64,
        bid: f64,
        ask: f64,
        volume_24h: f64,
        source: &str,
        timestamp: f64,
    ) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT INTO feed_ticks
                 (feed_name, market_id, price, bid, ask, volume_24h, source, timestamp, recorded_at)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
            )?
            .execute(params![
                feed_name,
                market_id,
                price,
                bid,
                ask,
                volume_24h,
                source,
                timestamp,
                now_f64(),
            ])?;
        Ok(())
    }

    /// Query feed ticks in a time range with optional limit.
    /// Returns Vec of (feed_name, price, bid, ask, volume, source, timestamp).
    pub fn query_ticks(
        &self,
        feed_name: &str,
        start_ts: f64,
        end_ts: f64,
        limit: u32,
    ) -> SqlResult<Vec<(String, f64, f64, f64, f64, String, f64)>> {
        let mut stmt = self.conn.prepare_cached(
            "SELECT feed_name, price, bid, ask, volume_24h, source, timestamp
             FROM feed_ticks
             WHERE feed_name = ?1 AND timestamp >= ?2 AND timestamp <= ?3
             ORDER BY timestamp ASC LIMIT ?4",
        )?;

        let rows = stmt
            .query_map(params![feed_name, start_ts, end_ts, limit], |row| {
                Ok((
                    row.get::<_, String>(0)?,
                    row.get::<_, f64>(1)?,
                    row.get::<_, f64>(2)?,
                    row.get::<_, f64>(3)?,
                    row.get::<_, f64>(4)?,
                    row.get::<_, String>(5)?,
                    row.get::<_, f64>(6)?,
                ))
            })?
            .collect::<SqlResult<Vec<_>>>()?;

        Ok(rows)
    }

    /// Count of stored ticks for a feed.
    pub fn tick_count(&self, feed_name: &str) -> SqlResult<u64> {
        self.conn.query_row(
            "SELECT COUNT(*) FROM feed_ticks WHERE feed_name = ?1",
            params![feed_name],
            |row| row.get(0),
        )
    }

    /// Purge ticks older than max_age_secs. Returns number of rows deleted.
    pub fn purge_old_ticks(&self, max_age_secs: f64) -> SqlResult<usize> {
        let cutoff = now_f64() - max_age_secs;
        self.conn.execute(
            "DELETE FROM feed_ticks WHERE recorded_at < ?1",
            params![cutoff],
        )
    }

    // -------------------------------------------------------------------
    // Strategy runs
    // -------------------------------------------------------------------

    /// Record the start of a strategy run.
    pub fn start_run(
        &self,
        strategy_name: &str,
        exchange: &str,
        config: Option<&str>,
    ) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT INTO strategy_runs
                 (run_id, strategy_name, exchange, started_at, config)
                 VALUES (?1,?2,?3,?4,?5)",
            )?
            .execute(params![
                self.run_id,
                strategy_name,
                exchange,
                now_f64(),
                config,
            ])?;
        info!(run_id = %self.run_id, strategy = %strategy_name, "strategy run started");
        Ok(())
    }

    /// Record the end of a strategy run.
    pub fn end_run(&self) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "UPDATE strategy_runs SET ended_at = ?1 WHERE run_id = ?2",
            )?
            .execute(params![now_f64(), self.run_id])?;
        info!(run_id = %self.run_id, "strategy run ended");
        Ok(())
    }

    // -------------------------------------------------------------------
    // Collector: orderbook snapshots
    // -------------------------------------------------------------------

    /// Insert a full L2 orderbook snapshot.
    pub fn insert_orderbook_snapshot(
        &self,
        market_id: &str,
        exchange: &str,
        bids_json: &str,
        asks_json: &str,
        bid_count: i64,
        ask_count: i64,
        best_bid: f64,
        best_ask: f64,
        mid_price: f64,
        spread: f64,
        timestamp: f64,
    ) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT INTO orderbook_snapshots
                 (market_id, exchange, bids_json, asks_json, bid_count, ask_count,
                  best_bid, best_ask, mid_price, spread, timestamp, recorded_at)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
            )?
            .execute(params![
                market_id,
                exchange,
                bids_json,
                asks_json,
                bid_count,
                ask_count,
                best_bid,
                best_ask,
                mid_price,
                spread,
                timestamp,
                now_f64(),
            ])?;
        Ok(())
    }

    /// Query orderbook snapshots for a market in a time range.
    pub fn query_orderbook_snapshots(
        &self,
        market_id: &str,
        start_ts: f64,
        end_ts: f64,
        limit: u32,
    ) -> SqlResult<Vec<(String, String, String, f64, f64, f64, f64, f64)>> {
        let mut stmt = self.conn.prepare_cached(
            "SELECT market_id, bids_json, asks_json, best_bid, best_ask,
                    mid_price, spread, timestamp
             FROM orderbook_snapshots
             WHERE market_id = ?1 AND timestamp >= ?2 AND timestamp <= ?3
             ORDER BY timestamp ASC LIMIT ?4",
        )?;

        let rows = stmt
            .query_map(params![market_id, start_ts, end_ts, limit], |row| {
                Ok((
                    row.get::<_, String>(0)?,
                    row.get::<_, String>(1)?,
                    row.get::<_, String>(2)?,
                    row.get::<_, f64>(3)?,
                    row.get::<_, f64>(4)?,
                    row.get::<_, f64>(5)?,
                    row.get::<_, f64>(6)?,
                    row.get::<_, f64>(7)?,
                ))
            })?
            .collect::<SqlResult<Vec<_>>>()?;

        Ok(rows)
    }

    /// Purge orderbook snapshots older than max_age_secs. Returns rows deleted.
    pub fn purge_old_ob_snapshots(&self, max_age_secs: f64) -> SqlResult<usize> {
        let cutoff = now_f64() - max_age_secs;
        self.conn.execute(
            "DELETE FROM orderbook_snapshots WHERE recorded_at < ?1",
            params![cutoff],
        )
    }

    // -------------------------------------------------------------------
    // Collector: market trades
    // -------------------------------------------------------------------

    /// Insert a market trade record. Uses INSERT OR IGNORE for dedup on (market_id, trade_id).
    pub fn insert_market_trade(
        &self,
        trade_id: &str,
        market_id: &str,
        exchange: &str,
        price: f64,
        size: f64,
        side: &str,
        maker: &str,
        taker: &str,
        timestamp: f64,
    ) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT OR IGNORE INTO market_trades
                 (trade_id, market_id, exchange, price, size, side, maker, taker,
                  timestamp, recorded_at)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
            )?
            .execute(params![
                trade_id, market_id, exchange, price, size, side, maker, taker,
                timestamp, now_f64(),
            ])?;
        Ok(())
    }

    /// Query market trades for a market in a time range.
    pub fn query_market_trades(
        &self,
        market_id: &str,
        start_ts: f64,
        end_ts: f64,
        limit: u32,
    ) -> SqlResult<Vec<(String, String, f64, f64, String, f64)>> {
        let mut stmt = self.conn.prepare_cached(
            "SELECT trade_id, market_id, price, size, side, timestamp
             FROM market_trades
             WHERE market_id = ?1 AND timestamp >= ?2 AND timestamp <= ?3
             ORDER BY timestamp ASC LIMIT ?4",
        )?;

        let rows = stmt
            .query_map(params![market_id, start_ts, end_ts, limit], |row| {
                Ok((
                    row.get::<_, String>(0)?,
                    row.get::<_, String>(1)?,
                    row.get::<_, f64>(2)?,
                    row.get::<_, f64>(3)?,
                    row.get::<_, String>(4)?,
                    row.get::<_, f64>(5)?,
                ))
            })?
            .collect::<SqlResult<Vec<_>>>()?;

        Ok(rows)
    }

    /// Purge market trades older than max_age_secs. Returns rows deleted.
    pub fn purge_old_market_trades(&self, max_age_secs: f64) -> SqlResult<usize> {
        let cutoff = now_f64() - max_age_secs;
        self.conn.execute(
            "DELETE FROM market_trades WHERE recorded_at < ?1",
            params![cutoff],
        )
    }

    // -------------------------------------------------------------------
    // Collector: market metadata
    // -------------------------------------------------------------------

    /// Upsert market metadata (INSERT OR REPLACE on composite PK).
    pub fn upsert_market_metadata(
        &self,
        market_id: &str,
        exchange: &str,
        name: &str,
        question: &str,
        description: &str,
        slug: &str,
        yes_token_id: &str,
        no_token_id: &str,
        condition_id: &str,
        neg_risk: bool,
        status: &str,
        volume: f64,
        liquidity: f64,
        expiry: &str,
        rewards_json: &str,
        created_at: &str,
    ) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT OR REPLACE INTO market_metadata
                 (market_id, exchange, name, question, description, slug,
                  yes_token_id, no_token_id, condition_id, neg_risk, status,
                  volume, liquidity, expiry, rewards_json, created_at, updated_at)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)",
            )?
            .execute(params![
                market_id,
                exchange,
                name,
                question,
                description,
                slug,
                yes_token_id,
                no_token_id,
                condition_id,
                neg_risk as i64,
                status,
                volume,
                liquidity,
                expiry,
                rewards_json,
                created_at,
                now_f64(),
            ])?;
        Ok(())
    }

    /// Query market metadata as JSON string.
    pub fn query_market_metadata(&self, market_id: &str) -> SqlResult<Option<String>> {
        let mut stmt = self.conn.prepare_cached(
            "SELECT market_id, exchange, name, question, description, slug,
                    yes_token_id, no_token_id, condition_id, neg_risk, status,
                    volume, liquidity, expiry, rewards_json, created_at, updated_at
             FROM market_metadata WHERE market_id = ?1",
        )?;

        let result = stmt.query_row(params![market_id], |row| {
            let json = serde_json::json!({
                "market_id": row.get::<_, String>(0)?,
                "exchange": row.get::<_, String>(1)?,
                "name": row.get::<_, String>(2)?,
                "question": row.get::<_, String>(3)?,
                "description": row.get::<_, String>(4)?,
                "slug": row.get::<_, String>(5)?,
                "yes_token_id": row.get::<_, String>(6)?,
                "no_token_id": row.get::<_, String>(7)?,
                "condition_id": row.get::<_, String>(8)?,
                "neg_risk": row.get::<_, i64>(9)? != 0,
                "status": row.get::<_, String>(10)?,
                "volume": row.get::<_, f64>(11)?,
                "liquidity": row.get::<_, f64>(12)?,
                "expiry": row.get::<_, String>(13)?,
                "rewards_json": row.get::<_, String>(14)?,
                "created_at": row.get::<_, String>(15)?,
                "updated_at": row.get::<_, f64>(16)?,
            });
            Ok(json.to_string())
        });

        match result {
            Ok(json) => Ok(Some(json)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e),
        }
    }

    // -------------------------------------------------------------------
    // Collector: event metadata
    // -------------------------------------------------------------------

    /// Upsert event metadata (INSERT OR REPLACE on composite PK).
    pub fn upsert_event_metadata(
        &self,
        event_id: &str,
        exchange: &str,
        name: &str,
        market_ids_json: &str,
        neg_risk: bool,
        condition_id: &str,
        expiry: &str,
        status: &str,
        total_volume: f64,
    ) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT OR REPLACE INTO event_metadata
                 (event_id, exchange, name, market_ids_json, neg_risk, condition_id,
                  expiry, status, total_volume, updated_at)
                 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
            )?
            .execute(params![
                event_id,
                exchange,
                name,
                market_ids_json,
                neg_risk as i64,
                condition_id,
                expiry,
                status,
                total_volume,
                now_f64(),
            ])?;
        Ok(())
    }

    // -------------------------------------------------------------------
    // Collector: run tracking
    // -------------------------------------------------------------------

    /// Start a collector run (audit trail).
    pub fn start_collector_run(&self, run_id: &str, config_json: &str) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "INSERT INTO collector_runs (run_id, started_at, config_json)
                 VALUES (?1, ?2, ?3)",
            )?
            .execute(params![run_id, now_f64(), config_json])?;
        Ok(())
    }

    /// End a collector run, recording counts.
    pub fn end_collector_run(
        &self,
        run_id: &str,
        market_count: i64,
        ob_count: i64,
        trade_count: i64,
    ) -> SqlResult<()> {
        self.conn
            .prepare_cached(
                "UPDATE collector_runs
                 SET ended_at = ?1, market_count = ?2,
                     ob_snapshot_count = ?3, trade_count = ?4
                 WHERE run_id = ?5",
            )?
            .execute(params![now_f64(), market_count, ob_count, trade_count, run_id])?;
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    fn test_fill() -> Fill {
        Fill {
            fill_id: "fill_1".into(),
            order_id: "ord_1".into(),
            market_id: "mkt_1".into(),
            side: Side::Yes,
            order_side: OrderSide::Buy,
            price: 0.55,
            size: 10.0,
            fee: 0.01,
            timestamp: 1700000000.0,
            token_id: None,
            exchange: "paper".into(),
            is_maker: false,
        }
    }

    fn test_order() -> Order {
        Order {
            id: "ord_1".into(),
            market_id: "mkt_1".into(),
            side: Side::Yes,
            order_side: OrderSide::Buy,
            price: 0.55,
            size: 10.0,
            filled_size: 0.0,
            remaining_size: 10.0,
            order_type: OrderType::Limit,
            time_in_force: TimeInForce::GTC,
            status: OrderStatus::New,
            created_at: 1700000000.0,
            status_reason: None,
            exchange: "paper".into(),
            amendment_count: 0,
            token_id: None,
            neg_risk: false,
        }
    }

    #[test]
    fn open_creates_tables() {
        let db = Database::open(":memory:").unwrap();
        assert!(!db.run_id.is_empty());
        // Verify tables exist by querying them
        assert_eq!(db.fill_count().unwrap(), 0);
    }

    #[test]
    fn insert_and_query_fill() {
        let db = Database::open(":memory:").unwrap();
        db.insert_fill(&test_fill()).unwrap();

        let fills = db.fills_since(0.0).unwrap();
        assert_eq!(fills.len(), 1);
        assert_eq!(fills[0].fill_id, "fill_1");
        assert_eq!(fills[0].market_id, "mkt_1");
        assert!((fills[0].price - 0.55).abs() < 1e-10);
        assert!((fills[0].size - 10.0).abs() < 1e-10);
    }

    #[test]
    fn fill_dedup_by_primary_key() {
        let db = Database::open(":memory:").unwrap();
        let fill = test_fill();

        db.insert_fill(&fill).unwrap();
        db.insert_fill(&fill).unwrap(); // Same fill_id — ignored

        assert_eq!(db.fill_count().unwrap(), 1);
    }

    #[test]
    fn fills_since_filters_correctly() {
        let db = Database::open(":memory:").unwrap();

        for i in 0..5 {
            let fill = Fill {
                fill_id: format!("fill_{}", i),
                timestamp: 1700000000.0 + i as f64,
                ..test_fill()
            };
            db.insert_fill(&fill).unwrap();
        }

        let fills = db.fills_since(1700000003.0).unwrap();
        assert_eq!(fills.len(), 2); // timestamps 3.0 and 4.0
    }

    #[test]
    fn query_fills_with_limit_and_market() {
        let db = Database::open(":memory:").unwrap();

        for i in 0..10 {
            let fill = Fill {
                fill_id: format!("fill_{}", i),
                timestamp: 1700000000.0 + i as f64,
                ..test_fill()
            };
            db.insert_fill(&fill).unwrap();
        }

        let fills = db.query_fills(5, None).unwrap();
        assert_eq!(fills.len(), 5);

        let fills = db.query_fills(100, Some("mkt_1")).unwrap();
        assert_eq!(fills.len(), 10);

        let fills = db.query_fills(100, Some("nonexistent")).unwrap();
        assert!(fills.is_empty());
    }

    #[test]
    fn insert_and_query_order_event() {
        let db = Database::open(":memory:").unwrap();
        db.insert_order_event(&test_order()).unwrap();

        let open = db.latest_open_order_ids().unwrap();
        assert_eq!(open.len(), 1);
        assert_eq!(open[0], "ord_1");
    }

    #[test]
    fn terminal_orders_excluded_from_open() {
        let db = Database::open(":memory:").unwrap();

        let mut order = test_order();
        db.insert_order_event(&order).unwrap();

        order.status = OrderStatus::Filled;
        db.insert_order_event(&order).unwrap();

        let open = db.latest_open_order_ids().unwrap();
        assert!(open.is_empty());
    }

    #[test]
    fn position_snapshot_roundtrip() {
        let db = Database::open(":memory:").unwrap();

        let positions = vec![
            Position {
                market_id: "mkt_1".into(),
                side: Side::Yes,
                size: 10.0,
                avg_entry_price: 0.55,
                realized_pnl: 1.23,
                unrealized_pnl: -0.45,
                token_id: Some("tok_abc".into()),
                exchange: "polymarket".into(),
            },
            Position {
                market_id: "mkt_2".into(),
                side: Side::No,
                size: 5.0,
                avg_entry_price: 0.40,
                realized_pnl: 0.0,
                unrealized_pnl: 0.10,
                token_id: None,
                exchange: "kalshi".into(),
            },
        ];

        db.snapshot_positions(&positions).unwrap();

        let (ts, loaded) = db.latest_position_snapshot().unwrap();
        assert!(ts > 0.0);
        assert_eq!(loaded.len(), 2);
        assert_eq!(loaded[0].market_id, "mkt_1");
        assert!((loaded[0].realized_pnl - 1.23).abs() < 1e-10);
        assert!(loaded[0].token_id.as_deref() == Some("tok_abc"));
        assert_eq!(loaded[1].market_id, "mkt_2");
    }

    #[test]
    fn empty_snapshot_returns_defaults() {
        let db = Database::open(":memory:").unwrap();
        let (ts, positions) = db.latest_position_snapshot().unwrap();
        assert!((ts - 0.0).abs() < 1e-10);
        assert!(positions.is_empty());
    }

    #[test]
    fn multiple_snapshots_returns_latest() {
        let db = Database::open(":memory:").unwrap();

        let pos_v1 = vec![Position {
            market_id: "mkt_1".into(),
            side: Side::Yes,
            size: 10.0,
            avg_entry_price: 0.50,
            realized_pnl: 0.0,
            unrealized_pnl: 0.0,
            token_id: None,
            exchange: String::new(),
        }];

        db.snapshot_positions(&pos_v1).unwrap();

        // Second snapshot with different data
        std::thread::sleep(std::time::Duration::from_millis(10));
        let pos_v2 = vec![Position {
            market_id: "mkt_1".into(),
            side: Side::Yes,
            size: 20.0,
            avg_entry_price: 0.55,
            realized_pnl: 1.0,
            unrealized_pnl: 0.5,
            token_id: None,
            exchange: String::new(),
        }];

        db.snapshot_positions(&pos_v2).unwrap();

        let (_, loaded) = db.latest_position_snapshot().unwrap();
        assert_eq!(loaded.len(), 1);
        assert!((loaded[0].size - 20.0).abs() < 1e-10);
    }

    #[test]
    fn risk_event() {
        let db = Database::open(":memory:").unwrap();
        db.insert_risk_event("kill_switch", Some(r#"{"reason":"manual"}"#))
            .unwrap();

        let count: u64 = db
            .conn
            .query_row("SELECT COUNT(*) FROM risk_events", [], |r| r.get(0))
            .unwrap();
        assert_eq!(count, 1);
    }

    #[test]
    fn strategy_run_lifecycle() {
        let db = Database::open(":memory:").unwrap();
        db.start_run("test_strategy", "paper", None).unwrap();
        db.end_run().unwrap();

        let ended: Option<f64> = db
            .conn
            .query_row(
                "SELECT ended_at FROM strategy_runs WHERE run_id = ?1",
                params![db.run_id],
                |row| row.get(0),
            )
            .unwrap();
        assert!(ended.is_some());
    }

    #[test]
    fn enum_roundtrips() {
        // Verify all enum conversions are consistent
        assert_eq!(str_to_side(side_to_str(Side::Yes)), Side::Yes);
        assert_eq!(str_to_side(side_to_str(Side::No)), Side::No);
        assert_eq!(
            str_to_order_side(order_side_to_str(OrderSide::Buy)),
            OrderSide::Buy
        );
        assert_eq!(
            str_to_order_side(order_side_to_str(OrderSide::Sell)),
            OrderSide::Sell
        );
        assert_eq!(
            str_to_order_type(order_type_to_str(OrderType::Limit)),
            OrderType::Limit
        );
        assert_eq!(
            str_to_order_type(order_type_to_str(OrderType::Market)),
            OrderType::Market
        );
        assert_eq!(str_to_tif(tif_to_str(TimeInForce::GTC)), TimeInForce::GTC);
        assert_eq!(str_to_tif(tif_to_str(TimeInForce::GTD)), TimeInForce::GTD);
        assert_eq!(str_to_tif(tif_to_str(TimeInForce::FOK)), TimeInForce::FOK);
        assert_eq!(str_to_tif(tif_to_str(TimeInForce::FAK)), TimeInForce::FAK);

        for status in [
            OrderStatus::New,
            OrderStatus::Submitted,
            OrderStatus::Accepted,
            OrderStatus::PartiallyFilled,
            OrderStatus::Filled,
            OrderStatus::Canceled,
            OrderStatus::Rejected,
        ] {
            assert_eq!(str_to_status(status_to_str(status)), status);
        }
    }
}
