"""
Local vision processing - no external sidecar needed.
This module provides the same functionality as the vision sidecar but runs locally.
"""

import asyncio
import base64
import hashlib
import time
from dataclasses import dataclass
from io import BytesIO
from typing import Optional
import cv2
import numpy as np
import pytesseract
from PIL import Image


@dataclass
class LocalVisionResult:
    """Result from local vision processing."""
    description: str
    should_escalate: bool
    escalation_reason: Optional[str]
    confidence: float
    detected_signals: dict
    inference_ms: int


class LocalVisionProcessor:
    """Local vision processor using OpenCV + pytesseract."""

    def __init__(self, confidence_threshold: float = 0.85):
        self.confidence_threshold = confidence_threshold
        # Simple LRU cache for repeated screenshots
        self._cache = {}
        self._cache_order = []
        self._cache_size = 5

    async def analyze_screenshot(
        self,
        screenshot_b64: str,
        last_action_type: str = "none",
        session_id: str = "default"
    ) -> Optional[LocalVisionResult]:
        """
        Analyze screenshot locally using OpenCV + pytesseract.
        Returns LocalVisionResult if processing succeeds, None if should escalate to LLM.
        """
        try:
            # Skip cache for mutating actions
            MUTATING = {"click", "type", "submit", "select", "upload", "press_key", "fill"}
            use_cache = last_action_type not in MUTATING

            if use_cache:
                cache_key = hashlib.md5(screenshot_b64[:2000].encode()).hexdigest()
                cached = self._get_from_cache(cache_key)
                if cached:
                    return cached

            # Process screenshot
            start_time = time.time()
            result = await asyncio.to_thread(self._process_sync, screenshot_b64)
            processing_time = int((time.time() - start_time) * 1000)

            if result:
                result.inference_ms = processing_time

                if use_cache:
                    self._add_to_cache(cache_key, result)

                return result

            return None

        except Exception:
            # Fail silently - caller should escalate to LLM
            return None

    def _process_sync(self, screenshot_b64: str) -> Optional[LocalVisionResult]:
        """Synchronous processing using OpenCV + pytesseract."""
        try:
            # Decode base64 to image
            image_data = base64.b64decode(screenshot_b64)
            image = Image.open(BytesIO(image_data))

            # Convert to OpenCV format
            cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)

            # Extract text using pytesseract
            text = pytesseract.image_to_string(cv_image, config='--psm 6')

            # Analyze image features
            detected_signals = self._analyze_features(cv_image, text)

            # Generate description
            description = self._generate_description(text, detected_signals)

            # Calculate confidence based on text quality and image complexity
            confidence = self._calculate_confidence(text, detected_signals, cv_image)

            # Determine if should escalate
            should_escalate = confidence < self.confidence_threshold
            escalation_reason = None

            if should_escalate:
                if confidence < 0.5:
                    escalation_reason = "Low confidence in scene understanding"
                elif detected_signals.get('has_overlay') or detected_signals.get('has_popup'):
                    escalation_reason = "Complex UI elements detected"
                elif detected_signals.get('is_loading'):
                    escalation_reason = "Dynamic loading content detected"
                else:
                    escalation_reason = "Complex content requires LLM analysis"

            return LocalVisionResult(
                description=description,
                should_escalate=should_escalate,
                escalation_reason=escalation_reason,
                confidence=confidence,
                detected_signals=detected_signals,
                inference_ms=0  # Will be set by caller
            )

        except Exception:
            return None

    def _analyze_features(self, cv_image: np.ndarray, text: str) -> dict:
        """Analyze image features to detect UI elements."""
        height, width = cv_image.shape[:2]

        # Convert to grayscale for analysis
        gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)

        # Detect edges
        edges = cv2.Canny(gray, 50, 150, apertureSize=3)
        edge_density = np.sum(edges > 0) / (width * height)

        # Detect rectangles (potential form fields, buttons)
        contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        rectangles = []

        for contour in contours:
            # Approximate contour to polygon
            epsilon = 0.02 * cv2.arcLength(contour, True)
            approx = cv2.approxPolyDP(contour, epsilon, True)

            if len(approx) == 4:  # Rectangle
                x, y, w, h = cv2.boundingRect(contour)
                if w > 50 and h > 20 and w < width * 0.8:  # Reasonable size
                    rectangles.append((x, y, w, h))

        # Analyze text content
        text_lower = text.lower().strip()

        # Simple heuristics for UI element detection
        has_form_keywords = any(keyword in text_lower for keyword in [
            'username', 'password', 'email', 'login', 'sign in', 'register', 'submit'
        ])

        has_navigation = any(keyword in text_lower for keyword in [
            'home', 'about', 'contact', 'menu', 'navigation', 'page'
        ])

        has_error_keywords = any(keyword in text_lower for keyword in [
            'error', 'invalid', 'required', 'please', 'try again', 'failed'
        ])

        # Loading indicators
        is_loading = any(keyword in text_lower for keyword in [
            'loading', 'please wait', 'processing', 'connecting'
        ])

        # Overlay/popup detection (high edge density in small areas)
        has_overlay = edge_density > 0.1 and len(rectangles) > 10

        return {
            'has_popup': has_overlay,
            'has_overlay': has_overlay,
            'is_loading': is_loading,
            'has_form_errors': has_error_keywords,
            'visible_text_summary': text_lower[:100],
            'rectangles_count': len(rectangles),
            'edge_density': edge_density,
            'has_forms': has_form_keywords,
            'has_navigation': has_navigation,
        }

    def _generate_description(self, text: str, signals: dict) -> str:
        """Generate a description of the screen content."""
        if not text.strip():
            if signals['rectangles_count'] > 5:
                return "Screen with multiple UI elements but no readable text"
            else:
                return "Screen content: minimal or no visible text"

        # Clean and summarize text
        text_summary = text.strip()[:200]

        # Add context based on detected signals
        context_parts = []

        if signals.get('has_forms'):
            context_parts.append("form fields detected")

        if signals.get('has_navigation'):
            context_parts.append("navigation elements")

        if signals.get('is_loading'):
            context_parts.append("loading state")

        if signals.get('has_form_errors'):
            context_parts.append("error messages visible")

        if context_parts:
            context = f" ({', '.join(context_parts)})"
        else:
            context = ""

        return f"Screen content: {text_summary}{context}"

    def _calculate_confidence(self, text: str, signals: dict, cv_image: np.ndarray) -> float:
        """Calculate confidence in the analysis."""
        confidence = 0.5  # Base confidence

        # Text quality factors
        text_length = len(text.strip())
        if text_length > 50:
            confidence += 0.2
        elif text_length > 20:
            confidence += 0.1

        # Readable text (letters, numbers, common words)
        readable_chars = sum(1 for c in text if c.isalnum() or c.isspace())
        if text_length > 0:
            readability = readable_chars / text_length
            confidence += readability * 0.2

        # UI element confidence
        if signals['rectangles_count'] > 0 and signals['rectangles_count'] < 20:
            confidence += 0.1

        # Reduce confidence for complex scenarios
        if signals.get('is_loading'):
            confidence -= 0.3

        if signals.get('has_overlay') or signals.get('has_popup'):
            confidence -= 0.2

        if signals['edge_density'] > 0.15:  # Very complex image
            confidence -= 0.2

        # Ensure confidence is in valid range
        return max(0.0, min(1.0, confidence))

    def _get_from_cache(self, cache_key: str) -> Optional[LocalVisionResult]:
        """Get result from cache."""
        return self._cache.get(cache_key)

    def _add_to_cache(self, cache_key: str, result: LocalVisionResult) -> None:
        """Add result to cache with LRU eviction."""
        if cache_key in self._cache:
            return

        self._cache[cache_key] = result
        self._cache_order.append(cache_key)

        # LRU eviction
        if len(self._cache_order) > self._cache_size:
            oldest = self._cache_order.pop(0)
            self._cache.pop(oldest, None)


# Global processor instance
_processor = None

def get_processor(confidence_threshold: float = 0.85) -> LocalVisionProcessor:
    """Get or create the global vision processor."""
    global _processor
    if _processor is None or _processor.confidence_threshold != confidence_threshold:
        _processor = LocalVisionProcessor(confidence_threshold)
    return _processor


async def analyze_screenshot_locally(
    screenshot_b64: str,
    last_action_type: str = "none",
    session_id: str = "default",
    confidence_threshold: float = 0.85
) -> Optional[LocalVisionResult]:
    """
    Main entry point for local screenshot analysis.
    Returns LocalVisionResult if successful, None if should escalate to LLM.
    """
    processor = get_processor(confidence_threshold)
    return await processor.analyze_screenshot(screenshot_b64, last_action_type, session_id)