#!/usr/bin/env python3
"""
Indicator harness — tests a custom indicator against synthetic OHLC data.

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

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

Outputs JSON to stdout:
{
    "success": bool,
    "output": str,   # human-readable summary of the indicator output
    "error": str     # error message / traceback if failed (null on success)
}
"""
import importlib.util
import json
import os
import sys
import traceback
import types
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 — 200 deterministic bars, no network required
# ---------------------------------------------------------------------------

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

    rng = np.random.default_rng(42)

    # Realistic BTC-style price random walk
    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]  # open = previous close

    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)

    return pd.DataFrame({
        "open":   opens,
        "high":   highs,
        "low":    lows,
        "close":  closes,
        "volume": volumes,
    })


def summarize(result, n: int) -> str:
    import pandas as pd

    if isinstance(result, pd.Series):
        nan_count = int(result.isna().sum())
        valid = result.dropna()
        sample = [round(float(v), 4) for v in valid.tail(5).values] if len(valid) else []
        return (
            f"Series({n} bars), NaN: {nan_count}/{n}, "
            f"last 5 valid values: {sample}"
        )
    elif isinstance(result, pd.DataFrame):
        cols = list(result.columns)
        nan_counts = {c: int(result[c].isna().sum()) for c in cols}
        sample = {}
        for col in cols:
            valid = result[col].dropna()
            if len(valid):
                sample[col] = [round(float(v), 4) for v in valid.tail(3).values]
        return (
            f"DataFrame({n} bars × {len(cols)} cols {cols}), "
            f"NaN counts: {nan_counts}, last 3 valid per col: {sample}"
        )
    else:
        return f"Unexpected return type: {type(result).__name__}"


def run(impl_path: Path, metadata_path: Path) -> dict:
    """
    Run an indicator against synthetic OHLC data and return results.

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

    # --- Load metadata ---
    input_series = ["close"]
    parameters: dict = {}
    try:
        with open(metadata_path) as f:
            meta = json.load(f)
        input_series = meta.get("input_series") or ["close"]
        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):
                # bare value (legacy)
                parameters[pname] = pinfo
    except Exception as e:
        return {"success": False, "error": f"Failed to read metadata: {e}"}

    # --- Generate synthetic data ---
    try:
        import numpy  # noqa: F401 — verify numpy available
        import pandas as pd
    except ImportError as e:
        return {"success": False, "error": f"Missing required package: {e}"}

    df = make_synthetic_ohlcv(n=200)
    n = len(df)

    # --- Load implementation ---
    # Clear from sys.modules first so edits are picked up
    module_name = f"_dexorder_indicator_{impl_path.parent.name}"
    sys.modules.pop(module_name, None)
    try:
        spec = importlib.util.spec_from_file_location(module_name, impl_path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)  # type: ignore[union-attr]
    except Exception:
        tb = traceback.format_exc()
        return {"success": False, "error": f"Import failed:\n{tb}"}

    # --- Find the indicator function ---
    fn_name = impl_path.parent.name.lower()
    fn = getattr(module, fn_name, None)
    if fn is None:
        candidates = [
            v for k, v in vars(module).items()
            if isinstance(v, types.FunctionType) and not k.startswith("_")
        ]
        fn = candidates[0] if candidates else None

    if fn is None:
        return {"success": False, "error": "No callable function found in implementation.py"}

    # --- Build positional args from input_series ---
    args = []
    for col in input_series:
        if col not in df.columns:
            return {"success": False, "error": f"input_series '{col}' not in synthetic df columns {list(df.columns)}"}
        args.append(df[col])

    # --- Execute ---
    try:
        result = fn(*args, **parameters)
    except Exception:
        tb = traceback.format_exc()
        return {"success": False, "error": f"Execution failed:\n{tb}"}

    # --- Validate output type ---
    if not isinstance(result, (pd.Series, pd.DataFrame)):
        return {
            "success": False,
            "error": (
                f"Indicator must return pd.Series or pd.DataFrame, "
                f"got {type(result).__name__}. "
                "Wrap the output if using pandas-ta internally."
            ),
        }

    return {"success": True, "output": summarize(result, n)}


def main():
    if len(sys.argv) < 3:
        print(json.dumps({"success": False, "error": "Usage: indicator_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()
