"""
Thread-safe CloudWatch query budget manager and metric helper for StackSage.

Extracted here to break the circular import between analysis.py and the
detector modules (ebs, ec2, rds, network, cloudwatch, architecture).
"""

import time
from datetime import datetime, timedelta
from typing import Any, Dict, List


# Thread-safe metric budget manager
class MetricBudget:
    """Thread-safe CloudWatch query budget manager with attempted/used counters.

    Attributes:
        attempted: Total reservation attempts across cw_avg calls.
        used: Successful reservations (queries that consumed budget).
        remaining: Remaining query budget for this run.
    """

    def __init__(self, max_queries: int):
        import threading

        self._lock = threading.Lock()
        self._remaining = max(0, int(max_queries))
        self._max = self._remaining
        self._attempted = 0
        self._used = 0

    def reserve(self, n: int = 1) -> bool:
        with self._lock:
            self._attempted += n
            if self._remaining >= n:
                self._remaining -= n
                self._used += n
                return True
            return False

    @property
    def remaining(self) -> int:
        with self._lock:
            return self._remaining

    @property
    def attempted(self) -> int:
        with self._lock:
            return self._attempted

    @property
    def used(self) -> int:
        with self._lock:
            return self._used

    @property
    def max(self) -> int:
        with self._lock:
            return self._max


# CloudWatch helper: average over lookback window (privacy-safe: aggregates only)
def cw_avg(
    session,
    namespace: str,
    metric_name: str,
    dimensions,  # Can be List[Dict[str, str]] or Dict[str, str]
    period: int = 300,
    days: int = 14,
    statistic: str = "Average",
    region_name: str = None,
    budget: MetricBudget = None,
    errors: List[str] = None,
    max_attempts: int = 3,
    evidence: Dict[str, Any] = None,
):
    """
    Fetch an aggregate average for a CloudWatch metric over a lookback window.

    Privacy: Only aggregate averages are used; no object payload keys or contents.

    Retries: Up to 2 retries (3 total attempts) on throttling with exponential backoff.
    Classification: Appends one of [throttle, not_authorized, no_data, other] to errors.

    Args:
        dimensions: CloudWatch dimensions. Can be:
            - Dict format (newer detectors): {"InstanceId": "i-123"}
            - List format (older detectors): [{"Name": "InstanceId", "Value": "i-123"}]

    Returns:
        float average value or None on failure/no data/budget exhaustion.
    """
    if session is None:
        return None
    import math
    from datetime import timezone

    # Convert dimensions dict to list format if needed (for boto3 compatibility)
    if isinstance(dimensions, dict):
        dimensions = [{"Name": k, "Value": v} for k, v in dimensions.items()]

    # AWS GetMetricStatistics hard limit: 1440 datapoints per request.
    # Auto-adjust period upward if the (days, period) combination would exceed it.
    # Period must also be a multiple of 60 seconds per the CloudWatch API contract.
    _MAX_DATAPOINTS = 1440
    _window_seconds = days * 86400
    _min_period = math.ceil(_window_seconds / _MAX_DATAPOINTS)
    _min_period_rounded = (
        math.ceil(_min_period / 60) * 60
    )  # round up to next 60s multiple
    period = max(period, _min_period_rounded)

    def record_event(event: Dict[str, Any]) -> None:
        if not isinstance(evidence, dict):
            return
        metrics = evidence.get("metrics")
        if not isinstance(metrics, list):
            metrics = []
            evidence["metrics"] = metrics
        metrics.append(event)

    def classify_error(e: Exception) -> str:
        msg = str(e).lower()
        if "throttl" in msg or "rate exceeded" in msg:
            return "throttle"
        if "not authorized" in msg or "accessdenied" in msg or "access denied" in msg:
            return "not_authorized"
        # "exceeds the limit" / "exceeds limit" — too many datapoints requested (period too small)
        if "exceeds" in msg and "datapoints" in msg:
            return "period_too_small"
        # Genuine "no data" from the API (rare as an exception; usually just empty Datapoints list)
        if "no datapoints" in msg:
            return "no_data"
        return "other"

    try:
        cw = session.client("cloudwatch", region_name=region_name)
        end = datetime.now(timezone.utc)
        start = end - timedelta(days=days)
        attempts = 0
        backoff = 0.5
        saw_throttle = False
        while attempts < max_attempts:
            attempts += 1
            try:
                if budget is not None and not budget.reserve(1):
                    # budget exhausted
                    record_event(
                        {
                            "namespace": namespace,
                            "metric": metric_name,
                            "statistic": statistic,
                            "period": period,
                            "days": days,
                            "region": region_name,
                            "status": "skipped_budget",
                        }
                    )
                    return None
                resp = cw.get_metric_statistics(
                    Namespace=namespace,
                    MetricName=metric_name,
                    Dimensions=dimensions,
                    StartTime=start,
                    EndTime=end,
                    Period=period,
                    Statistics=[statistic],
                )
                datapoints = resp.get("Datapoints", [])
                vals = []
                key = statistic
                for d in datapoints:
                    v = d.get(key)
                    if v is not None:
                        vals.append(float(v))
                if not vals:
                    # classify no data
                    if errors is not None:
                        errors.append(f"cw_error:{namespace}:{metric_name}:no_data")
                    record_event(
                        {
                            "namespace": namespace,
                            "metric": metric_name,
                            "statistic": statistic,
                            "period": period,
                            "days": days,
                            "region": region_name,
                            "status": "no_data",
                            "datapoints": 0,
                            "throttled": saw_throttle,
                        }
                    )
                    return None
                avg = sum(vals) / len(vals)
                record_event(
                    {
                        "namespace": namespace,
                        "metric": metric_name,
                        "statistic": statistic,
                        "period": period,
                        "days": days,
                        "region": region_name,
                        "status": "ok",
                        "datapoints": len(vals),
                        "value": avg,
                        "throttled": saw_throttle,
                    }
                )
                return avg
            except Exception as e:
                cls = classify_error(e)
                if errors is not None:
                    errors.append(f"cw_error:{namespace}:{metric_name}:{cls}")
                # retry only on throttle; otherwise break
                if cls == "throttle" and attempts < max_attempts:
                    saw_throttle = True
                    time.sleep(backoff)
                    backoff *= 2
                    continue
                record_event(
                    {
                        "namespace": namespace,
                        "metric": metric_name,
                        "statistic": statistic,
                        "period": period,
                        "days": days,
                        "region": region_name,
                        "status": "error",
                        "classification": cls,
                        "throttled": saw_throttle,
                    }
                )
                break
        return None
    except Exception:
        # client creation or unexpected
        if errors is not None:
            errors.append(f"cw_error:{namespace}:{metric_name}:other")
        record_event(
            {
                "namespace": namespace,
                "metric": metric_name,
                "statistic": statistic,
                "period": period,
                "days": days,
                "region": region_name,
                "status": "error",
                "classification": "other",
            }
        )
        return None
