"""Market data collector:persistent PIT (point-in-time) data archive.

Two modes:
1. Pipeline integration: ``collector()`` factory for ``hz.run()`` pipelines
2. Standalone daemon: ``collect()`` for 24/7 passive collection

Storage: SQLite for real-time writes, optional Parquet export for batch analysis.
"""

from __future__ import annotations

import json
import logging
import os
import signal
import time
from dataclasses import dataclass, field
from typing import Any, Callable

from horizon._horizon import Collector, CollectorConfig

logger = logging.getLogger("horizon.collector")


# ---------------------------------------------------------------------------
# Config dataclass (Python-side superset of Rust CollectorConfig)
# ---------------------------------------------------------------------------


@dataclass
class DataCollectorConfig:
    """Full collector configuration (Python side).

    Extends Rust CollectorConfig with Parquet export and retention settings.
    """

    markets: list[str] = field(default_factory=list)
    exchange: str = "polymarket"
    orderbook_interval: float = 5.0
    trade_poll_interval: float = 10.0
    metadata_interval: float = 300.0
    max_book_levels: int = 0
    db_path: str | None = None
    parquet_dir: str | None = None
    parquet_export_interval: float = 3600.0
    retention_hours: float = 0.0  # 0 = keep forever

    def to_rust_config(self) -> CollectorConfig:
        """Convert to Rust CollectorConfig."""
        db_path = self.db_path or os.path.join("data", "collector.db")
        return CollectorConfig(
            market_ids=self.markets,
            exchange=self.exchange,
            orderbook_interval_secs=self.orderbook_interval,
            trade_poll_interval_secs=self.trade_poll_interval,
            metadata_interval_secs=self.metadata_interval,
            max_book_levels=self.max_book_levels,
            db_path=db_path,
        )


# ---------------------------------------------------------------------------
# Pipeline function:for hz.run()
# ---------------------------------------------------------------------------


def collector(
    feed: str,
    db_path: str | None = None,
    orderbook_every_n: int = 1,
) -> Callable:
    """Pipeline factory that records orderbook snapshots from a feed.

    Usage in hz.run():
        pipeline=[
            collector("poly_btc", orderbook_every_n=5),
            my_strategy,
        ]

    Args:
        feed: Feed name whose orderbook to record (must match a feed in feeds dict).
        db_path: SQLite path. Defaults to ``data/collector.db``.
        orderbook_every_n: Record every Nth cycle (throttle). 1 = every cycle.

    Returns:
        A callable ``(Context) -> dict`` for pipeline composition.
    """
    _collector: Collector | None = None
    _cycle = 0
    _db_path = db_path or os.path.join("data", "collector.db")

    def _pipeline_fn(ctx: Any) -> dict:
        nonlocal _collector, _cycle

        # Lazy init
        if _collector is None:
            config = CollectorConfig(
                market_ids=[],  # pipeline mode:we record manually
                exchange="polymarket",
                db_path=_db_path,
            )
            _collector = Collector(config)
            logger.info("Collector initialized (pipeline mode) at %s", _db_path)

        _cycle += 1
        if _cycle % orderbook_every_n != 0:
            return {}

        # Read orderbook from context params (injected by strategy.py)
        orderbooks = ctx.params.get("orderbooks", {})
        ob = orderbooks.get(feed)
        if ob is None:
            return {}

        # Serialize levels
        bids = ob.bids()
        asks = ob.asks()
        bids_json = json.dumps(bids)
        asks_json = json.dumps(asks)

        best_bid = bids[0][0] if bids else 0.0
        best_ask = asks[0][0] if asks else 0.0
        mid = (best_bid + best_ask) / 2.0 if best_bid > 0 and best_ask > 0 else 0.0
        spread = (best_ask - best_bid) if best_ask > best_bid else 0.0

        exchange = ob.source if ob.source else "polymarket"

        try:
            _collector.record_orderbook(
                ctx.market,
                exchange,
                bids_json,
                asks_json,
                best_bid,
                best_ask,
                mid,
                spread,
                ob.timestamp,
            )
        except Exception as e:
            logger.warning("collector record_orderbook failed: %s", e)

        return {}

    return _pipeline_fn


# ---------------------------------------------------------------------------
# Standalone daemon
# ---------------------------------------------------------------------------


def collect(
    config: DataCollectorConfig | None = None,
    markets: list[str] | None = None,
    exchange: str = "polymarket",
    db_path: str | None = None,
    duration: float = 0.0,
) -> None:
    """Run the collector as a standalone daemon.

    Args:
        config: Full DataCollectorConfig. If provided, other args are ignored.
        markets: List of market IDs / token IDs to collect.
        exchange: Exchange name (``polymarket`` or ``kalshi``).
        db_path: SQLite database path.
        duration: Max run time in seconds. 0 = run until Ctrl+C.

    Raises:
        ValueError: If no markets are specified.
    """
    if config is None:
        if not markets:
            raise ValueError("markets must not be empty")
        config = DataCollectorConfig(
            markets=markets,
            exchange=exchange,
            db_path=db_path,
        )
    elif not config.markets:
        raise ValueError("config.markets must not be empty")

    rust_config = config.to_rust_config()
    c = Collector(rust_config)
    c.start()

    logger.info(
        "Collector started: %d markets on %s, db=%s",
        len(config.markets),
        config.exchange,
        rust_config.db_path,
    )

    # Signal handling
    should_stop = False

    def _handle_signal(signum, frame):
        nonlocal should_stop
        should_stop = True
        logger.info("Received signal %d, shutting down...", signum)

    signal.signal(signal.SIGINT, _handle_signal)
    signal.signal(signal.SIGTERM, _handle_signal)

    start = time.monotonic()
    last_status_log = start
    last_export = start
    last_purge = start
    status_interval = 60.0

    try:
        while not should_stop:
            time.sleep(1.0)

            now = time.monotonic()

            # Check duration limit
            if duration > 0 and (now - start) >= duration:
                logger.info("Duration limit reached (%.0fs)", duration)
                break

            # Periodic status logging
            if now - last_status_log >= status_interval:
                st = c.status()
                logger.info(
                    "Collector status: obs=%d trades=%d metadata=%d errors=%d uptime=%.0fs db=%.1fMB",
                    st.ob_snapshots_total,
                    st.trades_total,
                    st.metadata_updates_total,
                    st.errors_total,
                    st.uptime_secs,
                    st.db_size_bytes / (1024 * 1024),
                )
                last_status_log = now

            # Periodic parquet export
            if (
                config.parquet_dir
                and config.parquet_export_interval > 0
                and now - last_export >= config.parquet_export_interval
            ):
                try:
                    export_parquet(
                        rust_config.db_path,
                        config.parquet_dir,
                    )
                    last_export = now
                except Exception as e:
                    logger.warning("Parquet export failed: %s", e)

            # Periodic retention purge
            if (
                config.retention_hours > 0
                and now - last_purge >= 3600.0
            ):
                max_age = config.retention_hours * 3600.0
                deleted = c.purge(max_age)
                if deleted > 0:
                    logger.info("Purged %d old rows", deleted)
                last_purge = now

    finally:
        c.stop()
        logger.info("Collector stopped")


# ---------------------------------------------------------------------------
# Parquet export
# ---------------------------------------------------------------------------


def export_parquet(
    db_path: str,
    output_dir: str,
    start_ts: float = 0.0,
    end_ts: float = 0.0,
    tables: list[str] | None = None,
) -> list[str]:
    """Export collector SQLite tables to Parquet files.

    Requires ``pyarrow`` (optional dependency).

    Args:
        db_path: Path to collector SQLite database.
        output_dir: Directory to write Parquet files.
        start_ts: Start timestamp filter (0 = no filter).
        end_ts: End timestamp filter (0 = no filter).
        tables: List of table names to export. Defaults to all collector tables.

    Returns:
        List of written file paths.

    Raises:
        ImportError: If pyarrow is not installed.
    """
    try:
        import pyarrow as pa
        import pyarrow.parquet as pq
    except ImportError:
        raise ImportError(
            "pyarrow is required for Parquet export. "
            "Install with: pip install pyarrow"
        )

    import sqlite3

    if tables is None:
        tables = [
            "orderbook_snapshots",
            "market_trades",
            "market_metadata",
            "event_metadata",
        ]

    os.makedirs(output_dir, exist_ok=True)
    conn = sqlite3.connect(db_path)
    written = []

    try:
        for table in tables:
            # Build query with optional time filter
            ts_col = "timestamp" if table in ("orderbook_snapshots", "market_trades") else "updated_at"
            query = f"SELECT * FROM {table}"  # noqa: S608
            params: list[float] = []

            conditions = []
            if start_ts > 0:
                conditions.append(f"{ts_col} >= ?")
                params.append(start_ts)
            if end_ts > 0:
                conditions.append(f"{ts_col} <= ?")
                params.append(end_ts)
            if conditions:
                query += " WHERE " + " AND ".join(conditions)

            cursor = conn.execute(query, params)
            columns = [desc[0] for desc in cursor.description]
            rows = cursor.fetchall()

            if not rows:
                continue

            # Build pyarrow table
            col_data = {col: [row[i] for row in rows] for i, col in enumerate(columns)}
            table_pa = pa.table(col_data)

            # Write to parquet
            path = os.path.join(output_dir, f"{table}.parquet")
            pq.write_table(table_pa, path, compression="snappy")
            written.append(path)
            logger.info("Exported %d rows to %s", len(rows), path)
    finally:
        conn.close()

    return written


# ---------------------------------------------------------------------------
# Query helpers
# ---------------------------------------------------------------------------


def query_orderbooks(
    db_path: str,
    market_id: str,
    start_ts: float = 0.0,
    end_ts: float = 0.0,
    limit: int = 1000,
) -> list[dict]:
    """Query orderbook snapshots from a collector database.

    Args:
        db_path: Path to SQLite database.
        market_id: Market ID to query.
        start_ts: Start timestamp (0 = beginning).
        end_ts: End timestamp (0 = now).
        limit: Max rows to return.

    Returns:
        List of orderbook snapshot dicts.
    """
    if end_ts <= 0:
        end_ts = time.time() + 86400
    c = Collector(CollectorConfig(market_ids=[], db_path=db_path))
    raw = c.query_orderbooks(market_id, start_ts, end_ts, limit)
    return json.loads(raw) if raw else []


def query_collected_trades(
    db_path: str,
    market_id: str,
    start_ts: float = 0.0,
    end_ts: float = 0.0,
    limit: int = 1000,
) -> list[dict]:
    """Query collected trades from a collector database.

    Args:
        db_path: Path to SQLite database.
        market_id: Market ID to query.
        start_ts: Start timestamp (0 = beginning).
        end_ts: End timestamp (0 = now).
        limit: Max rows to return.

    Returns:
        List of trade dicts.
    """
    if end_ts <= 0:
        end_ts = time.time() + 86400
    c = Collector(CollectorConfig(market_ids=[], db_path=db_path))
    raw = c.query_trades(market_id, start_ts, end_ts, limit)
    return json.loads(raw) if raw else []
