"""Shared batched-concurrent PRICE-ONLY factor-score builder + the three live-booking ForwardStrategy services.

The universe + per-ticker daily bars are fetched ONCE (concurrently, not per-sleeve) into a single
precomputed `scores` dict via `compute_factor_scores`; the three sleeves (long / ls / tilt) each consume
that SAME shared dict and do NO I/O of their own.

The sleeve is PRICE-ONLY (momentum + low-vol): a single `adj_closes` fetch per ticker yields BOTH the
12-1 momentum and a trailing realized vol, so the whole cross-section needs nothing but prices — which
Tiingo serves broadly. Fundamentals are intentionally NOT fetched: the current Tiingo plan exposes
fundamentals for DOW-30 only, so a fundamentals fetch silently dropped ~270/300 names. The
FundamentalsClient port + TiingoFundamentalsClient adapter are retained (unused here) for a future
fundamentals upgrade.

Each service marks the prior target book to today's precomputed prices, books the realized daily return
(net of short-borrow for the long-short construction), and rebalances to a fresh factor book on the monthly
boundary. On the FIRST run the prior book is empty so the booked return is 0 and the tracker freezes; it
just seeds the initial target positions + their marking prices + the rebalance month into `extra`."""
from __future__ import annotations

import concurrent.futures
import logging
from collections.abc import Callable
from typing import Any

from fxhnt.application.forward_tracker import _today_iso
from fxhnt.domain.strategies import equity_factor as ef
from fxhnt.ports.market_data import DailyBarClient, UniverseSource

_logger = logging.getLogger(__name__)

def _momentum_12_1(closes: dict[str, float]) -> float | None:
    """12-1 month price momentum from a date-keyed adjusted-close series.

    Sorts the dates into an ascending close series and delegates the window math to the pure domain
    helper `equity_factor.momentum_12_1_from_closes` (the single source of truth shared with the
    backtest). Returns the trailing-year return that EXCLUDES the most recent ~month, or None when the
    history is too short."""
    dates = sorted(closes)
    return ef.momentum_12_1_from_closes([closes[d] for d in dates])


def _realized_vol(closes: dict[str, float], window: int = ef._VOL_WINDOW) -> float | None:
    """Trailing realized vol = sample stdev of the last `window` daily log returns.

    Sorts the dates into an ascending close series and delegates to the pure domain helper
    `equity_factor.realized_vol_from_closes` (shared with the backtest). Returns None when there is not
    enough history for a full window or when any close in the window is non-positive."""
    dates = sorted(closes)
    return ef.realized_vol_from_closes([closes[d] for d in dates], window=window)


def _gather_ticker(sym: str, bars: DailyBarClient) -> dict[str, Any] | None:
    """Fetch one ticker's adjusted-close history ONCE and derive its 12-1 momentum, trailing realized vol
    and latest close. Per-ticker resilient: ANY exception (the Tiingo daily endpoint 400/404s on a
    no-coverage name, ...) returns None. A name with insufficient history for momentum is also dropped (a
    name we can't score must not be in the book) rather than aborting the whole sleeve. Pure read — safe to
    run concurrently across a thread pool."""
    try:
        closes = bars.adj_closes(sym)
        mom = _momentum_12_1(closes)
        if mom is None:
            return None
        price = closes[max(closes)] if closes else None
        return {
            "sym": sym,
            "mom": mom,
            "vol": _realized_vol(closes),
            "price": price,
        }
    except Exception:
        return None


def _gather_universe(universe: UniverseSource, bars: DailyBarClient,
                     n: int, max_workers: int) -> list[dict[str, Any]]:
    """Pull the top-n universe and gather each ticker's prices CONCURRENTLY over a thread pool, dropping
    (and counting) any ticker whose gather failed. Returns the survivors' per-ticker records in the
    deterministic top-n order (concurrency fans out the I/O; results are re-ordered to the universe order
    so the cross-section + downstream weights are reproducible regardless of completion order)."""
    syms = universe.top_n(n)
    records: dict[str, dict[str, Any]] = {}
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
        futures = {pool.submit(_gather_ticker, sym, bars): sym for sym in syms}
        for fut in concurrent.futures.as_completed(futures):
            rec = fut.result()
            if rec is not None:
                records[rec["sym"]] = rec
    survivors = [records[sym] for sym in syms if sym in records]
    _logger.info("equity-factor: gathered %d/%d, dropped %d (no data)",
                 len(survivors), len(syms), len(syms) - len(survivors))
    return survivors


def compute_factor_scores(universe: UniverseSource, bars: DailyBarClient,
                          n: int = 150, max_workers: int = 12) -> dict[str, Any]:
    """Fetch the universe + prices ONCE (batched-concurrent) and compute the SHARED PRICE-ONLY factor
    cross-section the three sleeves consume. Returns {'date', 'syms', 'scores', 'prices'} over survivors
    only — gather each ticker's adj_closes concurrently (deriving momentum AND trailing realized vol from
    that single fetch), run the survivors through the pure momentum + low-vol -> composite_price pipeline,
    and emit the aligned syms/scores plus a {sym: latest_close} price map."""
    survivors = _gather_universe(universe, bars, n, max_workers)
    syms = [r["sym"] for r in survivors]
    mm = ef.momentum_score([r["mom"] for r in survivors])
    lv = ef.lowvol_score([r["vol"] for r in survivors])
    c = ef.composite_price(mm, lv)
    prices = {r["sym"]: r["price"] for r in survivors if r["price"] is not None}
    return {"date": _today_iso(), "syms": syms, "scores": c, "prices": prices}


class _EquityFactorStrategy:
    """Live-booking equity-factor ForwardStrategy over a SHARED precomputed `scores` dict (no I/O). Marks
    the prior target book to today's precomputed closes, books the realized return (net of short-borrow for
    the long-short construction), and rebalances to a fresh factor book on the monthly boundary. Carry state
    (`positions`, `prices`, `last_rebal`) lives in the tracker's opaque `extra`."""

    def __init__(self, construction: str, scores: dict[str, Any], borrow_annual: float = 0.01,
                 clock: Callable[[], str] = _today_iso) -> None:
        self._construction = construction
        self._scores = scores
        self._borrow_annual = borrow_annual
        self._clock = clock

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        prev: dict[str, float] = extra.get("positions", {})
        prev_prices: dict[str, float] = extra.get("prices", {})
        last_rebal: str | None = extra.get("last_rebal")

        today = self._clock()
        cur: dict[str, float] = self._scores["prices"]
        borrow_daily = (self._borrow_annual / 252.0) if self._construction == "ls" else 0.0
        r = ef.book_return(prev, prev_prices, cur, borrow_daily) if prev else 0.0

        if last_rebal is None or today[:7] != last_rebal[:7]:
            w = ef.construction_weights(self._scores["scores"], self._construction)
            new = {s: wt for s, wt in zip(self._scores["syms"], w) if wt != 0}
            last_rebal = today
        else:
            new = dict(prev)

        new_prices = {s: cur[s] for s in new if s in cur}
        return [(today, r)], {"positions": new, "prices": new_prices, "last_rebal": last_rebal}


class EquityFactorLong:
    """Long-only top-quintile equity-factor sleeve over a shared precomputed `scores` dict."""

    def __init__(self, scores: dict[str, Any], clock: Callable[[], str] = _today_iso) -> None:
        self._impl = _EquityFactorStrategy("long", scores, clock=clock)

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        return self._impl.advance(last_date, extra)


class EquityFactorTilt:
    """Long-only rank-weighted (tilt) equity-factor sleeve over a shared precomputed `scores` dict."""

    def __init__(self, scores: dict[str, Any], clock: Callable[[], str] = _today_iso) -> None:
        self._impl = _EquityFactorStrategy("tilt", scores, clock=clock)

    def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
        return self._impl.advance(last_date, extra)
