#!/usr/bin/env python3
"""
patch_forecast_page.py — wires the Daily Forecasts page into the dashboard.

Save to the project root and run from there:
    cd C:\\Users\\Administrator\\OneDrive\\fx_macro_intraday
    python patch_forecast_page.py

Changes:
  dashboard_server.py
    1. Protects /forecast.html + /forecast_data.json behind the same
       token check as the dashboard.
    2. Adds a forecast rebuild that fires at the daily rollover
       (17:00 New York = broker midnight), running build_forecast.py as
       a subprocess — so the forecast is live before Asia opens.
       Retries while Gold/NQ opens are pending (they print 18:00 NY)
       and backs off 15 min after failures.
       Logs to data\\logs\\forecast_build.log.
    3. Calls it from the existing 30-second update loop.
  dashboard.html
    4. Adds the Pages dropdown next to the Theme button
       (Dashboard / Daily Forecasts / Call-Put Walls placeholder),
       carrying ?key= through via location.search.

If the earlier 06:00-London version of this patch was already applied,
this patcher refuses and tells you to restore the .bak_forecast_ backup
first — it will not stack the two.

Safety: backups (.bak_forecast_<timestamp>), exact-match verify,
syntax check on the server file, re-runnable (skips applied changes).
"""

import py_compile
import shutil
import sys
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parent
SERVER = ROOT / "src" / "execution" / "dashboard_server.py"
DASH_HTML = ROOT / "dashboard.html"

SERVER_BLOCK = r'''# ── Daily vol & range forecast rebuild (Daily Forecasts page) ────────────
FORECAST_SCRIPT = BASE_PATH / "src" / "execution" / "build_forecast.py"
FORECAST_JSON   = BASE_PATH / "forecast_data.json"
FORECAST_LOG    = BASE_PATH / "data" / "logs" / "forecast_build.log"
_forecast_lock = threading.Lock()
_forecast_last_attempt = [0.0]


def _session_now():
    """FX session clock. Sessions roll at 17:00 New York (= broker
    midnight): the returned datetime's .date() is the session date —
    after Tue 5pm NY it reads Wednesday, and Sunday's rollover opens
    Monday's session. UTC-4 fallback if tzdata isn't installed."""
    from datetime import timezone, timedelta
    try:
        from zoneinfo import ZoneInfo
        ny = datetime.now(ZoneInfo("America/New_York"))
    except Exception:
        ny = datetime.now(timezone.utc) - timedelta(hours=4)
    return ny + timedelta(hours=7)


def _forecast_stale():
    """True when a new session's forecast should be built."""
    if not FORECAST_SCRIPT.exists():
        return False
    s = _session_now()
    if s.weekday() >= 5:                     # Sat/Sun sessions: none
        return False
    if s.hour == 0 and s.minute < 10:        # let the new daily bar print
        return False
    if not FORECAST_JSON.exists():
        return True
    try:
        with open(FORECAST_JSON, encoding="utf-8") as f:
            data = json.load(f)
    except Exception:
        return True
    if data.get("session_date", "") != s.strftime("%Y-%m-%d"):
        return True
    # Gold/NQ opens print 18:00 NY — retry until every anchor is real,
    # but only within the session's first two hours.
    if data.get("anchor_pending") and s.hour < 2:
        return True
    return False


def maybe_refresh_forecast():
    """Non-blocking: spawns build_forecast.py when a new session starts.
    At most one attempt per 15 min (covers failures and pending anchors)."""
    if time.time() - _forecast_last_attempt[0] < 900:
        return
    if not _forecast_stale():
        return
    if not _forecast_lock.acquire(blocking=False):
        return
    _forecast_last_attempt[0] = time.time()

    def _run():
        try:
            print(f"  [forecast] building for session {_session_now():%Y-%m-%d} ...")
            with open(FORECAST_LOG, "a", encoding="utf-8") as lf:
                lf.write(f"\n=== build {datetime.now()} ===\n")
                lf.flush()
                r = subprocess.run(
                    [sys.executable, str(FORECAST_SCRIPT)],
                    cwd=str(BASE_PATH), stdout=lf,
                    stderr=subprocess.STDOUT, timeout=300,
                )
            print(f"  [forecast] build finished (exit {r.returncode})")
        except Exception as e:
            print(f"  [forecast] build failed: {e}")
        finally:
            _forecast_lock.release()

    threading.Thread(target=_run, daemon=True).start()


'''

NAV_SELECT = '''    <button class="theme-btn" onclick="toggleTheme()">◐ Theme</button>
    <select class="theme-btn" style="cursor:pointer" onchange="if(this.value)location.href=this.value+location.search">
      <option value="./dashboard.html" selected>▤ Dashboard</option>
      <option value="./forecast.html">▤ Daily Forecasts</option>
      <option value="" disabled>▤ Call/Put Walls (soon)</option>
    </select>'''

PATCHES = [
    {
        "path": SERVER,
        "label": "protect /forecast.html + /forecast_data.json",
        "old": '"/dashboard_equity.json")',
        "new": '"/dashboard_equity.json",\n'
               '                           "/forecast.html", "/forecast_data.json")',
        "marker": '"/forecast.html", "/forecast_data.json")',
        "anchor": "_PROTECTED_PREFIXES",
        "compile": True,
    },
    {
        "path": SERVER,
        "label": "forecast rebuild machinery (rollover, 17:00 New York)",
        "old": 'def main():\n    print("=" * 60)',
        "new": SERVER_BLOCK + 'def main():\n    print("=" * 60)',
        "marker": "def _session_now",
        "conflict": "def _london_now",
        "conflict_msg": "old 06:00-London forecast block found — restore "
                        "dashboard_server.py from its .bak_forecast_ backup, "
                        "then rerun this patcher.",
        "anchor": "def main",
        "compile": True,
    },
    {
        "path": SERVER,
        "label": "call maybe_refresh_forecast() in the update loop",
        "old": '            maybe_refresh_live_trades()\n'
               '            time.sleep(30)',
        "new": '            maybe_refresh_live_trades()\n'
               '            maybe_refresh_forecast()\n'
               '            time.sleep(30)',
        "marker": "            maybe_refresh_forecast()",
        "anchor": "maybe_refresh_live_trades",
        "compile": True,
    },
    {
        "path": DASH_HTML,
        "label": "Pages dropdown next to the Theme button",
        "old": '    <button class="theme-btn" onclick="toggleTheme()">◐ Theme</button>',
        "new": NAV_SELECT,
        "marker": '<option value="./forecast.html">',
        "anchor": "theme-btn",
        "compile": False,
    },
]


def read_raw(path):
    with open(path, "r", encoding="utf-8", newline="") as f:
        return f.read()


def write_raw(path, text):
    with open(path, "w", encoding="utf-8", newline="") as f:
        f.write(text)


def dump_region(text, anchor):
    i = text.find(anchor)
    if i == -1:
        print(f"    (anchor {anchor!r} not found either — file layout differs)")
        return
    start = max(0, i - 80)
    end = min(len(text), i + 320)
    print("    Exact bytes around anchor (repr):")
    print("    " + repr(text[start:end]))


def main():
    print("=" * 62)
    print("DAILY FORECASTS PAGE PATCHER  (rollover edition)")
    print(f"  Root: {ROOT}")
    print("=" * 62)

    for p, where in [
        (ROOT / "src" / "execution" / "build_forecast.py", "src\\execution\\"),
        (ROOT / "forecast.html", "project root"),
    ]:
        if not p.exists():
            print(f"  NOTE: {p.name} not found yet — save it to {where} "
                  f"before restarting the server.")

    ts = time.strftime("%Y%m%d_%H%M%S")
    failures = 0
    touched = []
    backed_up = set()

    for spec in PATCHES:
        path = spec["path"]
        print(f"\n[{path.name}] {spec['label']}")

        if not path.exists():
            print(f"  FAIL: file not found at {path}")
            failures += 1
            continue

        text = read_raw(path)
        nl = "\r\n" if "\r\n" in text else "\n"
        old = spec["old"].replace("\r\n", "\n").replace("\n", nl)
        new = spec["new"].replace("\r\n", "\n").replace("\n", nl)

        if spec["marker"] in text:
            print("  OK: already patched — skipping.")
            continue

        if spec.get("conflict") and spec["conflict"] in text:
            print(f"  FAIL: {spec['conflict_msg']}")
            failures += 1
            continue

        n = text.count(old)
        if n != 1:
            print(f"  FAIL: expected exactly 1 match, found {n}.")
            dump_region(text, spec["anchor"])
            failures += 1
            continue

        if path not in backed_up:
            backup = path.with_name(path.name + f".bak_forecast_{ts}")
            shutil.copy2(path, backup)
            backed_up.add(path)
            print(f"  Backup: {backup.name}")

        write_raw(path, text.replace(old, new, 1))

        check = read_raw(path)
        if spec["marker"] not in check:
            print("  FAIL: post-write verification failed — check backup.")
            failures += 1
            continue

        print("  Patched OK.")
        if spec.get("compile") and path not in touched:
            touched.append(path)

    for path in touched:
        try:
            py_compile.compile(str(path), doraise=True)
            print(f"\n[{path.name}] Syntax check OK.")
        except Exception as e:
            print(f"\n[{path.name}] SYNTAX CHECK FAILED: {e}")
            print("  Restore from the .bak_forecast file and send me this output.")
            failures += 1

    print("\n" + "=" * 62)
    if failures:
        print(f"DONE WITH {failures} FAILURE(S) — send me the full output above.")
        sys.exit(1)
    print("ALL PATCHES APPLIED.")
    print("Next steps:")
    print("  1. Make sure build_forecast.py is in src\\execution\\ and")
    print("     forecast.html is in the project root (next to dashboard.html).")
    print("  2. Test the builder once:  python src\\execution\\build_forecast.py")
    print("  3. Restart the dashboard server (Ctrl+C, then):")
    print("       python src\\execution\\dashboard_server.py")
    print("  4. Phone: use the Pages dropdown, or go direct to")
    print("       https://bennettsdashboard.com/forecast.html?key=...")
    print("=" * 62)


if __name__ == "__main__":
    main()
