"""Boris safety mechanisms - rate limiting, circuit breaking, monitoring, and API retry logic."""
import subprocess
import time
import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

logger = logging.getLogger('boris.safety')


class CircuitState(Enum):
    CLOSED = "CLOSED"
    OPEN = "OPEN"
    HALF_OPEN = "HALF_OPEN"


@dataclass
class RateLimiter:
    """Configurable delay between DaveLoop spawns using time.sleep."""
    delay_seconds: float = 0
    _timestamps: list[float] = field(default_factory=list, repr=False)

    def wait(self):
        """Wait for the configured delay and track the call timestamp."""
        now = time.time()
        self._timestamps.append(now)
        if self.delay_seconds > 0:
            logger.debug(f"Rate limiter sleeping for {self.delay_seconds}s")
            print(f"[SAFETY] Rate limiter: waiting {self.delay_seconds}s before next spawn", flush=True)
            time.sleep(self.delay_seconds)

    @property
    def call_count(self) -> int:
        return len(self._timestamps)

    @property
    def last_call_time(self) -> Optional[float]:
        return self._timestamps[-1] if self._timestamps else None


@dataclass
class CircuitBreaker:
    """Tracks consecutive milestone failures with circuit breaker pattern.

    States:
        CLOSED  - Normal operation, requests flow through
        OPEN    - Tripped after N failures, blocks execution
        HALF_OPEN - Testing recovery after cooldown period
    """
    failure_threshold: int = 3
    cooldown_seconds: float = 60.0
    state: CircuitState = CircuitState.CLOSED
    consecutive_failures: int = 0
    _last_failure_time: Optional[float] = field(default=None, repr=False)

    def record_failure(self):
        """Record a milestone failure. Trips to OPEN after threshold."""
        self.consecutive_failures += 1
        self._last_failure_time = time.time()
        logger.warning(f"Circuit breaker: failure #{self.consecutive_failures} (threshold: {self.failure_threshold})")

        if self.consecutive_failures >= self.failure_threshold:
            self.state = CircuitState.OPEN
            print(f"[SAFETY] Circuit breaker TRIPPED after {self.consecutive_failures} consecutive failures. "
                  f"Execution halted. Cooldown: {self.cooldown_seconds}s", flush=True)
            logger.error(f"Circuit breaker tripped OPEN after {self.consecutive_failures} failures")

    def record_success(self):
        """Record a milestone success. Resets failure count and returns to CLOSED."""
        self.consecutive_failures = 0
        self.state = CircuitState.CLOSED
        logger.info("Circuit breaker: success recorded, reset to CLOSED")

    def allow_request(self) -> bool:
        """Check if execution is allowed.

        Returns False when OPEN (within cooldown).
        Transitions to HALF_OPEN after cooldown expires.
        """
        if self.state == CircuitState.CLOSED:
            return True

        if self.state == CircuitState.HALF_OPEN:
            logger.info("Circuit breaker: HALF_OPEN, allowing test request")
            return True

        if self.state == CircuitState.OPEN:
            if self._last_failure_time is None:
                return True
            elapsed = time.time() - self._last_failure_time
            if elapsed >= self.cooldown_seconds:
                self.state = CircuitState.HALF_OPEN
                logger.info(f"Circuit breaker: cooldown expired ({elapsed:.1f}s), transitioning to HALF_OPEN")
                print(f"[SAFETY] Circuit breaker cooldown expired. Testing recovery (HALF_OPEN).", flush=True)
                return True
            remaining = self.cooldown_seconds - elapsed
            logger.debug(f"Circuit breaker OPEN: {remaining:.1f}s remaining in cooldown")
            return False

        return True


@dataclass
class _MilestoneRecord:
    """Internal record for a single milestone's timing data."""
    start_time: float = 0.0
    end_time: float = 0.0
    duration: float = 0.0
    success: bool = False
    finished: bool = False


class EnhancedMonitor:
    """Tracks per-milestone timing, cumulative success/fail rates, and estimated remaining time.

    Works in sequential mode, extending Boris's existing swarm dashboard concept.
    """

    def __init__(self, total_milestones: int = 0):
        self._milestones: dict[str, _MilestoneRecord] = {}
        self._total_milestones = total_milestones

    def start_milestone(self, milestone_id: str):
        """Record the start time for a milestone."""
        record = _MilestoneRecord(start_time=time.time())
        self._milestones[milestone_id] = record
        logger.info(f"EnhancedMonitor: started milestone {milestone_id}")
        print(f"[MONITOR] Milestone {milestone_id} started", flush=True)

    def end_milestone(self, milestone_id: str, success: bool):
        """Record the end time and outcome for a milestone."""
        record = self._milestones.get(milestone_id)
        if record is None:
            logger.warning(f"EnhancedMonitor: end_milestone called for unknown milestone {milestone_id}")
            record = _MilestoneRecord(start_time=time.time())
            self._milestones[milestone_id] = record

        record.end_time = time.time()
        record.duration = record.end_time - record.start_time
        record.success = success
        record.finished = True

        status = "SUCCESS" if success else "FAILED"
        logger.info(f"EnhancedMonitor: milestone {milestone_id} {status} in {record.duration:.1f}s")
        print(f"[MONITOR] Milestone {milestone_id} {status} ({record.duration:.1f}s)", flush=True)

    def print_status(self):
        """Display cumulative success/fail rates and estimated remaining time."""
        finished = [r for r in self._milestones.values() if r.finished]
        in_progress = [mid for mid, r in self._milestones.items() if not r.finished]

        total_finished = len(finished)
        successes = sum(1 for r in finished if r.success)
        failures = total_finished - successes
        success_rate = (successes / total_finished * 100) if total_finished > 0 else 0.0

        print(f"\n[MONITOR] === Enhanced Status ===", flush=True)
        print(f"[MONITOR] Completed: {total_finished} | Success: {successes} | Failed: {failures}", flush=True)
        print(f"[MONITOR] Success rate: {success_rate:.1f}%", flush=True)

        if in_progress:
            print(f"[MONITOR] In progress: {', '.join(in_progress)}", flush=True)

        # Estimate remaining time based on average duration of finished milestones
        if total_finished > 0 and self._total_milestones > 0:
            avg_duration = sum(r.duration for r in finished) / total_finished
            remaining_count = self._total_milestones - total_finished - len(in_progress)
            if remaining_count > 0:
                est_remaining = avg_duration * remaining_count
                minutes = est_remaining / 60
                print(f"[MONITOR] Remaining: ~{remaining_count} milestones, est. {minutes:.1f} min", flush=True)
            else:
                print(f"[MONITOR] No milestones remaining", flush=True)
        elif self._total_milestones > 0:
            print(f"[MONITOR] Remaining: {self._total_milestones} milestones (no time estimate yet)", flush=True)

        print(f"[MONITOR] =============================\n", flush=True)

    @property
    def success_count(self) -> int:
        return sum(1 for r in self._milestones.values() if r.finished and r.success)

    @property
    def failure_count(self) -> int:
        return sum(1 for r in self._milestones.values() if r.finished and not r.success)

    @property
    def finished_count(self) -> int:
        return sum(1 for r in self._milestones.values() if r.finished)


# Patterns that indicate retryable API/transient errors in subprocess output
_RETRYABLE_PATTERNS = [
    "429",
    "rate limit",
    "too many requests",
    "500",
    "internal server error",
    "timed out",
    "timeout",
    "ETIMEDOUT",
    "ECONNRESET",
    "overloaded",
]


class APILimitHandler:
    """Wraps subprocess calls with retry logic for transient API errors.

    Uses exponential backoff on HTTP 429 (rate limit), timeout, and API 500 errors.
    Follows existing Boris patterns from engine.py for subprocess handling.
    """

    def __init__(self, max_retries: int = 3, base_delay: float = 5.0):
        self.max_retries = max_retries
        self.base_delay = base_delay

    def _is_retryable(self, output: str) -> bool:
        """Check if subprocess output contains retryable error patterns."""
        lower = output.lower()
        return any(pattern in lower for pattern in _RETRYABLE_PATTERNS)

    def run_with_retry(self, cmd, **kwargs) -> subprocess.CompletedProcess:
        """Wrap subprocess.run with retry logic for transient errors.

        Args:
            cmd: Command to pass to subprocess.run (list or string).
            **kwargs: Additional keyword arguments for subprocess.run.

        Returns:
            subprocess.CompletedProcess from the last attempt.

        Raises:
            The last exception if all retries are exhausted and the final attempt raises.
        """
        # Ensure we capture output for error detection
        kwargs.setdefault("capture_output", True)
        kwargs.setdefault("encoding", "utf-8")
        kwargs.setdefault("errors", "replace")

        last_result = None
        last_exception = None

        for attempt in range(self.max_retries + 1):
            try:
                result = subprocess.run(cmd, **kwargs)
                last_result = result

                # Success - return immediately
                if result.returncode == 0:
                    if attempt > 0:
                        logger.info(f"APILimitHandler: succeeded on attempt {attempt + 1}")
                    return result

                # Check if the error is retryable
                combined_output = (result.stdout or "") + (result.stderr or "")
                if self._is_retryable(combined_output) and attempt < self.max_retries:
                    delay = self.base_delay * (2 ** attempt)
                    logger.warning(
                        f"APILimitHandler: retryable error on attempt {attempt + 1}/{self.max_retries + 1}, "
                        f"waiting {delay:.1f}s"
                    )
                    print(
                        f"[SAFETY] API limit/transient error detected. "
                        f"Retrying in {delay:.1f}s (attempt {attempt + 1}/{self.max_retries + 1})",
                        flush=True,
                    )
                    time.sleep(delay)
                    continue

                # Non-retryable error - return as-is
                return result

            except subprocess.TimeoutExpired as e:
                last_exception = e
                if attempt < self.max_retries:
                    delay = self.base_delay * (2 ** attempt)
                    logger.warning(
                        f"APILimitHandler: timeout on attempt {attempt + 1}/{self.max_retries + 1}, "
                        f"waiting {delay:.1f}s"
                    )
                    print(
                        f"[SAFETY] Subprocess timeout. "
                        f"Retrying in {delay:.1f}s (attempt {attempt + 1}/{self.max_retries + 1})",
                        flush=True,
                    )
                    time.sleep(delay)
                    continue
                raise

        # All retries exhausted - return last result or raise last exception
        if last_result is not None:
            return last_result
        if last_exception is not None:
            raise last_exception
        # Should not reach here, but just in case
        raise RuntimeError("APILimitHandler: all retries exhausted with no result")
