#!/usr/bin/env python3
"""Continuous Hermes one-shot worker for XAUUSD hybrid M5 OHLCV collection only.

The parent process never reads or prints raw OHLCV. Each child run must save raw bars
straight into hybrid/data/ohlcv_M5.json and return only a compact statistical/progress
summary, so the parent/current chat only receives summaries.
"""
from __future__ import annotations

import json
import os
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path("/home/aryy/.hermes/profiles/finance/backtests/xauusd_2026")
STATE = ROOT / "hybrid/collector_state.json"
REPORT_DIR = ROOT / "hybrid/reports"
REPORT_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = REPORT_DIR / "m5_collector.log"
LOCK = ROOT / "hybrid/m5_collector.lock"

PROMPT = r"""
You are Aryy Finance M5 data collector. Process exactly ONE pending XAUUSD M5 OHLCV chunk for the hybrid backtest, save raw data to file, summarize via Python/statistics only, then stop.

Project directory: /home/aryy/.hermes/profiles/finance/backtests/xauusd_2026
Collector state: hybrid/collector_state.json
Scope: M5 ONLY. Do not collect M15/D/H4/M30/H1/W. Do not run scanner/backtest. Do not write trades/candidates/reviews.

Critical privacy/context rule from Aryy:
- Do NOT include raw OHLCV bars in the final answer.
- Save raw bars to hybrid/data/ohlcv_M5.json.
- Send back only summary statistics/progress: chunk id, range, kept bar count, output total bars, first/last output time, remaining M5 chunks, and any concise error.

Process:
1. Read hybrid/collector_state.json.
2. Find the first chunk where suffix='M5' and (status='pending' OR status='failed' with attempts < 3).
3. If no such M5 chunk exists, update collector_state.status_m5='complete' and final response 'M5 COMPLETE' with output total bars only.
4. Use TradingView MCP only:
   - chart_set_symbol OANDA:XAUUSD
   - chart_set_timeframe chunk.timeframe
   - chart_set_visible_range from=chunk.from to=chunk.to
   - data_get_ohlcv count=500 summary=false
5. Keep only bars with numeric time inside chunk.from..chunk.to plus one M5 tolerance (300 sec). Discard unrelated bars.
6. Merge kept bars into hybrid/data/ohlcv_M5.json. Preserve existing bars, dedupe by numeric time, sort ascending.
   Schema: {"metadata":{"symbol":"XAUUSD","timeframe":"M5","source":"tradingview_mcp_visible_range_chunks","start":"2026-01-01","end":"2026-07-17","bar_count":N,"first_time":...,"last_time":...,"last_updated":"UTC ISO"},"bars":[{"time":int,"timestamp":"UTC ISO","open":float,"high":float,"low":float,"close":float,"volume":number|null}]}
7. Update the chunk: attempts += 1, bars_collected = kept bar count, status='done' if kept bars > 0 else 'failed', last_error=null or concise reason.
8. Write/overwrite hybrid/reports/m5_processing_summary.json with statistical summary only: chunk counts, output bar count, first/last time, average/median range/body for saved M5 bars. Do this by Python script/terminal or equivalent, not by pasting raw bars.
9. Validate JSON for collector_state, ohlcv_M5.json (if exists), and m5_processing_summary.json with python3 -m json.tool.
10. Final response exactly one compact line, no raw bars.

Hard rules:
- Exactly ONE M5 chunk per child run.
- Do not use execute_code.
- Do not use screenshots/vision.
- Do not analyze strategy.
- Do not write data/journal.json, backtest-data, shadow-trades, candidates, or reviews.
- If MCP returns no in-range bars, mark only that chunk failed and stop; do not loop.
""".strip()


def log(msg: str) -> None:
    line = f"{datetime.now(timezone.utc).isoformat()} {msg}"
    print(line, flush=True)
    with LOG_FILE.open("a", encoding="utf-8") as f:
        f.write(line + "\n")


def load_state() -> dict:
    return json.loads(STATE.read_text())


def m5_counts(state: dict) -> dict[str, int]:
    out: dict[str, int] = {}
    for c in state.get("chunks", []):
        if c.get("suffix") != "M5":
            continue
        status = c.get("status", "unknown")
        out[status] = out.get(status, 0) + 1
    return out


def m5_complete(state: dict) -> bool:
    m5 = [c for c in state.get("chunks", []) if c.get("suffix") == "M5"]
    return bool(m5) and all(c.get("status") == "done" for c in m5)


def main() -> int:
    os.chdir(ROOT)
    if LOCK.exists():
        try:
            payload = json.loads(LOCK.read_text() or "{}")
        except Exception:
            payload = {}
        log(f"LOCK_EXISTS {LOCK} {payload}; exiting to avoid duplicate M5 worker")
        return 2
    LOCK.write_text(json.dumps({"started_at": datetime.now(timezone.utc).isoformat(), "pid": os.getpid()}, indent=2) + "\n")
    try:
        iteration = 0
        while True:
            state = load_state()
            cts = m5_counts(state)
            log(f"M5_STATE {cts}")
            if m5_complete(state):
                log("M5_COMPLETE all M5 chunks done")
                state["status_m5"] = "complete"
                state["m5_completed_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
                STATE.write_text(json.dumps(state, indent=2) + "\n")
                return 0
            iteration += 1
            log(f"RUN_M5_CHUNK iteration={iteration}")
            env = os.environ.copy()
            env["HERMES_PROFILE"] = "finance"
            cmd = [
                "hermes",
                "--provider", "custom:9router",
                "--model", "ag/gemini-pro-agent",
                "-z", PROMPT,
            ]
            proc = subprocess.run(cmd, cwd=str(ROOT), env=env, text=True, capture_output=True, timeout=900)
            log(f"HERMES_EXIT code={proc.returncode}")
            if proc.stdout.strip():
                # Child is instructed to output summary only; still truncate defensively.
                log("STDOUT " + proc.stdout.strip().replace("\n", " | ")[-2000:])
            if proc.stderr.strip():
                log("STDERR " + proc.stderr.strip().replace("\n", " | ")[-2000:])
            if proc.returncode != 0:
                log("STOP nonzero hermes exit; leaving state for inspection")
                return proc.returncode
            time.sleep(2)
    finally:
        try:
            LOCK.unlink(missing_ok=True)
        except Exception as exc:
            log(f"LOCK_REMOVE_ERROR {exc}")


if __name__ == "__main__":
    raise SystemExit(main())
