#!/usr/bin/env python3
"""Render the current D48 execution and risk-gate review from live read-only evidence."""

from __future__ import annotations

import base64
import json
import re
import subprocess
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.lines import Line2D
from matplotlib.patches import Rectangle


HOST = "ubuntu@43.164.75.52"
START = "2026-07-21T00:00:00Z"
END = "2026-07-23T06:15:00Z"
PAIRS = [
    "ZEC/USDT:USDT", "EDGE/USDT:USDT", "UNI/USDT:USDT",
    "BTC/USDT:USDT", "SOL/USDT:USDT", "ETH/USDT:USDT",
    "POWER/USDT:USDT", "DOGE/USDT:USDT", "BNB/USDT:USDT",
    "ADA/USDT:USDT", "XRP/USDT:USDT", "VANRY/USDT:USDT",
]
OUT = Path(__file__).resolve().parent

COLORS = {
    "up": "#00A344", "down": "#D50000", "signal": "#2563EB",
    "block": "#64748B", "order": "#D97706", "fill": "#111827",
    "stop": "#DC2626", "exit": "#0F766E", "profit": "#15803D",
    "loss": "#B91C1C", "indicator": "#087F8C", "secondary": "#5D6B78",
}

REMOTE = r'''
import base64,json,re,sqlite3,sys,urllib.parse,urllib.request
from datetime import datetime,timezone
start=datetime.fromisoformat(sys.argv[1].replace("Z","+00:00"))
end=datetime.fromisoformat(sys.argv[2].replace("Z","+00:00"))
pairs=sys.argv[3:]
cfg=json.load(open("/data/freqtrade/user_data/config.json"))
token=base64.b64encode((cfg["api_server"]["username"]+":"+cfg["api_server"]["password"]).encode()).decode()
def candles(pair):
 q=urllib.parse.urlencode({"pair":pair,"timeframe":"5m","limit":"1000"})
 req=urllib.request.Request("http://127.0.0.1:8080/api/v1/pair_candles?"+q,headers={"Authorization":"Basic "+token,"Accept":"application/json"})
 with urllib.request.urlopen(req,timeout=15) as r: p=json.load(r)
 return [dict(zip(p["columns"],row)) for row in p["data"]]
con=sqlite3.connect("file:/data/freqtrade/user_data/tradesv3-v5.sqlite?mode=ro",uri=True)
con.row_factory=sqlite3.Row
s=start.astimezone(timezone.utc).replace(tzinfo=None).isoformat(sep=" ",timespec="microseconds")
e=end.astimezone(timezone.utc).replace(tzinfo=None).isoformat(sep=" ",timespec="microseconds")
trades=[dict(r) for r in con.execute("""select id,pair,is_short,is_open,open_date,close_date,open_rate,close_rate,
 stake_amount,leverage,stop_loss,initial_stop_loss,close_profit,close_profit_abs,exit_reason,max_rate,min_rate
 from trades where open_date>=? and open_date<? order by open_date""",(s,e))]
orders={str(t["id"]):[dict(r) for r in con.execute("""select id,ft_order_side,order_type,status,price,average,
 order_date,order_filled_date,order_update_date,stop_price from orders where ft_trade_id=? order by order_date,id""",(t["id"],))] for t in trades}
pat=re.compile(r"^(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*entry_blocked reason=(?P<reason>\S+) pair=(?P<pair>\S+) side=(?P<side>\S+) occupied=(?P<occupied>.*)$")
blocks=[]
for line in open("/data/freqtrade/user_data/logs/freqtrade.log",errors="replace"):
 m=pat.search(line)
 if not m: continue
 stamp=datetime.fromisoformat(m.group("time")).replace(tzinfo=timezone.utc)
 if start<=stamp<end: blocks.append({**m.groupdict(),"time":stamp.isoformat()})
print(json.dumps({"fetched_at":datetime.now(timezone.utc).isoformat(),"candles":{p:candles(p) for p in pairs},"trades":trades,"orders":orders,"blocks":blocks},separators=(",",":"),default=str))
'''


def fetch() -> dict:
    result = subprocess.run(
        ["ssh", HOST, "python3", "-", START, END, *PAIRS],
        input=REMOTE, text=True, capture_output=True, check=True, timeout=90,
    )
    return json.loads(result.stdout)


def stamp(value: str) -> pd.Timestamp:
    parsed = pd.Timestamp(value.replace(" ", "T"))
    return parsed.tz_localize("UTC") if parsed.tzinfo is None else parsed.tz_convert("UTC")


def frame(rows: list[dict]) -> pd.DataFrame:
    result = pd.DataFrame(rows)
    result["date"] = pd.to_datetime(result["date"], utc=True)
    result = result.set_index("date").sort_index()
    for column in [
        "open", "high", "low", "close", "volume", "donchian_high",
        "donchian_low", "ema_trend", "atr", "atr_baseline", "adx",
        "enter_short",
    ]:
        result[column] = pd.to_numeric(result.get(column), errors="coerce")
    return result


def draw_candles(ax: plt.Axes, data: pd.DataFrame, minutes: int) -> None:
    width = minutes / 1440 * 0.68
    floor = max((data.high.max() - data.low.min()) / 1200, 1e-12)
    for when, row in data.iterrows():
        x = mdates.date2num(when.to_pydatetime())
        color = COLORS["up"] if row.close >= row.open else COLORS["down"]
        ax.vlines(x, row.low, row.high, color=color, linewidth=.55, zorder=2)
        height = max(abs(row.close - row.open), floor)
        bottom = min(row.open, row.close)
        if abs(row.close - row.open) < floor:
            bottom = (row.open + row.close - height) / 2
        ax.add_patch(Rectangle(
            (x - width / 2, bottom), width, height,
            facecolor=color, edgecolor=color, linewidth=0, zorder=2,
        ))


def style(ax: plt.Axes) -> None:
    ax.set_facecolor("white")
    ax.grid(True, color="#64748B", alpha=.10, linewidth=.7)
    for spine in ax.spines.values():
        spine.set_color("#CBD5E1")
    ax.tick_params(labelsize=8, colors="#475569")
    ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d\n%H:%M", tz=timezone.utc))


def signal_before(data: pd.DataFrame, when: pd.Timestamp) -> tuple[pd.Timestamp, pd.Series]:
    eligible = data[
        (data.index + pd.Timedelta(minutes=5) <= when)
        & (data.index >= when - pd.Timedelta(minutes=15))
        & (data.enter_short.fillna(0) == 1)
    ]
    if eligible.empty:
        raise RuntimeError(f"No authentic short signal before {when}")
    return eligible.index[-1], eligible.iloc[-1]


def entry_order(trade: dict, orders: list[dict]) -> dict:
    side = "sell" if trade["is_short"] else "buy"
    return next(o for o in orders if o["order_type"] == "limit" and o["ft_order_side"] == side)


def render_execution(ev: dict, frames: dict[str, pd.DataFrame]) -> tuple[Path, list[dict]]:
    trades = ev["trades"]
    fig, axes = plt.subplots(3, 2, figsize=(20, 16), dpi=150, facecolor="white")
    metrics = []
    for ax, trade in zip(axes.flat, trades):
        pair = trade["pair"]
        entry = entry_order(trade, ev["orders"][str(trade["id"])])
        order_time = stamp(entry["order_date"])
        fill_time = stamp(entry["order_filled_date"] or trade["open_date"])
        signal_time, signal = signal_before(frames[pair], order_time)
        end = stamp(trade["close_date"]) if trade["close_date"] else stamp(END)
        local = frames[pair].loc[signal_time - pd.Timedelta(hours=2):end + pd.Timedelta(hours=1)]
        draw_candles(ax, local, 5)
        ax.plot(local.index, local.donchian_low, color=COLORS["indicator"], linewidth=.9)
        ax.plot(local.index, local.ema_trend, color=COLORS["secondary"], linewidth=.8, alpha=.75)
        ax.scatter(signal_time, signal.close, marker="v", s=85, color=COLORS["signal"], edgecolor="white", zorder=7)
        ax.hlines(float(entry["price"]), order_time, fill_time, color=COLORS["order"], linestyle="--", linewidth=1.5)
        ax.scatter(fill_time, float(entry["average"]), s=42, color=COLORS["fill"], zorder=8)
        stops = [o for o in ev["orders"][str(trade["id"])] if o["ft_order_side"] == "stoploss" and o.get("stop_price")]
        for stop in stops:
            left = stamp(stop["order_date"])
            right = stamp(stop.get("order_filled_date") or stop.get("order_update_date") or trade["close_date"] or END)
            ax.hlines(float(stop["stop_price"]), left, right, color=COLORS["stop"], linewidth=1.0)
        if trade["close_date"]:
            ax.scatter(end, trade["close_rate"], marker="D", s=58, color=COLORS["exit"], edgecolor="white", zorder=9)
            pnl = float(trade["close_profit_abs"])
            outcome = f"{pnl:+.2f} USDT / {trade['close_profit']*100:+.2f}%"
            outcome_color = COLORS["profit"] if pnl > 0 else COLORS["loss"]
        else:
            pnl = None
            outcome = "OPEN at review cutoff"
            outcome_color = COLORS["secondary"]
        ax.text(.015, .965, outcome, transform=ax.transAxes, va="top", fontsize=9, color=outcome_color,
                bbox={"facecolor": "white", "edgecolor": outcome_color, "alpha": .9, "boxstyle": "round,pad=.3"})
        latency = (fill_time - order_time).total_seconds()
        hold = frames[pair].loc[fill_time:end]
        open_rate = float(trade["open_rate"])
        mfe = (open_rate - float(hold.low.min())) / open_rate * 200
        mae = (open_rate - float(hold.high.max())) / open_rate * 200
        metrics.append({
            "case_id": f"trade-{trade['id']}", "trade_id": trade["id"], "pair": pair,
            "signal_time": signal_time.isoformat(), "order_time": order_time.isoformat(),
            "fill_time": fill_time.isoformat(), "fill_latency_s": latency,
            "mfe_levered_pct": mfe, "mae_levered_pct": mae, "pnl_usdt": pnl,
            "closed": not bool(trade["is_open"]),
        })
        ax.set_title(f"{pair} · SHORT · trade #{trade['id']}", loc="left", fontsize=11, fontweight="bold")
        style(ax)
    for ax in axes.flat[len(trades):]:
        ax.axis("off")
    legend = [
        Line2D([0], [0], marker="v", color="none", markerfacecolor=COLORS["signal"], label="Authentic strategy short signal", markersize=8),
        Line2D([0], [0], color=COLORS["order"], linestyle="--", label="Submitted limit order"),
        Line2D([0], [0], marker="o", color="none", markerfacecolor=COLORS["fill"], label="Fill", markersize=7),
        Line2D([0], [0], color=COLORS["stop"], label="Active stop"),
        Line2D([0], [0], marker="D", color="none", markerfacecolor=COLORS["exit"], label="Exit", markersize=7),
    ]
    fig.legend(handles=legend, loc="lower center", ncol=5, frameon=False, fontsize=9)
    fig.suptitle("Bot1 D48 · 2026-07-21—23 UTC · 5m execution reconstruction", fontsize=16, fontweight="bold")
    fig.text(.5, .025, "Authentic Freqtrade analyzed candles and SQLite orders; open outcomes frozen at review cutoff.", ha="center", fontsize=9, color="#64748B")
    fig.tight_layout(rect=[0, .06, 1, .95])
    path = OUT / "bot1_2026-07-21_23_execution_5m.png"
    fig.savefig(path, bbox_inches="tight", facecolor="white")
    plt.close(fig)
    return path, metrics


def block_episodes(blocks: list[dict]) -> list[dict]:
    episodes, latest = [], {}
    for block in blocks:
        when = stamp(block["time"])
        key = (block["pair"], block["side"], block["reason"])
        if key not in latest or when - latest[key][0] > pd.Timedelta(minutes=10):
            episode = {**block, "start": block["time"], "end": block["time"], "logs": 1}
            episodes.append(episode)
        else:
            episode = latest[key][1]
            episode["end"] = block["time"]
            episode["logs"] += 1
        latest[key] = (when, episode)
    return episodes


def block_metrics(episodes: list[dict], frames: dict[str, pd.DataFrame]) -> list[dict]:
    metrics = []
    for index, episode in enumerate(episodes, 1):
        when = stamp(episode["start"])
        data = frames[episode["pair"]]
        signal_time, signal = signal_before(data, when)
        horizon_end = min(signal_time + pd.Timedelta(hours=6), stamp(END))
        future = data.loc[signal_time:horizon_end]
        anchor = float(signal.close)
        metrics.append({
            "case_id": f"block-{index}", **episode, "signal_time": signal_time.isoformat(),
            "signal_price": anchor, "horizon_end": horizon_end.isoformat(),
            "horizon_complete": horizon_end >= signal_time + pd.Timedelta(hours=6),
            "mfe_6h_levered_pct": (anchor - float(future.low.min())) / anchor * 200,
            "mae_6h_levered_pct": (anchor - float(future.high.max())) / anchor * 200,
        })
    return metrics


def render_blocks(metrics: list[dict], frames: dict[str, pd.DataFrame]) -> Path:
    selected = metrics[-4:]
    fig, axes = plt.subplots(2, 2, figsize=(20, 12), dpi=150, facecolor="white")
    for ax, item in zip(axes.flat, selected):
        pair = item["pair"]
        when = stamp(item["start"])
        exact_signal = stamp(item["signal_time"])
        raw = frames[pair].loc[when - pd.Timedelta(hours=2):stamp(item["horizon_end"])]
        local = raw.resample("15min", label="left", closed="left").agg(
            {"open": "first", "high": "max", "low": "min", "close": "last",
             "volume": "sum", "donchian_low": "last", "ema_trend": "last"}
        ).dropna(subset=["open", "close"])
        draw_candles(ax, local, 15)
        ax.plot(local.index, local.donchian_low, color=COLORS["indicator"], linewidth=.9)
        ax.plot(local.index, local.ema_trend, color=COLORS["secondary"], linewidth=.8, alpha=.75)
        anchor = item["signal_price"]
        ax.scatter(exact_signal, anchor, marker="v", s=85, color=COLORS["signal"], edgecolor="white", zorder=7)
        ax.scatter(when, anchor, marker="x", s=100, color=COLORS["block"], linewidth=2.2, zorder=8)
        horizon_label = "6h" if item["horizon_complete"] else "to cutoff"
        note = (
            f"RISK_GATE_BLOCK · {item['reason']}\n"
            f"{horizon_label} hindsight MFE {item['mfe_6h_levered_pct']:+.2f}% · "
            f"MAE {item['mae_6h_levered_pct']:+.2f}%"
        )
        ax.text(.015, .965, note, transform=ax.transAxes, va="top", fontsize=9, color="#334155",
                bbox={"facecolor": "white", "edgecolor": "#94A3B8", "linestyle": ":", "alpha": .92, "boxstyle": "round,pad=.35"})
        ax.set_title(f"{pair} · exact 5m signal/block over resampled 15m structure", loc="left", fontsize=10, fontweight="bold")
        style(ax)
    fig.legend(handles=[
        Line2D([0], [0], marker="v", color="none", markerfacecolor=COLORS["signal"], label="Authentic 5m strategy signal", markersize=8),
        Line2D([0], [0], marker="x", color=COLORS["block"], label="Risk gate block", markersize=8, linestyle="none"),
        Line2D([0], [0], color=COLORS["indicator"], label="Donchian low (15m last)"),
        Line2D([0], [0], color=COLORS["secondary"], label="EMA200 (15m last)"),
    ], loc="lower center", ncol=4, frameon=False, fontsize=9)
    fig.suptitle("Bot1 D48 · latest blocked short episodes · 15m structure", fontsize=16, fontweight="bold")
    fig.text(.5, .025, "15m candles are resampled from authentic analyzed 5m candles; outcome boxes are hindsight only.", ha="center", fontsize=9, color="#64748B")
    fig.tight_layout(rect=[0, .06, 1, .95])
    path = OUT / "bot1_2026-07-21_23_blocks_15m.png"
    fig.savefig(path, bbox_inches="tight", facecolor="white")
    plt.close(fig)
    return path


def review_case_trade(trade: dict, metric: dict, orders: list[dict]) -> dict:
    entry = entry_order(trade, orders)
    closed = not bool(trade["is_open"])
    return {
        "case_id": metric["case_id"], "pair": trade["pair"], "side": "short",
        "reference": {"kind": "reference_signal", "present": False, "time": None},
        "strategy_signal": {"present": True, "time": metric["signal_time"]},
        "gate": {"status": "passed", "reason": None},
        "order": {"submitted": True, "time": metric["order_time"], "intended_price": float(entry["price"])},
        "fill": {"filled": True, "time": metric["fill_time"], "price": float(entry["average"])},
        "exit": {
            "present": closed, "time": stamp(trade["close_date"]).isoformat() if closed else None,
            "reason": trade["exit_reason"] if closed else None,
            "stop_price": float(trade["stop_loss"]) if trade["stop_loss"] else None,
        },
        "outcome": {
            "pnl_ratio": trade["close_profit"], "mfe_ratio": metric["mfe_levered_pct"] / 100,
            "mae_ratio": metric["mae_levered_pct"] / 100,
        },
        "closed": closed,
        "evidence": ["Freqtrade /pair_candles authentic analyzed 5m", "tradesv3-v5.sqlite trades/orders"],
    }


def review_case_block(item: dict) -> dict:
    return {
        "case_id": item["case_id"], "pair": item["pair"], "side": item["side"],
        "reference": {"kind": "reference_signal", "present": False, "time": None},
        "strategy_signal": {"present": True, "time": item["signal_time"]},
        "gate": {"status": "blocked", "reason": item["reason"]},
        "order": {"submitted": False, "time": None, "intended_price": None},
        "fill": {"filled": False, "time": None, "price": None},
        "exit": {"present": False, "time": None, "reason": None, "stop_price": None},
        "outcome": {
            "pnl_ratio": None, "mfe_ratio": item["mfe_6h_levered_pct"] / 100,
            "mae_ratio": item["mae_6h_levered_pct"] / 100,
        },
        "evidence": ["Freqtrade /pair_candles authentic analyzed 5m", "freqtrade.log entry_blocked"],
    }


def main() -> int:
    ev = fetch()
    frames = {pair: frame(rows) for pair, rows in ev["candles"].items()}
    execution_path, execution_metrics = render_execution(ev, frames)
    episodes = block_episodes(ev["blocks"])
    blocks = block_metrics(episodes, frames)
    block_path = render_blocks(blocks, frames)
    metrics = {
        "fetched_at": ev["fetched_at"], "window": {"start": START, "end": END},
        "execution": execution_metrics, "blocks": blocks,
    }
    (OUT / "metrics.json").write_text(json.dumps(metrics, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    trade_by_id = {trade["id"]: trade for trade in ev["trades"]}
    review = {
        "review_id": "2026-07-23-bot1-current",
        "as_of": ev["fetched_at"],
        "strategy": {"bot_id": "bot1", "name": "VolatilityBreakout", "version": "5.3-d48-shortgate"},
        "window": {"start": START, "end": END, "timeframe": "5m"},
        "cases": [
            *[
                review_case_trade(trade_by_id[item["trade_id"]], item, ev["orders"][str(item["trade_id"])])
                for item in execution_metrics
            ],
            *[review_case_block(item) for item in blocks],
        ],
    }
    (OUT / "review.json").write_text(json.dumps(review, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    manifest = {
        "review_id": review["review_id"], "fetched_at": ev["fetched_at"],
        "candle_source": "Freqtrade /pair_candles authentic analyzed 5m",
        "charts": [execution_path.name, block_path.name], "visual_review": None,
    }
    (OUT / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(execution_path)
    print(block_path)
    print(OUT / "review.json")
    return 0


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