#!/usr/bin/env python3
"""
Gold Trading Agent - Production 24/7
OpenClaw spawns this as a persistent sub-agent.
Architecture:
  1. Fetch live price (Swissquote)
  2. Run strategy logic (SMA/EMA/RSI/MACD)
  3. Risk governor check
  4. Execute via MT5TradeServer EA (mt5_cmd.json)
  5. Log everything, maintain state across cycles
"""

import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional

# ─── Paths ────────────────────────────────────────────────────────────────────
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MT5_FILES = Path.home() / "Library/Application Support/net.metaquotes.wine.metatrader5/drive_c/Program Files/MetaTrader 5/MQL5/Files"
CMD_FILE = MT5_FILES / "mt5_cmd.json"
RSP_FILE = MT5_FILES / "mt5_rsp.json"
LOG_DIR = PROJECT_ROOT / "logs"
LOG_FILE = LOG_DIR / f"trading_agent.{datetime.now(timezone.utc).strftime('%Y%m%d')}.log"
STATE_FILE = PROJECT_ROOT / "data/bridge/.agent_state.json"

MT5_FILES.mkdir(parents=True, exist_ok=True)
LOG_DIR.mkdir(parents=True, exist_ok=True)


# ─── Logging ─────────────────────────────────────────────────────────────────
def log(level: str, msg: str, **kwargs) -> None:
    ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
    extra = " ".join(f"{k}={v}" for k, v in kwargs.items())
    line = f"[{ts}] [{level}] {msg} {extra}".strip()
    print(line)
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    with LOG_FILE.open("a", encoding="utf-8") as f:
        f.write(line + "\n")


# ─── State ────────────────────────────────────────────────────────────────────
def load_state() -> Dict[str, Any]:
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text(encoding="utf-8"))
        except Exception:
            pass
    return {
        "position_open": False, "position_ticket": 0,
        "position_side": None, "position_price": 0,
        "consecutive_losses": 0, "daily_loss": 0,
        "last_signal_id": None, "last_run": None,
        "consecutive_timeouts": 0, "total_cycles": 0,
        "kill_switch": False,
    }


def save_state(state: Dict) -> None:
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")


# ─── MT5 Communication ─────────────────────────────────────────────────────────
def _send_to_mt5(cmd: Dict[str, Any], timeout: float = 20.0) -> Optional[Dict]:
    """Send command to MT5TradeServer EA, read response."""
    if RSP_FILE.exists():
        try:
            RSP_FILE.unlink()
        except Exception:
            pass

    CMD_FILE.write_text(json.dumps(cmd, ensure_ascii=False), encoding="utf-8")
    start = time.time()
    while time.time() - start < timeout:
        if RSP_FILE.exists():
            try:
                return json.loads(RSP_FILE.read_text(encoding="utf-8"))
            except Exception:
                pass
        time.sleep(0.5)
    return None


def mt5_ping() -> bool:
    r = _send_to_mt5({"action": "ping"}, timeout=5)
    return r is not None and r.get("success")


def mt5_get_positions() -> list:
    r = _send_to_mt5({"action": "positions"}, timeout=10)
    if r and r.get("success"):
        return r.get("positions", [])
    return []


def mt5_get_account() -> Dict:
    r = _send_to_mt5({"action": "account"}, timeout=10)
    return r or {}


def mt5_order(side: str, volume: float = 0.01, sl: float = 0, tp: float = 0) -> Dict:
    """Place market order via EA."""
    r = _send_to_mt5({
        "action": side.lower(),
        "symbol": "XAUUSD",
        "volume": volume,
        "sl": sl,
        "tp": tp,
    }, timeout=25)
    return r or {"success": False, "message": "MT5_TIMEOUT"}


def mt5_close(ticket: int) -> Dict:
    r = _send_to_mt5({"action": "close", "ticket": ticket}, timeout=25)
    return r or {"success": False, "message": "MT5_TIMEOUT"}


# ─── Live Price ───────────────────────────────────────────────────────────────
def get_live_price() -> Optional[Dict]:
    try:
        import urllib.request
        url = "https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD"
        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
        with urllib.request.urlopen(req, timeout=8) as resp:
            data = json.loads(resp.read())
        entry = data[0]["spreadProfilePrices"][0]
        bid, ask = float(entry["bid"]), float(entry["ask"])
        return {
            "bid": bid, "ask": ask,
            "mid": (bid + ask) / 2,
            "spread": round((ask - bid) * 100, 1),
            "success": True,
        }
    except Exception as e:
        log("WARN", f"Price fetch failed: {e}")
        return None


# ─── Strategy ─────────────────────────────────────────────────────────────────
def compute_indicators(price_mid: float) -> Dict[str, Any]:
    """
    Simplified indicator computation.
    In production, load historical bars from MT5.
    Here using price-based heuristics.
    """
    # RSI estimate (simplified - use mid price variations)
    rsi = 50.0  # neutral
    macd_signal = 0  # neutral
    ema_fast = price_mid
    ema_slow = price_mid

    return {
        "ema_fast": ema_fast,
        "ema_slow": ema_slow,
        "rsi": rsi,
        "macd": macd_signal,
        "regime": "TREND",  # default to trend
        "confidence": 0.50,
    }


def generate_signal(
    price: Dict,
    state: Dict,
    indicators: Dict,
) -> Dict[str, Any]:
    """
    Decision logic:
      - If no position and regime=TREND and confidence>=0.68 → OPEN BUY
      - If position open and (regime changes OR RSI overbought OR TP hit) → CLOSE
      - Otherwise → HOLD
    """
    mid = price["mid"]
    ind = indicators
    sig_id = f"sig_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}"

    # ─── Exit logic ──
    if state["position_open"]:
        should_close = False
        reason = ""

        # Check if RSI overbought (>70) or oversold (<30)
        if ind["rsi"] > 70:
            should_close = True
            reason = "RSI_overbought"
        elif ind["rsi"] < 30:
            should_close = True
            reason = "RSI_oversold"

        # Check if TP roughly hit (±0.5%)
        if state["position_side"] == "BUY" and mid >= state.get("position_tp", 0) * 0.995:
            should_close = True
            reason = "TP_hit"
        elif state["position_side"] == "SELL" and mid <= state.get("position_sl", 0) * 1.005:
            should_close = True
            reason = "SL_hit"

        if should_close:
            return {
                "signal_id": sig_id,
                "action": "CLOSE",
                "side": state["position_side"],
                "reason": reason,
                "volume": 0.01,
            }

        return {"signal_id": sig_id, "action": "HOLD", "reason": "position_held"}

    # ─── Entry logic ──
    if ind["confidence"] >= 0.68 and ind["regime"] == "TREND":
        sl_dist = mid * 0.015  # 1.5% stop loss
        sl = round(mid - sl_dist, 3)
        tp = round(mid + sl_dist * 1.5, 3)  # 1.5:1 RR
        return {
            "signal_id": sig_id,
            "action": "OPEN",
            "side": "BUY",
            "volume": 0.01,
            "sl_price": sl,
            "tp_price": tp,
            "entry_price": round(mid, 3),
            "confidence": ind["confidence"],
            "reason": "TREND_confirmed",
        }

    return {"signal_id": sig_id, "action": "HOLD", "reason": f"confidence={ind['confidence']:.2f}"}


# ─── Risk Governor ────────────────────────────────────────────────────────────
def governor_check(signal: Dict, state: Dict, price: Dict) -> Dict[str, Any]:
    """
    Hard rules that cannot be bypassed.
    """
    account = mt5_get_account()
    equity = account.get("equity", 0) or account.get("balance", 10000)

    # Rule 1: Kill switch
    if state.get("kill_switch"):
        return {"approved": False, "reason": "KILL_SWITCH"}

    # Rule 2: Consecutive losses ≥ 4 → cooldown
    if state["consecutive_losses"] >= 4:
        return {"approved": False, "reason": "COOLDOWN_consecutive_losses"}

    # Rule 3: Daily loss > 2.5%
    if state["daily_loss"] > 2.5:
        return {"approved": False, "reason": "CIRCUIT_BREAKER_daily_loss"}

    # Rule 4: Data quality - spread too wide (>2 USD = 200 points)
    if price.get("spread", 0) > 2.0:
        return {"approved": False, "reason": "LOW_LIQUIDITY_spread"}

    # Rule 5: Confidence < 0.68 → hold
    if signal.get("action") == "OPEN" and signal.get("confidence", 1) < 0.68:
        return {"approved": False, "reason": "LOW_CONFIDENCE"}

    # Rule 6: No SL for new orders
    if signal.get("action") == "OPEN" and signal.get("sl_price", 0) <= 0:
        return {"approved": False, "reason": "NO_STOP_LOSS"}

    # Rule 7: Max 1 position
    positions = mt5_get_positions()
    if signal.get("action") == "OPEN" and len(positions) > 0:
        return {"approved": False, "reason": "POSITION_ALREADY_OPEN"}

    return {"approved": True}


# ─── Execution ────────────────────────────────────────────────────────────────
def execute_signal(signal: Dict) -> Dict[str, Any]:
    action = signal.get("action")
    log("INFO", f"Executing: {action} {signal.get('side','')}", reason=signal.get("reason",""))

    if action == "HOLD":
        return {"status": "RECONCILED", "reason": signal.get("reason", "HOLD")}

    if action == "CLOSE":
        result = mt5_close(signal.get("position_ticket", 0))
        if result.get("success"):
            return {"status": "FILLED", "reason": f"CLOSE {signal.get('reason','')}", "deal_id": result.get("deal_id", 0)}
        return {"status": "REJECTED", "reason": result.get("message", "CLOSE_FAILED")}

    if action == "OPEN":
        result = mt5_order(
            side=signal["side"],
            volume=signal.get("volume", 0.01),
            sl=signal.get("sl_price", 0),
            tp=signal.get("tp_price", 0),
        )
        if result.get("success"):
            return {
                "status": "FILLED",
                "reason": f"OPEN {signal['side']} {signal.get('reason','')}",
                "order_id": result.get("order_id", 0),
                "deal_id": result.get("deal_id", 0),
            }
        return {"status": "REJECTED", "reason": result.get("message", "ORDER_FAILED")}

    return {"status": "REJECTED", "reason": f"UNKNOWN_ACTION: {action}"}


# ─── Main Loop ───────────────────────────────────────────────────────────────
def run_cycle() -> Dict[str, Any]:
    """One trading cycle. Returns summary dict."""
    state = load_state()
    state["total_cycles"] += 1
    state["last_run"] = datetime.now(timezone.utc).isoformat()
    cycle = state["total_cycles"]

    result = {
        "cycle": cycle,
        "timestamp": state["last_run"],
        "position_open": state["position_open"],
        "price_fetch": False,
        "signal": None,
        "governor": None,
        "execution": None,
        "state_updated": False,
    }

    # ── 1. Price ──
    price = get_live_price()
    if not price or not price.get("success"):
        log("WARN", "Cycle skipped: no price data")
        save_state(state)
        return result

    result["price_fetch"] = True
    result["bid"] = price["bid"]
    result["ask"] = price["ask"]
    result["spread"] = price.get("spread", 0)

    # ── 2. Strategy ──
    indicators = compute_indicators(price["mid"])
    signal = generate_signal(price, state, indicators)
    result["signal"] = signal
    log("INFO", f"Signal: {signal['action']} {signal.get('side','')} {signal.get('reason','')}",
        confidence=signal.get("confidence",""))

    # ── 3. Governor ──
    gov = governor_check(signal, state, price)
    result["governor"] = gov

    if not gov["approved"]:
        log("INFO", f"Governor REJECTED: {gov['reason']}", action=signal["action"])
        save_state(state)
        return result

    # ── 4. Execute ──
    exec_result = execute_signal(signal)
    result["execution"] = exec_result
    log("INFO", f"Execution: {exec_result['status']}", reason=exec_result.get("reason",""))

    # ── 5. Update state ──
    if exec_result["status"] == "FILLED":
        if signal["action"] == "OPEN":
            state["position_open"] = True
            state["position_side"] = signal["side"]
            state["position_price"] = price["mid"]
            state["position_ticket"] = exec_result.get("deal_id", 0)
            state["position_sl"] = signal.get("sl_price", 0)
            state["position_tp"] = signal.get("tp_price", 0)
            state["consecutive_timeouts"] = 0
            log("INFO", f"Position OPENED: {signal['side']} @{price['mid']:.3f}",
                SL=signal.get("sl_price"), TP=signal.get("tp_price"))
        elif signal["action"] == "CLOSE":
            pnl = exec_result.get("deal_id", 0)  # simplified
            state["position_open"] = False
            state["position_ticket"] = 0
            state["consecutive_timeouts"] = 0
            if signal.get("reason") in ("RSI_overbought", "TP_hit"):
                state["consecutive_losses"] = 0
            else:
                state["consecutive_losses"] += 1
            log("INFO", f"Position CLOSED: {signal['reason']}")
    elif exec_result["status"] in ("REJECTED", "TIMEOUT"):
        state["consecutive_timeouts"] += 1
        if state["consecutive_timeouts"] >= 3:
            log("WARN", "3 consecutive timeouts - possible MT5 issue")

    state["last_signal_id"] = signal.get("signal_id")
    result["state_updated"] = True
    save_state(state)
    return result


# ─── Entry Point ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--cycles", type=int, default=1, help="Number of cycles to run (0=infinite)")
    parser.add_argument("--interval", type=int, default=30, help="Seconds between cycles")
    args = parser.parse_args()

    log("INFO", "Gold Trading Agent starting", cycles=args.cycles, interval=args.interval)

    # Verify MT5 is alive
    if not mt5_ping():
        log("WARN", "MT5 not responding on startup - will retry each cycle")
    else:
        log("INFO", "MT5 connection verified")

    cycles = 0
    while True:
        cycles += 1
        log("INFO", f"=== Cycle {cycles} ===")

        try:
            r = run_cycle()
            log("INFO", f"Cycle {cycles} done",
                price=r["price_fetch"],
                signal=r["signal"].get("action") if r["signal"] else "NONE",
                exec=r["execution"].get("status") if r["execution"] else "NONE")
        except Exception as e:
            log("ERROR", f"Cycle failed: {e}", exc=str(e)[:200])

        if args.cycles > 0 and cycles >= args.cycles:
            log("INFO", f"Done. Ran {cycles} cycles.")
            break

        log("INFO", f"Sleeping {args.interval}s...")
        time.sleep(args.interval)
