"""
Point-in-time filtering (StockLab overhaul, Part 19 — CRITICAL audit fix).

`docs/FINANCIAL_FORMULAS.md` claimed this file and an `as_of_snapshot` function already existed
and centrally enforced `filing_date <= D`. They did not exist anywhere in the codebase before this
pass — see docs/AUDIT_METRICS.md's point-in-time finding. This is the real implementation of that
promise, built dependency-free like every other engine module so it's directly unit-testable.

**What this fixes:** given a list of `LineItems` (most-recent-first) and an as-of date `D`,
`as_of_snapshot` returns only the periods a point-in-time-honest observer at date `D` could
actually have seen — `filing_date <= D` (falling back to `period_end <= D` when `filing_date` is
missing, which is a materially weaker guarantee since `period_end` is always earlier than the
actual filing/publication date; flagged in the returned `used_period_end_fallback` count so a
caller — the backtesting engine, specifically — can measure how much of its universe is relying on
the weaker fallback rather than a real filing date).

**What this does NOT fix (documented, not silently skipped):** wiring this into
`app/engines/normalize.py::build_snapshot()` and `app/workers/recompute.py::build_snapshot_from_db`
for LIVE (present-day) calculation is unnecessary — live calculation correctly wants "whatever is
known now". The gap this closes is for a **backtest** that asks "what would this snapshot have
looked like as of some past date D" — that caller does not exist yet in this codebase (see
`app/engines/backtesting/` — new this pass, SCAFFOLD status). This function is the building block
a real backtest replay loop needs, not a retrofit of the live-calculation path, which would be the
wrong fix (live calculation should keep using the latest data).
"""
from __future__ import annotations

from dataclasses import dataclass
from datetime import date
from typing import Sequence

from app.engines.types import LineItems


@dataclass(frozen=True)
class AsOfFilterResult:
    periods: list[LineItems]
    excluded_count: int          # periods with filing_date (or period_end fallback) after D
    used_period_end_fallback_count: int  # periods that had no filing_date and fell back to period_end


def as_of_snapshot(periods_most_recent_first: Sequence[LineItems], as_of: date) -> AsOfFilterResult:
    """Filters a most-recent-first period list down to what was actually knowable as of `as_of`.
    A period with `filing_date` in the future relative to `as_of` (or, lacking a filing_date, a
    `period_end` in the future relative to `as_of`) is excluded — this is exactly the look-ahead
    bias a backtest must not have. Order is preserved (still most-recent-first among what remains).
    """
    kept: list[LineItems] = []
    excluded = 0
    fallback_used = 0
    for p in periods_most_recent_first:
        if p.filing_date is not None:
            eligible_date = p.filing_date
        else:
            eligible_date = p.period_end
            fallback_used += 1
        if eligible_date <= as_of:
            kept.append(p)
        else:
            excluded += 1
            if p.filing_date is None:
                fallback_used -= 1  # don't count an excluded period toward the fallback tally
    return AsOfFilterResult(periods=kept, excluded_count=excluded, used_period_end_fallback_count=fallback_used)
