#!/usr/bin/env python3
"""Adds the loss-control guard to YOUR Sailar-x folder without overwriting your own changes (like your Serper work).

  python apply_guard_patch.py            (run it inside your Sailar-x folder, the one that contains run.py)

- Backs up every file it changes into  backup_before_guard/<time>/
- Changes only small, known spots. If a spot has been edited by you and cannot be found, that edit is SKIPPED and reported.
- Safe to run twice: edits that are already in place are left alone.
"""
import shutil
import sys
import time
from pathlib import Path

ROOT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path(__file__).resolve().parent

MT5_HELPERS = '''    def _pick(self, ours, ticket):
        """The exact position to close. Never 'the first one in the list'."""
        if ticket:
            for q in ours:
                if str(q.ticket) == str(ticket):
                    return q
        if len(ours) == 1:
            return ours[0]
        raise OrderRejected(f"position {ticket} not found and {len(ours)} Sailar-x positions are open: refusing to guess which to close")

    def _resolve(self, symbol, ticket):
        t = int(ticket)
        if self.m.positions_get(ticket=t):
            return t
        ours = self._ours(symbol) or []
        if len(ours) == 1:
            return ours[0].ticket
        raise OrderRejected(f"position {t} not found and {len(ours)} Sailar-x positions are open: refusing to guess which to protect")

    def list_positions(self, symbol):
        pos = self.m.positions_get(symbol=symbol)
        if pos is None:
            return None
        return [{"ticket": q.ticket, "volume": q.volume, "magic": q.magic, "ours": q.magic == self.magic,
                 "side": "BUY" if q.type == self.m.POSITION_TYPE_BUY else "SELL", "price": q.price_open, "sl": q.sl, "tp": q.tp,
                 "profit": float(q.profit), "time": int(getattr(q, "time", 0))} for q in pos]

    def close_position(self, symbol, ticket):
        q = (self.m.positions_get(ticket=int(ticket)) or [None])[0]
        if q is None:
            raise OrderRejected(f"position {ticket} not found")
        info, tick = self._info(q.symbol), self._tick(q.symbol)
        buy = q.type == self.m.POSITION_TYPE_SELL
        r = self._send({"action": self.m.TRADE_ACTION_DEAL, "symbol": q.symbol, "deviation": 20, "magic": self.magic,
                        "comment": "sailar-x extra", "type_time": self.m.ORDER_TIME_GTC, "type_filling": self._filling(info),
                        "position": q.ticket, "volume": q.volume, "type": self.m.ORDER_TYPE_BUY if buy else self.m.ORDER_TYPE_SELL,
                        "price": tick.ask if buy else tick.bid})
        deals = self.m.history_deals_get(position=int(q.ticket)) or []
        pnl = sum(d.profit + d.commission + d.swap + getattr(d, "fee", 0.0) for d in deals)
        return {"price": float(r.price), "pnl": float(pnl), "volume": q.volume}

'''

EDITS = {
    "sailar/engine.py": [
        ("import the guard",
         "from .brain.context import build_context\n",
         "from .brain.context import build_context\nfrom .guard import Guard\n"),
        ("create the guard",
         "        self.brain = None            # set by the runner: the AI second-opinion analyst\n",
         "        self.brain = None            # set by the runner: the AI second-opinion analyst\n"
         "        self.guard = Guard(cfg, params)   # loss control: spread, losing-streak pause, daily cap, cost-aware break-even\n"),
        ("check before every entry",
         '        dist = sig.atr * self.p["atr_stop_mult"]\n        side = "buy" if sig.side == "BUY" else "sell"\n',
         '        dist = sig.atr * self.p["atr_stop_mult"]\n        side = "buy" if sig.side == "BUY" else "sell"\n'
         "        ok, why = self.guard.entry_check(self, sig, dist, now)\n"
         "        if not ok:\n"
         '            msgs.append(f"{sig.side} signal skipped by the guard: {why}")\n'
         "            return msgs\n"),
        ("break-even that covers the spread",
         '            new_sl = t["entry"]\n',
         '            new_sl = self.guard.be_level(self, t)   # entry + a cushion for the spread\n'),
        ("label trades closed outside Sailar-x",
         '                msgs.append(self._record_close(t, info["price"], info["reason"], now, info["pnl"], info["fees"]))\n',
         '                why_closed = "EXTERNAL" if info["reason"] == "CLOSED" else info["reason"]\n'
         '                if why_closed == "EXTERNAL":\n'
         '                    self.j.event("WARN", "a trade was closed OUTSIDE Sailar-x (by hand in MT5 or by another program)", now)\n'
         '                msgs.append(self._record_close(t, info["price"], why_closed, now, info["pnl"], info["fees"]))\n'),
    ],
    "sailar/engine.py#2": [
        ("one trade at a time: extras are dealt with",
         "        if t is not None:\n            # ── Break-even trailing",
         "        self.guard.one_position(self, now, msgs)      # ONE trade at a time: anything extra is dealt with\n"
         "        t = self.j.current_trade(self.v)\n"
         "        if t is not None:\n            # ── Break-even trailing"),
        ("close the exact trade, not 'the first one in the list'",
         '        fill = self.b.market_order(self.symbol, side, t["qty"], reduce_only=True)\n',
         '        kw = {"ticket": t.get("ticket")} if getattr(self.b, "server_side_stops", False) else {}\n'
         '        fill = self.b.market_order(self.symbol, side, t["qty"], reduce_only=True, **kw)\n'),
    ],
    "sailar/engine.py#3": [
        ("AI watch: protective powers over an open trade",
         '                    msgs.append(f"trend-flip close failed, will retry: {e}")\n                self.last_bar = df.index[-1]\n                return msgs\n',
         '                    msgs.append(f"trend-flip close failed, will retry: {e}")\n                self.last_bar = df.index[-1]\n                return msgs\n'
         "            if self.guard.watch(self, t, df, price, now, msgs):      # the AI may tighten the stop or exit (protective powers only)\n"
         "                self.last_bar = df.index[-1]\n"
         "                return msgs\n"
         "            t = self.j.current_trade(self.v)                         # the stop may have been tightened\n"),
    ],
    "sailar/brain/context.py": [
        ("crypto: tell the AI what kind of instrument this is",
         "import pandas as pd\n",
         "import pandas as pd\nfrom ..guard import is_forex\n"),
        ("crypto: instrument type",
         '        "symbol": sym, "timeframe": eng.cfg["primary_timeframe"], "utc_time": str(now), "weekday": now.day_name(),\n',
         '        "symbol": sym, "timeframe": eng.cfg["primary_timeframe"], "utc_time": str(now), "weekday": now.day_name(),\n'
         '        "instrument_type": "forex" if is_forex(sym) else "crypto/other (ignore pips, use the pct fields)",\n'),
        ("crypto: stop size as % of price",
         '"stop_pips": pp(dist), "target_pips": pp(dist * eng.p["rr"]),',
         '"stop_pips": pp(dist), "target_pips": pp(dist * eng.p["rr"]), "stop_pct_of_price": _f(dist / sig.price * 100, 3),'),
        ("crypto: spread and ATR as % of price",
         '"market": {"spread_pips": _f(spread, 2), "atr_pips": pp(sig.atr)},',
         '"market": {"spread_pips": _f(spread, 2), "atr_pips": pp(sig.atr), "atr_pct_of_price": _f(sig.atr / sig.price * 100, 3),\n'
         '                   "spread_pct_of_price": _f(spread * pip / sig.price * 100, 4) if spread is not None and pip else None},'),
    ],
    "sailar/broker.py": [
        ("MT5: close by position number",
         "    def market_order(self, symbol, side, qty, reduce_only=False):\n        info = self._info(symbol)",
         "    def market_order(self, symbol, side, qty, reduce_only=False, ticket=None):\n        info = self._info(symbol)"),
        ("MT5: pick the right position to close",
         '                raise OrderRejected("no open position to close")\n            p = ours[0]\n',
         '                raise OrderRejected("no open position to close")\n            p = self._pick(ours, ticket)\n'),
        ("MT5: protect the right position",
         "        pos_ticket = int(ticket)\n        ours = self._ours(symbol)\n        if ours:\n            pos_ticket = ours[0].ticket\n",
         "        pos_ticket = self._resolve(symbol, ticket)\n"),
        ("MT5: list / close helpers",
         "    def open_qty(self, symbol):\n        ours = self._ours(symbol)",
         MT5_HELPERS + "    def open_qty(self, symbol):\n        ours = self._ours(symbol)"),
    ],
    "sailar/runner.py": [
        ("import the lock",
         "from .brain import make_brain\n",
         "from .brain import make_brain\nfrom .guard import acquire_lock\n"),
        ("one copy only",
         '    j = Journal(STATE / "journal.db")\n',
         '    acquire_lock(STATE / "sailar.lock")      # refuses to start if another Sailar-x is already running\n'
         '    j = Journal(STATE / "journal.db")\n'),
    ],
    "sailar/brain/llm.py": [
        ("Groq: retry once without JSON mode when the model fails to produce valid JSON",
         "        try:\n"
         '            r = _http("POST", self.BASE + "/chat/completions", self._hdr(), body, timeout)\n'
         "        except LLMError as e:\n"
         "            self._note_failure(e)\n"
         "            raise\n",
         "        try:\n"
         '            r = _http("POST", self.BASE + "/chat/completions", self._hdr(), body, timeout)\n'
         "        except LLMError as e:\n"
         '            if json_mode and "json_validate_failed" in str(e):      # reasoning models can run out of room mid-JSON\n'
         '                body.pop("response_format", None)\n'
         '                body["max_tokens"] = max(max_tokens, 1500)\n'
         "                try:\n"
         '                    r = _http("POST", self.BASE + "/chat/completions", self._hdr(), body, timeout)\n'
         "                except LLMError as e2:\n"
         "                    self._note_failure(e2)\n"
         "                    raise\n"
         "            else:\n"
         "                self._note_failure(e)\n"
         "                raise\n"),
    ],
    "sailar/brain/brain.py": [
        ("more room for reasoning models",
         'timeout=self.c.get("review_timeout", 8), max_tokens=500, prefer="groq")',
         'timeout=self.c.get("review_timeout", 8), max_tokens=900, prefer="groq")'),
        ("failed reviews are logged as ERROR, not APPROVE",
         '{"decision": vd.decision, "confidence": vd.confidence,',
         '{"decision": vd.decision if vd.ok else "ERROR", "confidence": vd.confidence,'),
    ],
}


def main():
    if not (ROOT / "run.py").exists() and not (ROOT / "sailar").exists():
        sys.exit(f"Run this inside your Sailar-x folder (the one with run.py). Not found in: {ROOT}")
    stamp = time.strftime("%Y%m%d-%H%M%S")
    backup = ROOT / "backup_before_guard" / stamp
    applied = skipped = already = 0
    guard = ROOT / "sailar" / "guard.py"
    if not guard.exists():
        sys.exit("sailar/guard.py is missing. Copy it from the zip into your sailar/ folder first.")
    for rel, edits in EDITS.items():
        rel = rel.split("#")[0]
        path = ROOT / rel
        if not path.exists():
            print(f"SKIP  {rel}: file not found")
            skipped += len(edits)
            continue
        text = original = path.read_text(encoding="utf-8")
        for label, old, new in edits:
            if new in text:
                print(f"ok    {rel}: {label} (already in place)")
                already += 1
            elif text.count(old) == 1:
                text = text.replace(old, new)
                print(f"DONE  {rel}: {label}")
                applied += 1
            else:
                print(f"SKIP  {rel}: {label}  <- could not find the spot (you edited it?). Send me this file and I will merge it.")
                skipped += 1
        if text != original:
            (backup / rel).parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(path, backup / rel)
            path.write_text(text, encoding="utf-8")
    print(f"\n{applied} applied, {already} already there, {skipped} skipped." +
          (f" Originals saved in {backup}" if applied else ""))
    if skipped:
        print("Some edits were skipped. The rest still work, but send me the files named above.")


if __name__ == "__main__":
    main()
