"""Shared live trading-policy state and availability checks."""

from __future__ import annotations

from collections.abc import Iterable
from datetime import date, datetime, timezone
from zoneinfo import ZoneInfo

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.paper_trade import PaperTrade

# Gate-reset "day" boundary matches US cash equities session calendar, not UTC.
_REENTRY_DAY_TZ = ZoneInfo("America/New_York")


def _ny_trading_date(moment: datetime) -> date:
    """Calendar date in America/New_York for a gate-reset observation."""
    if moment.tzinfo is None:
        moment = moment.replace(tzinfo=timezone.utc)
    return moment.astimezone(_REENTRY_DAY_TZ).date()


async def _latest_initial_stop_trades(
    db: AsyncSession,
    *,
    closed_before: datetime | None = None,
) -> dict[int, PaperTrade]:
    """Return a ticker's latest closed trade only when it was an initial stop."""
    ranked_stmt = (
        select(
            PaperTrade.id.label("trade_id"),
            func.row_number()
            .over(
                partition_by=PaperTrade.ticker_id,
                order_by=(PaperTrade.closed_at.desc(), PaperTrade.id.desc()),
            )
            .label("recency"),
        )
        .where(
            PaperTrade.status == "closed",
            PaperTrade.closed_at.is_not(None),
        )
    )
    if closed_before is not None:
        ranked_stmt = ranked_stmt.where(PaperTrade.closed_at <= closed_before)
    ranked = ranked_stmt.subquery()
    stmt = (
        select(PaperTrade)
        .join(ranked, ranked.c.trade_id == PaperTrade.id)
        .where(
            ranked.c.recency == 1,
            PaperTrade.close_reason == "stop",
        )
    )
    result = await db.execute(stmt)
    return {trade.ticker_id: trade for trade in result.scalars()}


async def get_reentry_gate_locks(db: AsyncSession) -> dict[int, datetime]:
    """Return tickers still waiting for a post-stop gate failure.

    A later qualified setup is actionable only after the daily scanner has
    observed an unqualified evaluation after the latest initial-stop exit and
    then a fresh qualification. The returned timestamp is the stop time and is
    useful for diagnostics; callers normally only need the keys.
    """
    latest = await _latest_initial_stop_trades(db)
    return {
        ticker_id: trade.closed_at
        for ticker_id, trade in latest.items()
        if trade.reentry_gate_requalified_at is None and trade.closed_at is not None
    }


async def observe_reentry_gate_transitions(
    db: AsyncSession,
    *,
    evaluated_ticker_ids: Iterable[int],
    qualified_ticker_ids: Iterable[int],
    observed_at: datetime | None = None,
) -> set[int]:
    """Persist gate-failure and later requalification observations.

    Only tickers whose scan completed successfully belong in
    ``evaluated_ticker_ids``. This prevents a scanner exception from being
    mistaken for a real gate exit. The caller owns the transaction; this helper
    flushes so the new state is immediately visible in that transaction.
    """
    evaluated = {int(ticker_id) for ticker_id in evaluated_ticker_ids}
    if not evaluated:
        return set()
    qualified = {int(ticker_id) for ticker_id in qualified_ticker_ids}
    timestamp = observed_at or datetime.now(timezone.utc)
    latest = await _latest_initial_stop_trades(db, closed_before=timestamp)
    updated: set[int] = set()
    for ticker_id in evaluated:
        trade = latest.get(ticker_id)
        if trade is None or trade.reentry_gate_requalified_at is not None:
            continue
        if trade.reentry_gate_failed_at is None:
            if ticker_id not in qualified:
                trade.reentry_gate_failed_at = timestamp
                updated.add(ticker_id)
        elif ticker_id in qualified:
            # Study semantics: requalify only on a *subsequent* daily observation.
            # Same America/New_York calendar day as the failure does not unlock,
            # even if multiple full-universe scans run (manual + near-close).
            if _ny_trading_date(trade.reentry_gate_failed_at) < _ny_trading_date(
                timestamp
            ):
                trade.reentry_gate_requalified_at = timestamp
                updated.add(ticker_id)

    if updated:
        await db.flush()
    return updated
