#!/usr/bin/env python3
"""
BTC Accumulation Signal Optimizer -- Orchestrator
Coordinates the optimization loop across VPS, Windows PC (GPU), and Mac Mini (LLM).
"""

import json
import os
import subprocess
import sys
import threading
import time
from datetime import datetime, timezone

# Paths
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "data")
CONFIG_DIR = os.path.join(BASE_DIR, "config")
RESULTS_DIR = os.path.join(BASE_DIR, "results")
ITERATIONS_LOG = os.path.join(RESULTS_DIR, "iterations.jsonl")

# Remote machines
WINDOWS_HOST = "bizzle@100.76.218.38"
WINDOWS_DIR = "btc-ml-optimizer"
MAC_MINI_HOST = "bizzle@bizzles-mac-mini-1"

# Convergence
MAX_ITERATIONS = 50
CONVERGENCE_WINDOW = 5
CONVERGENCE_THRESHOLD = 0.01  # 1% improvement
TARGET_COST_IMPROVEMENT = 20.0  # Backward-compatible name: terminal wealth objective
MIN_SIGNAL_COUNT = 30  # Minimum strong buy signals for valid results
ML_TIMEOUT = 600  # 10 minutes

# Colors
class C:
    BOLD = "\033[1m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    RED = "\033[91m"
    CYAN = "\033[96m"
    MAGENTA = "\033[95m"
    DIM = "\033[2m"
    RESET = "\033[0m"


def log(msg, color=""):
    ts = datetime.now(timezone.utc).strftime("%H:%M:%S")
    print(f"{C.DIM}[{ts}]{C.RESET} {color}{msg}{C.RESET}")


def objective_score(results):
    """Return the equal-capital portfolio objective used for model selection."""
    return float(results.get("terminal_wealth_improvement_pct", 0.0))


def run_cmd(cmd, timeout=120, check=True):
    """Run a shell command and return stdout."""
    result = subprocess.run(
        cmd, shell=True, capture_output=True, text=True, timeout=timeout
    )
    if check and result.returncode != 0:
        raise RuntimeError(f"Command failed: {cmd}\n{result.stderr}")
    return result.stdout.strip()


def ensure_data():
    """Make sure BTC data is fetched."""
    data_4h = os.path.join(DATA_DIR, "btc_4h.csv")
    data_1h = os.path.join(DATA_DIR, "btc_1h.csv")
    if os.path.exists(data_4h) and os.path.exists(data_1h):
        log("Data files already exist", C.GREEN)
        return
    log("Fetching BTC data...", C.YELLOW)
    run_cmd(f"cd {BASE_DIR} && python3 scripts/fetch_data.py", timeout=300)


def setup_windows_remote():
    """Ensure the remote directory exists on Windows."""
    log("Ensuring Windows remote directory exists...", C.CYAN)
    run_cmd(f'ssh {WINDOWS_HOST} "if not exist {WINDOWS_DIR} mkdir {WINDOWS_DIR}"', timeout=30)


def scp_to_windows(local_path, remote_name):
    """SCP a file to the Windows PC."""
    run_cmd(f"scp -q {local_path} {WINDOWS_HOST}:{WINDOWS_DIR}/{remote_name}", timeout=60)


def scp_from_windows(remote_name, local_path):
    """SCP a file from the Windows PC."""
    run_cmd(f"scp -q {WINDOWS_HOST}:{WINDOWS_DIR}/{remote_name} {local_path}", timeout=60)


def run_ml_training():
    """Run the ML engine on the Windows PC via SSH."""
    cmd = (
        f'ssh {WINDOWS_HOST} '
        f'"cd {WINDOWS_DIR} && python train_and_backtest.py '
        f'--config config.json --data btc_4h.csv --output results.json"'
    )
    log("Running ML training on Windows PC (GPU)...", C.MAGENTA)
    result = subprocess.run(
        cmd, shell=True, capture_output=True, text=True, timeout=ML_TIMEOUT
    )
    if result.returncode != 0:
        raise RuntimeError(f"ML training failed:\n{result.stderr}\n{result.stdout}")
    for line in result.stdout.strip().split("\n"):
        log(f"  {C.DIM}{line}", C.DIM)
    return True


def load_iteration_history():
    """Load iteration history from JSONL log."""
    history = []
    if os.path.exists(ITERATIONS_LOG):
        with open(ITERATIONS_LOG) as f:
            for line in f:
                line = line.strip()
                if line:
                    history.append(json.loads(line))
    return history


def save_iteration(iteration_data):
    """Append an iteration to the JSONL log."""
    with open(ITERATIONS_LOG, "a") as f:
        f.write(json.dumps(iteration_data) + "\n")


def check_convergence(history):
    """Check if optimization has converged."""
    if len(history) < CONVERGENCE_WINDOW + 1:
        return False, "Not enough iterations"

    # Only consider valid results (enough signals)
    valid = [h for h in history if h.get("signal_count", 0) >= MIN_SIGNAL_COUNT]

    if not valid:
        return False, "No valid results yet"

    recent = history[-CONVERGENCE_WINDOW:]
    scores = [h.get("cost_improvement", 0) for h in recent]

    # Check if best score exceeds target
    best_score = max(h.get("cost_improvement", 0) for h in valid)
    if best_score >= TARGET_COST_IMPROVEMENT:
        return True, f"Target cost improvement reached: {best_score:.1f}%"

    # Check if improvement has stalled
    best_recent = max(scores)
    worst_recent = min(scores)
    if best_recent > 0 and (best_recent - worst_recent) / best_recent < CONVERGENCE_THRESHOLD:
        return True, f"Converged: variance < {CONVERGENCE_THRESHOLD*100}% over {CONVERGENCE_WINDOW} iterations"

    return False, ""


def print_header():
    print(f"""
{C.BOLD}{C.CYAN}========================================================
       BTC Accumulation Signal Optimizer
     VPS -> Windows GPU -> Mac Mini LLM -> Loop
========================================================{C.RESET}
""")


def print_results(results, iteration):
    objective = objective_score(results)
    color = C.GREEN if objective > 15 else C.YELLOW if objective > 10 else C.RED
    print(f"""
{C.BOLD}--- Iteration {iteration} Results ---{C.RESET}
  Terminal Wealth vs DCA: {color}{C.BOLD}{objective:.1f}%{C.RESET}
  Legacy Cost Basis Delta: {results.get('cost_basis_improvement_pct', 0):.1f}%
  Avg Cost (Model): ${results.get('avg_cost_basis_model', 0):,.2f}
  Avg Cost (DCA):   ${results.get('avg_cost_basis_dca', 0):,.2f}
  Strong Signals:   {results.get('strong_buy_signal_count', 0)}
  Signal Frequency: {results.get('signal_frequency_pct', 0):.1f}%
  Quality Score:    {results.get('pct_quality_strong_buy', 0):.1%}
  Model R2:         {results.get('model_r2_score', 0):.4f}
  Score@Bottoms:    {results.get('avg_score_at_actual_bottoms', 0):.1f}
  Score@Tops:       {results.get('avg_score_at_actual_tops', 0):.1f}
  Window Improvements: {results.get('per_window_cost_improvement', [])}
""")


def main():
    print_header()
    os.makedirs(RESULTS_DIR, exist_ok=True)

    ensure_data()

    config_path = os.path.join(CONFIG_DIR, "initial_config.json")
    best_config_path = os.path.join(CONFIG_DIR, "best_config.json")

    if os.path.exists(best_config_path):
        log("Resuming from best_config.json", C.GREEN)
        with open(best_config_path) as f:
            config = json.load(f)
    else:
        with open(config_path) as f:
            config = json.load(f)

    history = load_iteration_history()
    start_iter = len(history) + 1
    best_score = max((h.get("cost_improvement", 0) for h in history), default=0)

    log(f"Starting at iteration {start_iter}, best cost improvement so far: {best_score:.1f}%", C.BOLD)

    setup_windows_remote()

    log("Uploading ML engine to Windows...", C.CYAN)
    scp_to_windows(os.path.join(BASE_DIR, "ml_engine", "train_and_backtest.py"), "train_and_backtest.py")

    for tf in ["1h", "4h"]:
        data_file = os.path.join(DATA_DIR, f"btc_{tf}.csv")
        if os.path.exists(data_file):
            log(f"Uploading btc_{tf}.csv to Windows...", C.CYAN)
            scp_to_windows(data_file, f"btc_{tf}.csv")

    sys.path.insert(0, os.path.join(BASE_DIR, "llm_client"))
    from analyzer import analyze_and_suggest

    for iteration in range(start_iter, MAX_ITERATIONS + 1):
        log(f"\n{'='*50}", C.BOLD)
        log(f"ITERATION {iteration}/{MAX_ITERATIONS}", f"{C.BOLD}{C.CYAN}")
        log(f"Model: {config.get('model_type', 'unknown')}, "
            f"LR: {config.get('hyperparameters', {}).get('learning_rate', '?')}, "
            f"Depth: {config.get('hyperparameters', {}).get('max_depth', '?')}", C.DIM)
        log(f"{'='*50}", C.BOLD)

        tmp_config = os.path.join(BASE_DIR, "config", "current_config.json")
        with open(tmp_config, "w") as f:
            json.dump(config, f, indent=2)
        scp_to_windows(tmp_config, "config.json")

        try:
            run_ml_training()
        except (RuntimeError, subprocess.TimeoutExpired) as e:
            log(f"ML training failed: {e}", C.RED)
            log("Reverting to previous config and continuing...", C.YELLOW)
            if history:
                config = history[-1].get("config", config)
            continue

        results_local = os.path.join(RESULTS_DIR, f"results_iter_{iteration}.json")
        scp_from_windows("results.json", results_local)

        with open(results_local) as f:
            results = json.load(f)

        print_results(results, iteration)

        current_score = objective_score(results)
        signal_count = results.get("strong_buy_signal_count", 0)
        is_best = current_score > best_score and signal_count >= MIN_SIGNAL_COUNT

        if is_best:
            best_score = current_score
            with open(best_config_path, "w") as f:
                json.dump(config, f, indent=2)
            log(f"NEW BEST! Terminal Wealth Improvement: {best_score:.1f}%", f"{C.BOLD}{C.GREEN}")

        iter_data = {
            "iteration": iteration,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "cost_improvement": current_score,
            "objective_improvement": current_score,
            "objective": "equal_periodic_contribution_terminal_wealth",
            "avg_30d_return": results.get("avg_quality_score_strong_buy", 0),
            "avg_90d_return": results.get("pct_quality_strong_buy", 0),
            "signal_count": signal_count,
            "signal_frequency": results.get("signal_frequency_pct", 0),
            "r2_score": results.get("model_r2_score", 0),
            "score_at_bottoms": results.get("avg_score_at_actual_bottoms", 0),
            "score_at_tops": results.get("avg_score_at_actual_tops", 0),
            "model_type": config.get("model_type", "unknown"),
            "is_best": is_best,
            "config": config,
            "results": results,
        }
        save_iteration(iter_data)
        history.append(iter_data)

        converged, reason = check_convergence(history)
        if converged:
            log(f"\nOptimization converged: {reason}", f"{C.BOLD}{C.GREEN}")
            break

        if iteration >= MAX_ITERATIONS:
            log(f"\nMax iterations ({MAX_ITERATIONS}) reached.", C.YELLOW)
            break

        log("\nConsulting LLM for strategy modifications...", C.MAGENTA)
        try:
            summary_history = [
                {
                    "iteration": h["iteration"],
                    "cost_improvement": h.get("cost_improvement", 0),
                    "signal_count": h.get("signal_count", 0),
                    "r2_score": h.get("r2_score", 0),
                    "model_type": h.get("model_type", "unknown"),
                }
                for h in history
            ]
            new_config, reasoning = analyze_and_suggest(config, results, summary_history)
            log(f"  LLM reasoning: {reasoning[:200]}...", C.DIM)
            config = new_config
        except Exception as e:
            log(f"LLM call failed: {e}", C.RED)
            log("Continuing with current config + random perturbation...", C.YELLOW)
            import random
            hp = config.get("hyperparameters", {})
            hp["learning_rate"] = hp.get("learning_rate", 0.01) * random.uniform(0.8, 1.2)
            hp["max_depth"] = max(3, min(10, hp.get("max_depth", 5) + random.choice([-1, 0, 1])))
            config["hyperparameters"] = hp

    print(f"""
{C.BOLD}{C.GREEN}========================================================
              Optimization Complete!
========================================================{C.RESET}

  Total Iterations:     {len(history)}
  Best Terminal Wealth Improvement: {C.BOLD}{best_score:.1f}%{C.RESET}
  Best Config:          {best_config_path}
  Iteration Log:        {ITERATIONS_LOG}
""")


# --- Library API for dashboard integration ---

_stop_event = threading.Event()
_status = {
    "state": "idle",
    "iteration": 0,
    "max_iterations": MAX_ITERATIONS,
    "best_score": 0.0,
    "error": None,
    "llm_suggestions": [],
}
_status_lock = threading.Lock()


def get_status():
    """Get current optimization status (thread-safe)."""
    with _status_lock:
        return dict(_status)


def update_status(**kwargs):
    """Update status fields (thread-safe)."""
    with _status_lock:
        _status.update(kwargs)


def run_optimization_loop(callback=None, config_override=None):
    """Run the optimization loop from a background thread."""
    _stop_event.clear()
    update_status(state="running", iteration=0, error=None, best_score=0.0)

    try:
        os.makedirs(RESULTS_DIR, exist_ok=True)
        ensure_data()

        config_path = os.path.join(CONFIG_DIR, "initial_config.json")
        best_config_path = os.path.join(CONFIG_DIR, "best_config.json")

        if config_override:
            config = config_override
        elif os.path.exists(best_config_path):
            with open(best_config_path) as f:
                config = json.load(f)
        else:
            with open(config_path) as f:
                config = json.load(f)

        history = load_iteration_history()
        start_iter = len(history) + 1
        best_score = max((h.get("cost_improvement", 0) for h in history), default=0)
        update_status(best_score=best_score)

        setup_windows_remote()
        scp_to_windows(os.path.join(BASE_DIR, "ml_engine", "train_and_backtest.py"), "train_and_backtest.py")
        for tf in ["1h", "4h"]:
            data_file = os.path.join(DATA_DIR, f"btc_{tf}.csv")
            if os.path.exists(data_file):
                scp_to_windows(data_file, f"btc_{tf}.csv")

        sys.path.insert(0, os.path.join(BASE_DIR, "llm_client"))
        from analyzer import analyze_and_suggest

        for iteration in range(start_iter, MAX_ITERATIONS + 1):
            if _stop_event.is_set():
                update_status(state="completed")
                return
            update_status(iteration=iteration)

            tmp_config = os.path.join(BASE_DIR, "config", "current_config.json")
            with open(tmp_config, "w") as f:
                json.dump(config, f, indent=2)
            scp_to_windows(tmp_config, "config.json")

            try:
                run_ml_training()
            except (RuntimeError, subprocess.TimeoutExpired) as e:
                if callback:
                    callback(iteration, {"error": str(e)})
                if history:
                    config = history[-1].get("config", config)
                continue

            results_local = os.path.join(RESULTS_DIR, f"results_iter_{iteration}.json")
            scp_from_windows("results.json", results_local)
            with open(results_local) as f:
                results = json.load(f)

            current_score = results.get("cost_basis_improvement_pct", 0)
            signal_count = results.get("strong_buy_signal_count", 0)
            is_best = current_score > best_score and signal_count >= MIN_SIGNAL_COUNT

            if is_best:
                best_score = current_score
                with open(best_config_path, "w") as f:
                    json.dump(config, f, indent=2)
                update_status(best_score=best_score)

            iter_data = {
                "iteration": iteration,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "cost_improvement": current_score,
                "signal_count": signal_count,
                "signal_frequency": results.get("signal_frequency_pct", 0),
                "r2_score": results.get("model_r2_score", 0),
                "score_at_bottoms": results.get("avg_score_at_actual_bottoms", 0),
                "score_at_tops": results.get("avg_score_at_actual_tops", 0),
                "quality": results.get("pct_quality_strong_buy", 0),
                "model_type": config.get("model_type", "unknown"),
                "is_best": is_best,
                "config": config,
                "results": results,
            }
            save_iteration(iter_data)
            history.append(iter_data)

            if callback:
                callback(iteration, iter_data)

            converged, reason = check_convergence(history)
            if converged:
                update_status(state="completed")
                return

            if iteration >= MAX_ITERATIONS:
                update_status(state="completed")
                return

            if _stop_event.is_set():
                update_status(state="completed")
                return

            try:
                summary_history = [
                    {k: h[k] for k in ("iteration", "cost_improvement", "signal_count", "r2_score", "model_type")
                     if k in h}
                    for h in history
                ]
                new_config, reasoning = analyze_and_suggest(config, results, summary_history)
                with _status_lock:
                    _status["llm_suggestions"].append({
                        "iteration": iteration,
                        "reasoning": reasoning,
                    })
                # Also persist LLM suggestion to iteration log
                iter_data["llm_reasoning"] = reasoning
                iter_data["llm_applied"] = True
                config = new_config
            except Exception as e:
                import random, traceback
                err_msg = f"LLM call failed: {type(e).__name__}: {e}"
                print(f"  WARNING: {err_msg}")
                traceback.print_exc()
                with _status_lock:
                    _status["llm_suggestions"].append({
                        "iteration": iteration,
                        "reasoning": f"ERROR: {err_msg} — using random perturbation",
                    })
                iter_data["llm_reasoning"] = err_msg
                iter_data["llm_applied"] = False
                hp = config.get("hyperparameters", {})
                hp["learning_rate"] = hp.get("learning_rate", 0.01) * random.uniform(0.8, 1.2)
                hp["max_depth"] = max(3, min(10, hp.get("max_depth", 5) + random.choice([-1, 0, 1])))
                config["hyperparameters"] = hp

        update_status(state="completed")

    except Exception as e:
        update_status(state="error", error=str(e))
        raise


def request_stop():
    """Request graceful stop of the optimization loop."""
    _stop_event.set()


if __name__ == "__main__":
    main()
