"""System stress detection for adaptive token rewards.

Monitors system signals like flakiness, backlog growth, and CI failures
to determine the current stress mode. Token rewards are then adjusted
based on the stress mode to incentivize agents to focus on critical areas.
"""

from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from enum import StrEnum
from pathlib import Path
from typing import Any

import yaml

from config import settings

logger = logging.getLogger(__name__)

# Path to stress mode configuration
STRESS_CONFIG_PATH = Path(settings.repo_root) / "config" / "stress_modes.yaml"


class StressMode(StrEnum):
    """System stress modes.

    - CALM: Normal operations, incentivize exploration and refactoring
    - ELEVATED: Some stress signals detected, balance incentives
    - HIGH: Critical stress, strongly incentivize bug fixes and stabilization
    """

    CALM = "calm"
    ELEVATED = "elevated"
    HIGH = "high"


@dataclass
class StressSignal:
    """A single stress signal reading."""

    name: str
    value: float
    threshold: float
    weight: float
    timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())

    @property
    def is_triggered(self) -> bool:
        """Whether this signal exceeds its threshold."""
        return self.value >= self.threshold

    @property
    def contribution(self) -> float:
        """Calculate this signal's contribution to stress score."""
        if not self.is_triggered:
            return 0.0
        # Contribution is weighted ratio of value to threshold
        return min(1.0, (self.value / max(self.threshold, 1.0))) * self.weight


@dataclass
class StressSnapshot:
    """Complete stress assessment at a point in time."""

    mode: StressMode
    score: float
    signals: list[StressSignal]
    multipliers: dict[str, float]
    timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "mode": self.mode.value,
            "score": round(self.score, 3),
            "signals": [
                {
                    "name": s.name,
                    "value": s.value,
                    "threshold": s.threshold,
                    "triggered": s.is_triggered,
                    "contribution": round(s.contribution, 3),
                }
                for s in self.signals
            ],
            "multipliers": self.multipliers,
            "timestamp": self.timestamp,
        }


@dataclass
class StressThresholds:
    """Thresholds for entering/exiting stress modes."""

    elevated_min: float = 0.3
    high_min: float = 0.6

    def get_mode_for_score(self, score: float) -> StressMode:
        """Determine stress mode based on score."""
        if score >= self.high_min:
            return StressMode.HIGH
        elif score >= self.elevated_min:
            return StressMode.ELEVATED
        return StressMode.CALM


# In-memory storage for stress state
_current_snapshot: StressSnapshot | None = None
_last_check_time: datetime | None = None
_config_cache: dict[str, Any] | None = None
_config_mtime: float = 0.0


def _load_stress_config() -> dict[str, Any]:
    """Load stress mode configuration from YAML.

    Returns:
        Configuration dictionary with default fallbacks
    """
    global _config_cache, _config_mtime

    # Check if config file has been modified
    if STRESS_CONFIG_PATH.exists():
        mtime = STRESS_CONFIG_PATH.stat().st_mtime
        if mtime != _config_mtime or _config_cache is None:
            try:
                raw = STRESS_CONFIG_PATH.read_text()
                _config_cache = yaml.safe_load(raw) or {}
                _config_mtime = mtime
                logger.debug("Loaded stress config from %s", STRESS_CONFIG_PATH)
            except (OSError, yaml.YAMLError) as exc:
                logger.warning("Failed to load stress config: %s", exc)
                _config_cache = {}

    if _config_cache is None:
        _config_cache = {}

    return _config_cache


def get_default_config() -> dict[str, Any]:
    """Get default stress configuration."""
    return {
        "thresholds": {
            "elevated_min": 0.3,
            "high_min": 0.6,
        },
        "signals": {
            "flaky_test_rate": {
                "threshold": 0.15,  # 15% flaky test rate
                "weight": 0.3,
                "description": "Percentage of tests that are flaky",
            },
            "p1_backlog_growth": {
                "threshold": 5,  # 5 new P1 issues
                "weight": 0.25,
                "description": "Net growth in P1 priority issues",
            },
            "ci_failure_rate": {
                "threshold": 0.2,  # 20% CI failure rate
                "weight": 0.25,
                "description": "Percentage of CI runs failing",
            },
            "open_bug_count": {
                "threshold": 20,  # 20 open bugs
                "weight": 0.2,
                "description": "Total open issues labeled as bugs",
            },
        },
        "multipliers": {
            StressMode.CALM.value: {
                "test_improve": 1.0,
                "docs_update": 1.2,  # Calm periods good for docs
                "issue_count": 1.0,
                "issue_reduce": 1.0,
                "daily_run": 1.0,
                "custom": 1.0,
                "exploration": 1.3,  # Encourage exploration
                "refactor": 1.2,  # Encourage refactoring
            },
            StressMode.ELEVATED.value: {
                "test_improve": 1.2,  # Start emphasizing tests
                "docs_update": 1.0,
                "issue_count": 1.1,
                "issue_reduce": 1.1,
                "daily_run": 1.0,
                "custom": 1.0,
                "exploration": 1.0,
                "refactor": 0.9,  # Discourage risky refactors
            },
            StressMode.HIGH.value: {
                "test_improve": 1.5,  # Strongly incentivize testing
                "docs_update": 0.8,  # Deprioritize docs
                "issue_count": 1.3,  # Reward closing issues
                "issue_reduce": 1.4,  # Strongly reward reducing backlog
                "daily_run": 1.1,
                "custom": 1.0,
                "exploration": 0.7,  # Discourage exploration
                "refactor": 0.6,  # Discourage refactors during crisis
            },
        },
    }


def _get_config_value(key_path: str, default: Any = None) -> Any:
    """Get a value from config using dot notation path."""
    config = _load_stress_config()
    keys = key_path.split(".")
    value = config
    for key in keys:
        if isinstance(value, dict):
            value = value.get(key)
        else:
            return default
    return value if value is not None else default


def _calculate_flaky_test_rate() -> float:
    """Calculate current flaky test rate from available data."""
    try:
        # Try to load from daily run metrics or test results
        test_results_path = Path(settings.repo_root) / ".loop" / "test_results.jsonl"
        if not test_results_path.exists():
            return 0.0

        # Count recent test runs and flaky results
        now = datetime.now(UTC)
        cutoff = now - timedelta(days=7)

        total_runs = 0
        flaky_runs = 0

        if test_results_path.exists():
            for line in test_results_path.read_text().strip().splitlines():
                try:
                    entry = json.loads(line)
                    ts_str = entry.get("timestamp", "")
                    if not ts_str:
                        continue
                    ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
                    if ts >= cutoff:
                        total_runs += 1
                        if entry.get("is_flaky", False):
                            flaky_runs += 1
                except (json.JSONDecodeError, ValueError):
                    continue

        return flaky_runs / max(total_runs, 1)
    except Exception as exc:
        logger.debug("Failed to calculate flaky test rate: %s", exc)
        return 0.0


def _calculate_p1_backlog_growth() -> float:
    """Calculate P1 issue backlog growth."""
    try:
        from dashboard.routes.daily_run import GiteaClient, _load_config

        config = _load_config()
        token = config.get("token")
        client = GiteaClient(config, token)

        if not client.is_available():
            return 0.0

        # Get current P1 issues
        now = datetime.now(UTC)
        cutoff_current = now - timedelta(days=7)
        cutoff_previous = now - timedelta(days=14)

        issues = client.get_paginated("issues", {"state": "all", "labels": "P1", "limit": 100})

        current_count = 0
        previous_count = 0

        for issue in issues:
            created_at = issue.get("created_at", "")
            if not created_at:
                continue
            try:
                created = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
                if created >= cutoff_current:
                    current_count += 1
                elif created >= cutoff_previous:
                    previous_count += 1
            except (ValueError, TypeError):
                continue

        # Return net growth (positive means growing backlog)
        return max(0, current_count - previous_count)
    except Exception as exc:
        logger.debug("Failed to calculate P1 backlog growth: %s", exc)
        return 0.0


def _calculate_ci_failure_rate() -> float:
    """Calculate CI failure rate from recent runs."""
    try:
        # Try to get CI metrics from Gitea or local files
        ci_results_path = Path(settings.repo_root) / ".loop" / "ci_results.jsonl"
        if not ci_results_path.exists():
            return 0.0

        now = datetime.now(UTC)
        cutoff = now - timedelta(days=7)

        total_runs = 0
        failed_runs = 0

        for line in ci_results_path.read_text().strip().splitlines():
            try:
                entry = json.loads(line)
                ts_str = entry.get("timestamp", "")
                if not ts_str:
                    continue
                ts = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
                if ts >= cutoff:
                    total_runs += 1
                    if entry.get("status") != "success":
                        failed_runs += 1
            except (json.JSONDecodeError, ValueError):
                continue

        return failed_runs / max(total_runs, 1)
    except Exception as exc:
        logger.debug("Failed to calculate CI failure rate: %s", exc)
        return 0.0


def _calculate_open_bug_count() -> float:
    """Calculate current open bug count."""
    try:
        from dashboard.routes.daily_run import GiteaClient, _load_config

        config = _load_config()
        token = config.get("token")
        client = GiteaClient(config, token)

        if not client.is_available():
            return 0.0

        issues = client.get_paginated("issues", {"state": "open", "labels": "bug", "limit": 100})

        return float(len(issues))
    except Exception as exc:
        logger.debug("Failed to calculate open bug count: %s", exc)
        return 0.0


def _collect_stress_signals() -> list[StressSignal]:
    """Collect all stress signals from the system."""
    config = _load_stress_config()
    default_config = get_default_config()
    signals_config = config.get("signals", default_config["signals"])

    signals = []

    # Define signal collectors
    collectors = {
        "flaky_test_rate": _calculate_flaky_test_rate,
        "p1_backlog_growth": _calculate_p1_backlog_growth,
        "ci_failure_rate": _calculate_ci_failure_rate,
        "open_bug_count": _calculate_open_bug_count,
    }

    for signal_name, collector in collectors.items():
        signal_cfg = signals_config.get(signal_name, {})
        default_cfg = default_config["signals"].get(signal_name, {})

        try:
            value = collector()
            threshold = signal_cfg.get("threshold", default_cfg.get("threshold", 1.0))
            weight = signal_cfg.get("weight", default_cfg.get("weight", 0.25))

            signals.append(
                StressSignal(
                    name=signal_name,
                    value=value,
                    threshold=threshold,
                    weight=weight,
                )
            )
        except Exception as exc:
            logger.debug("Failed to collect signal %s: %s", signal_name, exc)

    return signals


def _calculate_stress_score(signals: list[StressSignal]) -> float:
    """Calculate overall stress score from signals.

    Score is weighted sum of triggered signal contributions,
    normalized to 0-1 range.
    """
    if not signals:
        return 0.0

    total_weight = sum(s.weight for s in signals)
    if total_weight == 0:
        return 0.0

    triggered_contribution = sum(s.contribution for s in signals)
    return min(1.0, triggered_contribution / total_weight)


def _get_multipliers_for_mode(mode: StressMode) -> dict[str, float]:
    """Get token multipliers for a specific stress mode."""
    config = _load_stress_config()
    default_config = get_default_config()

    multipliers = config.get("multipliers", default_config["multipliers"])
    mode_multipliers = multipliers.get(mode.value, {})
    default_mode_multipliers = default_config["multipliers"].get(mode.value, {})

    # Merge with defaults
    result = default_mode_multipliers.copy()
    result.update(mode_multipliers)

    return result


def detect_stress_mode(
    force_refresh: bool = False,
    min_check_interval_seconds: int = 60,
) -> StressSnapshot:
    """Detect current system stress mode.

    Args:
        force_refresh: Force a new check even if recently checked
        min_check_interval_seconds: Minimum seconds between checks

    Returns:
        StressSnapshot with mode, score, signals, and multipliers
    """
    global _current_snapshot, _last_check_time

    now = datetime.now(UTC)

    # Return cached snapshot if recent and not forced
    if not force_refresh and _current_snapshot is not None and _last_check_time is not None:
        elapsed = (now - _last_check_time).total_seconds()
        if elapsed < min_check_interval_seconds:
            return _current_snapshot

    # Collect signals and calculate stress
    signals = _collect_stress_signals()
    score = _calculate_stress_score(signals)

    # Determine mode from score
    config = _load_stress_config()
    default_config = get_default_config()
    thresholds_cfg = config.get("thresholds", default_config["thresholds"])
    thresholds = StressThresholds(
        elevated_min=thresholds_cfg.get("elevated_min", 0.3),
        high_min=thresholds_cfg.get("high_min", 0.6),
    )
    mode = thresholds.get_mode_for_score(score)

    # Get multipliers for this mode
    multipliers = _get_multipliers_for_mode(mode)

    # Create snapshot
    snapshot = StressSnapshot(
        mode=mode,
        score=score,
        signals=signals,
        multipliers=multipliers,
        timestamp=now.isoformat(),
    )

    # Cache result
    _current_snapshot = snapshot
    _last_check_time = now

    # Log mode changes
    if _current_snapshot is not None and _current_snapshot.mode != mode:
        logger.info(
            "Stress mode changed: %s -> %s (score: %.2f)",
            _current_snapshot.mode.value if _current_snapshot else "none",
            mode.value,
            score,
        )

    return snapshot


def get_current_stress_mode() -> StressMode:
    """Get current stress mode (uses cached or fresh detection)."""
    snapshot = detect_stress_mode()
    return snapshot.mode


def get_multiplier(quest_type: str, mode: StressMode | None = None) -> float:
    """Get token multiplier for a quest type.

    Args:
        quest_type: Type of quest (test_improve, issue_count, etc.)
        mode: Specific mode to get multiplier for, or None for current

    Returns:
        Multiplier value (1.0 = normal, 1.5 = 50% bonus, etc.)
    """
    if mode is None:
        mode = get_current_stress_mode()

    multipliers = _get_multipliers_for_mode(mode)
    return multipliers.get(quest_type, 1.0)


def apply_multiplier(base_reward: int, quest_type: str) -> int:
    """Apply stress-based multiplier to a base reward.

    Args:
        base_reward: Base token reward amount
        quest_type: Type of quest for multiplier lookup

    Returns:
        Adjusted reward amount (always >= 1)
    """
    multiplier = get_multiplier(quest_type)
    adjusted = int(base_reward * multiplier)
    return max(1, adjusted)


def get_stress_summary() -> dict[str, Any]:
    """Get a human-readable summary of current stress state."""
    snapshot = detect_stress_mode()

    # Generate explanation
    explanations = {
        StressMode.CALM: "System is calm. Good time for exploration and refactoring.",
        StressMode.ELEVATED: "Elevated stress detected. Focus on stability and tests.",
        StressMode.HIGH: "HIGH STRESS MODE. Prioritize bug fixes and test hardening.",
    }

    triggered_signals = [s for s in snapshot.signals if s.is_triggered]

    return {
        "mode": snapshot.mode.value,
        "score": round(snapshot.score, 3),
        "explanation": explanations.get(snapshot.mode, "Unknown mode"),
        "active_signals": [
            {
                "name": s.name,
                "value": round(s.value, 3),
                "threshold": s.threshold,
            }
            for s in triggered_signals
        ],
        "current_multipliers": snapshot.multipliers,
        "last_updated": snapshot.timestamp,
    }


def reset_stress_state() -> None:
    """Reset stress state cache (useful for testing)."""
    global _current_snapshot, _last_check_time, _config_cache, _config_mtime
    _current_snapshot = None
    _last_check_time = None
    _config_cache = None
    _config_mtime = 0.0
