"""BLCE Agent Pool — heuristic multi-provider routing and adaptive orchestration.

Provides:
- ``AgentPool`` — scores providers for task types and executes with fallback.
- ``AdaptiveOrchestrator`` — wraps AgentRuntime with bottleneck detection.

All routing is deterministic/heuristic — no actual LLM API calls are made.
"""
from __future__ import annotations

import logging
import time
from typing import Any, Callable, Dict, List, Optional

from .contracts import ProviderScore, RuntimeResult

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Scoring heuristics
# ---------------------------------------------------------------------------

_TASK_PROVIDER_SCORES: Dict[str, Dict[str, float]] = {
    "parse": {"deterministic": 0.95, "claude": 0.50, "codex": 0.50, "gemini": 0.45},
    "extract": {"deterministic": 0.95, "claude": 0.55, "codex": 0.50, "gemini": 0.50},
    "normalize": {"deterministic": 0.90, "claude": 0.60, "codex": 0.55, "gemini": 0.55},
    "semantic": {"claude": 0.90, "gemini": 0.85, "deterministic": 0.50, "codex": 0.60},
    "explain": {"claude": 0.90, "gemini": 0.85, "codex": 0.65, "deterministic": 0.40},
    "cross_reference": {"codex": 0.85, "deterministic": 0.80, "claude": 0.70, "gemini": 0.70},
    "merge": {"codex": 0.85, "deterministic": 0.80, "claude": 0.75, "gemini": 0.70},
    "quality": {"deterministic": 0.85, "claude": 0.75, "codex": 0.70, "gemini": 0.70},
    "governance": {"deterministic": 0.90, "claude": 0.65, "codex": 0.60, "gemini": 0.55},
}

_DEFAULT_SCORE: Dict[str, float] = {
    "deterministic": 0.60,
    "claude": 0.50,
    "codex": 0.45,
    "gemini": 0.45,
}


class AgentPool:
    """Heuristic provider routing with fallback execution.

    Routes tasks to the best-scoring provider and falls back to the next
    provider on failure.  All scoring is deterministic — no LLM calls.

    Parameters
    ----------
    providers : list[str] | None
        Available provider names.  Defaults to
        ``["deterministic", "claude", "codex", "gemini"]``.
    max_retries : int
        Maximum fallback attempts before giving up.
    """

    def __init__(
        self,
        providers: Optional[List[str]] = None,
        max_retries: int = 2,
    ):
        self.providers = providers or ["deterministic", "claude", "codex", "gemini"]
        self.max_retries = max_retries
        self._stats: Dict[str, Dict[str, Any]] = {
            p: {"calls": 0, "successes": 0, "failures": 0, "total_duration": 0.0}
            for p in self.providers
        }
        self._task_history: List[Dict[str, Any]] = []

    # -- scoring -----------------------------------------------------------

    def score_providers(
        self,
        task_type: str,
        context: Optional[Dict[str, Any]] = None,
    ) -> List[ProviderScore]:
        """Score all providers for *task_type* and return sorted list (best first)."""
        scores_map = _TASK_PROVIDER_SCORES.get(task_type, _DEFAULT_SCORE)
        results: List[ProviderScore] = []

        for prov in self.providers:
            base = scores_map.get(prov, _DEFAULT_SCORE.get(prov, 0.40))

            # Context-aware adjustments
            cost_w = 1.0
            latency_w = 1.0
            reason = f"base score for {task_type}"

            if context:
                if context.get("prefer_speed"):
                    if prov == "deterministic":
                        base = min(1.0, base + 0.10)
                        reason += " +speed_bonus"
                        latency_w = 0.8
                if context.get("prefer_accuracy"):
                    if prov in ("claude", "codex"):
                        base = min(1.0, base + 0.05)
                        reason += " +accuracy_bonus"

            results.append(ProviderScore(
                provider=prov,
                score=round(base, 3),
                reason=reason,
                cost_weight=cost_w,
                latency_weight=latency_w,
            ))

        results.sort(key=lambda s: s.score, reverse=True)
        return results

    # -- routing -----------------------------------------------------------

    def route_task(
        self,
        task_type: str,
        context: Optional[Dict[str, Any]] = None,
    ) -> str:
        """Return the top-scored provider name for *task_type*."""
        scores = self.score_providers(task_type, context)
        return scores[0].provider if scores else "deterministic"

    # -- execution ---------------------------------------------------------

    def execute_with_fallback(
        self,
        task_type: str,
        callable_fn: Callable[..., Any],
        context: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Execute *callable_fn* using the best provider with fallback.

        On failure, retries with the next provider in score order.
        Returns a dict with ``provider``, ``result``, and ``attempts``.
        """
        scores = self.score_providers(task_type, context)
        attempts = 0
        last_error = ""

        for ps in scores[: self.max_retries + 1]:
            provider = ps.provider
            attempts += 1
            t0 = time.monotonic()
            try:
                result = callable_fn(provider=provider)
                dur = time.monotonic() - t0
                self._record(provider, True, dur)
                entry = {
                    "task_type": task_type,
                    "provider": provider,
                    "success": True,
                    "attempts": attempts,
                    "duration": round(dur, 4),
                }
                self._task_history.append(entry)
                return {
                    "provider": provider,
                    "result": result,
                    "attempts": attempts,
                    "success": True,
                }
            except Exception as exc:
                dur = time.monotonic() - t0
                self._record(provider, False, dur)
                last_error = str(exc)
                logger.debug("Provider %s failed for %s: %s", provider, task_type, exc)

        entry = {
            "task_type": task_type,
            "provider": "none",
            "success": False,
            "attempts": attempts,
            "error": last_error,
        }
        self._task_history.append(entry)
        return {
            "provider": "none",
            "result": None,
            "attempts": attempts,
            "success": False,
            "error": last_error,
        }

    # -- stats -------------------------------------------------------------

    def get_provider_stats(self) -> Dict[str, Any]:
        """Return usage counts, success rates, and avg durations per provider."""
        out: Dict[str, Any] = {}
        for prov, s in self._stats.items():
            calls = s["calls"]
            out[prov] = {
                "calls": calls,
                "successes": s["successes"],
                "failures": s["failures"],
                "success_rate": round(s["successes"] / calls, 3) if calls else 0.0,
                "avg_duration": round(s["total_duration"] / calls, 4) if calls else 0.0,
            }
        return out

    def get_task_history(self) -> List[Dict[str, Any]]:
        """Return the full execution log."""
        return list(self._task_history)

    # -- internals ---------------------------------------------------------

    def _record(self, provider: str, success: bool, duration: float) -> None:
        if provider not in self._stats:
            self._stats[provider] = {
                "calls": 0, "successes": 0, "failures": 0, "total_duration": 0.0,
            }
        self._stats[provider]["calls"] += 1
        if success:
            self._stats[provider]["successes"] += 1
        else:
            self._stats[provider]["failures"] += 1
        self._stats[provider]["total_duration"] += duration


# ---------------------------------------------------------------------------
# Adaptive Orchestrator
# ---------------------------------------------------------------------------

class AdaptiveOrchestrator:
    """Wraps AgentRuntime with bottleneck detection and scaling recommendations.

    Parameters
    ----------
    pool : AgentPool
        The provider pool for routing decisions.
    runtime : AgentRuntime | None
        An optional AgentRuntime instance for phase execution.
    """

    def __init__(self, pool: AgentPool, runtime=None):
        self.pool = pool
        self.runtime = runtime

    def detect_bottleneck(self, task_details: List[Dict[str, Any]]) -> Optional[str]:
        """Find the slowest completed phase from task_details.

        Returns the phase name or ``None`` if no completed tasks exist.
        """
        completed = [
            d for d in task_details
            if d.get("status") == "completed" and d.get("duration", 0) > 0
        ]
        if not completed:
            return None
        slowest = max(completed, key=lambda d: d.get("duration", 0))
        return slowest.get("name")

    def recommend_scaling(self, task_details: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Suggest worker adjustments based on task durations.

        Returns a dict with ``bottleneck``, ``recommendation``,
        and ``suggested_workers``.
        """
        bottleneck = self.detect_bottleneck(task_details)
        if not bottleneck:
            return {
                "bottleneck": None,
                "recommendation": "No bottleneck detected",
                "suggested_workers": 4,
            }

        # Find median duration
        durations = [
            d.get("duration", 0) for d in task_details
            if d.get("status") == "completed"
        ]
        if not durations:
            return {
                "bottleneck": bottleneck,
                "recommendation": "Insufficient data",
                "suggested_workers": 4,
            }

        avg_dur = sum(durations) / len(durations)
        bottleneck_dur = max(durations)

        if bottleneck_dur > avg_dur * 2:
            suggestion = min(8, len(durations))
            recommendation = (
                f"Phase '{bottleneck}' is {bottleneck_dur / avg_dur:.1f}x slower "
                f"than average. Consider increasing workers to {suggestion}."
            )
        else:
            suggestion = 4
            recommendation = "All phases within normal range."

        return {
            "bottleneck": bottleneck,
            "recommendation": recommendation,
            "suggested_workers": suggestion,
        }

    def run_adaptive(self, context: Dict[str, Any]) -> RuntimeResult:
        """Run the pipeline with provider-aware routing.

        If a runtime is configured, executes it and returns the result.
        Otherwise returns an empty RuntimeResult.
        """
        if self.runtime is not None:
            for task in self.runtime.tasks.values():
                task.kwargs["context"] = context
            result = self.runtime.run()
            return result

        return RuntimeResult(
            tasks_submitted=0,
            tasks_completed=0,
        )
