#!/usr/bin/env python3
"""
jazz_http.py - Combustion-Synced HTTP Client (WO#NET_JAZZ_06)
==============================================================

HTTP client that times requests to hit CPU valleys.

Instead of stacking cores with burst requests, this client
"surfs" the CPU load curve - firing when the system is idle.

Usage:
    from helix_cdc.jazz.jazz_http import JazzClient, jazz_get, jazz_post

    # Simple API
    response = jazz_get("https://api.example.com/data")
    response = jazz_post("https://api.example.com/action", json={"key": "value"})

    # Client with metrics
    client = JazzClient(valley_threshold=40.0)
    result = client.get("https://api.example.com/data")
    print(client.get_metrics())

Benefits:
    - Reduced CPU contention on loaded systems
    - More consistent RTT (less variance)
    - Automatic backoff when system is busy
    - Metrics collection for tuning

This is for YOUR edge/local API calls AND for routing calls
back to external APIs (ChatGPT, etc.).
"""

from __future__ import annotations
import json as json_lib
import time
import statistics
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Dict, Any, Optional, List, Callable
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
import ssl
import gzip
from io import BytesIO

from helix_cdc.jazz.combustion_sync import (
    CombustionSampler,
    fire_on_valley,
    get_cpu_load,
)


@dataclass
class JazzResponse:
    """Response from a Jazz HTTP request."""
    status_code: int
    body: bytes
    headers: Dict[str, str]
    rtt_ms: float
    cpu_at_fire: float
    valley_found: bool
    wait_time_ms: float
    timestamp: str

    def json(self) -> Any:
        """Parse body as JSON."""
        return json_lib.loads(self.body.decode('utf-8'))

    def text(self) -> str:
        """Body as string."""
        return self.body.decode('utf-8')

    def __repr__(self) -> str:
        return f"JazzResponse(status={self.status_code}, rtt={self.rtt_ms:.1f}ms, cpu={self.cpu_at_fire:.1f}%, valley={self.valley_found})"


@dataclass
class JazzMetrics:
    """Collected metrics from a JazzClient."""
    total_requests: int = 0
    successful_requests: int = 0
    failed_requests: int = 0
    valleys_hit: int = 0
    valleys_missed: int = 0
    total_wait_ms: float = 0.0
    total_rtt_ms: float = 0.0
    rtt_samples: List[float] = field(default_factory=list)
    cpu_samples: List[float] = field(default_factory=list)

    @property
    def avg_rtt_ms(self) -> float:
        return statistics.mean(self.rtt_samples) if self.rtt_samples else 0.0

    @property
    def stdev_rtt_ms(self) -> float:
        return statistics.stdev(self.rtt_samples) if len(self.rtt_samples) > 1 else 0.0

    @property
    def avg_cpu(self) -> float:
        return statistics.mean(self.cpu_samples) if self.cpu_samples else 0.0

    @property
    def valley_hit_rate(self) -> float:
        total = self.valleys_hit + self.valleys_missed
        return self.valleys_hit / total if total > 0 else 0.0

    def to_dict(self) -> Dict[str, Any]:
        return {
            "total_requests": self.total_requests,
            "successful_requests": self.successful_requests,
            "failed_requests": self.failed_requests,
            "valleys_hit": self.valleys_hit,
            "valleys_missed": self.valleys_missed,
            "valley_hit_rate": self.valley_hit_rate,
            "avg_rtt_ms": self.avg_rtt_ms,
            "stdev_rtt_ms": self.stdev_rtt_ms,
            "avg_cpu": self.avg_cpu,
            "total_wait_ms": self.total_wait_ms,
        }


class JazzClient:
    """
    HTTP client that surfs CPU valleys.

    Instead of firing requests immediately (stacking cores),
    this client waits for CPU to drop below threshold before firing.
    """

    def __init__(
        self,
        valley_threshold: float = 40.0,
        max_wait: float = 0.5,
        poll_interval: float = 0.01,
        timeout: float = 30.0,
        default_headers: Optional[Dict[str, str]] = None,
        collect_metrics: bool = True,
    ):
        """
        Initialize JazzClient.

        Args:
            valley_threshold: CPU % below which to fire (0-100)
            max_wait: Max seconds to wait for valley
            poll_interval: How often to check CPU
            timeout: HTTP timeout in seconds
            default_headers: Headers to include in all requests
            collect_metrics: Whether to collect performance metrics
        """
        self.valley_threshold = valley_threshold
        self.max_wait = max_wait
        self.poll_interval = poll_interval
        self.timeout = timeout
        self.default_headers = default_headers or {}
        self.collect_metrics = collect_metrics

        self._sampler = CombustionSampler(
            valley_threshold=valley_threshold,
            sample_interval=poll_interval,
        )
        self._metrics = JazzMetrics() if collect_metrics else None

        # Prime the sampler
        for _ in range(5):
            self._sampler.sample()
            time.sleep(0.01)

    def _wait_for_valley(self) -> tuple[bool, float, float]:
        """
        Wait for CPU valley.

        Returns:
            (valley_found, wait_time_ms, cpu_at_fire)
        """
        start = time.perf_counter()
        cpu = get_cpu_load()

        while cpu > self.valley_threshold:
            if time.perf_counter() - start > self.max_wait:
                break
            time.sleep(self.poll_interval)
            cpu = get_cpu_load()

        wait_time = (time.perf_counter() - start) * 1000
        valley_found = cpu <= self.valley_threshold

        return valley_found, wait_time, cpu

    def _do_request(
        self,
        method: str,
        url: str,
        headers: Optional[Dict[str, str]] = None,
        data: Optional[bytes] = None,
        json: Optional[Any] = None,
    ) -> JazzResponse:
        """
        Execute HTTP request after waiting for valley.
        """
        # Wait for valley
        valley_found, wait_time_ms, cpu_at_fire = self._wait_for_valley()

        # Build request
        all_headers = {**self.default_headers, **(headers or {})}
        all_headers.setdefault("User-Agent", "JazzClient/1.0")

        if json is not None:
            data = json_lib.dumps(json).encode('utf-8')
            all_headers.setdefault("Content-Type", "application/json")

        req = Request(url, data=data, headers=all_headers, method=method)

        # Execute with timing
        rtt_start = time.perf_counter()
        try:
            ctx = ssl.create_default_context()
            with urlopen(req, timeout=self.timeout, context=ctx) as resp:
                rtt_ms = (time.perf_counter() - rtt_start) * 1000
                body = resp.read()

                # Handle gzip
                if resp.headers.get('Content-Encoding') == 'gzip':
                    body = gzip.GzipFile(fileobj=BytesIO(body)).read()

                response = JazzResponse(
                    status_code=resp.status,
                    body=body,
                    headers=dict(resp.headers),
                    rtt_ms=rtt_ms,
                    cpu_at_fire=cpu_at_fire,
                    valley_found=valley_found,
                    wait_time_ms=wait_time_ms,
                    timestamp=datetime.now(timezone.utc).isoformat(),
                )
                success = True

        except HTTPError as e:
            rtt_ms = (time.perf_counter() - rtt_start) * 1000
            response = JazzResponse(
                status_code=e.code,
                body=e.read() if e.fp else b'',
                headers=dict(e.headers) if e.headers else {},
                rtt_ms=rtt_ms,
                cpu_at_fire=cpu_at_fire,
                valley_found=valley_found,
                wait_time_ms=wait_time_ms,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
            success = False

        except URLError as e:
            rtt_ms = (time.perf_counter() - rtt_start) * 1000
            response = JazzResponse(
                status_code=0,
                body=str(e).encode('utf-8'),
                headers={},
                rtt_ms=rtt_ms,
                cpu_at_fire=cpu_at_fire,
                valley_found=valley_found,
                wait_time_ms=wait_time_ms,
                timestamp=datetime.now(timezone.utc).isoformat(),
            )
            success = False

        # Update metrics
        if self._metrics:
            self._metrics.total_requests += 1
            if success and response.status_code < 400:
                self._metrics.successful_requests += 1
            else:
                self._metrics.failed_requests += 1

            if valley_found:
                self._metrics.valleys_hit += 1
            else:
                self._metrics.valleys_missed += 1

            self._metrics.total_wait_ms += wait_time_ms
            self._metrics.total_rtt_ms += rtt_ms
            self._metrics.rtt_samples.append(rtt_ms)
            self._metrics.cpu_samples.append(cpu_at_fire)

        return response

    def get(
        self,
        url: str,
        headers: Optional[Dict[str, str]] = None,
    ) -> JazzResponse:
        """HTTP GET with valley timing."""
        return self._do_request("GET", url, headers=headers)

    def post(
        self,
        url: str,
        headers: Optional[Dict[str, str]] = None,
        data: Optional[bytes] = None,
        json: Optional[Any] = None,
    ) -> JazzResponse:
        """HTTP POST with valley timing."""
        return self._do_request("POST", url, headers=headers, data=data, json=json)

    def put(
        self,
        url: str,
        headers: Optional[Dict[str, str]] = None,
        data: Optional[bytes] = None,
        json: Optional[Any] = None,
    ) -> JazzResponse:
        """HTTP PUT with valley timing."""
        return self._do_request("PUT", url, headers=headers, data=data, json=json)

    def delete(
        self,
        url: str,
        headers: Optional[Dict[str, str]] = None,
    ) -> JazzResponse:
        """HTTP DELETE with valley timing."""
        return self._do_request("DELETE", url, headers=headers)

    def get_metrics(self) -> Dict[str, Any]:
        """Get collected metrics."""
        if self._metrics:
            return self._metrics.to_dict()
        return {}

    def reset_metrics(self) -> None:
        """Reset collected metrics."""
        if self._metrics:
            self._metrics = JazzMetrics()


# Module-level convenience functions with shared sampler
_global_client: Optional[JazzClient] = None


def _get_global_client() -> JazzClient:
    global _global_client
    if _global_client is None:
        _global_client = JazzClient()
    return _global_client


def jazz_get(url: str, headers: Optional[Dict[str, str]] = None) -> JazzResponse:
    """
    Valley-timed HTTP GET.

    Example:
        resp = jazz_get("https://api.example.com/data")
        print(resp.json())
    """
    return _get_global_client().get(url, headers=headers)


def jazz_post(
    url: str,
    headers: Optional[Dict[str, str]] = None,
    data: Optional[bytes] = None,
    json: Optional[Any] = None,
) -> JazzResponse:
    """
    Valley-timed HTTP POST.

    Example:
        resp = jazz_post("https://api.example.com/action", json={"key": "value"})
        print(resp.status_code)
    """
    return _get_global_client().post(url, headers=headers, data=data, json=json)


def get_jazz_metrics() -> Dict[str, Any]:
    """Get metrics from the global Jazz client."""
    return _get_global_client().get_metrics()


def reset_jazz_metrics() -> None:
    """Reset global Jazz client metrics."""
    _get_global_client().reset_metrics()


# CLI for testing
def _cli():
    import argparse

    parser = argparse.ArgumentParser(description="Jazz HTTP Client")
    parser.add_argument("url", help="URL to fetch")
    parser.add_argument("--method", default="GET", choices=["GET", "POST", "PUT", "DELETE"])
    parser.add_argument("--data", help="POST data (JSON)")
    parser.add_argument("--count", type=int, default=5, help="Number of requests")
    parser.add_argument("--valley-threshold", type=float, default=40.0)
    parser.add_argument("--compare", action="store_true", help="Compare burst vs jazz")
    args = parser.parse_args()

    client = JazzClient(valley_threshold=args.valley_threshold)

    print("=" * 60)
    print("JAZZ HTTP CLIENT TEST")
    print("=" * 60)
    print(f"URL: {args.url}")
    print(f"Requests: {args.count}")
    print(f"Valley threshold: {args.valley_threshold}%")
    print()

    if args.compare:
        # Burst mode (no waiting)
        print("[1/2] Burst mode (no valley sync)...")
        burst_rtts = []
        for _ in range(args.count):
            start = time.perf_counter()
            try:
                req = Request(args.url, headers={"User-Agent": "BurstTest/1.0"})
                with urlopen(req, timeout=10) as resp:
                    resp.read()
                    rtt = (time.perf_counter() - start) * 1000
                    burst_rtts.append(rtt)
            except Exception:
                pass

        if burst_rtts:
            print(f"  RTT mean: {statistics.mean(burst_rtts):.2f}ms")
            print(f"  RTT stdev: {statistics.stdev(burst_rtts) if len(burst_rtts) > 1 else 0:.2f}ms")
        print()

        time.sleep(0.5)

    # Jazz mode
    print(f"[{'2/2' if args.compare else '1/1'}] Jazz mode (valley sync)...")
    for i in range(args.count):
        if args.method == "GET":
            resp = client.get(args.url)
        elif args.method == "POST":
            json_data = json_lib.loads(args.data) if args.data else None
            resp = client.post(args.url, json=json_data)
        else:
            resp = client.get(args.url)

        print(f"  [{i+1}/{args.count}] {resp}")

    print()
    metrics = client.get_metrics()
    print("Metrics:")
    print(f"  Requests: {metrics['total_requests']} ({metrics['successful_requests']} ok, {metrics['failed_requests']} fail)")
    print(f"  Valley hit rate: {metrics['valley_hit_rate']*100:.1f}%")
    print(f"  Avg RTT: {metrics['avg_rtt_ms']:.2f}ms (stdev: {metrics['stdev_rtt_ms']:.2f}ms)")
    print(f"  Avg CPU at fire: {metrics['avg_cpu']:.1f}%")
    print(f"  Total wait time: {metrics['total_wait_ms']:.1f}ms")
    print("=" * 60)


if __name__ == "__main__":
    _cli()
