"""Random search optimizer for OpenClaw Trading.

This module provides random search optimization for strategy parameters
with support for early stopping.
"""

import time
from collections.abc import Callable
from typing import Any

from openclaw.backtest.engine import BacktestResult
from openclaw.optimizer.base import (
    OptimizationResult,
    OptimizerConfig,
    ParameterSpace,
    StrategyOptimizer,
)


class RandomSearchOptimizer(StrategyOptimizer):
    """Random search optimizer.

    This optimizer samples random parameter combinations from the parameter space
    and keeps track of the best ones. It's often more efficient than grid search
    for high-dimensional spaces.

    Args:
        parameter_space: Parameter space to optimize over
        config: Optimizer configuration
        n_samples: Number of random samples (default: 100)
    """

    def __init__(
        self,
        parameter_space: ParameterSpace,
        config: OptimizerConfig | None = None,
        n_samples: int = 100,
    ):
        """Initialize random search optimizer."""
        super().__init__(parameter_space, config)
        self.n_samples = min(n_samples, config.max_iterations if config else n_samples)

    def optimize(
        self,
        backtest_fn: Callable[[dict[str, Any]], BacktestResult],
        callback: Callable[[int, dict[str, Any], float], None] | None = None,
    ) -> OptimizationResult:
        """Run random search optimization.

        Args:
            backtest_fn: Function that takes parameters and returns BacktestResult
            callback: Optional callback function (iteration, params, score)

        Returns:
            OptimizationResult with best parameters and all results
        """
        start_time = time.time()
        self.logger.info("Starting random search optimization")

        results = []
        no_improvement_count = 0
        previous_best = float("-inf")

        n_jobs = self.config.n_jobs
        batch_size = max(1, min(10, self.n_samples // max(1, n_jobs) if n_jobs > 0 else 10))

        iteration = 0
        while iteration < self.n_samples:
            # Generate batch of random parameters
            batch_size_actual = min(batch_size, self.n_samples - iteration)
            param_batch = [
                self.parameter_space.sample_random()
                for _ in range(batch_size_actual)
            ]

            # Evaluate batch
            if n_jobs == 1 or batch_size_actual == 1:
                # Sequential execution
                for params in param_batch:
                    score, result = self._evaluate_params(params, backtest_fn)
                    results.append((params, score, result))
                    improved = self._update_best(params, score, result)

                    if callback:
                        callback(iteration, params, score)

                    # Early stopping check
                    if improved:
                        if self._best_score - previous_best < self.config.early_stopping_min_delta:
                            no_improvement_count += 1
                        else:
                            no_improvement_count = 0
                        previous_best = self._best_score
                    else:
                        no_improvement_count += 1

                    iteration += 1

                    if self._check_early_stopping(iteration, no_improvement_count):
                        break

                    self.logger.debug(
                        f"Iteration {iteration}/{self.n_samples}: "
                        f"params={params}, score={score:.4f}"
                    )
            else:
                # Parallel execution
                batch_results = self._evaluate_params_parallel(param_batch, backtest_fn, n_jobs)

                for params, score, result in batch_results:
                    results.append((params, score, result))
                    improved = self._update_best(params, score, result)

                    if callback:
                        callback(iteration, params, score)

                    # Early stopping check
                    if improved:
                        if self._best_score - previous_best < self.config.early_stopping_min_delta:
                            no_improvement_count += 1
                        else:
                            no_improvement_count = 0
                        previous_best = self._best_score
                    else:
                        no_improvement_count += 1

                    iteration += 1

                    if self._check_early_stopping(iteration, no_improvement_count):
                        break

                    self.logger.debug(
                        f"Iteration {iteration}/{self.n_samples}: "
                        f"params={params}, score={score:.4f}"
                    )

            if self._check_early_stopping(iteration, no_improvement_count):
                break

        optimization_time = time.time() - start_time
        converged = no_improvement_count < self.config.early_stopping_patience

        self.logger.info(
            f"Random search complete. Best score: {self._best_score:.4f}, "
            f"Best params: {self._best_params}, "
            f"Iterations: {iteration}"
        )

        return OptimizationResult(
            best_params=self._best_params or {},
            best_score=self._best_score,
            best_result=self._best_result,
            all_results=results,
            optimization_time=optimization_time,
            n_iterations=len(results),
            converged=converged,
        )

    def optimize_with_warm_start(
        self,
        backtest_fn: Callable[[dict[str, Any]], BacktestResult],
        initial_params: list[tuple[dict[str, Any], float]],
        callback: Callable[[int, dict[str, Any], float], None] | None = None,
    ) -> OptimizationResult:
        """Run optimization with warm start from previous results.

        Args:
            backtest_fn: Function that takes parameters and returns BacktestResult
            initial_params: List of (params, score) tuples from previous runs
            callback: Optional callback function (iteration, params, score)

        Returns:
            OptimizationResult with best parameters and all results
        """
        start_time = time.time()
        self.logger.info("Starting random search with warm start")

        # Initialize with warm start data
        results = []
        for params, score in initial_params:
            results.append((params, score, None))
            self._update_best(params, score, None)

        self.logger.info(f"Loaded {len(initial_params)} initial results")

        # Continue with random search
        no_improvement_count = 0
        previous_best = self._best_score
        n_jobs = self.config.n_jobs

        for iteration in range(len(initial_params), self.n_samples):
            params = self.parameter_space.sample_random()
            score, result = self._evaluate_params(params, backtest_fn)

            results.append((params, score, result))
            improved = self._update_best(params, score, result)

            if callback:
                callback(iteration, params, score)

            # Early stopping check
            if improved:
                if self._best_score - previous_best < self.config.early_stopping_min_delta:
                    no_improvement_count += 1
                else:
                    no_improvement_count = 0
                previous_best = self._best_score
            else:
                no_improvement_count += 1

            if self._check_early_stopping(iteration, no_improvement_count):
                break

            self.logger.debug(
                f"Iteration {iteration + 1}/{self.n_samples}: "
                f"params={params}, score={score:.4f}"
            )

        optimization_time = time.time() - start_time
        converged = no_improvement_count < self.config.early_stopping_patience

        return OptimizationResult(
            best_params=self._best_params or {},
            best_score=self._best_score,
            best_result=self._best_result,
            all_results=results,
            optimization_time=optimization_time,
            n_iterations=len(results),
            converged=converged,
        )
