"""Context search space utilities for hyperparameter optimization.

This module provides utilities for creating hyperopt search spaces
for context parameters in LeCrapaud.

Example:
    >>> from lecrapaud import create_context_search_space
    >>> from hyperopt import hp
    >>> space = create_context_search_space(
    ...     optimization_metric=["LOGLOSS", "ROC_AUC"],
    ...     use_class_weights=[True, False],
    ...     percentile=[10, 20, 30],
    ... )
"""

from typing import Any, Callable, Optional


def create_context_search_space(**kwargs) -> dict:
    """
    Create a hyperopt search space for ANY context parameters.

    Pass lists of values to create hp.choice, or hp.* objects directly.

    Args:
        **kwargs: Parameter names and their possible values.
            - Lists: Converted to hp.choice
            - hp.* objects: Used as-is
            - Single values: Used as-is (no choice)
            - None: Ignored

    Returns:
        dict: Hyperopt search space dictionary

    Examples:
        # Simple choices
        create_context_search_space(
            optimization_metric=["LOGLOSS", "ROC_AUC"],
            use_class_weights=[True, False],
            percentile=[10, 20, 30],
        )

        # With continuous ranges
        from hyperopt import hp
        create_context_search_space(
            val_size=hp.uniform("val_size", 0.1, 0.3),
            max_p_value=hp.loguniform("max_p_value", -5, -1),
        )
    """
    from hyperopt import hp

    space = {}

    for param_name, values in kwargs.items():
        if values is None:
            continue

        # Already a hyperopt expression (has pos_args attribute from hp.* functions)
        if hasattr(values, "pos_args"):
            space[param_name] = values
        # List of choices
        elif isinstance(values, list):
            if len(values) == 0:
                continue  # Skip empty lists
            elif len(values) == 1:
                space[param_name] = values[0]  # Single value, no choice
            else:
                space[param_name] = hp.choice(param_name, values)
        # Single value
        else:
            space[param_name] = values

    return space


def build_context_from_params(
    base_context: dict,
    sampled_params: dict,
    context_builder: Optional[Callable[[dict], dict]] = None,
) -> dict:
    """
    Build a full context dict from base context and sampled hyperparams.

    Args:
        base_context: Base context dictionary with default values
        sampled_params: Parameters sampled from the hyperopt search space
        context_builder: Optional function to post-process the context
            (e.g., compute derived parameters)

    Returns:
        dict: Complete context dictionary ready for LeCrapaud
    """
    context = base_context.copy()
    context.update(sampled_params)

    if context_builder:
        context = context_builder(context)

    return context


def get_metric_direction(metric: str) -> str:
    """
    Get optimization direction for a metric.

    Uses METRICS_CONFIG from utils.py, with additional support for business metrics.

    Args:
        metric: Metric name (case-insensitive)

    Returns:
        str: "minimize" or "maximize"

    Raises:
        ValueError: If metric is unknown
    """
    from lecrapaud.utils import get_metric_direction as utils_get_metric_direction
    from lecrapaud.utils import METRICS_CONFIG

    # Business metrics not in METRICS_CONFIG
    if metric.lower() == "business_cost":
        return "minimize"

    # Try utils version (handles uppercase)
    try:
        return utils_get_metric_direction(metric.upper())
    except ValueError:
        pass

    # Unknown metric
    valid_metrics = list(METRICS_CONFIG.keys()) + ["business_cost"]
    raise ValueError(f"Unknown metric: {metric}. Valid metrics: {valid_metrics}")


def get_default_search_space() -> dict:
    """
    Get a default search space for common context parameters.

    Returns:
        dict: Default search space with common optimization parameters
    """
    from hyperopt import hp

    return create_context_search_space(
        # Feature selection
        percentile=[10, 20, 30, 40],
        corr_threshold=[70, 80, 90],
        cumulative_importance=[0.7, 0.8, 0.9],
        # Model selection
        use_class_weights=[True, False],
        # Data split
        val_size=[0.15, 0.20, 0.25],
    )


def describe_search_space(space: dict, n_samples: int = 5) -> str:
    """
    Return a human-readable description of a hyperopt search space.

    Shows unique sampled values to illustrate what each parameter can be.

    Args:
        space: Hyperopt search space dictionary
        n_samples: Number of samples to draw for showing possible values

    Returns:
        str: Human-readable description
    """
    from hyperopt import pyll
    from hyperopt.pyll.stochastic import sample
    import random

    lines = []
    for param_name, param_space in space.items():
        # Check if it's a hyperopt object
        if isinstance(param_space, pyll.base.Apply):
            # Sample multiple times to see possible values
            try:
                seen = []
                for i in range(n_samples * 3):  # Oversample to get unique values
                    random.seed(i)
                    val = sample(param_space)
                    # Convert to hashable for dedup
                    val_repr = repr(val)
                    if val_repr not in [repr(v) for v in seen]:
                        seen.append(val)
                    if len(seen) >= n_samples:
                        break

                # Format the values
                if len(seen) == 1:
                    lines.append(f"  {param_name}: {_format_value(seen[0])}")
                else:
                    formatted = [_format_value(v) for v in seen]
                    lines.append(f"  {param_name}: {' | '.join(formatted)}")
            except Exception as e:
                lines.append(f"  {param_name}: hp.choice(...)")
        elif isinstance(param_space, list):
            lines.append(f"  {param_name}: {_format_value(param_space)}")
        else:
            lines.append(f"  {param_name}: {_format_value(param_space)}")

    return "Search Space:\n" + "\n".join(lines)


def _format_value(val) -> str:
    """Format a value for display in search space description."""
    if isinstance(val, list):
        if len(val) == 0:
            return "[]"
        elif len(val) == 1 and isinstance(val[0], dict):
            # Single dict in list (like pca config)
            return f"[{{{_format_dict_short(val[0])}}}]"
        elif all(isinstance(v, dict) for v in val):
            return f"[{len(val)} configs]"
        elif len(val) <= 3:
            return str(val)
        else:
            return f"[{len(val)} items]"
    elif isinstance(val, dict):
        return f"{{{_format_dict_short(val)}}}"
    else:
        return repr(val)


def _format_dict_short(d: dict, max_keys: int = 2) -> str:
    """Format dict showing only first few keys."""
    keys = list(d.keys())[:max_keys]
    parts = [f"{k}={repr(d[k])[:20]}" for k in keys]
    if len(d) > max_keys:
        parts.append("...")
    return ", ".join(parts)


def print_search_space(space: dict) -> None:
    """Print a human-readable description of a hyperopt search space."""
    print(describe_search_space(space))


def merge_search_spaces(*spaces: dict) -> dict:
    """
    Merge multiple search spaces into one.

    Later spaces override earlier ones for conflicting keys.

    Args:
        *spaces: Search space dictionaries to merge

    Returns:
        dict: Merged search space
    """
    result = {}
    for space in spaces:
        result.update(space)
    return result
