#!/usr/bin/env python3
"""
watcher_v4.py — Stock Bot GitHub Command Bridge
================================================
Pure Python rewrite of watcher_service.sh v3.

Key improvements over v3:
  - Commands checked EVERY cycle (not just on new commits)
  - 10-second poll interval (was 30s)
  - SHA cache in memory (saves 1 API call per result push)
  - Separate code-sync from command execution
  - No temp files for JSON handling
  - Works on any server path via config
  - Proper error recovery and logging
  - Never touches directories outside Stock_Bot/
"""

import os
import sys
import json
import time
import base64
import subprocess
import urllib.request
import urllib.error
from datetime import datetime, timezone

# ── Configuration ─────────────────────────────────────────────────────────────
PAT         = "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"
REPO        = "Jake-Culberson/Claud-Code"
API         = f"https://api.github.com/repos/{REPO}/contents"
WORK_DIR    = "/home/opc/trading"
VENV_PYTHON = "/home/opc/trading/venv/bin/python3.11"
BOT_SCRIPT  = "/home/opc/trading/StockTrading.py"
BOT_LOG     = "/home/opc/trading/trading.log"
BOT_ARGS    = ["--mode", "both", "--max-workers", "16"]
POLL_SECS   = 10       # command check interval
KEEPALIVE   = 999999   # Oracle: no bot to keep alive
VERSION     = "v4-oracle"

# ── Helpers ────────────────────────────────────────────────────────────────────
HEADERS = {
    "Authorization": f"Bearer {PAT}",
    "Content-Type":  "application/json",
    "User-Agent":    "StockBotWatcher/4.0",
}

def log(msg):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    print(line, flush=True)

def api_get(path):
    """GET a file from GitHub. Returns dict or {}."""
    try:
        req = urllib.request.Request(f"{API}/{path}", headers=HEADERS)
        with urllib.request.urlopen(req, timeout=15) as r:
            return json.loads(r.read())
    except Exception as e:
        log(f"API GET error ({path}): {e}")
        return {}

def api_put(path, content_bytes, msg, sha=""):
    """PUT a file to GitHub. Returns (ok, new_sha)."""
    payload = {
        "message": msg,
        "content": base64.b64encode(content_bytes).decode(),
    }
    if sha:
        payload["sha"] = sha
    data = json.dumps(payload).encode()
    try:
        req = urllib.request.Request(
            f"{API}/{path}", data=data, method="PUT", headers=HEADERS
        )
        with urllib.request.urlopen(req, timeout=20) as r:
            d = json.loads(r.read())
            new_sha = d.get("content", {}).get("sha", "")
            commit  = d.get("commit", {}).get("sha", "")[:10]
            return True, new_sha, commit
    except urllib.error.HTTPError as e:
        body = e.read().decode()[:200]
        log(f"API PUT error {e.code} ({path}): {body}")
        return False, sha, ""
    except Exception as e:
        log(f"API PUT error ({path}): {e}")
        return False, sha, ""

def git_fetch():
    """Fetch remote without modifying local files."""
    try:
        subprocess.run(
            ["git", "fetch", "origin", "main", "-q"],
            cwd=os.path.dirname(WORK_DIR),
            timeout=30, capture_output=True
        )
    except Exception as e:
        log(f"git fetch error: {e}")

def git_local_sha():
    try:
        r = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            cwd=os.path.dirname(WORK_DIR),
            capture_output=True, text=True, timeout=10
        )
        return r.stdout.strip()
    except:
        return ""

def git_remote_sha():
    try:
        r = subprocess.run(
            ["git", "rev-parse", "origin/main"],
            cwd=os.path.dirname(WORK_DIR),
            capture_output=True, text=True, timeout=10
        )
        return r.stdout.strip()
    except:
        return ""

def sync_code():
    """Pull updated code files from remote ref. Never commits."""
    repo_root = os.path.dirname(WORK_DIR)
    files_to_sync = [
        ("Stock_Bot/StockTrading.py",    os.path.join(WORK_DIR, "StockTrading.py")),
        ("Stock_Bot/watcher_v4.py",      os.path.join(WORK_DIR, "watcher_v4.py")),
        ("Stock_Bot/gen_dashboard.py",   os.path.join(WORK_DIR, "gen_dashboard.py")),
        ("Stock_Bot/watcher_helper.py",  os.path.join(WORK_DIR, "watcher_helper.py")),
    ]
    updated = []
    for remote_path, local_path in files_to_sync:
        try:
            result = subprocess.run(
                ["git", "show", f"origin/main:{remote_path}"],
                cwd=repo_root, capture_output=True, timeout=15
            )
            if result.returncode == 0 and result.stdout:
                with open(local_path, "wb") as f:
                    f.write(result.stdout)
                updated.append(os.path.basename(local_path))
        except Exception as e:
            log(f"  sync error for {remote_path}: {e}")

    # Advance local HEAD to remote (fast-forward only)
    try:
        subprocess.run(
            ["git", "merge", "--ff-only", "origin/main", "-q"],
            cwd=repo_root, capture_output=True, timeout=20
        )
    except:
        try:
            subprocess.run(
                ["git", "reset", "--hard", "origin/main", "-q"],
                cwd=repo_root, capture_output=True, timeout=20
            )
        except:
            pass

    if updated:
        log(f"Code synced: {', '.join(updated)}")

def ensure_dirs():
    """Make sure runtime directories exist."""
    dirs = [
        os.path.join(WORK_DIR, "logs", "debug"),
        os.path.join(WORK_DIR, "features", "daily"),
        os.path.join(WORK_DIR, "features", "weekly"),
        os.path.join(WORK_DIR, "data", "db"),
        os.path.join(WORK_DIR, "models"),
        os.path.join(WORK_DIR, "charts"),
        os.path.join(WORK_DIR, "backup", "logs"),
        os.path.join(WORK_DIR, "backup", "charts"),
        os.path.join(WORK_DIR, "backup", "saved_models"),
    ]
    for d in dirs:
        os.makedirs(d, exist_ok=True)

def bot_is_running():
    try:
        r = subprocess.run(
            ["pgrep", "-f", "StockTrading"],
            capture_output=True, timeout=5
        )
        return r.returncode == 0
    except:
        return False

def start_bot():
    log("Starting bot...")
    try:
        with open(BOT_LOG, "a") as log_f:
            proc = subprocess.Popen(
                [VENV_PYTHON, BOT_SCRIPT] + BOT_ARGS,
                cwd=WORK_DIR,
                stdout=log_f,
                stderr=log_f,
                start_new_session=True,
            )
        log(f"Bot started PID {proc.pid}")
        return proc.pid
    except Exception as e:
        log(f"Bot start failed: {e}")
        return None

def get_cmd():
    """
    Read cmd.json directly via API content endpoint.
    Returns (cmd_dict, sha) or (None, "").
    Faster than git show for command checking.
    """
    d = api_get("Stock_Bot/commands/cmd.json")
    if "content" not in d:
        return None, ""
    try:
        content = base64.b64decode(d["content"]).decode()
        return json.loads(content), d.get("sha", "")
    except Exception as e:
        log(f"cmd.json parse error: {e}")
        return None, ""

def push_result(cmd_id, exit_code, output, result_sha):
    """Push result.json to GitHub. Returns new SHA."""
    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    result = {
        "cmd_id":      cmd_id,
        "executed_at": now,
        "exit_code":   exit_code,
        "output":      output,
    }
    content = json.dumps(result, indent=2).encode()
    msg = f"vm: result for {cmd_id}"
    ok, new_sha, commit = api_put(
        "Stock_Bot/feedback/result.json", content, msg, result_sha
    )
    if ok:
        log(f"Result pushed ({commit})")
    else:
        log(f"Result push FAILED")
    return new_sha

def execute_cmd(cmd):
    """Run command payload, return (exit_code, output)."""
    payload = cmd.get("payload", "echo no_payload")
    cmd_id  = cmd.get("id", "unknown")
    log(f"Executing: {cmd_id}")

    # Write to temp script
    script_path = "/tmp/watcher_cmd.sh"
    with open(script_path, "w") as f:
        f.write("#!/bin/bash\n")
        f.write(payload)
    os.chmod(script_path, 0o755)

    try:
        result = subprocess.run(
            ["bash", script_path],
            capture_output=True, text=True,
            timeout=300,  # 5 min max per command
            cwd=WORK_DIR,
        )
        output    = (result.stdout + result.stderr)[:50000]  # cap at 50KB
        exit_code = result.returncode
    except subprocess.TimeoutExpired:
        output    = "ERROR: command timed out after 300 seconds"
        exit_code = 124
    except Exception as e:
        output    = f"ERROR: {e}"
        exit_code = 1

    log(f"  exit={exit_code}  output={len(output)} chars")
    return exit_code, output

# ── Main loop ──────────────────────────────────────────────────────────────────
def main():
    log(f"Watcher {VERSION} starting — poll={POLL_SECS}s workdir={WORK_DIR}")
    os.makedirs(WORK_DIR, exist_ok=True)
    ensure_dirs()

    last_keepalive  = 0
    last_cmd_id     = ""
    result_sha      = ""   # cached — avoids extra GET before every PUT
    last_local_sha  = ""

    # Pre-fetch result SHA once at startup
    d = api_get("Stock_Bot/feedback/result.json")
    result_sha = d.get("sha", "")
    log(f"Cached result.json SHA: {result_sha[:10] or 'none'}")

    while True:
        try:
            # ── 1. Fetch remote ──────────────────────────────────────────
            git_fetch()
            local_sha  = git_local_sha()
            remote_sha = git_remote_sha()

            # ── 2. Sync code if remote has new commits ───────────────────
            if local_sha != remote_sha and remote_sha:
                log(f"New commits detected — syncing code")
                sync_code()
                ensure_dirs()
                last_local_sha = git_local_sha()

            # ── 3. Check for commands EVERY cycle (not just on new commits)
            cmd, cmd_sha = get_cmd()
            if cmd:
                cmd_id   = cmd.get("id", "unknown")
                executed = cmd.get("executed", False)

                if not executed and cmd_id != last_cmd_id:
                    last_cmd_id = cmd_id
                    exit_code, output = execute_cmd(cmd)
                    result_sha = push_result(cmd_id, exit_code, output, result_sha)

            # ── 4. Keepalive — restart bot if down ──────────────────────
            now = time.time()
            if now - last_keepalive >= KEEPALIVE:
                last_keepalive = now
                if not bot_is_running():
                    log("KEEPALIVE: bot not running — starting")
                    start_bot()
                else:
                    log("KEEPALIVE: bot OK")

        except Exception as e:
            log(f"Main loop error: {e}")

        time.sleep(POLL_SECS)

if __name__ == "__main__":
    main()
