#!/usr/bin/env python3
"""
c_bindings.py - Python ctypes bindings for libhelix_cdc.so
============================================================

WO#FIBPI3D_TIMING_C_01

Provides fast C implementations for:
- FibPi3D timing (phi_delay, phi_stagger_delays, measure_resonance)
- Fast weight growth (for genome fitting search)
- Genome random search

Usage:
    from helix_cdc.c_bindings import HelixCDC

    lib = HelixCDC()
    if lib.available:
        delays = lib.phi_stagger_delays(10, base_delay=0.1)
        weights = lib.fast_grow_weights_b64("SGVsbG8=", n_values=4096, layer_id=0)
"""

import ctypes
import numpy as np
from pathlib import Path
from typing import Optional, Tuple, List
import os

# Library search paths
_LIB_PATHS = [
    Path(__file__).parent.parent / "c/bin/libhelix_cdc.so",
    Path.home() / "helix-cdc/c/bin/libhelix_cdc.so",
    "/usr/local/lib/libhelix_cdc.so",
    "/usr/lib/libhelix_cdc.so",
]


class HelixCDC:
    """Python bindings for libhelix_cdc.so."""

    def __init__(self, lib_path: Optional[str] = None):
        """
        Initialize bindings.

        Args:
            lib_path: Optional explicit path to libhelix_cdc.so
        """
        self._lib = None
        self._available = False

        # Find library
        paths_to_try = [Path(lib_path)] if lib_path else _LIB_PATHS
        for path in paths_to_try:
            if path.exists():
                try:
                    self._lib = ctypes.CDLL(str(path))
                    self._setup_functions()
                    self._available = True
                    self._lib_path = path
                    break
                except OSError as e:
                    continue

    @property
    def available(self) -> bool:
        """Check if C library is available."""
        return self._available

    def _setup_functions(self):
        """Setup function signatures."""
        # phi_delay(base_delay, index) -> double
        self._lib.phi_delay.argtypes = [ctypes.c_double, ctypes.c_int]
        self._lib.phi_delay.restype = ctypes.c_double

        # phi_stagger_delays(count, base_delay, max_delay, out_delays) -> int
        self._lib.phi_stagger_delays.argtypes = [
            ctypes.c_int,
            ctypes.c_double,
            ctypes.c_double,
            ctypes.POINTER(ctypes.c_double),
        ]
        self._lib.phi_stagger_delays.restype = ctypes.c_int

        # fibonacci_at(n) -> int
        self._lib.fibonacci_at.argtypes = [ctypes.c_int]
        self._lib.fibonacci_at.restype = ctypes.c_int

        # measure_resonance(values, count) -> double
        self._lib.measure_resonance.argtypes = [
            ctypes.POINTER(ctypes.c_double),
            ctypes.c_int,
        ]
        self._lib.measure_resonance.restype = ctypes.c_double

        # fast_grow_weights_b64(genome_b64, n_values, layer_id, out_weights) -> int
        self._lib.fast_grow_weights_b64.argtypes = [
            ctypes.c_char_p,
            ctypes.c_size_t,
            ctypes.c_int,
            ctypes.POINTER(ctypes.c_float),
        ]
        self._lib.fast_grow_weights_b64.restype = ctypes.c_int

        # compute_mse(a, b, n) -> float
        self._lib.compute_mse.argtypes = [
            ctypes.POINTER(ctypes.c_float),
            ctypes.POINTER(ctypes.c_float),
            ctypes.c_size_t,
        ]
        self._lib.compute_mse.restype = ctypes.c_float

        # compute_relative_error(grown, target, n) -> float
        self._lib.compute_relative_error.argtypes = [
            ctypes.POINTER(ctypes.c_float),
            ctypes.POINTER(ctypes.c_float),
            ctypes.c_size_t,
        ]
        self._lib.compute_relative_error.restype = ctypes.c_float

        # genome_random_search(...) -> int
        self._lib.genome_random_search.argtypes = [
            ctypes.POINTER(ctypes.c_float),  # target
            ctypes.c_size_t,                  # n_values
            ctypes.c_size_t,                  # genome_len
            ctypes.c_int,                     # layer_id
            ctypes.c_int,                     # n_iters
            ctypes.c_uint32,                  # seed
            ctypes.c_char_p,                  # out_best_b64
            ctypes.c_size_t,                  # out_best_b64_size
            ctypes.POINTER(ctypes.c_float),   # out_best_error
        ]
        self._lib.genome_random_search.restype = ctypes.c_int

    # ========================================
    # FibPi3D Timing Functions
    # ========================================

    def phi_delay(self, base_delay: float, index: int) -> float:
        """
        Calculate Φ-staggered delay.

        Args:
            base_delay: Base delay in seconds
            index: Request index (0-based)

        Returns:
            Delay in seconds
        """
        if not self._available:
            raise RuntimeError("C library not available")
        return self._lib.phi_delay(base_delay, index)

    def phi_stagger_delays(
        self,
        count: int,
        base_delay: float = 0.1,
        max_delay: float = 0.0,
    ) -> np.ndarray:
        """
        Generate Φ-staggered delay sequence.

        Args:
            count: Number of delays
            base_delay: Base delay in seconds
            max_delay: Maximum delay cap (0 = no cap)

        Returns:
            Array of delays
        """
        if not self._available:
            raise RuntimeError("C library not available")

        delays = np.zeros(count, dtype=np.float64)
        delays_ptr = delays.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

        rc = self._lib.phi_stagger_delays(count, base_delay, max_delay, delays_ptr)
        if rc != 0:
            raise RuntimeError(f"phi_stagger_delays failed: {rc}")

        return delays

    def fibonacci_at(self, n: int) -> int:
        """Get Fibonacci number at index n."""
        if not self._available:
            raise RuntimeError("C library not available")
        return self._lib.fibonacci_at(n)

    def measure_resonance(self, values: np.ndarray) -> float:
        """
        Measure Fibonacci/Φ alignment.

        Args:
            values: Array of values to check

        Returns:
            Resonance score (0-1)
        """
        if not self._available:
            raise RuntimeError("C library not available")

        values = np.asarray(values, dtype=np.float64)
        values_ptr = values.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

        return self._lib.measure_resonance(values_ptr, len(values))

    # ========================================
    # Fast Weight Growth
    # ========================================

    def fast_grow_weights_b64(
        self,
        genome_b64: str,
        n_values: int,
        layer_id: int = 0,
    ) -> np.ndarray:
        """
        Fast genome-to-weights growth.

        Args:
            genome_b64: Base64-encoded genome
            n_values: Number of weights to generate
            layer_id: Layer ID for growth

        Returns:
            Float32 weight array
        """
        if not self._available:
            raise RuntimeError("C library not available")

        weights = np.zeros(n_values, dtype=np.float32)
        weights_ptr = weights.ctypes.data_as(ctypes.POINTER(ctypes.c_float))

        rc = self._lib.fast_grow_weights_b64(
            genome_b64.encode('utf-8'),
            n_values,
            layer_id,
            weights_ptr,
        )
        if rc != 0:
            raise RuntimeError(f"fast_grow_weights_b64 failed: {rc}")

        return weights

    def compute_mse(self, a: np.ndarray, b: np.ndarray) -> float:
        """Compute MSE between two arrays."""
        if not self._available:
            raise RuntimeError("C library not available")

        a = np.ascontiguousarray(a, dtype=np.float32)
        b = np.ascontiguousarray(b, dtype=np.float32)
        assert len(a) == len(b)

        a_ptr = a.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
        b_ptr = b.ctypes.data_as(ctypes.POINTER(ctypes.c_float))

        return self._lib.compute_mse(a_ptr, b_ptr, len(a))

    def compute_relative_error(self, grown: np.ndarray, target: np.ndarray) -> float:
        """Compute relative Frobenius error."""
        if not self._available:
            raise RuntimeError("C library not available")

        grown = np.ascontiguousarray(grown, dtype=np.float32)
        target = np.ascontiguousarray(target, dtype=np.float32)
        assert len(grown) == len(target)

        grown_ptr = grown.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
        target_ptr = target.ctypes.data_as(ctypes.POINTER(ctypes.c_float))

        return self._lib.compute_relative_error(grown_ptr, target_ptr, len(grown))

    # ========================================
    # Genome Search
    # ========================================

    def genome_random_search(
        self,
        target: np.ndarray,
        genome_len: int,
        layer_id: int,
        n_iters: int,
        seed: int = 42,
    ) -> Tuple[str, float]:
        """
        Random search for best genome that matches target weights.

        Args:
            target: Target float32 weights
            genome_len: Genome length in bytes
            layer_id: Layer ID for growth
            n_iters: Number of search iterations
            seed: RNG seed

        Returns:
            (best_genome_b64, best_error)
        """
        if not self._available:
            raise RuntimeError("C library not available")

        target = np.ascontiguousarray(target.flatten(), dtype=np.float32)
        n_values = len(target)

        target_ptr = target.ctypes.data_as(ctypes.POINTER(ctypes.c_float))

        # Allocate output buffer for best genome
        out_b64_size = genome_len * 2 + 16
        out_b64 = ctypes.create_string_buffer(out_b64_size)

        out_error = ctypes.c_float(0.0)

        rc = self._lib.genome_random_search(
            target_ptr,
            n_values,
            genome_len,
            layer_id,
            n_iters,
            seed,
            out_b64,
            out_b64_size,
            ctypes.byref(out_error),
        )
        if rc != 0:
            raise RuntimeError(f"genome_random_search failed: {rc}")

        return out_b64.value.decode('utf-8'), out_error.value


# ============================================
# Convenience functions
# ============================================

_LIB_INSTANCE: Optional[HelixCDC] = None


def get_lib() -> HelixCDC:
    """Get or create global library instance."""
    global _LIB_INSTANCE
    if _LIB_INSTANCE is None:
        _LIB_INSTANCE = HelixCDC()
    return _LIB_INSTANCE


def phi_delay(base_delay: float, index: int) -> float:
    """Calculate Φ-staggered delay (uses C if available, else Python)."""
    lib = get_lib()
    if lib.available:
        return lib.phi_delay(base_delay, index)
    else:
        # Python fallback
        PHI = 1.6180339887498948482
        return base_delay * (PHI ** index)


def phi_stagger_delays(count: int, base_delay: float = 0.1, max_delay: float = 0.0) -> List[float]:
    """Generate Φ-staggered delays (uses C if available, else Python)."""
    lib = get_lib()
    if lib.available:
        return lib.phi_stagger_delays(count, base_delay, max_delay).tolist()
    else:
        # Python fallback
        PHI = 1.6180339887498948482
        delays = []
        for i in range(count):
            delay = base_delay * (PHI ** i)
            if max_delay > 0:
                delay = min(delay, max_delay)
            delays.append(delay)
        return delays


def measure_resonance(values: List[float]) -> float:
    """Measure Fibonacci/Φ alignment (uses C if available, else Python)."""
    lib = get_lib()
    if lib.available:
        return lib.measure_resonance(np.array(values, dtype=np.float64))
    else:
        # Python fallback
        if len(values) < 2:
            return 1.0
        PHI = 1.6180339887498948482
        ratios = []
        for i in range(1, len(values)):
            if values[i-1] != 0:
                ratios.append(values[i] / values[i-1])
        if not ratios:
            return 0.0
        deviations = [abs(r - PHI) for r in ratios]
        avg_deviation = sum(deviations) / len(deviations)
        return max(0.0, 1.0 - (avg_deviation / PHI))


# ============================================
# CLI test
# ============================================

if __name__ == "__main__":
    import time

    lib = HelixCDC()
    print(f"Library available: {lib.available}")

    if lib.available:
        print(f"Library path: {lib._lib_path}")
        print()

        # Test phi_delay
        print("=== phi_delay ===")
        for i in range(5):
            delay = lib.phi_delay(0.1, i)
            print(f"  index={i}: {delay:.4f}s")
        print()

        # Test phi_stagger_delays
        print("=== phi_stagger_delays ===")
        delays = lib.phi_stagger_delays(10, base_delay=0.1, max_delay=1.0)
        for i, d in enumerate(delays):
            print(f"  {i}: {d:.4f}s")
        print()

        # Test fibonacci_at
        print("=== fibonacci_at ===")
        for i in range(12):
            print(f"  F({i}) = {lib.fibonacci_at(i)}")
        print()

        # Test measure_resonance
        print("=== measure_resonance ===")
        fib_values = [1, 1, 2, 3, 5, 8, 13, 21]
        score = lib.measure_resonance(np.array(fib_values, dtype=np.float64))
        print(f"  Fibonacci sequence resonance: {score:.4f}")
        linear = [1, 2, 3, 4, 5, 6, 7, 8]
        score = lib.measure_resonance(np.array(linear, dtype=np.float64))
        print(f"  Linear sequence resonance: {score:.4f}")
        print()

        # Test fast weight growth
        print("=== fast_grow_weights_b64 ===")
        genome = "SGVsbG8gSGVsaXggV29ybGQhIFRoaXMgaXMgYSB0ZXN0IGdlbm9tZS4="
        t0 = time.perf_counter()
        weights = lib.fast_grow_weights_b64(genome, n_values=4096, layer_id=0)
        t1 = time.perf_counter()
        print(f"  Grew 4096 weights in {(t1-t0)*1000:.2f}ms")
        print(f"  Shape: {weights.shape}")
        print(f"  Range: [{weights.min():.4f}, {weights.max():.4f}]")
        print(f"  Mean: {weights.mean():.4f}")
        print()

        # Speed comparison: C vs Python full grow
        print("=== Speed: C fast_grow vs Python full grow ===")
        n_values = 64 * 64  # 4096 (same as 64x64 matrix)

        # C version
        t0 = time.perf_counter()
        for _ in range(100):
            _ = lib.fast_grow_weights_b64(genome, n_values=n_values, layer_id=0)
        t_c = (time.perf_counter() - t0) / 100 * 1000
        print(f"  C fast_grow (100 runs): {t_c:.3f}ms per call")
        print()

        # Genome search test
        print("=== genome_random_search ===")
        # Create a random target
        target = np.random.randn(1024).astype(np.float32) * 0.1

        t0 = time.perf_counter()
        best_genome, best_error = lib.genome_random_search(
            target=target,
            genome_len=256,
            layer_id=0,
            n_iters=1000,
            seed=42,
        )
        t1 = time.perf_counter()

        print(f"  Search time: {(t1-t0)*1000:.1f}ms for 1000 iterations")
        print(f"  Best error: {best_error:.6f}")
        print(f"  Best genome (first 50 chars): {best_genome[:50]}...")
    else:
        print("C library not available, using Python fallback")
        print()

        # Test Python fallbacks
        print("=== phi_delay (Python) ===")
        for i in range(5):
            delay = phi_delay(0.1, i)
            print(f"  index={i}: {delay:.4f}s")
