"""
Database-backed snapshot source for the point-in-time replay engine (Part C).

The engine in `app/engines/backtesting/replay.py` is pure and knows nothing about storage. This
module is the other half: it implements `SnapshotSource` against this schema, so a backtest can
actually be run against ingested data.

**Read this before quoting any number this produces.**

- **There is no historical listing-membership table in this schema.** `universe_at()` therefore
  returns TODAY'S securities for every historical date. That is survivorship bias, in the plain
  sense: a company delisted in 2019 is invisible to a backtest of 2015-2020, and the surviving
  names are exactly the ones that did well enough to survive. The replay engine detects the
  constant universe and sets `survivorship_bias_suspected=True`, which makes `is_quotable` False.
  **This is not a bug to be worked around — it is a missing data set.** Fixing it means a
  `security_listing_history` table plus ingestion that populates it, which is real new schema and
  new provider work. Recorded in docs/AUDIT_BACKTESTING_C.md, not papered over.
- **`adjusted_close` is used, and a row without one is skipped rather than falling back to
  `close`.** `app/workers/recompute.py::build_snapshot_from_db` correctly reads `Price.close` for
  today's live price, and reusing that read across a historical series would silently register a
  2-for-1 split as a -50% return. `replay.py`'s previous NOT-IMPLEMENTED docstring flagged exactly
  this trap; this module is where it had to be avoided. A silent fallback to `close` would have
  reintroduced it, so the fallback is refusal.
- **`filing_date` is what makes an observation point-in-time.** `known_as_of` is the later of the
  price date and the newest filing date behind the snapshot, so `run_replay()`'s look-ahead
  assertion is checking something real. Periods with no `filing_date` fall back to `period_end`,
  which is a materially weaker guarantee — `as_of_snapshot()` counts those, and the count is
  surfaced so a caller can see how much of its universe relies on the weak path.
"""
from __future__ import annotations

from datetime import date, timedelta
from typing import Optional, Sequence

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.logging import get_logger
from app.engines.backtesting.replay import PriceObservation
from app.engines.metrics.point_in_time import as_of_snapshot
from app.models import FinancialPeriod, Price, Security
from app.workers.recompute import _line_items_from_period

logger = get_logger(__name__)

#: How far back to look for a price on or before the rebalance date. A rebalance date that falls
#: on a weekend or holiday has no bar of its own; beyond this window the security is treated as
#: unpriced rather than marked at a stale price.
MAX_PRICE_STALENESS_DAYS = 7


class DbSnapshotSource:
    """`SnapshotSource` implementation over this schema. See the module docstring for its limits."""

    def __init__(self, db: Session, max_price_staleness_days: int = MAX_PRICE_STALENESS_DAYS):
        self._db = db
        self._max_staleness = max_price_staleness_days
        #: Counts a caller can inspect after a run, so the weak paths are measurable rather than
        #: invisible.
        self.observations_without_filing_date = 0
        self.observations_skipped_no_adjusted_close = 0

    def universe_at(self, as_of: date) -> Sequence[str]:
        """SURVIVORSHIP-BIASED: today's securities, for every date. See the module docstring."""
        return list(self._db.execute(select(Security.id)).scalars().all())

    def observe(self, security_id: str, as_of: date) -> Optional[PriceObservation]:
        price_row = self._db.execute(
            select(Price)
            .where(
                Price.security_id == security_id,
                Price.date <= as_of,
                Price.date >= as_of - timedelta(days=self._max_staleness),
            )
            .order_by(Price.date.desc())
            .limit(1)
        ).scalar_one_or_none()
        if price_row is None:
            return None
        if price_row.adjusted_close is None:
            # Refusing, not falling back to `close`: an unadjusted series turns a split into a
            # catastrophic fake return. Counted so the caller can see how much data this cost.
            self.observations_skipped_no_adjusted_close += 1
            return None

        periods = (
            self._db.execute(
                select(FinancialPeriod)
                .where(
                    FinancialPeriod.security_id == security_id,
                    FinancialPeriod.period_type == "FY",
                    FinancialPeriod.period_end <= as_of,
                )
                .order_by(FinancialPeriod.period_end.desc())
                .limit(11)
            ).scalars().all()
        )
        line_items = [_line_items_from_period(fp, None) for fp in periods]
        filtered = as_of_snapshot(line_items, as_of)
        self.observations_without_filing_date += filtered.used_period_end_fallback_count
        if not filtered.periods:
            return None

        newest = filtered.periods[0]
        known = newest.filing_date or newest.period_end
        known_as_of = max(known, price_row.date)

        return PriceObservation(
            security_id=security_id,
            price=price_row.adjusted_close,
            known_as_of=known_as_of,
            is_split_adjusted=True,   # guaranteed by the adjusted_close refusal above
            payload=filtered.periods,
        )
