"""Recover state on engine boot (``docs/06-operational-flow.md`` §6).

The recovery loop never trades: it only aligns the SQLite state with
the broker. Any unresolvable discrepancy (a position the broker does
not know about, a leg that disappeared, etc.) arms the kill switch.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime

from cerbero_bite.runtime.dependencies import RuntimeContext
from cerbero_bite.state import PositionRecord, transaction
from cerbero_bite.state import connect as connect_state

__all__ = ["recover_state"]


_log = logging.getLogger("cerbero_bite.runtime.recovery")


async def recover_state(ctx: RuntimeContext, *, now: datetime | None = None) -> None:
    """Reconcile SQLite positions with the broker's open positions."""
    when = (now or ctx.clock()).astimezone(UTC)

    conn = connect_state(ctx.db_path)
    try:
        in_flight: list[PositionRecord] = ctx.repository.list_positions(
            conn, status="awaiting_fill"
        )
        in_flight += ctx.repository.list_positions(conn, status="closing")
        opens = ctx.repository.list_positions(conn, status="open")
    finally:
        conn.close()

    if not in_flight and not opens:
        ctx.audit_log.append(
            event="RECOVERY_DONE", payload={"reconciled": 0}, now=when
        )
        return

    broker_positions = await ctx.deribit.get_positions(currency="USDC")
    broker_by_instrument: dict[str, dict[str, object]] = {}
    for p in broker_positions:
        instrument = p.get("instrument")
        if isinstance(instrument, str):
            broker_by_instrument[instrument] = p

    discrepancies: list[str] = []

    # Awaiting-fill / closing → resolve to open or cancelled.
    for record in in_flight:
        seen = (
            record.short_instrument in broker_by_instrument
            and record.long_instrument in broker_by_instrument
        )
        conn = connect_state(ctx.db_path)
        try:
            with transaction(conn):
                if seen:
                    # Both awaiting_fill and closing collapse to "open"
                    # once the broker confirms the legs are present.
                    next_status = "open"
                    ctx.repository.update_position_status(
                        conn,
                        record.proposal_id,
                        status=next_status,  # type: ignore[arg-type]
                        opened_at=record.opened_at or when,
                        now=when,
                    )
                else:
                    ctx.repository.update_position_status(
                        conn,
                        record.proposal_id,
                        status="cancelled",
                        closed_at=when,
                        close_reason="recovery_no_fill",
                        now=when,
                    )
                    discrepancies.append(
                        f"{record.proposal_id}: was {record.status}, broker shows nothing"
                    )
        finally:
            conn.close()

    # Open positions → must be present on broker. If not, alarm.
    for record in opens:
        if (
            record.short_instrument in broker_by_instrument
            and record.long_instrument in broker_by_instrument
        ):
            continue
        discrepancies.append(
            f"{record.proposal_id}: open in DB but missing on broker"
        )

    if discrepancies:
        await ctx.alert_manager.critical(
            source="recovery",
            message="state inconsistencies detected: " + "; ".join(discrepancies),
            component="runtime.recovery",
        )

    ctx.audit_log.append(
        event="RECOVERY_DONE",
        payload={
            "reconciled": len(in_flight) + len(opens),
            "discrepancies": len(discrepancies),
        },
        now=when,
    )
    _log.info(
        "recovery done: reconciled=%d discrepancies=%d",
        len(in_flight) + len(opens),
        len(discrepancies),
    )
