"""The deterministic forward engine (spec sub-project A §3).

One `run_track` call: read the track's declared definition, manage its immutable anchor (inception on first
run, auto-re-inception on a definition-hash change), then produce the forward record per record_mode —
`recompute` recomputes [T0, today] from PIT data; `observed` appends today's booked row to the immutable log
and carries the strategy's book-state. The nav/summary are derived caches. No mutable, hand-editable state."""
from __future__ import annotations

import datetime as dt
from typing import Any

from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.forward_definition import definition_hash, inception_override, track_definition
from fxhnt.application.forward_models import ForwardNavRow as NavRowDTO
from fxhnt.application.forward_models import ForwardSummary
from fxhnt.application.forward_recompute import derive_nav, recompute_series


def _write_nav_and_summary(repo: ForwardNavRepo, strategy_id: str,
                           rows: list[tuple[str, float]], at: dt.datetime) -> dict[str, Any]:
    nav_rows, summary = derive_nav(rows)
    repo.replace_nav(
        strategy_id,
        [NavRowDTO(strategy_id=strategy_id, date=d, ret=r, nav=n)
         for (d, r), (_, n) in zip(rows, nav_rows, strict=True)], at=at)
    sm = ForwardSummary(strategy_id=strategy_id, as_of=summary["last_date"] or "",
                        days=summary["days"], nav=summary["nav"], total_return=summary["total_return"],
                        sharpe=summary["sharpe"], maxdd=summary["maxdd"])
    # gate WAIT is the safe default here; the gate evaluation (forward_ingest) sets the real verdict later.
    repo.upsert_summary(sm, gate_status="WAIT", gate_reason="building", at=at)
    return summary


def run_track(repo: ForwardNavRepo, strategy_id: str, strategy: Any, today: str, at: dt.datetime) -> dict[str, Any]:
    params, version, record_mode = track_definition(strategy_id)
    if record_mode not in ("recompute", "observed"):
        raise ValueError(f"unknown record_mode {record_mode!r} for {strategy_id}")
    h = definition_hash(params, version)

    active = repo.active_anchor(strategy_id)
    if active is None and repo.has_forward_history(strategy_id):
        raise RuntimeError(
            f"{strategy_id}: no ACTIVE forward anchor but prior forward history exists — the anchor row was "
            f"lost. Refusing to silently re-inception (that would reset the OOS clock to day 0 and discard "
            f"the forward record). Restore the anchor from backup before re-running.")
    reinception = active is None or active.definition_hash != h
    if reinception:
        if active is not None:
            repo.archive_anchor(strategy_id, at=at)
        # a faithful re-home pins its pre-re-home OOS start via `inception_t0` (version-scoped, not hashed);
        # otherwise the clock starts today. This is what makes a DB rebuild reproduce the recovered window.
        # Guard: never incept in the FUTURE — a pin later than `today` (clock skew, or a not-yet-reached date)
        # falls back to today (ISO dates compare lexicographically == chronologically).
        ov = inception_override(strategy_id)
        t0 = ov if (ov is not None and ov <= today) else today
        repo.set_anchor(strategy_id, t0=t0, definition_hash=h, definition_version=version,
                        params_json=params, record_mode=record_mode, at=at)
        if record_mode == "observed":
            # a new definition means a fresh log — clear any carried book-state (positions etc. built under
            # the OLD definition) so the first booking after re-inception starts from empty carry, not stale
            # positions.
            repo.set_book_state(strategy_id, {}, at=at)
    else:
        t0 = active.t0

    if record_mode == "recompute":
        rows = recompute_series(strategy, t0)
    elif record_mode == "observed":
        # book TODAY on the carried book-state; inject `today` so the pure strategy needs no clock
        extra = repo.get_book_state(strategy_id)
        extra["_today"] = today
        booked, new_extra = strategy.advance(None, extra)
        for d, r in booked:
            if d >= t0:
                repo.append_record(strategy_id, str(d), float(r), at=at)
        new_extra.pop("_today", None)
        repo.set_book_state(strategy_id, new_extra, at=at)
        rows = repo.record_series(strategy_id, since=t0)
    else:  # pragma: no cover - guarded by track_definition + test_forward_definition
        raise ValueError(f"unknown record_mode {record_mode!r} for {strategy_id}")

    summary = _write_nav_and_summary(repo, strategy_id, rows, at=at)
    return {"t0": t0, "record_mode": record_mode, "reinception": reinception, **summary}
