"""B0 Dagster asset graph — the combined-book lifecycle track.

Four assets that mirror the daily operational sequence:
  crypto_bars        → ingest crypto pit NPZ files into the warehouse
  futures_bars       → ingest futures DBN front-month arrays into the warehouse
  combined_forward_nav → run the forward tracker (builds CombinedBook, steps the JSON state)
  cockpit_forward    → normalize state files and upsert into the operational cockpit DB

The plain build_combined_forward_nav function is exposed for unit tests
(no live Dagster instance needed).

NOTE: this module intentionally omits `from __future__ import annotations` because
Dagster 1.12 uses runtime type-hint inspection on the `context` parameter; PEP-563
lazy annotations make the hint a string, breaking the validator.
"""
import os
from typing import List

from dagster import AssetExecutionContext, asset

from fxhnt.application.combined_book import CombinedBook
from fxhnt.application.combined_book_source import CombinedBookSource
from fxhnt.application.forward_tracker import CombinedBookForwardTracker, ForwardStatus

_FUTURES = ["ES", "NQ", "YM", "RTY", "EMD", "NKD"]


def _data_dir() -> str:
    return os.environ.get("FXHNT_SURFER_DATA_DIR", "/data/surfer")


def build_combined_forward_nav(
    source: CombinedBookSource, futures: List[str], state_path: str
) -> ForwardStatus:
    """Build and step the forward tracker from the given source. Unit-testable without Dagster."""
    book = CombinedBook("warehouse", futures, source=source)
    return CombinedBookForwardTracker(book, state_path).step()


@asset
def crypto_bars(context: AssetExecutionContext) -> int:
    """Bronze FETCH (Binance, free, daily) → silver → warehouse: refresh + ingest the crypto_pit panel.
    Without the fetch the forward track would ingest stale data and never advance."""
    from fxhnt.adapters.data.crypto_fetch import fetch_crypto_pit
    from fxhnt.adapters.warehouse.duckdb_feature_store import DuckDbFeatureStore
    from fxhnt.application.silver_ingest_helpers import ingest_crypto_pit
    from fxhnt.config import get_settings

    s = get_settings()
    s.ensure_dirs()
    try:                                  # best-effort: a transient Binance hiccup must not kill the nightly run
        fetched, total = fetch_crypto_pit(_data_dir())
        context.log.info(f"crypto_bars fetched {fetched}/{total} symbols")
    except Exception as e:  # noqa: BLE001
        context.log.warning(f"crypto_bars fetch failed (ingesting existing data): {e}")
    n = ingest_crypto_pit(DuckDbFeatureStore(s.warehouse_path), f"{_data_dir()}/crypto_pit")
    context.log.info(f"crypto_bars ingested {n} rows")
    return n


@asset(deps=[crypto_bars])   # serialize after crypto_bars: DuckDB is single-writer (one lock on warehouse.duckdb)
def futures_bars(context: AssetExecutionContext) -> int:
    """Bronze → silver → warehouse: ingest continuous front-month futures arrays.

    Skips (with a warning) any root whose DBN file is missing — never fails the asset.
    """
    import datetime as dt
    import os

    from fxhnt.adapters.data.dbn_local import read_front_arrays
    from fxhnt.adapters.warehouse.duckdb_feature_store import DuckDbFeatureStore
    from fxhnt.adapters.warehouse.silver_futures import normalize_futures
    from fxhnt.application.warehouse_ingest import WarehouseIngest
    from fxhnt.config import get_settings

    s = get_settings()
    s.ensure_dirs()
    # Futures fetch is Databento (paid) — refresh only on Sundays (UTC), cost-capped, like the retired cron.
    if dt.datetime.now(dt.timezone.utc).isoweekday() == 7 and os.environ.get("DATABENTO_API_KEY"):
        from fxhnt.adapters.data.futures_fetch import fetch_futures
        start = (dt.date.today() - dt.timedelta(days=365 * s.databento.fetch_years)).isoformat()
        try:
            recs, ok, spent = fetch_futures(_data_dir(), start=start, max_cost_usd=s.databento.max_cost_usd)
            context.log.info(f"futures_bars fetched {ok} roots, {recs} records, ${spent:.2f} (since {start})")
        except Exception as e:  # noqa: BLE001
            context.log.warning(f"futures_bars fetch failed (ingesting existing data): {e}")
    ingest = WarehouseIngest(DuckDbFeatureStore(s.warehouse_path))
    total = 0
    for root in _FUTURES:
        try:
            days, inst, close, high, low = read_front_arrays(f"{_data_dir()}/{root}.dbn", root)
            total += ingest.ingest_bars(root, normalize_futures(root, days, inst, close, high, low))
        except Exception as e:  # noqa: BLE001 — missing file is expected in dev/test
            context.log.warning(f"futures_bars {root} skipped: {e}")
    context.log.info(f"futures_bars ingested {total} rows")
    return total


@asset(deps=[crypto_bars, futures_bars])
def combined_forward_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Build the CombinedBook from the warehouse and advance the forward-validation state file."""
    from fxhnt.application.combined_book_factory import make_combined_book_source
    from fxhnt.config import get_settings

    s = get_settings()
    st = build_combined_forward_nav(
        make_combined_book_source(s, _data_dir()),
        _FUTURES,
        f"{_data_dir()}/fxhnt_combined_forward.json",
    )
    context.log.info(
        f"combined_forward_nav: {st.forward_days} forward days through {st.last_date}"
    )
    return {
        "inception": st.inception,
        "last_date": st.last_date,
        "forward_days": st.forward_days,
        "forward_return_pct": st.forward_return_pct,
        "forward_sharpe": st.forward_sharpe,
    }


def _run_paper_tracker(context: AssetExecutionContext, name: str, build_strategy, state_file: str) -> dict:  # type: ignore[type-arg,no-untyped-def]
    """Best-effort for TRANSIENT failures only: build the strategy from its live adapter, step the
    tracker, persist + report state. A transient network/API error logs a warning and returns the
    on-disk state without crashing the run. Genuine bugs (ValueError, KeyError, AttributeError, ...)
    propagate and fail the asset loudly rather than silently flatlining the track."""
    import json
    import urllib.error

    from fxhnt.application.forward_tracker import ForwardTracker

    path = f"{_data_dir()}/{state_file}.json"
    try:
        st = ForwardTracker(build_strategy(), path).step()
        context.log.info(f"{name}: {st.forward_days}d through {st.last_date}")
        return {"forward_days": st.forward_days, "last_date": st.last_date}
    except (
        urllib.error.URLError,
        TimeoutError,
        ConnectionError,
        OSError,
        json.JSONDecodeError,
    ) as e:  # transient network/IO failure must not kill the nightly run
        context.log.warning(f"{name} step failed (state unchanged): {e}")
        return {"forward_days": 0, "last_date": None}


@asset
def sixtyforty_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Paper-forward 60/40 baseline (Tiingo native total-return adjClose, 33yr history)."""
    from fxhnt.adapters.data.tiingo_daily import TiingoDailyClient
    from fxhnt.application.paper_strategies import SixtyFortyStrategy

    return _run_paper_tracker(
        context, "sixtyforty_nav", lambda: SixtyFortyStrategy(TiingoDailyClient()), "sixtyforty_state"
    )


@asset
def multistrat_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Paper-forward multi-strategy book (Tiingo native total-return adjClose; BTC-USD via crypto endpoint)."""
    from fxhnt.adapters.data.tiingo_daily import TiingoDailyClient
    from fxhnt.application.paper_strategies import MultiStratStrategy

    return _run_paper_tracker(
        context, "multistrat_nav", lambda: MultiStratStrategy(TiingoDailyClient()), "multistrat_state"
    )


@asset
def gd_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Paper-forward growth-discipline book (Tiingo native total-return adjClose; BTC-USD via crypto endpoint)."""
    from fxhnt.adapters.data.tiingo_daily import TiingoDailyClient
    from fxhnt.application.paper_strategies import GrowthDisciplineStrategy

    return _run_paper_tracker(
        context, "gd_nav", lambda: GrowthDisciplineStrategy(TiingoDailyClient()), "gd_paper_state"
    )


@asset
def funding_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Paper-forward crypto funding harvest (Binance public futures API, no key)."""
    from fxhnt.adapters.data.binance_funding import BinanceFundingClient
    from fxhnt.application.paper_strategies import FundingCarryStrategy

    return _run_paper_tracker(
        context, "funding_nav", lambda: FundingCarryStrategy(BinanceFundingClient()), "funding_paper_state"
    )


@asset
def crossvenue_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Paper-forward cross-venue funding arb (Binance/Bybit/Hyperliquid public APIs)."""
    from fxhnt.adapters.data.cross_venue import CrossVenueFundingClient
    from fxhnt.application.paper_strategies import CrossVenueStrategy

    return _run_paper_tracker(
        context, "crossvenue_nav", lambda: CrossVenueStrategy(CrossVenueFundingClient()), "crossvenue_state"
    )


@asset
def poc_nav(context: AssetExecutionContext) -> dict:  # type: ignore[type-arg]
    """Paper-forward surfer PoC (crypto residual-momentum + tail-managed VRP; fetches Binance + reads crypto_pit)."""
    from fxhnt.application.paper_strategies import MomentumVrpStrategy

    return _run_paper_tracker(context, "poc_nav", lambda: MomentumVrpStrategy(), "poc_state")


@asset(deps=[combined_forward_nav, sixtyforty_nav, multistrat_nav, gd_nav, funding_nav, crossvenue_nav, poc_nav])
def cockpit_forward(context: AssetExecutionContext) -> None:
    """Normalize tracker state files and upsert rows + summary into the operational cockpit DB."""
    from fxhnt.application.forward_ingest import ingest_forward_state
    from fxhnt.config import get_settings

    ingest_forward_state(get_settings().operational_dsn, _data_dir())
    context.log.info("cockpit_forward ingested")
