"""OpenEvolve — a faithful port of the real OpenEvolve (the open AlphaEvolve).

Every component is the OpenEvolve-specific subclass from :mod:`.components`:

* Population  → :class:`MapElitesIslandsPopulation` — per-island MAP-Elites grids over
  ``feature_dimensions`` + global elite archive + lazy ring migration (``ProgramDatabase``).
* Selection   → :class:`OpenEvolveSelectionPolicy` — round-robin island rotation + 3-tier
  explore/exploit/random parent sampling + island-best/elite/diverse/random inspirations.
* PromptBuilder → :class:`OpenEvolvePromptBuilder` — the OpenEvolve diff template, section for section.
* Proposer    → :class:`~galapagos.components.proposer.DiffProposer` — SEARCH/REPLACE + full-rewrite
  parsing, matching OpenEvolve's ``apply_diff`` / ``parse_full_rewrite``.

The scaffold adds OpenEvolve's per-iteration island bookkeeping: after each admitted child it
increments that island's generation counter and, when ``max(generations) - last_migration >=
migration_interval``, performs ring migration (``process_parallel.py`` island-management block).
"""
from __future__ import annotations

import logging

from ...config import GalapagosConfig
from ...models import GalapagosModel
from ...records import Genome
from ..base_scaffold import GalapagosScaffold
from ..registry import register_scaffold
# one module per component (the OpenEvolve scaffold method)
from .memory import OpenEvolveMemory
from .population import MapElitesIslandsPopulation
from .prompt_builder import OpenEvolvePromptBuilder
from .proposer import OpenEvolveProposer
from .selection_policy import OpenEvolveSelectionPolicy

log = logging.getLogger(__name__)


@register_scaffold("openevolve")
class OpenEvolveScaffold(GalapagosScaffold):
    name = "openevolve"

    @classmethod
    def build_components(cls, config: GalapagosConfig, model: GalapagosModel | None) -> dict:
        seed = int(config.seed)
        pop = config.population
        sel = config.selection_policy

        num_islands = int(pop.num_islands)
        feature_dimensions = list(pop.feature_dimensions)

        return {
            "population": MapElitesIslandsPopulation(
                num_islands=num_islands,
                archive_size=int(pop.archive_size),
                population_size=int(pop.population_size),
                feature_dimensions=feature_dimensions,
                feature_bins=int(pop.feature_bins),
                migration_interval=int(pop.migration_interval),
                migration_rate=float(pop.migration_rate),
                diversity_reference_size=int(pop.diversity_reference_size),
            ),
            "selection_policy": OpenEvolveSelectionPolicy(
                seed=seed,
                num_islands=num_islands,
                exploration_ratio=float(sel.exploration_ratio),
                exploitation_ratio=float(sel.exploitation_ratio),
                elite_selection_ratio=float(sel.elite_selection_ratio),
                num_inspirations=int(sel.num_inspirations),
                num_diverse=int(sel.num_diverse),
                feature_dimensions=feature_dimensions,
            ),
            "prompt_builder": OpenEvolvePromptBuilder(
                num_top_programs=int(sel.num_inspirations),
                num_diverse=int(sel.num_diverse),
                feature_dimensions=feature_dimensions,
            ),
            "proposer": OpenEvolveProposer(),
            "memory": OpenEvolveMemory(),
        }

    def setup(self, task) -> None:
        super().setup(task)
        # OpenEvolve's EvaluatorConfig defaults to a 3-stage cascade with thresholds [0.5, 0.75, 0.9]
        # (vs the framework/SkyDiscover default [0.3, 0.6]). Apply OpenEvolve's gates unless the task
        # card declared its own — the task knows its stage semantics best. Only bites cascade tasks.
        from ...components.evaluator import ContainerEvaluator, SubprocessEvaluator
        # Both deployments (local subprocess + Docker sandbox) carry cascade_thresholds/max_retries,
        # so OpenEvolve's gates apply identically whether the task runs locally or in a container.
        if isinstance(self.evaluator, (SubprocessEvaluator, ContainerEvaluator)):
            card_eval = (getattr(getattr(self.task, "card", None), "evaluation", None) or {})
            if card_eval.get("cascade_thresholds") is None:
                self.evaluator.cascade_thresholds = [0.5, 0.75, 0.9]
            # OpenEvolve EvaluatorConfig.max_retries default = 3 (re-run a candidate on transient failure).
            self.evaluator.max_retries = int(card_eval.get("max_retries", 3))

    # ---- OpenEvolve island bookkeeping (process_parallel.py island-management block) ---------
    def after_step(self, child: Genome, result) -> None:
        """After an admitted child: bump its island's generation counter, then migrate if due."""
        if result is None:  # NO_DIFF step (no child evaluated/added) — nothing to bump
            return
        pop = self.population
        if not isinstance(pop, MapElitesIslandsPopulation):
            return
        island = child.metadata.get("island", pop.current_island)
        pop.increment_island_generation(island)
        if pop.should_migrate():
            pop.migrate()
            log.debug("ring migration — island=%s iter=%d", island, self.state.iteration)
