"""Real-Time Streaming Analysis (Phase 11).

Analyzes test execution in real-time for early flakiness detection.
Processes captured events as they arrive and emits insights.
"""

from __future__ import annotations

import logging
import re
from collections.abc import AsyncIterator
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

from autodebug.llm_first.models import (
    FlakinessCategory,
    FlakySignal,
    StreamingInsight,
)

logger = logging.getLogger(__name__)


@dataclass
class CapturedEvent:
    """A captured event from test execution."""

    seq: int
    type: str  # HTTP, SQL, REDIS, TIME, RNG, etc.
    timestamp: float

    # Type-specific fields
    method: Optional[str] = None  # HTTP method
    url: Optional[str] = None  # HTTP URL
    query: Optional[str] = None  # SQL query
    command: Optional[str] = None  # Redis command
    function: Optional[str] = None  # Function name for TIME/RNG

    # Common fields
    duration_ms: Optional[float] = None
    status_code: Optional[int] = None
    error: Optional[str] = None

    # Raw data
    data: Dict[str, Any] = field(default_factory=dict)


@dataclass
class StreamingState:
    """State tracked during streaming analysis."""

    event_count: int = 0
    events_by_type: Dict[str, int] = field(default_factory=dict)

    # Pattern tracking
    has_retry_pattern: bool = False
    seen_external_apis: List[str] = field(default_factory=list)
    sql_queries: List[str] = field(default_factory=list)

    # Timing tracking
    last_event_time: float = 0.0
    gaps_detected: int = 0

    # State tracking
    shared_state_access: List[str] = field(default_factory=list)

    def add_event(self, event: CapturedEvent) -> None:
        """Add event to state tracking."""
        self.event_count += 1
        self.events_by_type[event.type] = self.events_by_type.get(event.type, 0) + 1

        # Track timing gaps
        if self.last_event_time > 0:
            gap = event.timestamp - self.last_event_time
            if gap > 1.0:  # 1 second gap
                self.gaps_detected += 1
        self.last_event_time = event.timestamp

        # Track external APIs
        if event.type == "HTTP" and event.url:
            if not self._is_internal_url(event.url):
                self.seen_external_apis.append(event.url)

        # Track SQL queries
        if event.type == "SQL" and event.query:
            self.sql_queries.append(event.query)

        # Check for retry patterns
        if event.type == "HTTP":
            if self._looks_like_retry(event):
                self.has_retry_pattern = True

    def _is_internal_url(self, url: str) -> bool:
        """Check if URL is internal."""
        internal_patterns = [
            "localhost",
            "127.0.0.1",
            "0.0.0.0",
            ".local",
            "testserver",
        ]
        return any(p in url.lower() for p in internal_patterns)

    def _looks_like_retry(self, event: CapturedEvent) -> bool:
        """Check if event looks like a retry."""
        # Check if we've seen this URL recently
        recent_http = [
            e for e in self.seen_external_apis[-5:]
            if event.url and e == event.url
        ]
        return len(recent_http) > 1


class StreamingAnalyzer:
    """Analyze test execution in real-time for early flakiness detection."""

    def __init__(self, instructor_client=None):
        """Initialize streaming analyzer.

        Args:
            instructor_client: Optional LLM client for complex analysis.
        """
        self.instructor_client = instructor_client

        # Pattern definitions
        self._sql_order_pattern = re.compile(
            r"\bSELECT\b.*\bFROM\b(?!.*\bORDER\s+BY\b)",
            re.IGNORECASE | re.DOTALL,
        )
        self._external_api_pattern = re.compile(
            r"https?://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)",
            re.IGNORECASE,
        )

    async def analyze_stream(
        self,
        event_stream: AsyncIterator[CapturedEvent],
    ) -> AsyncIterator[StreamingInsight]:
        """Process events as they arrive, emit insights.

        Args:
            event_stream: Async iterator of captured events.

        Yields:
            Streaming insights as they are detected.
        """
        state = StreamingState()

        async for event in event_stream:
            state.add_event(event)

            # Check for warning patterns
            async for warning in self._check_patterns(event, state):
                yield warning

            # Check for likely flakiness
            signal = await self._detect_flaky_signal(event, state)
            if signal:
                yield StreamingInsight(
                    type="flaky_warning",
                    message=f"Potential flakiness detected: {signal.reason}",
                    confidence=signal.confidence,
                    event_index=state.event_count,
                    details={"event_type": event.type},
                )

    async def _check_patterns(
        self,
        event: CapturedEvent,
        state: StreamingState,
    ) -> AsyncIterator[StreamingInsight]:
        """Check for warning patterns.

        Args:
            event: Current event.
            state: Current state.

        Yields:
            Warning insights.
        """
        # Check for slow operations
        if event.duration_ms and event.duration_ms > 5000:
            yield StreamingInsight(
                type="slow_operation",
                message=f"Slow {event.type} operation: {event.duration_ms:.0f}ms",
                confidence=0.6,
                event_index=state.event_count,
                details={"duration_ms": event.duration_ms},
            )

        # Check for error responses
        if event.error:
            yield StreamingInsight(
                type="error_detected",
                message=f"Error in {event.type}: {event.error[:100]}",
                confidence=0.7,
                event_index=state.event_count,
            )

        # Check for HTTP 5xx errors
        if event.type == "HTTP" and event.status_code and event.status_code >= 500:
            yield StreamingInsight(
                type="server_error",
                message=f"HTTP {event.status_code} from {event.url}",
                confidence=0.8,
                event_index=state.event_count,
            )

    async def _detect_flaky_signal(
        self,
        event: CapturedEvent,
        state: StreamingState,
    ) -> Optional[FlakySignal]:
        """Detect flakiness signals in real-time.

        Args:
            event: Current event.
            state: Current state.

        Returns:
            Flaky signal if detected.
        """
        signals = []

        # SQL without ORDER BY
        if event.type == "SQL" and event.query:
            if self._is_select_without_order(event.query):
                signals.append(FlakySignal(
                    reason="SQL SELECT without ORDER BY may return non-deterministic results",
                    confidence=0.7,
                    event_index=state.event_count,
                ))

        # HTTP to external service without retry
        if event.type == "HTTP" and event.url:
            if self._is_external_api(event.url) and not state.has_retry_pattern:
                signals.append(FlakySignal(
                    reason="External API call without retry logic",
                    confidence=0.6,
                    event_index=state.event_count,
                ))

        # time.sleep() call
        if event.type == "TIME" and event.function == "sleep":
            signals.append(FlakySignal(
                reason="time.sleep() call indicates timing-dependent behavior",
                confidence=0.5,
                event_index=state.event_count,
            ))

        # Random without seed
        if event.type == "RNG" and event.function in ("random", "randint", "choice"):
            signals.append(FlakySignal(
                reason="Random call without controlled seed",
                confidence=0.6,
                event_index=state.event_count,
            ))

        # Large timing gap
        if state.gaps_detected > 0:
            signals.append(FlakySignal(
                reason="Large timing gap detected - possible race condition",
                confidence=0.4,
                event_index=state.event_count,
            ))

        return max(signals, key=lambda s: s.confidence) if signals else None

    def _is_select_without_order(self, query: str) -> bool:
        """Check if SQL SELECT lacks ORDER BY.

        Args:
            query: SQL query.

        Returns:
            True if SELECT without ORDER BY.
        """
        # Skip if it's not a SELECT or has ORDER BY
        query_upper = query.upper()

        if "SELECT" not in query_upper:
            return False

        if "ORDER BY" in query_upper:
            return False

        # Skip aggregate queries (COUNT, MAX, etc.)
        if any(agg in query_upper for agg in ["COUNT(", "MAX(", "MIN(", "SUM(", "AVG("]):
            return False

        # Skip single-row queries (LIMIT 1 or WHERE id = )
        if "LIMIT 1" in query_upper:
            return False

        return True

    def _is_external_api(self, url: str) -> bool:
        """Check if URL is an external API.

        Args:
            url: URL to check.

        Returns:
            True if external.
        """
        internal_hosts = [
            "localhost",
            "127.0.0.1",
            "0.0.0.0",
            ".local",
            "testserver",
            "httpbin",  # Test service
        ]
        return not any(host in url.lower() for host in internal_hosts)


class FlakinessPredictorFromCode:
    """Predict flakiness from test code (heuristic-based for Phase 6)."""

    # Pattern definitions with severity
    PATTERNS = {
        FlakinessCategory.TIMING: [
            (r"time\.sleep\s*\(", 0.7, "time.sleep() call"),
            (r"asyncio\.sleep\s*\(", 0.5, "asyncio.sleep() call"),
            (r"\.wait\s*\(\s*\d", 0.6, "wait with timeout"),
            (r"assert.*<\s*\d+\s*$", 0.6, "timing assertion"),
            (r"async\s+def.*:\s*$", 0.3, "async function"),
        ],
        FlakinessCategory.ORDERING: [
            (r"SELECT\s+\*?\s+FROM(?!.*ORDER\s+BY)", 0.7, "SQL without ORDER BY"),
            (r"\.keys\(\)", 0.5, "dict.keys() iteration"),
            (r"set\(", 0.4, "set usage"),
            (r"\[0\]\s*$", 0.5, "index access after unordered"),
        ],
        FlakinessCategory.RESOURCE: [
            (r"/tmp/", 0.6, "hardcoded /tmp path"),
            (r":\d{4,5}", 0.5, "hardcoded port"),
            (r"open\s*\(", 0.3, "file operation"),
        ],
        FlakinessCategory.STATE: [
            (r"global\s+\w+", 0.7, "global variable"),
            (r"cls\.\w+\s*=", 0.6, "class-level mutation"),
            (r"@pytest\.fixture\s*$", 0.3, "fixture without scope"),
        ],
        FlakinessCategory.NETWORK: [
            (r"requests\.get", 0.6, "HTTP GET without mock"),
            (r"httpx\.get", 0.6, "HTTP GET without mock"),
            (r"urllib\.request", 0.6, "urllib request"),
        ],
    }

    def predict_from_code(
        self,
        test_code: str,
    ) -> List[Dict[str, Any]]:
        """Predict flakiness patterns from test code.

        Args:
            test_code: Test source code.

        Returns:
            List of detected patterns.
        """
        detected = []

        for category, patterns in self.PATTERNS.items():
            for pattern, severity, description in patterns:
                if re.search(pattern, test_code, re.IGNORECASE):
                    detected.append({
                        "category": category.value,
                        "pattern": pattern,
                        "severity": severity,
                        "description": description,
                    })

        return detected

    def calculate_risk_score(
        self,
        detected_patterns: List[Dict[str, Any]],
    ) -> float:
        """Calculate overall risk score.

        Args:
            detected_patterns: Detected patterns.

        Returns:
            Risk score (0-1).
        """
        if not detected_patterns:
            return 0.0

        # Max severity + count bonus
        max_severity = max(p["severity"] for p in detected_patterns)
        count_bonus = min(0.2, len(detected_patterns) * 0.05)

        return min(1.0, max_severity + count_bonus)
