#!/usr/bin/env python3
"""
Strategy harness — validates a PandasStrategy against synthetic OHLC data.

Can be called in-process (preferred) via run() or as a subprocess for backward
compatibility.

Usage (subprocess): python strategy_harness.py <impl_path> <metadata_path>

Outputs JSON to stdout:
{
    "success": bool,
    "output": str,       # human-readable summary on success
    "trade_count": int,  # number of trades executed in the mini-backtest
    "error": str | null  # error message / traceback if failed
}
"""
import json
import os
import sys
import traceback
from pathlib import Path

# Ensure dexorder package is importable when run as a subprocess
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))


# ---------------------------------------------------------------------------
# Synthetic OHLCV data — 100 deterministic bars, no network required
# ---------------------------------------------------------------------------

def make_synthetic_ohlcv(n: int = 100):
    import numpy as np
    import pandas as pd

    rng = np.random.default_rng(42)
    returns = rng.normal(0, 0.015, n)
    closes = 40_000.0 * np.cumprod(1.0 + returns)

    opens = np.empty(n)
    opens[0] = closes[0]
    opens[1:] = closes[:-1]

    noise = np.abs(rng.normal(0, 0.005, n))
    highs = np.maximum(opens, closes) * (1.0 + noise)
    lows  = np.minimum(opens, closes) * (1.0 - noise)
    volumes = rng.uniform(1e6, 1e8, n)
    buy_vols = volumes * rng.uniform(0.4, 0.6, n)

    now_ns = 1_700_000_000_000_000_000  # arbitrary epoch in nanoseconds
    step_ns = 3_600_000_000_000         # 1 hour in nanoseconds
    timestamps = [now_ns + i * step_ns for i in range(n)]

    return pd.DataFrame({
        "timestamp": timestamps,
        "open":      opens,
        "high":      highs,
        "low":       lows,
        "close":     closes,
        "volume":    volumes,
        "buy_vol":   buy_vols,
        "sell_vol":  volumes - buy_vols,
        "open_interest": rng.uniform(1e8, 1e9, n),
    })


def run(impl_path: Path, metadata_path: Path) -> dict:
    """
    Validate a strategy against synthetic OHLC data and return results.

    Returns:
        dict with success, output, trade_count, error fields
    """
    impl_path = Path(impl_path)
    metadata_path = Path(metadata_path)

    # --- Load metadata (feeds, parameters) ---
    data_feeds: list[dict] = []
    parameters: dict = {}
    try:
        with open(metadata_path) as f:
            meta = json.load(f)
        data_feeds = meta.get("data_feeds") or []
        param_schema = meta.get("parameters") or {}
        for pname, pinfo in param_schema.items():
            if isinstance(pinfo, dict) and "default" in pinfo:
                parameters[pname] = pinfo["default"]
            elif not isinstance(pinfo, dict):
                parameters[pname] = pinfo
    except Exception as e:
        return {"success": False, "output": "", "trade_count": 0, "error": f"Failed to read metadata: {e}"}

    # --- Build synthetic feed keys ---
    if data_feeds:
        feed_configs = [(f.get("symbol", "BTC/USDT.SYNTH"), int(f.get("period_seconds", 3600)))
                        for f in data_feeds]
    else:
        feed_configs = [("BTC/USDT.SYNTH", 3600)]

    # --- Register custom indicators ---
    try:
        from dexorder.tools.python_tools import setup_custom_indicators
        data_dir = Path(os.environ.get("DATA_DIR", "/app/data"))
        setup_custom_indicators(data_dir)
    except Exception:
        pass

    # --- Load strategy class ---
    try:
        from dexorder.nautilus.backtest_runner import _load_strategy_class
        strategy_class = _load_strategy_class(impl_path)
    except Exception:
        tb = traceback.format_exc()
        return {"success": False, "output": "", "trade_count": 0, "error": f"Strategy load failed:\n{tb}"}

    # --- Run a minimal backtest with synthetic data ---
    try:
        import pandas as pd
        from dexorder.nautilus.pandas_strategy import PandasStrategyConfig, make_feed_key
        from dexorder.nautilus.backtest_runner import _setup_custom_indicators

        try:
            data_dir = Path(os.environ.get("DATA_DIR", "/app/data"))
            _setup_custom_indicators(data_dir)
        except Exception:
            pass

        # Build one synthetic DataFrame per feed
        feed_dfs: dict[str, pd.DataFrame] = {}
        for ticker, period_seconds in feed_configs:
            fk = make_feed_key(ticker, period_seconds)
            feed_dfs[fk] = make_synthetic_ohlcv(100)

        feed_keys = tuple(make_feed_key(t, p) for t, p in feed_configs)
        config = PandasStrategyConfig(
            strategy_id=f"{strategy_class.__name__}-HARNESS",
            feed_keys=feed_keys,
            initial_capital=10_000.0,
        )

        strat = strategy_class(config=config)

        for pname, pval in parameters.items():
            if hasattr(strat, pname):
                setattr(strat, pname, pval)

        # Replay bars: accumulate rows and call evaluate()
        buy_count = 0
        sell_count = 0
        evaluate_errors: list[str] = []
        rows_by_feed: dict[str, list] = {fk: [] for fk in feed_keys}

        for i in range(len(next(iter(feed_dfs.values())))):
            for fk, df in feed_dfs.items():
                row = df.iloc[i].to_dict()
                rows_by_feed[fk].append(row)
                current_dfs = {k: pd.DataFrame(v) for k, v in rows_by_feed.items()}

                _orig_buy = strat.buy
                _orig_sell = strat.sell
                _orig_flatten = strat.flatten

                class _BuyCounter:
                    def __call__(inner_self, *a, **kw):
                        nonlocal buy_count
                        buy_count += 1

                class _SellCounter:
                    def __call__(inner_self, *a, **kw):
                        nonlocal sell_count
                        sell_count += 1

                strat.buy = _BuyCounter()
                strat.sell = _SellCounter()
                strat.flatten = lambda *a, **kw: None

                try:
                    strat.evaluate(current_dfs)
                except Exception as e:
                    evaluate_errors.append(f"Bar {i}: {e}")
                    if len(evaluate_errors) > 3:
                        break
                finally:
                    strat.buy = _orig_buy
                    strat.sell = _orig_sell
                    strat.flatten = _orig_flatten

            if evaluate_errors and len(evaluate_errors) > 3:
                break

        if evaluate_errors:
            return {
                "success": False,
                "output": "",
                "trade_count": 0,
                "error": "evaluate() raised errors:\n" + "\n".join(evaluate_errors[:3]),
            }

        trade_count = buy_count + sell_count
        n_bars = len(next(iter(feed_dfs.values())))
        n_feeds = len(feed_dfs)
        output = (
            f"Strategy validated OK: {n_bars} bars × {n_feeds} feed(s), "
            f"buy_signals={buy_count}, sell_signals={sell_count}"
        )
        return {"success": True, "output": output, "trade_count": trade_count, "error": None}

    except Exception:
        tb = traceback.format_exc()
        return {"success": False, "output": "", "trade_count": 0, "error": f"Harness execution failed:\n{tb}"}


def main():
    if len(sys.argv) < 3:
        print(json.dumps({
            "success": False,
            "output": "",
            "trade_count": 0,
            "error": "Usage: strategy_harness.py <impl_path> <metadata_path>",
        }))
        sys.exit(1)

    result = run(Path(sys.argv[1]), Path(sys.argv[2]))
    print(json.dumps(result))


if __name__ == "__main__":
    main()
