#!/usr/bin/env python3
"""
combustion_sync.py - Φ-Synced Combustion Timing
================================================

True Jazz timing: network calls timed to hit CPU valleys.

The "combustion" metaphor:
- CPU cycles have peaks (busy) and valleys (idle)
- Network requests should ARRIVE during valleys
- Φ-sync means measuring the CPU rhythm and timing requests to "fall" into gaps

This is proactive scheduling, not reactive feedback:
- Measure CPU cycle frequency
- Calculate next valley time
- Fire request to arrive at valley

WO#NET_JAZZ_05 - Combustion Sync
"""

from __future__ import annotations
import time
import statistics
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Any, Optional, Callable

from helix_cdc.jazz.fibpi3d_timing import GOLDEN_RATIO, phi_delay


def sample_cpu_instant() -> float:
    """Get instantaneous CPU usage (0-100)."""
    try:
        with open("/proc/stat", "r") as f:
            line = f.readline()
        parts = line.split()
        idle = int(parts[4])
        total = sum(int(p) for p in parts[1:])
        return idle, total
    except Exception:
        return 0, 1


def get_cpu_load() -> float:
    """Get CPU load over 50ms window."""
    idle1, total1 = sample_cpu_instant()
    time.sleep(0.05)
    idle2, total2 = sample_cpu_instant()

    idle_delta = idle2 - idle1
    total_delta = total2 - total1

    if total_delta == 0:
        return 0.0

    return (1.0 - idle_delta / total_delta) * 100


@dataclass
class CombustionCycle:
    """Represents a CPU combustion cycle."""
    timestamp: float
    cpu_load: float
    is_valley: bool  # True if this is a low point


@dataclass
class CombustionSampler:
    """
    Samples CPU to find combustion rhythm.

    Detects valleys (low CPU periods) and peaks (high CPU periods).
    """
    sample_interval: float = 0.02  # 20ms between samples
    window_size: int = 20  # Rolling window of samples
    valley_threshold: float = 30.0  # CPU% below this = valley

    _samples: List[CombustionCycle] = field(default_factory=list)

    def sample(self) -> CombustionCycle:
        """Take one combustion sample."""
        cpu = get_cpu_load()
        is_valley = cpu < self.valley_threshold

        cycle = CombustionCycle(
            timestamp=time.time(),
            cpu_load=cpu,
            is_valley=is_valley,
        )

        self._samples.append(cycle)
        if len(self._samples) > self.window_size:
            self._samples.pop(0)

        return cycle

    def get_rhythm(self) -> Dict[str, Any]:
        """Analyze CPU rhythm from samples."""
        if len(self._samples) < 3:
            return {"samples": 0, "rhythm_detected": False}

        loads = [s.cpu_load for s in self._samples]
        valleys = [s for s in self._samples if s.is_valley]

        return {
            "samples": len(self._samples),
            "cpu_mean": statistics.mean(loads),
            "cpu_stdev": statistics.stdev(loads) if len(loads) > 1 else 0,
            "valley_count": len(valleys),
            "valley_ratio": len(valleys) / len(self._samples),
            "rhythm_detected": True,
        }

    def wait_for_valley(self, timeout: float = 1.0) -> bool:
        """
        Wait for next CPU valley (combustion gap).

        Returns True if valley found, False if timeout.
        """
        start = time.time()

        while time.time() - start < timeout:
            cycle = self.sample()
            if cycle.is_valley:
                return True
            time.sleep(self.sample_interval)

        return False

    def estimate_next_valley(self) -> Optional[float]:
        """
        Estimate time until next valley based on rhythm.

        Returns seconds until predicted valley, or None if can't predict.
        """
        if len(self._samples) < 5:
            return None

        # Find valley timestamps
        valley_times = [s.timestamp for s in self._samples if s.is_valley]

        if len(valley_times) < 2:
            return None

        # Estimate interval between valleys
        intervals = [valley_times[i+1] - valley_times[i]
                     for i in range(len(valley_times)-1)]
        avg_interval = statistics.mean(intervals)

        # Time since last valley
        last_valley = valley_times[-1]
        since_valley = time.time() - last_valley

        # Estimate time to next
        if since_valley >= avg_interval:
            return 0.0  # Should be now or soon
        else:
            return avg_interval - since_valley


def fire_on_valley(
    action: Callable[[], Any],
    sampler: Optional[CombustionSampler] = None,
    timeout: float = 0.5,
) -> Dict[str, Any]:
    """
    Fire action when CPU hits a valley.

    This is Φ-synced combustion timing - the request is timed
    to arrive when CPU is idle.

    Args:
        action: Function to call (e.g., HTTP request)
        sampler: Optional pre-configured sampler
        timeout: Max time to wait for valley

    Returns:
        Dict with timing info and action result
    """
    if sampler is None:
        sampler = CombustionSampler()
        # Prime the sampler
        for _ in range(5):
            sampler.sample()
            time.sleep(0.01)

    wait_start = time.time()
    valley_found = sampler.wait_for_valley(timeout=timeout)
    wait_time = time.time() - wait_start

    # Fire action
    action_start = time.time()
    try:
        result = action()
        action_time = time.time() - action_start
        action_ok = True
    except Exception as e:
        action_time = time.time() - action_start
        result = str(e)
        action_ok = False

    # Sample CPU after action to see if we hit the valley
    post_cpu = get_cpu_load()

    return {
        "valley_found": valley_found,
        "wait_time_ms": wait_time * 1000,
        "action_time_ms": action_time * 1000,
        "action_ok": action_ok,
        "post_cpu": post_cpu,
        "result": result,
        "timestamp": datetime.now().isoformat(),
    }


def combustion_wave(
    actions: List[Callable[[], Any]],
    sampler: Optional[CombustionSampler] = None,
    wave_delay: float = 0.0,
) -> List[Dict[str, Any]]:
    """
    Fire multiple actions, each on a valley.

    This creates a "wave" of requests that all hit valleys.

    Args:
        actions: List of functions to call
        sampler: Optional shared sampler
        wave_delay: Extra delay between actions (Φ-scaled)

    Returns:
        List of results for each action
    """
    if sampler is None:
        sampler = CombustionSampler()
        for _ in range(10):
            sampler.sample()
            time.sleep(0.01)

    results = []
    for i, action in enumerate(actions):
        result = fire_on_valley(action, sampler=sampler)
        result["wave_index"] = i
        results.append(result)

        if wave_delay > 0 and i < len(actions) - 1:
            # Φ-scaled wave delay
            delay = phi_delay(wave_delay, i)
            time.sleep(delay)

    return results


# ============================================================================
# CLI
# ============================================================================

def _cli():
    """Combustion Sync CLI."""
    import argparse
    from urllib.request import urlopen, Request

    parser = argparse.ArgumentParser(description="Φ-Synced Combustion Timing")
    parser.add_argument("--url", default="http://localhost:8770/healthcheck",
                        help="URL to test")
    parser.add_argument("--count", type=int, default=10,
                        help="Number of requests")
    parser.add_argument("--compare", action="store_true",
                        help="Compare combustion vs naive")
    args = parser.parse_args()

    def make_request():
        start = time.time()
        req = Request(args.url, headers={"User-Agent": "CombustionSync/1.0"})
        with urlopen(req, timeout=5) as resp:
            return (time.time() - start) * 1000  # RTT in ms

    print("=" * 60)
    print("COMBUSTION SYNC TEST")
    print("=" * 60)
    print(f"URL: {args.url}")
    print(f"Requests: {args.count}")
    print()

    # Prime sampler
    sampler = CombustionSampler()
    print("Sampling CPU rhythm...")
    for _ in range(20):
        sampler.sample()
        time.sleep(0.02)

    rhythm = sampler.get_rhythm()
    print(f"CPU mean: {rhythm['cpu_mean']:.1f}%")
    print(f"Valley ratio: {rhythm['valley_ratio']:.2f}")
    print()

    if args.compare:
        # Naive burst
        print("[1/2] Naive burst (no sync)...")
        naive_rtts = []
        for _ in range(args.count):
            try:
                rtt = make_request()
                naive_rtts.append(rtt)
            except Exception:
                pass

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

        time.sleep(0.5)

    # Combustion sync
    print(f"[{'2/2' if args.compare else '1/1'}] Combustion sync (valley timing)...")
    actions = [make_request for _ in range(args.count)]
    results = combustion_wave(actions, sampler=sampler, wave_delay=0.02)

    valleys_hit = sum(1 for r in results if r["valley_found"])
    rtts = [r["result"] for r in results if r["action_ok"] and isinstance(r["result"], (int, float))]

    print(f"  Valleys hit: {valleys_hit}/{len(results)}")
    if rtts:
        print(f"  RTT mean: {statistics.mean(rtts):.2f}ms")
        print(f"  RTT stdev: {statistics.stdev(rtts) if len(rtts) > 1 else 0:.2f}ms")

    print()
    print("=" * 60)


if __name__ == "__main__":
    _cli()
