"""
Background evolution scheduler.

A daemon thread that periodically checks whether conditions are met
(AC power, enough trajectories, idle, minimum interval, not paused,
GRPO library present) and runs one GRPO training cycle if all pass.

Also gates text evolution (GEPA skill optimization) cycles with a
shared lock so GRPO and text-evolution never run concurrently.

Started from mugi_backend.py at app launch. Failures are logged
silently; the user never sees training.
"""
from __future__ import annotations

import os
import threading
from datetime import datetime, timedelta

EVOLUTION_CHECK_INTERVAL_SECONDS = 30 * 60  # 30 minutes
EVOLUTION_MIN_INTERVAL_HOURS = 24
EVOLUTION_MIN_TRAJECTORIES = 30

_scheduler_thread: threading.Thread | None = None
_scheduler_stop = threading.Event()

# Shared lock: serializes GRPO training vs text evolution so both never run at once.
_evolution_lock = threading.Lock()


def _log_evolution(msg: str, level: str = "WARNING") -> None:
    try:
        from tools._logging import _debug_log
        _debug_log(f"[evolution/scheduler] {msg}", level)
    except Exception:
        pass


def _is_agent_idle() -> bool:
    """True if no agent run is currently in progress."""
    try:
        from persistence import get_current_agent_thread_id
        return get_current_agent_thread_id() is None
    except Exception:
        return True


def _is_evolution_paused() -> bool:
    """True if evolution_paused is set in prefs."""
    try:
        from utils.app_prefs_store import load_app_prefs_cached
        prefs = load_app_prefs_cached()
        return bool(prefs.get("evolution_paused"))
    except Exception:
        pass
    return False


def _is_text_evolution_paused() -> bool:
    """True if text_evolution_paused is set in prefs."""
    try:
        from utils.app_prefs_store import load_app_prefs_cached
        prefs = load_app_prefs_cached()
        return bool(prefs.get("text_evolution_paused"))
    except Exception:
        pass
    return False


def _is_first_run_ready() -> bool:
    """True if setup wizard is complete and chat is ready."""
    try:
        from utils.app_prefs_store import load_app_prefs_cached
        prefs = load_app_prefs_cached()
        return bool(prefs.get("first_run_ready_for_chat"))
    except Exception:
        pass
    return False


def _check_and_run() -> None:
    """Check all trigger conditions and run GRPO if met."""
    try:
        from evolution.power import is_ac_powered
        from evolution.meta import read_evolution_meta
        from evolution.grpo_runner import run_grpo_training, get_grpo_status, _has_grpo_library
        from evolution.trajectory_logger import count_trajectory_records

        if _is_evolution_paused():
            return

        if not _is_agent_idle():
            return

        if not is_ac_powered():
            return

        status = get_grpo_status()
        if status.get("running"):
            return

        if not _has_grpo_library():
            return

        meta = read_evolution_meta()

        last_at = meta.get("last_training_at")
        if last_at:
            try:
                last_dt = datetime.fromisoformat(last_at)
                if datetime.now() - last_dt < timedelta(hours=EVOLUTION_MIN_INTERVAL_HOURS):
                    return
            except Exception:
                pass

        traj_count = count_trajectory_records(since_iso=last_at)
        if traj_count < EVOLUTION_MIN_TRAJECTORIES:
            return

        if not _evolution_lock.acquire(blocking=False):
            return
        try:
            _log_evolution(
                f"Trigger conditions met ({traj_count} trajectories). Starting GRPO training.",
                "INFO",
            )
            result = run_grpo_training()
            _log_evolution(f"GRPO result: {result.get('message', '')}", "INFO")
        finally:
            _evolution_lock.release()

    except Exception as e:
        _log_evolution(f"_check_and_run failed: {e}", "ERROR")


def _check_and_run_text_evolution() -> None:
    """Check conditions and run one text evolution (GEPA) cycle if met."""
    try:
        from core.config import get_text_evolution_config
        te_cfg = get_text_evolution_config()

        if not te_cfg.get("enabled"):
            return

        if _is_text_evolution_paused():
            return

        if not _is_first_run_ready():
            return

        if not _is_agent_idle():
            return

        from evolution.power import is_ac_powered
        if not te_cfg.get("allow_on_battery") and not is_ac_powered():
            return

        from evolution.skill_gepa import (
            has_dspy_library, get_text_evolution_status,
            get_text_evolution_meta, clear_stale_running,
        )

        if not has_dspy_library():
            return

        te_status = get_text_evolution_status()
        if te_status.get("running"):
            return

        te_meta = get_text_evolution_meta()
        min_interval = te_cfg.get("min_interval_hours", 24)
        last_at = te_meta.get("last_text_evolution_at")
        if last_at:
            try:
                last_dt = datetime.fromisoformat(last_at)
                if datetime.now(last_dt.tzinfo) - last_dt < timedelta(hours=min_interval):
                    return
            except Exception:
                pass

        # Load eval tasks (auto-generate if needed once we have a skill target)
        from evolution.skill_eval_harness import ensure_eval_tasks_for_skill

        # Pick a skill to optimize (from mining or first available)
        skill_name, baseline_text = _pick_skill_target(te_cfg)
        if not skill_name or not baseline_text:
            return

        eval_tasks = ensure_eval_tasks_for_skill(skill_name, baseline_text, te_cfg)
        min_tasks = te_cfg.get("min_eval_tasks", 3)
        if len(eval_tasks) < min_tasks:
            return

        if not _evolution_lock.acquire(blocking=False):
            return
        try:
            _log_evolution(f"Starting text evolution for skill '{skill_name}'", "INFO")
            from evolution.skill_gepa import run_gepa_optimization
            result = run_gepa_optimization(
                skill_name=skill_name,
                baseline_text=baseline_text,
                eval_tasks=eval_tasks,
                config=te_cfg,
                stop_check=lambda: _scheduler_stop.is_set(),
            )
            _log_evolution(f"Text evolution result: improved={result.get('improved')}", "INFO")
        finally:
            _evolution_lock.release()

    except Exception as e:
        _log_evolution(f"_check_and_run_text_evolution failed: {e}", "ERROR")


def _pick_skill_target(config: dict) -> tuple[str | None, str | None]:
    """Select which skill to optimize this cycle. Returns (name, baseline_text)."""
    try:
        from evolution.skill_mining import mine_optimization_targets
        targets = mine_optimization_targets(max_results=5)
    except Exception:
        targets = []

    # Try each mined target; fall back to first available skill with eval tasks
    from tools.skills_tools import _normalize_name, _safe_skill_path, _list_skill_dirs
    from evolution.pending_store import has_pending_for_skill

    candidates = [t["skill_name"] for t in targets] if targets else _list_skill_dirs()
    for name in candidates:
        norm = _normalize_name(name)
        if has_pending_for_skill(norm):
            continue
        path, err = _safe_skill_path(norm, "SKILL.md")
        if not path or err or not os.path.isfile(path):
            continue
        try:
            with open(path, "r", encoding="utf-8") as f:
                text = f.read()
            if text.strip():
                return norm, text
        except Exception:
            continue
    return None, None


def _scheduler_loop() -> None:
    """Main loop for the background scheduler thread."""
    while not _scheduler_stop.is_set():
        _scheduler_stop.wait(EVOLUTION_CHECK_INTERVAL_SECONDS)
        if _scheduler_stop.is_set():
            break
        try:
            _check_and_run()
        except Exception as e:
            _log_evolution(f"scheduler_loop GRPO iteration failed: {e}", "ERROR")
        try:
            _check_and_run_text_evolution()
        except Exception as e:
            _log_evolution(f"scheduler_loop text-evolution iteration failed: {e}", "ERROR")


def start_evolution_scheduler() -> None:
    """Start the background evolution scheduler (daemon thread). Safe to call multiple times."""
    global _scheduler_thread
    if _scheduler_thread is not None and _scheduler_thread.is_alive():
        return

    # Crash recovery: clear stale running flags
    try:
        from evolution.skill_gepa import clear_stale_running
        clear_stale_running()
    except Exception:
        pass

    _scheduler_stop.clear()
    _scheduler_thread = threading.Thread(
        target=_scheduler_loop,
        name="EvolutionScheduler",
        daemon=True,
    )
    _scheduler_thread.start()
    _log_evolution("Evolution scheduler started.", "INFO")


def stop_evolution_scheduler() -> None:
    """Signal the scheduler to stop (for clean shutdown)."""
    _scheduler_stop.set()
