from __future__ import annotations

from datetime import datetime, timezone

import polars as pl

from uforia.config import AppConfig, IngestionConfig
from uforia.ingest.deribit.archiver import (
    archive_deribit_options_once,
    process_archiver_cycle,
    rebuild_option_chain_from_archive,
)
from uforia.storage import DatasetName, DatasetStore, ReadMode


class FakeDeribitClient:
    def __init__(self, now: datetime) -> None:
        self._now = now

    def now(self) -> datetime:
        return self._now

    def get_book_summaries(self, currency: str, kind: str = "option") -> list[dict[str, object]]:
        _ = currency, kind
        return [
            {
                "instrument_name": "BTC-29MAR26-100000-C",
                "bid_price": 0.12,
                "ask_price": 0.13,
                "mid_price": 0.125,
                "mark_price": 0.126,
                "mark_iv": 60.0,
                "underlying_price": 100000.0,
                "interest_rate": 0.0,
                "open_interest": 1.0,
                "volume": 2.0,
                "creation_timestamp": int(self._now.timestamp() * 1000),
            },
            {
                "instrument_name": "BTC-29MAR26-100000-P",
                "bid_price": 0.11,
                "ask_price": 0.12,
                "mid_price": 0.115,
                "mark_price": 0.116,
                "mark_iv": 60.0,
                "underlying_price": 100000.0,
                "interest_rate": 0.0,
                "open_interest": 1.0,
                "volume": 2.0,
                "creation_timestamp": int(self._now.timestamp() * 1000),
            },
        ]

    def get_ticker(self, instrument_name: str) -> dict[str, object]:
        if instrument_name.endswith("-C"):
            delta = 0.5
            bid = 0.12
            ask = 0.13
            mid = 0.125
            mark = 0.126
        else:
            delta = -0.5
            bid = 0.11
            ask = 0.12
            mid = 0.115
            mark = 0.116
        return {
            "instrument_name": instrument_name,
            "best_bid_price": bid,
            "best_ask_price": ask,
            "mark_price": mark,
            "mark_iv": 60.0,
            "underlying_price": 100000.0,
            "interest_rate": 0.0,
            "open_interest": 1.0,
            "stats": {"volume": 2.0},
            "timestamp": int(self._now.timestamp() * 1000),
            "greeks": {"delta": delta, "gamma": 0.02, "vega": 0.1},
            "bid_iv": 59.0,
            "ask_iv": 61.0,
            "best_bid_amount": 1.0,
            "best_ask_amount": 1.0,
            "mid_price": mid,
        }

    def get_tickers(
        self, instrument_names: list[str], *, max_workers: int = 16
    ) -> list[dict[str, object]]:
        _ = max_workers
        return [self.get_ticker(name) for name in instrument_names]

    def get_instruments(
        self, currency: str, kind: str = "option", expired: bool = False
    ) -> list[dict[str, object]]:
        _ = currency, kind, expired
        expiry = datetime(2026, 3, 29, tzinfo=timezone.utc)
        return [
            {
                "instrument_name": "BTC-29MAR26-100000-C",
                "expiration_timestamp": int(expiry.timestamp() * 1000),
                "option_type": "call",
                "strike": 100000.0,
            },
            {
                "instrument_name": "BTC-29MAR26-100000-P",
                "expiration_timestamp": int(expiry.timestamp() * 1000),
                "option_type": "put",
                "strike": 100000.0,
            },
        ]


def test_archive_once_writes_raw_normalized_and_skips_existing(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    now = datetime(2026, 3, 1, 11, 2, tzinfo=timezone.utc)
    client = FakeDeribitClient(now)
    config = AppConfig(ingestion=IngestionConfig(archive_grace_seconds=90))

    first = archive_deribit_options_once(
        store,
        as_of=datetime(2026, 3, 1, 11, 0, tzinfo=timezone.utc),
        client=client,
        config=config,
    )
    second = archive_deribit_options_once(
        store,
        as_of=datetime(2026, 3, 1, 11, 0, tzinfo=timezone.utc),
        client=client,
        config=config,
    )

    raw = store.read(DatasetName.RAW_DERIBIT_OPTION_SNAPSHOT, read_mode=ReadMode.ALL_RUNS)
    options = store.read(DatasetName.OPTION_CHAIN_SNAPSHOT, read_mode=ReadMode.ALL_RUNS)
    runs = store.read(DatasetName.INGESTION_RUNS, read_mode=ReadMode.ALL_RUNS)

    assert first.status.value == "success"
    assert second.status.value == "skipped_existing"
    assert raw.height == 1
    assert options.height == 2
    assert runs.height == 2
    assert runs.filter(pl.col("status") == "success").height == 1
    assert runs.filter(pl.col("status") == "skipped_existing").height == 1


def test_process_archiver_cycle_emits_gaps_and_current_success(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    seed_ts = datetime(2026, 3, 1, 9, 0, tzinfo=timezone.utc)
    store.write(
        DatasetName.INGESTION_RUNS,
        pl.DataFrame(
            [
                {
                    "ts": seed_ts,
                    "date": seed_ts.date(),
                    "dataset": DatasetName.OPTION_CHAIN_SNAPSHOT.value,
                    "venue": "deribit",
                    "underlying": "BTC",
                    "status": "success",
                    "rows_written_raw": 1,
                    "rows_written_normalized": 2,
                    "error_message": None,
                    "duration_ms": 100,
                    "logical_ts": seed_ts,
                    "run_id": "seed-success",
                    "run_ts": datetime(2026, 3, 1, 9, 1, tzinfo=timezone.utc),
                    "write_mode": "immutable",
                }
            ]
        ),
    )
    now = datetime(2026, 3, 1, 12, 2, tzinfo=timezone.utc)
    results = process_archiver_cycle(
        store,
        now=now,
        client=FakeDeribitClient(now),
        config=AppConfig(ingestion=IngestionConfig(archive_grace_seconds=90)),
    )

    statuses = [result.status.value for result in results]
    runs = store.read(DatasetName.INGESTION_RUNS, read_mode=ReadMode.ALL_RUNS)

    assert statuses == ["gap", "gap", "success"]
    assert runs.filter(pl.col("status") == "gap").height == 2
    assert runs.filter(pl.col("status") == "success").height == 2


def test_rebuild_option_chain_from_archive_rehydrates_gamma(tmp_path) -> None:
    store = DatasetStore(tmp_path)
    now = datetime(2026, 3, 1, 11, 2, tzinfo=timezone.utc)
    client = FakeDeribitClient(now)
    config = AppConfig(ingestion=IngestionConfig(archive_grace_seconds=90))

    archive_deribit_options_once(
        store,
        as_of=datetime(2026, 3, 1, 11, 0, tzinfo=timezone.utc),
        client=client,
        config=config,
    )
    result = rebuild_option_chain_from_archive(
        store,
        start=datetime(2026, 3, 1, 11, 0, tzinfo=timezone.utc),
        end=datetime(2026, 3, 1, 11, 0, tzinfo=timezone.utc),
        underlying="BTC",
    )

    latest = store.read(DatasetName.OPTION_CHAIN_SNAPSHOT)

    assert result["snapshots"] == 1
    assert latest.height == 2
    assert latest["gamma"].null_count() == 0
