"""Smart merge engine for deterministic recipe selection and local discovery."""

from __future__ import annotations

import hashlib
import json
import math
import os
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple

import numpy as np

from model_osmosis.merging.strategies import get_strategy
from model_osmosis.optimizer.self_optimize import (
    get_subsystem_metadata,
    optimizer_home,
    record_subsystem_selection,
    resolve_subsystem_mode,
)
from model_osmosis.utils.determinism import make_numpy_rng


SELECTOR_VERSION = "smart-merge-mlp-v1"
DEFAULT_PACK_VERSION = "builtin-pack-v1"
DEFAULT_BUDGET_MS = 1800
MAX_SAMPLE_GROUPS = 12
STATE_SCHEMA_VERSION = 1
ENGINE_MODES = {"auto", "off"}


@dataclass(frozen=True)
class MergeRecipe:
    """A merge recipe over existing merge primitives."""

    recipe_id: str
    strategy_name: str
    strategy_kwargs: Dict[str, Any]


@dataclass(frozen=True)
class StrategySelection:
    """Selected strategy metadata."""

    strategy_engine_mode: str
    strategy_name: str
    strategy_kwargs: Dict[str, Any]
    recipe_id: str
    recipe_pack_version: str
    selector_version: str
    selection_latency_ms: float
    fallback_reason: str
    canary_status: str
    rollback_reason: str

    def to_report_dict(self) -> Dict[str, Any]:
        return {
            "strategy_engine_mode": self.strategy_engine_mode,
            "strategy_name": self.strategy_name,
            "strategy_kwargs": dict(sorted((self.strategy_kwargs or {}).items(), key=lambda item: item[0])),
            "recipe_id": self.recipe_id,
            "recipe_pack_version": self.recipe_pack_version,
            "selector_version": self.selector_version,
            "selection_latency_ms": round(float(self.selection_latency_ms), 4),
            "fallback_reason": self.fallback_reason,
            "optimizer_mode": self.strategy_engine_mode,
            "profile_id": self.recipe_id,
            "pack_version": self.recipe_pack_version,
            "canary_status": self.canary_status,
            "rollback_reason": self.rollback_reason,
        }


def resolve_strategy_engine_mode(requested: Optional[str]) -> str:
    """Resolve strategy engine mode from CLI and environment."""
    return resolve_subsystem_mode(
        requested,
        subsystem_env_var="OSMOSIS_STRATEGY_ENGINE",
    )


def collect_tensor_groups_from_state_dicts(
    state_dicts: Sequence[Dict[str, Any]],
    *,
    seed: int = 42,
    max_groups: int = MAX_SAMPLE_GROUPS,
) -> List[List[np.ndarray]]:
    """Collect deterministic tensor groups with exact compatibility."""
    if len(state_dicts) < 2:
        return []

    key_sets = [set(sd.keys()) for sd in state_dicts if isinstance(sd, dict)]
    if len(key_sets) != len(state_dicts) or not key_sets:
        return []
    common = sorted(set.intersection(*key_sets))
    if not common:
        return []

    rng = make_numpy_rng(seed)
    order = np.arange(len(common), dtype=np.int64)
    if len(order) > max_groups:
        order = rng.choice(order, size=max_groups, replace=False)
    selected_names = [common[int(i)] for i in sorted(order.tolist())]

    groups: List[List[np.ndarray]] = []
    for name in selected_names:
        tensors: List[np.ndarray] = []
        compatible = True
        first_shape: Optional[Tuple[int, ...]] = None
        first_dtype: Optional[str] = None
        for state in state_dicts:
            value = state[name]
            if hasattr(value, "detach"):
                value = value.detach().cpu().numpy()
            arr = np.asarray(value)
            if first_shape is None:
                first_shape = tuple(arr.shape)
                first_dtype = str(arr.dtype)
            elif tuple(arr.shape) != first_shape or str(arr.dtype) != first_dtype:
                compatible = False
                break
            tensors.append(arr)
        if compatible and len(tensors) == len(state_dicts):
            groups.append(tensors)
    return groups


def collect_tensor_groups_from_osmo_parameters(
    osmos: Sequence[Any],
    compatible_names: Sequence[str],
    *,
    seed: int = 42,
    max_groups: int = MAX_SAMPLE_GROUPS,
) -> List[List[np.ndarray]]:
    """Collect deterministic tensor groups from compatible Osmo parameters."""
    if len(osmos) < 2:
        return []
    names = sorted(str(n) for n in compatible_names)
    if not names:
        return []

    rng = make_numpy_rng(seed)
    order = np.arange(len(names), dtype=np.int64)
    if len(order) > max_groups:
        order = rng.choice(order, size=max_groups, replace=False)
    selected_names = [names[int(i)] for i in sorted(order.tolist())]

    groups: List[List[np.ndarray]] = []
    for name in selected_names:
        tensors = [np.asarray(osmo.parameters[name]) for osmo in osmos if name in osmo.parameters]
        if len(tensors) == len(osmos):
            groups.append(tensors)
    return groups


def default_selection(
    *,
    strategy_name: str,
    strategy_kwargs: Optional[Dict[str, Any]] = None,
    strategy_engine_mode: str = "off",
    recipe_id: str = "static::weighted_average",
    recipe_pack_version: str = DEFAULT_PACK_VERSION,
    fallback_reason: str = "",
    canary_status: str = "shadow",
    rollback_reason: str = "",
) -> StrategySelection:
    return StrategySelection(
        strategy_engine_mode=strategy_engine_mode,
        strategy_name=str(strategy_name),
        strategy_kwargs=dict(strategy_kwargs or {}),
        recipe_id=str(recipe_id),
        recipe_pack_version=str(recipe_pack_version),
        selector_version=SELECTOR_VERSION,
        selection_latency_ms=0.0,
        fallback_reason=str(fallback_reason),
        canary_status=str(canary_status),
        rollback_reason=str(rollback_reason),
    )


def select_recipe_for_tensor_groups(
    *,
    tensor_groups: Sequence[Sequence[np.ndarray]],
    model_count: int,
    seed: int = 42,
    strategy_engine_mode: str = "auto",
    budget_ms: int = DEFAULT_BUDGET_MS,
    user_weights_provided: bool = False,
    context_label: str = "merge",
    source_refs: Optional[Sequence[str]] = None,
) -> StrategySelection:
    """Select best recipe deterministically using MLP + proxy scoring."""
    started = time.perf_counter()
    mode = resolve_strategy_engine_mode(strategy_engine_mode)
    if mode == "off":
        metadata = get_subsystem_metadata("strategy")
        return default_selection(
            strategy_name="weighted_average",
            strategy_engine_mode=mode,
            fallback_reason="strategy_engine_off",
            canary_status=str(metadata.get("canary_status", "shadow")),
            rollback_reason=str(metadata.get("rollback_reason", "")),
        )

    pack, pack_reason = _load_recipe_pack()
    recipes = _recipes_from_pack(pack)
    if not recipes:
        metadata = get_subsystem_metadata("strategy")
        return default_selection(
            strategy_name="weighted_average",
            strategy_engine_mode=mode,
            fallback_reason="no_valid_recipes",
            recipe_pack_version=str(pack.get("pack_version") or DEFAULT_PACK_VERSION),
            canary_status=str(metadata.get("canary_status", "shadow")),
            rollback_reason=str(metadata.get("rollback_reason", "")),
        )

    context = _extract_context_features(
        tensor_groups=tensor_groups,
        model_count=model_count,
        user_weights_provided=user_weights_provided,
    )
    state = _load_local_state()
    recipe_scores = _score_recipes(
        recipes=recipes,
        context=context,
        state=state,
    )
    ranked = sorted(
        recipe_scores.items(),
        key=lambda item: (-float(item[1]), str(item[0])),
    )
    top_recipe_ids = [item[0] for item in ranked[:3]]
    top_recipes = [r for r in recipes if r.recipe_id in set(top_recipe_ids)]
    top_recipes = sorted(top_recipes, key=lambda rec: top_recipe_ids.index(rec.recipe_id))

    best_recipe = top_recipes[0]
    best_score = float("-inf")
    fallback_reason = pack_reason
    groups = [list(group) for group in tensor_groups][:MAX_SAMPLE_GROUPS]
    for recipe in top_recipes:
        score = _proxy_score_recipe(
            recipe=recipe,
            tensor_groups=groups,
            seed=seed,
        )
        if score > best_score:
            best_score = score
            best_recipe = recipe

    # Tiny local exploration around winner.
    elapsed_ms = (time.perf_counter() - started) * 1000.0
    if elapsed_ms < max(200.0, float(budget_ms) * 0.65):
        budget_left_ms = max(0.0, float(budget_ms) - elapsed_ms)
        winner, winner_score = _local_explore(
            winner=best_recipe,
            winner_score=best_score,
            tensor_groups=groups,
            seed=seed,
            budget_left_ms=budget_left_ms,
        )
        best_recipe = winner
        best_score = winner_score

    elapsed_ms = (time.perf_counter() - started) * 1000.0
    if elapsed_ms > float(budget_ms):
        metadata = get_subsystem_metadata("strategy")
        return default_selection(
            strategy_name="weighted_average",
            strategy_engine_mode=mode,
            fallback_reason="selection_budget_exceeded",
            recipe_pack_version=str(pack.get("pack_version") or DEFAULT_PACK_VERSION),
            canary_status=str(metadata.get("canary_status", "shadow")),
            rollback_reason=str(metadata.get("rollback_reason", "")),
        )

    # Update local priors deterministically for top recipes.
    fingerprint = _context_fingerprint(
        context=context,
        source_refs=source_refs or [],
        seed=seed,
        context_label=context_label,
    )
    _update_local_state_priors(
        state=state,
        winner_recipe_id=best_recipe.recipe_id,
        ranked_recipe_ids=top_recipe_ids,
        context_fingerprint=fingerprint,
        best_score=best_score,
    )
    _save_local_state(state)
    optimizer_metadata = record_subsystem_selection(
        subsystem="strategy",
        profile_id=best_recipe.recipe_id,
        pack_version=str(pack.get("pack_version") or DEFAULT_PACK_VERSION),
        selector_version=str(pack.get("selector_version") or SELECTOR_VERSION),
        selection_latency_ms=elapsed_ms,
        quality_ok=True,
        fallback_reason=fallback_reason,
        quality_score=best_score,
    )

    return StrategySelection(
        strategy_engine_mode=mode,
        strategy_name=best_recipe.strategy_name,
        strategy_kwargs=dict(best_recipe.strategy_kwargs),
        recipe_id=best_recipe.recipe_id,
        recipe_pack_version=str(pack.get("pack_version") or DEFAULT_PACK_VERSION),
        selector_version=str(pack.get("selector_version") or SELECTOR_VERSION),
        selection_latency_ms=elapsed_ms,
        fallback_reason=fallback_reason,
        canary_status=str(optimizer_metadata.get("canary_status", "shadow")),
        rollback_reason=str(optimizer_metadata.get("rollback_reason", "")),
    )


def _builtin_pack() -> Dict[str, Any]:
    recipes = [
        {"recipe_id": "wa_v1", "strategy_name": "weighted_average", "strategy_kwargs": {}},
        {"recipe_id": "slerp_v1", "strategy_name": "slerp", "strategy_kwargs": {}},
        {"recipe_id": "ties_d50", "strategy_name": "ties", "strategy_kwargs": {"density": 0.50}},
        {"recipe_id": "ties_d65", "strategy_name": "ties", "strategy_kwargs": {"density": 0.65}},
        {"recipe_id": "dare_r40", "strategy_name": "dare", "strategy_kwargs": {"drop_rate": 0.40}},
        {"recipe_id": "breadcrumbs_d30", "strategy_name": "breadcrumbs", "strategy_kwargs": {"density": 0.30}},
        {"recipe_id": "consensus_t75", "strategy_name": "consensus", "strategy_kwargs": {"consensus_threshold": 0.75}},
    ]
    payload = {
        "pack_version": DEFAULT_PACK_VERSION,
        "selector_version": SELECTOR_VERSION,
        "recipes": recipes,
    }
    payload["checksum_sha256"] = _sha256_json(payload)
    return payload


def _strategy_engine_home() -> Path:
    env_home = str(os.environ.get("OSMOSIS_STRATEGY_HOME", "")).strip()
    if env_home:
        return Path(env_home)
    return optimizer_home() / "strategy_engine"


def _pack_file_path() -> Optional[Path]:
    env_path = str(os.environ.get("OSMOSIS_STRATEGY_PACK", "")).strip()
    if env_path:
        return Path(env_path)
    default = _strategy_engine_home() / "recipe_pack.json"
    return default if default.exists() else None


def _state_file_path() -> Path:
    return _strategy_engine_home() / "local_state.json"


def _load_recipe_pack() -> Tuple[Dict[str, Any], str]:
    path = _pack_file_path()
    if path is None:
        return _builtin_pack(), "builtin_pack"

    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return _builtin_pack(), "invalid_pack_json_fallback_builtin"

    if not isinstance(data, dict):
        return _builtin_pack(), "invalid_pack_schema_fallback_builtin"

    payload = {
        "pack_version": data.get("pack_version", ""),
        "selector_version": data.get("selector_version", ""),
        "recipes": data.get("recipes", []),
    }
    payload_bytes = _canonical_json_bytes(payload)
    expected = str(data.get("checksum_sha256", "")).strip().lower()
    computed = hashlib.sha256(payload_bytes).hexdigest()
    if expected and expected != computed:
        return _builtin_pack(), "pack_checksum_mismatch_fallback_builtin"
    if not expected:
        data["checksum_sha256"] = computed

    # Optional offline signature verification for externally supplied recipe packs.
    trust_block = data.get("trust")
    if isinstance(trust_block, dict):
        try:
            from model_osmosis.format.trust import verify_trust_block_with_backend

            verify = verify_trust_block_with_backend(
                trust_block=trust_block,
                payload_bytes=payload_bytes,
                backend_name=str(trust_block.get("backend") or ""),
            )
            status = str(verify.get("trust_status", "unsigned"))
            if status not in {"trusted", "signed_untrusted"}:
                return _builtin_pack(), "pack_signature_invalid_fallback_builtin"
        except Exception:
            return _builtin_pack(), "pack_signature_invalid_fallback_builtin"

    return data, "external_pack"


def _recipes_from_pack(pack: Dict[str, Any]) -> List[MergeRecipe]:
    entries = pack.get("recipes", []) if isinstance(pack, dict) else []
    recipes: List[MergeRecipe] = []
    for item in entries:
        if not isinstance(item, dict):
            continue
        recipe_id = str(item.get("recipe_id", "")).strip()
        strategy_name = str(item.get("strategy_name", "")).strip()
        strategy_kwargs = item.get("strategy_kwargs", {})
        if not recipe_id or not strategy_name or not isinstance(strategy_kwargs, dict):
            continue
        try:
            # Validate that strategy is known.
            if strategy_name == "dare":
                _ = get_strategy(strategy_name, seed=42, **dict(strategy_kwargs))
            else:
                _ = get_strategy(strategy_name, **dict(strategy_kwargs))
        except Exception:
            continue
        recipes.append(
            MergeRecipe(
                recipe_id=recipe_id,
                strategy_name=strategy_name,
                strategy_kwargs=dict(strategy_kwargs),
            )
        )

    # Merge local recipes from state store.
    state = _load_local_state()
    for item in state.get("local_recipes", []):
        if not isinstance(item, dict):
            continue
        recipe_id = str(item.get("recipe_id", "")).strip()
        strategy_name = str(item.get("strategy_name", "")).strip()
        strategy_kwargs = item.get("strategy_kwargs", {})
        if not recipe_id or not strategy_name or not isinstance(strategy_kwargs, dict):
            continue
        if any(existing.recipe_id == recipe_id for existing in recipes):
            continue
        recipes.append(
            MergeRecipe(
                recipe_id=recipe_id,
                strategy_name=strategy_name,
                strategy_kwargs=dict(strategy_kwargs),
            )
        )
    return sorted(recipes, key=lambda recipe: recipe.recipe_id)


def _extract_context_features(
    *,
    tensor_groups: Sequence[Sequence[np.ndarray]],
    model_count: int,
    user_weights_provided: bool,
) -> Dict[str, float]:
    groups = [list(group) for group in tensor_groups if len(group) >= 2]
    disagreement_values: List[float] = []
    sizes: List[float] = []
    scales: List[float] = []
    for group in groups:
        tensors = [np.asarray(t) for t in group]
        first = tensors[0].astype(np.float64)
        mean = np.mean(np.stack([t.astype(np.float64) for t in tensors], axis=0), axis=0)
        denom = float(np.mean(np.abs(mean)) + 1e-9)
        pair_diffs: List[float] = []
        for idx in range(len(tensors) - 1):
            for jdx in range(idx + 1, len(tensors)):
                diff = np.mean(np.abs(tensors[idx].astype(np.float64) - tensors[jdx].astype(np.float64)))
                pair_diffs.append(float(diff / denom))
        if pair_diffs:
            disagreement_values.append(float(np.mean(pair_diffs)))
        sizes.append(float(first.size))
        scales.append(float(np.mean(np.abs(mean))))

    mean_disagreement = float(np.mean(disagreement_values)) if disagreement_values else 0.0
    std_disagreement = float(np.std(disagreement_values)) if disagreement_values else 0.0
    mean_size = float(np.mean(sizes)) if sizes else 0.0
    mean_scale = float(np.mean(scales)) if scales else 0.0
    has_groups = 1.0 if groups else 0.0
    return {
        "model_count_norm": min(1.0, float(model_count) / 8.0),
        "group_count_norm": min(1.0, float(len(groups)) / float(MAX_SAMPLE_GROUPS)),
        "mean_disagreement": min(2.0, mean_disagreement),
        "std_disagreement": min(2.0, std_disagreement),
        "mean_size_log": float(math.log10(max(mean_size, 1.0))),
        "mean_scale_log": float(math.log10(max(mean_scale, 1e-12)) + 12.0),
        "user_weights": 1.0 if user_weights_provided else 0.0,
        "has_groups": has_groups,
        "bias": 1.0,
    }


def _score_recipes(
    *,
    recipes: Sequence[MergeRecipe],
    context: Dict[str, float],
    state: Dict[str, Any],
) -> Dict[str, float]:
    feature_vec = np.array(
        [
            context["model_count_norm"],
            context["group_count_norm"],
            context["mean_disagreement"],
            context["std_disagreement"],
            context["mean_size_log"],
            context["mean_scale_log"],
            context["user_weights"],
            context["has_groups"],
            context["bias"],
        ],
        dtype=np.float64,
    )
    # Tiny fixed MLP (inference-only).
    w1 = np.array(
        [
            [0.45, -0.25, 0.35, 0.28, -0.17, 0.11, 0.09, 0.02],
            [0.21, 0.42, -0.31, 0.17, 0.07, -0.18, 0.12, -0.05],
            [-0.18, 0.33, 0.47, -0.22, 0.29, 0.16, -0.11, 0.08],
            [0.12, -0.07, 0.38, 0.31, -0.19, 0.26, -0.03, 0.14],
            [0.16, 0.08, -0.11, 0.29, 0.41, -0.13, 0.19, 0.23],
            [-0.09, 0.27, 0.13, -0.21, 0.22, 0.35, 0.24, -0.12],
            [0.11, -0.16, 0.05, 0.14, -0.08, 0.21, 0.33, 0.27],
            [0.19, 0.06, -0.12, 0.04, 0.18, -0.09, 0.25, 0.31],
            [0.08, 0.11, 0.09, 0.07, 0.06, 0.1, 0.12, 0.13],
        ],
        dtype=np.float64,
    )
    b1 = np.array([0.03, -0.06, 0.08, 0.01, -0.02, 0.04, 0.0, 0.02], dtype=np.float64)
    hidden = np.tanh(feature_vec @ w1 + b1)

    w2 = np.array(
        [
            [0.32, 0.14, -0.09, -0.12, -0.06, -0.18],
            [0.07, 0.29, 0.11, -0.08, 0.05, -0.05],
            [-0.18, 0.06, 0.34, 0.18, 0.2, 0.27],
            [0.13, 0.08, 0.2, 0.35, 0.14, 0.12],
            [0.16, 0.21, 0.05, 0.04, 0.31, 0.09],
            [-0.05, 0.03, 0.22, 0.11, 0.17, 0.33],
            [0.24, -0.04, 0.1, 0.07, 0.03, 0.02],
            [0.12, 0.07, 0.18, 0.09, 0.11, 0.16],
        ],
        dtype=np.float64,
    )
    b2 = np.array([0.25, 0.08, -0.01, -0.04, -0.03, -0.05], dtype=np.float64)
    family_logits = hidden @ w2 + b2
    family_index = {
        "weighted_average": 0,
        "slerp": 1,
        "ties": 2,
        "dare": 3,
        "breadcrumbs": 4,
        "consensus": 5,
    }

    priors = state.get("recipe_priors", {}) if isinstance(state, dict) else {}
    scores: Dict[str, float] = {}
    for recipe in recipes:
        idx = family_index.get(recipe.strategy_name, 0)
        score = float(family_logits[idx])
        score += _recipe_param_bonus(recipe, context)
        score += float(priors.get(recipe.recipe_id, 0.0) or 0.0)
        scores[recipe.recipe_id] = score
    return scores


def _recipe_param_bonus(recipe: MergeRecipe, context: Dict[str, float]) -> float:
    disagreement = float(context.get("mean_disagreement", 0.0))
    if recipe.strategy_name == "weighted_average":
        return 0.12 - 0.18 * min(1.0, disagreement)
    if recipe.strategy_name == "slerp":
        return 0.08 - 0.12 * max(0.0, disagreement - 0.15)
    if recipe.strategy_name in {"ties", "breadcrumbs"}:
        density = float(recipe.strategy_kwargs.get("density", 0.5))
        target = 0.65 if disagreement > 0.35 else 0.45
        return 0.10 - abs(density - target)
    if recipe.strategy_name == "dare":
        drop = float(recipe.strategy_kwargs.get("drop_rate", 0.5))
        target = 0.40 if disagreement > 0.30 else 0.25
        return 0.08 - abs(drop - target)
    if recipe.strategy_name == "consensus":
        thresh = float(recipe.strategy_kwargs.get("consensus_threshold", 0.7))
        target = 0.75 if disagreement > 0.25 else 0.60
        return 0.09 - abs(thresh - target)
    return 0.0


def _proxy_score_recipe(
    *,
    recipe: MergeRecipe,
    tensor_groups: Sequence[Sequence[np.ndarray]],
    seed: int,
) -> float:
    if not tensor_groups:
        return 0.0
    kwargs = dict(recipe.strategy_kwargs)
    if recipe.strategy_name == "dare" and "seed" not in kwargs:
        kwargs["seed"] = int(seed)
    strategy = get_strategy(recipe.strategy_name, **kwargs)

    scores: List[float] = []
    for group in tensor_groups:
        tensors = [np.asarray(t) for t in group]
        merged = strategy.merge_tensors(tensors, weights=None).astype(np.float64)
        stack = np.stack([t.astype(np.float64) for t in tensors], axis=0)
        mean = np.mean(stack, axis=0)
        spread = float(np.mean((stack - mean) ** 2) + 1e-9)
        mse = float(np.mean((merged - mean) ** 2))
        mean_abs = float(np.mean(np.abs(mean)) + 1e-9)
        merged_abs = float(np.mean(np.abs(merged)) + 1e-9)
        norm_penalty = abs(math.log(max(1e-9, merged_abs / mean_abs)))
        score = -(mse / spread) - 0.15 * norm_penalty
        if not np.isfinite(score):
            return float("-inf")
        scores.append(float(score))
    return float(np.mean(scores))


def _local_explore(
    *,
    winner: MergeRecipe,
    winner_score: float,
    tensor_groups: Sequence[Sequence[np.ndarray]],
    seed: int,
    budget_left_ms: float,
) -> Tuple[MergeRecipe, float]:
    started = time.perf_counter()
    best_recipe = winner
    best_score = float(winner_score)
    candidates = _perturb_recipe(winner, seed=seed)
    for candidate in candidates:
        elapsed = (time.perf_counter() - started) * 1000.0
        if elapsed > max(150.0, budget_left_ms):
            break
        score = _proxy_score_recipe(
            recipe=candidate,
            tensor_groups=tensor_groups,
            seed=seed,
        )
        if score > best_score + 1e-6:
            best_recipe = candidate
            best_score = score
    return best_recipe, best_score


def _perturb_recipe(recipe: MergeRecipe, *, seed: int) -> List[MergeRecipe]:
    rng = make_numpy_rng(seed + 17)
    strategy = recipe.strategy_name
    kwargs = dict(recipe.strategy_kwargs)
    variants: List[MergeRecipe] = []
    if strategy in {"ties", "breadcrumbs"}:
        value = float(kwargs.get("density", 0.5))
        for delta in (-0.1, 0.1):
            new_value = min(0.95, max(0.05, value + delta))
            new_id = f"local::{strategy}::density::{new_value:.2f}"
            variants.append(
                MergeRecipe(new_id, strategy, {"density": round(new_value, 2)})
            )
    elif strategy == "dare":
        value = float(kwargs.get("drop_rate", 0.4))
        for delta in (-0.1, 0.1):
            new_value = min(0.9, max(0.05, value + delta))
            new_id = f"local::dare::drop_rate::{new_value:.2f}"
            variants.append(
                MergeRecipe(new_id, "dare", {"drop_rate": round(new_value, 2)})
            )
    elif strategy == "consensus":
        value = float(kwargs.get("consensus_threshold", 0.7))
        for delta in (-0.05, 0.05):
            new_value = min(0.98, max(0.5, value + delta))
            new_id = f"local::consensus::threshold::{new_value:.2f}"
            variants.append(
                MergeRecipe(
                    new_id,
                    "consensus",
                    {"consensus_threshold": round(new_value, 2)},
                )
            )
    else:
        # Simple deterministic family perturbation for base recipes.
        families = ["weighted_average", "slerp", "ties", "dare", "breadcrumbs", "consensus"]
        idx = int(rng.integers(0, len(families)))
        alt = families[idx]
        if alt == "ties":
            variants.append(MergeRecipe("local::ties::density::0.55", alt, {"density": 0.55}))
        elif alt == "dare":
            variants.append(MergeRecipe("local::dare::drop_rate::0.35", alt, {"drop_rate": 0.35}))
        elif alt == "breadcrumbs":
            variants.append(MergeRecipe("local::breadcrumbs::density::0.25", alt, {"density": 0.25}))
        elif alt == "consensus":
            variants.append(
                MergeRecipe("local::consensus::threshold::0.72", alt, {"consensus_threshold": 0.72})
            )
        else:
            variants.append(MergeRecipe(f"local::{alt}::v1", alt, {}))
    return variants


def _context_fingerprint(
    *,
    context: Dict[str, float],
    source_refs: Sequence[str],
    seed: int,
    context_label: str,
) -> str:
    payload = {
        "context": {k: float(v) for k, v in sorted(context.items(), key=lambda item: item[0])},
        "source_refs": sorted(str(ref) for ref in source_refs),
        "seed": int(seed),
        "context_label": str(context_label),
    }
    data = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
    return hashlib.sha256(data).hexdigest()[:24]


def _load_local_state() -> Dict[str, Any]:
    path = _state_file_path()
    if not path.exists():
        return {
            "schema_version": STATE_SCHEMA_VERSION,
            "recipe_priors": {},
            "local_recipes": [],
            "contexts": {},
        }
    try:
        state = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {
            "schema_version": STATE_SCHEMA_VERSION,
            "recipe_priors": {},
            "local_recipes": [],
            "contexts": {},
        }
    if not isinstance(state, dict):
        return {
            "schema_version": STATE_SCHEMA_VERSION,
            "recipe_priors": {},
            "local_recipes": [],
            "contexts": {},
        }
    state.setdefault("schema_version", STATE_SCHEMA_VERSION)
    state.setdefault("recipe_priors", {})
    state.setdefault("local_recipes", [])
    state.setdefault("contexts", {})
    return state


def _save_local_state(state: Dict[str, Any]) -> None:
    home = _strategy_engine_home()
    home.mkdir(parents=True, exist_ok=True)
    path = _state_file_path()
    payload = {
        "schema_version": int(state.get("schema_version", STATE_SCHEMA_VERSION)),
        "recipe_priors": {
            str(k): float(v)
            for k, v in sorted((state.get("recipe_priors", {}) or {}).items(), key=lambda item: str(item[0]))
        },
        "local_recipes": sorted(
            [
                {
                    "recipe_id": str(item.get("recipe_id", "")),
                    "strategy_name": str(item.get("strategy_name", "")),
                    "strategy_kwargs": dict(
                        sorted((item.get("strategy_kwargs", {}) or {}).items(), key=lambda kv: str(kv[0]))
                    ),
                }
                for item in (state.get("local_recipes", []) or [])
                if isinstance(item, dict)
            ],
            key=lambda item: str(item.get("recipe_id", "")),
        )[:32],
        "contexts": {
            str(k): {
                "winner_recipe_id": str(v.get("winner_recipe_id", "")),
                "score": float(v.get("score", 0.0) or 0.0),
            }
            for k, v in sorted((state.get("contexts", {}) or {}).items(), key=lambda item: str(item[0]))
            if isinstance(v, dict)
        },
    }
    path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")


def _update_local_state_priors(
    *,
    state: Dict[str, Any],
    winner_recipe_id: str,
    ranked_recipe_ids: Sequence[str],
    context_fingerprint: str,
    best_score: float,
) -> None:
    priors = state.setdefault("recipe_priors", {})
    for idx, recipe_id in enumerate(ranked_recipe_ids):
        current = float(priors.get(recipe_id, 0.0) or 0.0)
        if recipe_id == winner_recipe_id:
            current += 0.02
        else:
            current -= 0.005 * float(idx + 1)
        priors[recipe_id] = max(-0.35, min(0.35, current))

    contexts = state.setdefault("contexts", {})
    contexts[str(context_fingerprint)] = {
        "winner_recipe_id": str(winner_recipe_id),
        "score": float(best_score),
    }

    local_recipes = state.setdefault("local_recipes", [])
    if winner_recipe_id.startswith("local::"):
        if not any(str(item.get("recipe_id", "")) == winner_recipe_id for item in local_recipes if isinstance(item, dict)):
            # Parse minimal local recipe shape from id.
            parsed = _local_recipe_from_id(winner_recipe_id)
            if parsed is not None:
                local_recipes.append(parsed)


def _local_recipe_from_id(recipe_id: str) -> Optional[Dict[str, Any]]:
    rid = str(recipe_id)
    parts = rid.split("::")
    if len(parts) < 3 or parts[0] != "local":
        return None
    strategy = parts[1]
    if strategy in {"weighted_average", "slerp"}:
        return {"recipe_id": rid, "strategy_name": strategy, "strategy_kwargs": {}}
    if strategy in {"ties", "breadcrumbs"} and len(parts) >= 4 and parts[2] == "density":
        try:
            density = float(parts[3])
        except Exception:
            return None
        return {
            "recipe_id": rid,
            "strategy_name": strategy,
            "strategy_kwargs": {"density": round(density, 2)},
        }
    if strategy == "dare" and len(parts) >= 4 and parts[2] == "drop_rate":
        try:
            drop = float(parts[3])
        except Exception:
            return None
        return {
            "recipe_id": rid,
            "strategy_name": "dare",
            "strategy_kwargs": {"drop_rate": round(drop, 2)},
        }
    if strategy == "consensus" and len(parts) >= 4 and parts[2] == "threshold":
        try:
            threshold = float(parts[3])
        except Exception:
            return None
        return {
            "recipe_id": rid,
            "strategy_name": "consensus",
            "strategy_kwargs": {"consensus_threshold": round(threshold, 2)},
        }
    return None


def _sha256_json(data: Dict[str, Any]) -> str:
    return hashlib.sha256(_canonical_json_bytes(data)).hexdigest()


def _canonical_json_bytes(data: Dict[str, Any]) -> bytes:
    return json.dumps(data, sort_keys=True, separators=(",", ":")).encode("utf-8")
