"""
Queue Resolution Service

Analyzes task prompts and recommends optimal worker queues using LLM-based analysis.
Runs on the control plane API server using Agno+LiteLLM (not Temporal).

Features:
- Pattern-based analysis with LLM tie-breaking
- Circuit breaker for LLM failures
- Exponential backoff retries
- Request-level caching
- Timeout protection
- Graceful degradation
"""

from typing import Dict, Any, List, Optional
import structlog
import re
import json
import asyncio
import time
from functools import lru_cache
from datetime import datetime, timezone, timedelta

from sqlalchemy.orm import Session
from control_plane_api.app.models.worker import WorkerQueue
from control_plane_api.app.services.agno_service import agno_service

logger = structlog.get_logger()


class CircuitBreaker:
    """Circuit breaker for LLM calls to prevent cascading failures"""

    def __init__(
        self,
        failure_threshold: int = 5,
        recovery_timeout: int = 60,
        expected_exception: type = Exception
    ):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.expected_exception = expected_exception

        self.failure_count = 0
        self.last_failure_time = None
        self.state = "closed"  # closed, open, half_open

    def call(self, func):
        """Decorator to protect function calls"""
        async def wrapper(*args, **kwargs):
            if self.state == "open":
                if self._should_attempt_reset():
                    self.state = "half_open"
                    logger.info("Circuit breaker entering half-open state")
                else:
                    raise Exception("Circuit breaker is OPEN - LLM calls disabled")

            try:
                result = await func(*args, **kwargs)
                self._on_success()
                return result
            except self.expected_exception as e:
                self._on_failure()
                raise e

        return wrapper

    def _should_attempt_reset(self) -> bool:
        """Check if enough time has passed to attempt recovery"""
        if not self.last_failure_time:
            return True
        return (datetime.now(timezone.utc) - self.last_failure_time).total_seconds() >= self.recovery_timeout

    def _on_success(self):
        """Reset circuit breaker on success"""
        self.failure_count = 0
        self.state = "closed"

    def _on_failure(self):
        """Track failure and potentially open circuit"""
        self.failure_count += 1
        self.last_failure_time = datetime.now(timezone.utc)

        if self.failure_count >= self.failure_threshold:
            self.state = "open"
            logger.error(
                "Circuit breaker OPENED",
                failure_count=self.failure_count,
                threshold=self.failure_threshold
            )


class QueueResolutionService:
    """Service for intelligent worker queue selection using LLM analysis"""

    def __init__(self):
        self.llm_circuit_breaker = CircuitBreaker(
            failure_threshold=5,
            recovery_timeout=60,
            expected_exception=Exception
        )

        # Cache for queue lists (short TTL to stay fresh)
        self._queue_cache: Dict[str, tuple[List[Dict], float]] = {}
        self._queue_cache_ttl = 30  # 30 seconds

        # Performance metrics
        self._metrics = {
            "llm_calls": 0,
            "llm_failures": 0,
            "pattern_only_resolutions": 0,
            "cache_hits": 0,
        }

    async def resolve_queue(
        self,
        db: Session,
        prompt: str,
        organization_id: str,
        preferred_runtime: str = "claude_code",
    ) -> Dict[str, Any]:
        """
        Analyze task prompt and recommend optimal worker queue.

        Args:
            db: Database session
            prompt: Task prompt to analyze
            organization_id: Organization ID
            preferred_runtime: Preferred runtime (filters queues)

        Returns:
            {
                "worker_queue_id": str,
                "worker_queue_name": str,
                "reasoning": str,
                "confidence": "high" | "medium" | "low",
                "alternatives": List[Dict]
            }
        """
        start_time = time.time()

        logger.info(
            "Resolving worker queue",
            organization_id=organization_id,
            preferred_runtime=preferred_runtime,
            prompt_length=len(prompt)
        )

        try:
            # STEP 1: Fetch available worker queues (with caching)
            available_queues = await self._get_available_queues_cached(
                db=db,
                organization_id=organization_id,
                runtime=preferred_runtime
            )

            if not available_queues:
                raise ValueError(
                    f"No active worker queues with registered workers found for organization {organization_id}. "
                    f"Please ensure at least one worker is running and heartbeating for runtime '{preferred_runtime}'."
                )

            # STEP 2: Analyze task requirements using pattern matching (fast, deterministic)
            task_analysis = self._analyze_task_patterns(prompt)

            logger.info(
                "Task analysis complete",
                task_category=task_analysis.get("task_category"),
                capabilities=task_analysis.get("required_capabilities"),
                complexity=task_analysis.get("complexity")
            )

            # STEP 3: Score queues based on analysis
            scored_queues = self._score_queues(task_analysis, available_queues)

            # STEP 4: Use LLM for final recommendation if needed
            # Only use LLM if top 2 scores are close (within 15 points)
            if len(scored_queues) >= 2 and abs(scored_queues[0]["score"] - scored_queues[1]["score"]) <= 15:
                logger.info("Scores are close, attempting LLM recommendation")

                try:
                    llm_recommendation = await self._llm_recommend_queue_with_retry(
                        prompt=prompt,
                        task_analysis=task_analysis,
                        top_queues=scored_queues[:3]  # Top 3 candidates
                    )

                    # Find the recommended queue in scored_queues
                    recommended = next(
                        (q for q in scored_queues if q["queue_id"] == llm_recommendation["queue_id"]),
                        scored_queues[0]  # Fallback to highest scored
                    )

                    # Update reasoning with LLM insight
                    recommended["reasoning"] = f"{recommended['reasoning']}; LLM: {llm_recommendation.get('reasoning', '')}"

                except Exception as e:
                    logger.warning("LLM recommendation failed, using pattern-based", error=str(e))
                    recommended = scored_queues[0]
                    self._metrics["pattern_only_resolutions"] += 1
            else:
                # Clear winner, use pattern-based recommendation
                recommended = scored_queues[0]
                self._metrics["pattern_only_resolutions"] += 1

            elapsed = time.time() - start_time
            logger.info(
                "Queue resolution complete",
                queue_id=recommended["queue_id"],
                queue_name=recommended["queue_name"],
                confidence=recommended["confidence"],
                score=recommended["score"],
                elapsed_ms=int(elapsed * 1000)
            )

            return {
                "worker_queue_id": recommended["queue_id"],
                "worker_queue_name": recommended["queue_name"],
                "reasoning": recommended["reasoning"],
                "confidence": recommended["confidence"],
                "alternatives": scored_queues[1:3]  # Next 2 alternatives
            }

        except ValueError as e:
            # Re-raise validation errors (no queues found, etc.)
            logger.error("Queue resolution validation error", error=str(e))
            raise
        except Exception as e:
            logger.error("Queue resolution failed", error=str(e), exc_info=True)
            raise RuntimeError(f"Failed to resolve worker queue: {str(e)}")

    async def _get_available_queues_cached(
        self,
        db: Session,
        organization_id: str,
        runtime: str
    ) -> List[Dict[str, Any]]:
        """
        Fetch active worker queues from database with caching.

        Returns:
            List of queue dicts with metadata
        """
        cache_key = f"{organization_id}:{runtime}"

        # Check cache
        if cache_key in self._queue_cache:
            cached_queues, cached_time = self._queue_cache[cache_key]
            if time.time() - cached_time < self._queue_cache_ttl:
                self._metrics["cache_hits"] += 1
                logger.debug("Queue cache hit", cache_key=cache_key)
                return cached_queues

        # Cache miss - fetch from database
        queues = await self._get_available_queues(db, organization_id, runtime)

        # Update cache
        self._queue_cache[cache_key] = (queues, time.time())

        # Clean old cache entries (keep cache size manageable)
        self._cleanup_cache()

        return queues

    async def _get_available_queues(
        self,
        db: Session,
        organization_id: str,
        runtime: str
    ) -> List[Dict[str, Any]]:
        """
        Fetch active worker queues from database with active worker validation.

        CRITICAL: Only returns queues that have active workers registered.

        Returns:
            List of queue dicts with metadata, filtered to only include queues with active workers
        """
        try:
            from control_plane_api.app.models.worker import WorkerHeartbeat
            from sqlalchemy import func
            from datetime import datetime, timedelta

            # Build query with registered worker count
            # Real-time worker availability is validated at execution time via Redis
            query_results = db.query(
                WorkerQueue,
                func.count(WorkerHeartbeat.id).label('active_workers')
            ).outerjoin(
                WorkerHeartbeat,
                (WorkerHeartbeat.worker_queue_id == WorkerQueue.id) &
                (WorkerHeartbeat.organization_id == organization_id)  # CRITICAL: Org scoping
            ).filter(
                WorkerQueue.organization_id == organization_id,
                WorkerQueue.status == "active",
            ).group_by(WorkerQueue.id).all()

            result = []
            for queue, active_workers in query_results:
                # CRITICAL: Skip queues with no active workers
                if active_workers == 0:
                    logger.debug(
                        "Skipping queue with no active workers",
                        queue_id=str(queue.id),
                        queue_name=queue.name
                    )
                    continue

                try:
                    # Extract runtime from settings JSONB or tags
                    settings = queue.settings or {}
                    queue_runtime = settings.get("runtime", "claude_code")  # Default to claude_code

                    # Also check tags for runtime indicators
                    tags_list = queue.tags or []
                    if "claude_code" in tags_list:
                        queue_runtime = "claude_code"
                    elif "agno" in tags_list or "default" in tags_list:
                        queue_runtime = "default"

                    queue_dict = {
                        "id": str(queue.id),
                        "name": queue.name,
                        "description": queue.description or "",
                        "tags": tags_list,
                        "max_workers": queue.max_workers or 10,
                        "runtime": queue_runtime,
                        "capabilities": settings.get("capabilities", {}),
                        "created_at": queue.created_at.isoformat() if queue.created_at else None,
                    }

                    # Set current_load based on active worker count
                    max_workers = queue.max_workers or 10
                    queue_dict["current_load"] = active_workers / max_workers if max_workers > 0 else 0
                    queue_dict["active_workers"] = active_workers

                    result.append(queue_dict)

                except Exception as e:
                    logger.warning(
                        "Failed to process queue",
                        queue_id=str(queue.id),
                        error=str(e)
                    )
                    continue

            # Filter by runtime
            if runtime:
                result = [q for q in result if q["runtime"] == runtime]

            logger.info(
                "Fetched available queues with active workers",
                organization_id=organization_id,
                runtime=runtime,
                count=len(result),
                total_queues_checked=len(query_results),
                queues_with_workers=len(result)
            )

            return result

        except Exception as e:
            logger.error("Failed to fetch queues from database", error=str(e))
            raise

    @lru_cache(maxsize=256)
    def _analyze_task_patterns(self, prompt: str) -> Dict[str, Any]:
        """
        Analyze task prompt using pattern matching to extract requirements.
        Cached for performance.

        Returns:
            {
                "required_capabilities": List[str],
                "task_category": str,
                "complexity": str,
                "keywords": List[str],
                "prompt_length": int
            }
        """
        prompt_lower = prompt.lower()

        # Pattern-based capability detection
        capability_patterns = {
            "kubernetes": r"\b(k8s|kubernetes|kubectl|pod|deployment|service|helm)\b",
            "docker": r"\b(docker|container|image|dockerfile|compose)\b",
            "git": r"\b(git|github|gitlab|commit|branch|merge|pr|pull request)\b",
            "terraform": r"\b(terraform|tf|infrastructure|iac|provisioning)\b",
            "aws": r"\b(aws|ec2|s3|lambda|cloudformation|eks|ecs)\b",
            "database": r"\b(database|postgres|mysql|mongodb|sql|redis)\b",
            "python": r"\b(python|pip|django|flask|fastapi|pytest)\b",
            "nodejs": r"\b(node|npm|yarn|javascript|typescript|next\.js|react)\b",
            "cicd": r"\b(ci|cd|pipeline|jenkins|github actions|gitlab ci)\b",
            "security": r"\b(security|vulnerability|cve|audit|penetration test)\b",
        }

        required_capabilities = []
        for capability, pattern in capability_patterns.items():
            if re.search(pattern, prompt_lower):
                required_capabilities.append(capability)

        # Categorize task
        category_patterns = {
            "deployment": r"\b(deploy|release|rollout|publish|ship)\b",
            "infrastructure": r"\b(provision|infrastructure|setup|terraform|cloudformation)\b",
            "development": r"\b(code|develop|implement|build|create|refactor)\b",
            "monitoring": r"\b(monitor|alert|metric|log|trace|observe)\b",
            "security": r"\b(security|scan|vulnerability|audit|compliance|pentest)\b",
            "testing": r"\b(test|qa|verify|validate|check|e2e)\b",
            "data": r"\b(data|etl|pipeline|analytics|warehouse)\b",
        }

        task_category = "general"
        for category, pattern in category_patterns.items():
            if re.search(pattern, prompt_lower):
                task_category = category
                break

        # Estimate complexity
        complexity_indicators = len(re.findall(
            r"\b(complex|multiple|all|entire|comprehensive|distributed|microservice)\b",
            prompt_lower
        ))

        if len(prompt) > 200 or complexity_indicators > 2:
            complexity = "high"
        elif len(prompt) > 100 or complexity_indicators > 0:
            complexity = "medium"
        else:
            complexity = "low"

        # Extract action keywords
        action_keywords = re.findall(
            r"\b(deploy|create|build|setup|configure|monitor|test|scan|analyze|fix|update|migrate)\b",
            prompt_lower
        )

        return {
            "required_capabilities": required_capabilities,
            "task_category": task_category,
            "complexity": complexity,
            "keywords": list(set(action_keywords)),
            "prompt_length": len(prompt),
        }

    def _score_queues(
        self,
        task_analysis: Dict[str, Any],
        queues: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """
        Score and rank queues based on task requirements.

        Scoring:
        - Tag/capability overlap: +30 points
        - Low load: +20 points
        - Runtime match: +20 points
        - Capability metadata: +30 points

        Returns:
            Sorted list of scored queues (highest first)
        """
        scores = []

        for queue in queues:
            score = 0
            reasons = []

            # 1. Tag/capability overlap (30 points max)
            required_caps = set(task_analysis.get("required_capabilities", []))
            queue_tags = set(queue.get("tags", []))

            overlap = required_caps & queue_tags
            if overlap:
                overlap_score = min(30, len(overlap) * 10)
                score += overlap_score
                reasons.append(f"Matching capabilities: {', '.join(overlap)}")

            # 2. Low load (20 points max)
            current_load = queue.get("current_load", 0)
            max_workers = queue.get("max_workers", 10)
            load_ratio = current_load / max_workers if max_workers > 0 else 1.0

            if load_ratio < 0.3:
                score += 20
                reasons.append("Low current load (<30%)")
            elif load_ratio < 0.7:
                score += 10
                reasons.append("Moderate load")

            # 3. Runtime match (20 points - already filtered)
            score += 20
            reasons.append(f"Runtime: {queue.get('runtime')}")

            # 4. Capability metadata match (30 points max)
            capabilities = queue.get("capabilities", {})
            if capabilities:
                cap_keys = set(str(k).lower() for k in capabilities.keys())
                keyword_match = any(
                    kw in cap_keys
                    for kw in task_analysis.get("keywords", [])
                )
                if keyword_match:
                    score += 30
                    reasons.append("Capability config matches task")

            # 5. Description relevance (bonus 10 points)
            description = queue.get("description", "").lower()
            if any(kw in description for kw in task_analysis.get("keywords", [])):
                score += 10
                reasons.append("Description matches task")

            # Normalize score to 0-100
            normalized_score = min(100, score)

            # Determine confidence
            if normalized_score >= 70:
                confidence = "high"
            elif normalized_score >= 40:
                confidence = "medium"
            else:
                confidence = "low"

            scores.append({
                "queue_id": queue["id"],
                "queue_name": queue["name"],
                "score": normalized_score,
                "confidence": confidence,
                "reasoning": "; ".join(reasons) if reasons else "Default selection",
            })

        # Sort by score descending
        scores.sort(key=lambda x: x["score"], reverse=True)

        return scores

    async def _llm_recommend_queue_with_retry(
        self,
        prompt: str,
        task_analysis: Dict[str, Any],
        top_queues: List[Dict[str, Any]],
        max_retries: int = 3,
        initial_timeout: float = 10.0
    ) -> Dict[str, Any]:
        """
        Use LLM to make final recommendation with exponential backoff retry.

        Args:
            prompt: Original task prompt
            task_analysis: Pattern-based analysis
            top_queues: Top 3 scored queues
            max_retries: Maximum retry attempts
            initial_timeout: Initial timeout in seconds

        Returns:
            {
                "queue_id": str,
                "reasoning": str
            }
        """
        for attempt in range(max_retries):
            timeout = initial_timeout * (2 ** attempt)  # Exponential backoff

            try:
                logger.debug(
                    "LLM recommendation attempt",
                    attempt=attempt + 1,
                    max_retries=max_retries,
                    timeout=timeout
                )

                result = await asyncio.wait_for(
                    self._llm_recommend_queue(prompt, task_analysis, top_queues),
                    timeout=timeout
                )

                # Success - return result
                return result

            except asyncio.TimeoutError:
                logger.warning(
                    "LLM call timeout",
                    attempt=attempt + 1,
                    timeout=timeout
                )

                if attempt < max_retries - 1:
                    # Wait before retry (exponential backoff)
                    await asyncio.sleep(0.5 * (2 ** attempt))
                else:
                    # Final attempt failed
                    self._metrics["llm_failures"] += 1
                    raise

            except Exception as e:
                logger.warning(
                    "LLM call failed",
                    attempt=attempt + 1,
                    error=str(e)
                )

                if attempt < max_retries - 1:
                    await asyncio.sleep(0.5 * (2 ** attempt))
                else:
                    self._metrics["llm_failures"] += 1
                    raise

        # Should never reach here, but just in case
        raise RuntimeError("LLM recommendation exhausted all retries")

    async def _llm_recommend_queue(
        self,
        prompt: str,
        task_analysis: Dict[str, Any],
        top_queues: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """
        Use LLM to make final recommendation when scores are close.
        Protected by circuit breaker.

        Args:
            prompt: Original task prompt
            task_analysis: Pattern-based analysis
            top_queues: Top 3 scored queues

        Returns:
            {
                "queue_id": str,
                "reasoning": str
            }
        """
        self._metrics["llm_calls"] += 1

        # Build context for LLM
        queue_descriptions = []
        for idx, queue in enumerate(top_queues, 1):
            tags = queue.get('tags', [])
            queue_descriptions.append(
                f"{idx}. {queue['queue_name']} (ID: {queue['queue_id']})\n"
                f"   - Tags: {', '.join(tags) if tags else 'none'}\n"
                f"   - Score: {queue['score']}\n"
                f"   - Pattern reasoning: {queue['reasoning']}"
            )

        system_prompt = """You are an expert at selecting optimal worker queues for tasks.
Analyze the task and recommend the best queue from the candidates provided.

Consider:
- Task requirements and complexity
- Queue capabilities and tags
- Pattern-based analysis results

Respond with ONLY a JSON object (no markdown, no explanation):
{
  "queue_id": "uuid-of-selected-queue",
  "reasoning": "Brief explanation (1-2 sentences) of why this queue is best"
}"""

        user_prompt = f"""Task prompt: "{prompt}"

Task analysis:
- Category: {task_analysis.get('task_category')}
- Required capabilities: {', '.join(task_analysis.get('required_capabilities', [])) or 'none'}
- Complexity: {task_analysis.get('complexity')}

Top queue candidates:
{chr(10).join(queue_descriptions)}

Which queue should execute this task?"""

        try:
            # Check circuit breaker state
            if self.llm_circuit_breaker.state == "open":
                logger.warning("Circuit breaker OPEN - skipping LLM call")
                raise Exception("Circuit breaker is OPEN")

            # Use agno_service for LLM call
            result = await agno_service.execute_agent_async(
                prompt=user_prompt,
                system_prompt=system_prompt,
                model="kubiya/claude-sonnet-4",  # Fast model for analysis
                temperature=0.3,  # Lower temp for consistent recommendations
                max_tokens=300,
                stream=False,
            )

            if result.get("success"):
                response_text = result.get("response", "")

                # Parse JSON response
                try:
                    # Try to extract JSON from response
                    json_match = re.search(r'\{[^}]+\}', response_text, re.DOTALL)
                    if json_match:
                        recommendation = json.loads(json_match.group())

                        # Validate response has required fields
                        if "queue_id" not in recommendation:
                            raise ValueError("LLM response missing queue_id field")

                        logger.info(
                            "LLM queue recommendation success",
                            queue_id=recommendation.get("queue_id"),
                            reasoning=recommendation.get("reasoning", "")[:100]  # Truncate for logs
                        )

                        self.llm_circuit_breaker._on_success()
                        return recommendation

                    else:
                        logger.warning("No JSON found in LLM response", response=response_text[:200])
                        raise ValueError("Invalid LLM response format")

                except json.JSONDecodeError as e:
                    logger.warning("Failed to parse LLM JSON response", response=response_text[:200], error=str(e))
                    raise
            else:
                error_msg = result.get("error", "Unknown error")
                logger.error("LLM execution failed", error=error_msg)
                raise RuntimeError(f"LLM execution failed: {error_msg}")

        except Exception as e:
            self.llm_circuit_breaker._on_failure()
            logger.error("LLM recommendation error", error=str(e))
            raise

        # Fallback: return first queue (highest scored)
        logger.info("Using fallback (highest scored queue)")
        return {
            "queue_id": top_queues[0]["queue_id"],
            "reasoning": "Pattern-based selection (LLM unavailable)"
        }

    def _cleanup_cache(self):
        """Remove expired cache entries"""
        current_time = time.time()
        expired_keys = [
            key for key, (_, cached_time) in self._queue_cache.items()
            if current_time - cached_time > self._queue_cache_ttl * 2  # Clean entries 2x TTL old
        ]

        for key in expired_keys:
            del self._queue_cache[key]

        if expired_keys:
            logger.debug("Cleaned cache entries", count=len(expired_keys))

    def get_metrics(self) -> Dict[str, Any]:
        """Get performance metrics for monitoring"""
        return {
            **self._metrics,
            "circuit_breaker_state": self.llm_circuit_breaker.state,
            "circuit_breaker_failures": self.llm_circuit_breaker.failure_count,
            "cache_size": len(self._queue_cache),
        }


# Singleton instance
queue_resolution_service = QueueResolutionService()
