"""Track fix outcomes through their lifecycle.

Connects to the ActiveLearner to trigger learning updates
when feedback is received.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Dict, List, Optional

if TYPE_CHECKING:
    from autodebug.learning.active_learner import ActiveLearner


@dataclass
class FeedbackEvent:
    """A feedback event in the fix lifecycle."""

    fix_id: str
    event_type: str  # "generated", "presented", "accepted", "rejected", "applied"
    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    metadata: Dict[str, Any] = field(default_factory=dict)


class FeedbackTracker:
    """Track fix outcomes through their lifecycle.

    Coordinates with ActiveLearner to trigger learning updates
    and maintains a history of feedback events.
    """

    # F-14: Maximum age (seconds) for a pending fix before it is
    # treated as "no outcome" and removed from tracking.
    PENDING_TIMEOUT_SECONDS = 7 * 24 * 3600  # 7 days

    def __init__(self, learner: Optional[ActiveLearner] = None):
        """Initialize the feedback tracker.

        Args:
            learner: Optional ActiveLearner for triggering updates
        """
        self._learner = learner
        self.events: List[FeedbackEvent] = []
        self.fix_contexts: Dict[str, Dict[str, Any]] = {}

        # F-14: Track pending outcomes -- fixes that have been accepted
        # but not yet verified via "applied_worked" / "applied_failed".
        self._pending_outcomes: Dict[str, datetime] = {}

    @property
    def learner(self) -> Optional[ActiveLearner]:
        """Get or create the active learner."""
        if self._learner is None:
            try:
                from autodebug.learning.active_learner import ActiveLearner

                self._learner = ActiveLearner()
            except ImportError:
                pass
        return self._learner

    def track_fix_generated(
        self,
        fix_id: str,
        exception_type: str,
        exception_message: str,
        strategy: str,
        **context: Any,
    ) -> None:
        """Track when a fix is generated.

        Args:
            fix_id: Unique identifier for the fix
            exception_type: Type of exception being fixed
            exception_message: Exception message
            strategy: Strategy used to generate the fix
            **context: Additional context (code_context, unified_diff, etc.)
        """
        # Store context for later use
        self.fix_contexts[fix_id] = {
            "exception_type": exception_type,
            "exception_message": exception_message,
            "strategy": strategy,
            **context,
        }

        self._record_event(
            fix_id,
            "generated",
            {
                "exception_type": exception_type,
                "strategy": strategy,
            },
        )

    def track_fix_presented(self, fix_id: str) -> None:
        """Track when a fix is shown to the user.

        Args:
            fix_id: Unique identifier for the fix
        """
        self._record_event(fix_id, "presented", {})

    def track_fix_accepted(
        self,
        fix_id: str,
        modifications: Optional[str] = None,
    ) -> None:
        """Track when user accepts a fix.

        Args:
            fix_id: Unique identifier for the fix
            modifications: Optional user modifications to the fix
        """
        self._record_event(
            fix_id,
            "accepted",
            {"modifications": modifications},
        )

        # F-14: Mark as pending final outcome (applied_worked/applied_failed)
        self._pending_outcomes[fix_id] = datetime.now(timezone.utc)

        # Trigger learning
        outcome = "modified" if modifications else "accepted"
        self._trigger_learning(fix_id, outcome, modifications)

    def track_fix_rejected(
        self,
        fix_id: str,
        reason: Optional[str] = None,
    ) -> None:
        """Track when user rejects a fix.

        Args:
            fix_id: Unique identifier for the fix
            reason: Optional reason for rejection
        """
        self._record_event(fix_id, "rejected", {"reason": reason})

        # Trigger learning
        self._trigger_learning(fix_id, "rejected")

    def track_fix_applied(
        self,
        fix_id: str,
        success: bool,
        error: Optional[str] = None,
    ) -> None:
        """Track when fix is actually applied and tested.

        Args:
            fix_id: Unique identifier for the fix
            success: Whether the fix worked (tests pass)
            error: Optional error message if fix failed
        """
        self._record_event(
            fix_id,
            "applied",
            {"success": success, "error": error},
        )

        # F-14: Resolve pending outcome
        self._pending_outcomes.pop(fix_id, None)

        # Trigger learning with apply result
        outcome = "applied_worked" if success else "applied_failed"
        self._trigger_learning(fix_id, outcome)

    def _record_event(
        self,
        fix_id: str,
        event_type: str,
        metadata: Dict[str, Any],
    ) -> None:
        """Record a feedback event.

        Args:
            fix_id: Fix identifier
            event_type: Type of event
            metadata: Event metadata
        """
        event = FeedbackEvent(
            fix_id=fix_id,
            event_type=event_type,
            timestamp=datetime.now(timezone.utc),
            metadata=metadata,
        )
        self.events.append(event)

        # Keep events bounded
        if len(self.events) > 10000:
            self.events = self.events[-5000:]

    def _trigger_learning(
        self,
        fix_id: str,
        outcome: str,
        modifications: Optional[str] = None,
    ) -> None:
        """Trigger active learning update.

        Args:
            fix_id: Fix identifier
            outcome: Outcome type
            modifications: Optional user modifications
        """
        if not self.learner:
            return

        context = self.fix_contexts.get(fix_id, {})

        # Extract ML features from context
        features = self._extract_features(context)

        from autodebug.learning.active_learner import LearningSignal

        signal = LearningSignal(
            fix_id=fix_id,
            exception_type=context.get("exception_type", "unknown"),
            exception_message=context.get("exception_message", ""),
            strategy_used=context.get("strategy", "unknown"),
            outcome=outcome,
            user_modifications=modifications,
            timestamp=datetime.now(timezone.utc),
            features=features,
        )

        self.learner.record_feedback(signal)

    def _extract_features(self, context: Dict[str, Any]) -> Dict[str, float]:
        """Extract ML features from fix context.

        Args:
            context: Fix context dictionary

        Returns:
            Feature dictionary for ML model
        """
        features: Dict[str, float] = {}

        # Diff size features
        diff = context.get("unified_diff", "")
        features["diff_lines"] = float(diff.count("\n"))
        features["diff_additions"] = float(diff.count("\n+"))
        features["diff_deletions"] = float(diff.count("\n-"))
        features["diff_size"] = float(len(diff))

        # Code context features
        code_context = context.get("code_context", "")
        features["context_lines"] = float(len(code_context.split("\n")))
        features["code_context"] = code_context  # Pass through for ActiveLearner

        # File path for extension detection
        file_path = context.get("file", context.get("failing_file", ""))
        if file_path:
            features["file"] = file_path  # Pass through for ActiveLearner

        # Stack depth
        stack_trace = context.get("stack_trace", [])
        if isinstance(stack_trace, list):
            features["stack_depth"] = float(len(stack_trace))

        # Strategy-specific features
        strategy = context.get("strategy", "")
        features["is_minimal_strategy"] = 1.0 if "minimal" in strategy.lower() else 0.0
        features["is_defensive_strategy"] = 1.0 if "defensive" in strategy.lower() else 0.0
        features["is_refactor_strategy"] = 1.0 if "refactor" in strategy.lower() else 0.0

        # Confidence if available
        confidence = context.get("confidence")
        if confidence is not None:
            features["confidence"] = float(confidence)

        # Time-to-decision: seconds between "presented" and decision
        fix_id = context.get("fix_id", "")
        if fix_id:
            presented_time = self._get_event_time(fix_id, "presented")
            if presented_time:
                decision_time = datetime.now(timezone.utc)
                delta = (decision_time - presented_time).total_seconds()
                features["time_to_decision"] = float(min(delta, 3600))

        # Whether fix was modified
        features["was_modified"] = 1.0 if context.get("modifications") else 0.0

        # Previous attempts for same exception type
        exc_type = context.get("exception_type", "")
        if exc_type:
            prev = sum(
                1 for e in self.events
                if e.event_type in ("accepted", "rejected", "applied")
                and self.fix_contexts.get(e.fix_id, {}).get("exception_type") == exc_type
            )
            features["previous_attempts"] = float(prev)

        return features

    def _get_event_time(self, fix_id: str, event_type: str) -> Optional[datetime]:
        """Get timestamp of a specific event for a fix.

        Args:
            fix_id: Fix identifier
            event_type: Event type to find

        Returns:
            Timestamp or None
        """
        for event in reversed(self.events):
            if event.fix_id == fix_id and event.event_type == event_type:
                return event.timestamp
        return None

    def get_fix_history(self, fix_id: str) -> List[FeedbackEvent]:
        """Get event history for a fix.

        Args:
            fix_id: Fix identifier

        Returns:
            List of events for this fix
        """
        return [e for e in self.events if e.fix_id == fix_id]

    def get_recent_events(
        self,
        event_type: Optional[str] = None,
        limit: int = 100,
    ) -> List[FeedbackEvent]:
        """Get recent feedback events.

        Args:
            event_type: Filter by event type
            limit: Maximum events to return

        Returns:
            List of recent events
        """
        filtered = self.events
        if event_type:
            filtered = [e for e in filtered if e.event_type == event_type]
        return filtered[-limit:]

    def reconcile_pending_outcomes(self) -> List[str]:
        """Reconcile stale pending outcomes.

        F-14: Fixes that were accepted but never received a final
        "applied_worked" or "applied_failed" signal within the
        timeout window are treated as unknown outcomes and removed
        from pending tracking.

        Returns:
            List of fix_ids that were timed out
        """
        now = datetime.now(timezone.utc)
        timed_out = []
        for fix_id, accepted_at in list(self._pending_outcomes.items()):
            age_seconds = (now - accepted_at).total_seconds()
            if age_seconds > self.PENDING_TIMEOUT_SECONDS:
                timed_out.append(fix_id)
                del self._pending_outcomes[fix_id]
                self._record_event(
                    fix_id,
                    "pending_timeout",
                    {"age_seconds": age_seconds},
                )
        return timed_out

    def get_pending_count(self) -> int:
        """Get number of fixes awaiting final outcome.

        Returns:
            Count of pending fixes
        """
        return len(self._pending_outcomes)

    def get_stats(self) -> Dict[str, Any]:
        """Get feedback tracking statistics.

        Returns:
            Dict with tracking stats
        """
        # F-14: Reconcile stale pending outcomes on stats retrieval
        self.reconcile_pending_outcomes()

        event_counts: Dict[str, int] = {}
        for event in self.events:
            event_counts[event.event_type] = event_counts.get(event.event_type, 0) + 1

        return {
            "total_events": len(self.events),
            "tracked_fixes": len(self.fix_contexts),
            "event_counts": event_counts,
            "pending_outcomes": self.get_pending_count(),
        }
