# SPDX-License-Identifier: Apache-2.0
"""Per-section JSON freshness checker.

The engine writes per-section reports under ``/data/reports/`` —
``holdings_summary.json``, ``performance.json``, ``bond_analysis.json``,
``analyst_recommendations_summary.json``, ``portfolio_news.json``,
``scenario.json``, ``whatchanged.json``, ``portfolio_analysis.json``
(synthesize) etc. Each section is generated by a separate
``investorclaw <subcommand>`` invocation; the bridge's auto-init only
runs ``setup → refresh → seed_ask`` (analyst gets refreshed by
``refresh``, the rest don't).

When sections age past ~24h, every cobol prompt's narrator detects
staleness and triggers an in-process per-section refresh — at
~60s/section that quickly blows the curl timeout. v4.4.0 closes this
by detecting stale sections at bridge startup and firing the full
``_regenerate_sweep`` in background so subsequent asks find fresh
JSONs in cache.

This module provides ``stale_sections(reports_dir, max_age_hours)``
which the bridge calls during ``_auto_initialize`` to decide whether
to follow up with a background sweep.
"""
from __future__ import annotations

import os
import time
from pathlib import Path
from typing import Any


# Section JSONs the bridge tracks. Keys are the section names that
# match the engine's ``investorclaw <name>`` subcommands; values are
# the canonical filename(s) under ``/data/reports/``.
#
# NOTE: This registry mirrors the ic-engine command outputs at v4.x.
# Sync-check against argonautsystems/ic-engine on each IC_ENGINE_REF bump.
# Do not auto-discover section files — over-discovery would over-trigger
# sweeps on engine debug-output files.
_SECTION_FILES: dict[str, str] = {
    "holdings": "holdings_summary.json",
    "performance": "performance.json",
    "bonds": "bond_analysis.json",
    "analyst": "analyst_recommendations_summary.json",
    "news": "portfolio_news.json",
    "whatchanged": "whatchanged.json",
    "scenario": "scenario.json",
    "synthesize": "portfolio_analysis.json",
    # Optional sections — only flagged stale if file exists but is old;
    # missing files are NOT flagged because the section may not apply
    # to every portfolio (e.g. cashflow on bond-less portfolios).
    "cashflow": "cashflow.json",
    "peer": "peer.json",
    "markets": "markets.json",
    "optimize": "optimize.json",
    "rebalance": "rebalance.json",
}

# Sections that MUST be present + fresh for the cobol-prompt happy-path.
# Missing file flags stale even on a fresh portfolio because every
# portfolio gets these computed.
_CORE_SECTIONS: frozenset[str] = frozenset({
    "holdings",
    "performance",
    "bonds",
    "analyst",
    "news",
    "whatchanged",
    "scenario",
    "synthesize",
})


def _reports_dir() -> Path:
    return Path(os.environ.get("IC_REPORTS_DIR", "/data/reports"))


def stale_sections(
    reports_dir: Path | None = None,
    max_age_hours: float = 24.0,
) -> dict[str, Any]:
    """Inspect ``/data/reports/`` and report per-section freshness.

    Returns:
        ``{
            "reports_dir": str,
            "max_age_hours": float,
            "now": float,
            "sections": {
                "<name>": {
                    "filename": str,
                    "exists": bool,
                    "mtime": float | None,
                    "age_hours": float | None,
                    "stale": bool,
                    "core": bool,
                },
                ...
            },
            "stale_names": [<name>, ...],
            "stale_count": int,
            "any_core_stale": bool,
        }``

    A section is "stale" if either:
      - the file exists and its mtime is older than ``max_age_hours``, OR
      - the section is in ``_CORE_SECTIONS`` and the file is missing.
    Optional sections with missing files are NOT flagged stale.
    """
    rdir = reports_dir or _reports_dir()
    now = time.time()
    cutoff = now - (max_age_hours * 3600)

    sections: dict[str, dict[str, Any]] = {}
    stale_names: list[str] = []
    any_core_stale = False

    for section_name, filename in _SECTION_FILES.items():
        path = rdir / filename
        is_core = section_name in _CORE_SECTIONS
        info: dict[str, Any] = {
            "filename": filename,
            "exists": False,
            "mtime": None,
            "age_hours": None,
            "stale": False,
            "core": is_core,
        }
        try:
            stat = path.stat()
            info["exists"] = True
            info["mtime"] = stat.st_mtime
            if stat.st_mtime > now:
                info["age_hours"] = 0.0
                info["stale"] = True
            else:
                info["age_hours"] = round((now - stat.st_mtime) / 3600, 2)
            if stat.st_size == 0:
                info["stale"] = True
            elif stat.st_mtime < cutoff:
                info["stale"] = True
        except FileNotFoundError:
            if is_core:
                info["stale"] = True
        except OSError:
            if is_core:
                info["stale"] = True

        if info["stale"]:
            stale_names.append(section_name)
            if is_core:
                any_core_stale = True
        sections[section_name] = info

    return {
        "reports_dir": str(rdir),
        "max_age_hours": max_age_hours,
        "now": now,
        "sections": sections,
        "stale_names": stale_names,
        "stale_count": len(stale_names),
        "any_core_stale": any_core_stale,
    }


def should_run_full_sweep(
    reports_dir: Path | None = None,
    max_age_hours: float = 24.0,
) -> tuple[bool, dict[str, Any]]:
    """Convenience: returns ``(should_sweep, freshness_report)``.

    The bridge's auto-init calls this to decide whether to fire
    ``_regenerate_sweep`` in background after the standard
    ``setup → refresh → seed_ask`` initialize. We sweep when ANY
    core section is stale; optional-section staleness alone doesn't
    trigger the sweep (the user can hit the dashboard regenerate
    button if they want them refreshed).
    """
    report = stale_sections(reports_dir=reports_dir, max_age_hours=max_age_hours)
    return report["any_core_stale"], report
