"""
VisionShell - Agent Physics Vision
Standalone version for cubesquare package.

NO Python for-loops in hot paths.
ALL batch operations use C backend via _core.
ZERO dependencies on pipe_v001, CreatureGarage, or hou.

Physics vision calculations using CST constants:
    - Sky dome: hex → morton → coords → sky color
    - Sun position: hex → coords → orbital mechanics
    - Ray tracing: wavelength → basin (L7), direction → morton path
    - Camera simulation
"""
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass
import math

from .._core import PHI, K5, K11, PRIME_131
from .._core import morton_encode, morton_decode, compute_basin

# Physics constants derived from CST
LIGHT_SPEED_FERNANDEZ = 1.0 / math.sqrt(3)  # Abstract light speed = 1/sqrt(3)
HEX_DIAGONAL = math.sqrt(3)  # Hex diagonal = 1 Fernandez Light Unit
PLANCK_CST = PHI / (K11 * 1e34)  # CST-derived Planck-like constant

# Wavelength to color mapping (L7 Frequency layer)
# Basin 88-101 maps to visible spectrum 380nm-750nm
WAVELENGTH_MIN = 380  # nm (violet)
WAVELENGTH_MAX = 750  # nm (red)
L7_BASIN_MIN = 88
L7_BASIN_MAX = 101


@dataclass
class SkyColor:
    """RGB color with wavelength info."""
    r: float
    g: float
    b: float
    wavelength_nm: float
    basin: int


@dataclass
class SunPosition:
    """Sun position in CST coordinate space."""
    x: float
    y: float
    z: float
    elevation: float  # degrees above horizon
    azimuth: float    # degrees from north
    intensity: float  # 0-1
    morton_code: int


@dataclass
class RayResult:
    """Result of ray tracing."""
    start_hex: str
    direction: Tuple[float, float, float]
    wavelength_nm: float
    basin: int
    steps: int
    final_coords: Tuple[int, int, int]
    final_morton: int
    absorbed: bool


def wavelength_to_basin(wavelength_nm: float) -> int:
    """Map wavelength to L7 basin (88-101). O(1) operation."""
    # Clamp to visible spectrum
    wl = max(WAVELENGTH_MIN, min(WAVELENGTH_MAX, wavelength_nm))
    # Linear interpolation to basin range
    t = (wl - WAVELENGTH_MIN) / (WAVELENGTH_MAX - WAVELENGTH_MIN)
    basin = int(L7_BASIN_MIN + t * (L7_BASIN_MAX - L7_BASIN_MIN))
    return basin


def basin_to_wavelength(basin: int) -> float:
    """Map L7 basin to wavelength. O(1) operation."""
    if basin < L7_BASIN_MIN:
        return WAVELENGTH_MIN
    if basin > L7_BASIN_MAX:
        return WAVELENGTH_MAX
    t = (basin - L7_BASIN_MIN) / (L7_BASIN_MAX - L7_BASIN_MIN)
    return WAVELENGTH_MIN + t * (WAVELENGTH_MAX - WAVELENGTH_MIN)


def wavelength_to_rgb(wavelength_nm: float) -> Tuple[float, float, float]:
    """Convert wavelength to RGB color. O(1) operation."""
    # Approximate visible spectrum to RGB
    wl = wavelength_nm

    if wl < 380:
        r, g, b = 0.0, 0.0, 0.0
    elif wl < 440:
        r = -(wl - 440) / (440 - 380)
        g = 0.0
        b = 1.0
    elif wl < 490:
        r = 0.0
        g = (wl - 440) / (490 - 440)
        b = 1.0
    elif wl < 510:
        r = 0.0
        g = 1.0
        b = -(wl - 510) / (510 - 490)
    elif wl < 580:
        r = (wl - 510) / (580 - 510)
        g = 1.0
        b = 0.0
    elif wl < 645:
        r = 1.0
        g = -(wl - 645) / (645 - 580)
        b = 0.0
    elif wl <= 750:
        r = 1.0
        g = 0.0
        b = 0.0
    else:
        r, g, b = 0.0, 0.0, 0.0

    # Intensity adjustment at edges
    if wl < 420:
        factor = 0.3 + 0.7 * (wl - 380) / (420 - 380)
    elif wl > 700:
        factor = 0.3 + 0.7 * (750 - wl) / (750 - 700)
    else:
        factor = 1.0

    return (r * factor, g * factor, b * factor)


def hex_to_coords(hex_id: str) -> Tuple[int, int, int]:
    """Extract coordinates from hex ID. O(1) operation."""
    packed = int(hex_id, 16)
    morton = packed & 0xFFFFF  # Bits 0-19
    return morton_decode(morton)


def coords_to_elevation_azimuth(x: int, y: int, z: int) -> Tuple[float, float]:
    """Convert CST coords to elevation/azimuth. O(1) operation."""
    # Normalize to unit sphere
    nx = (x - 24) / 24.0  # Center at (24, 24, 24), scale to [-1, 1]
    ny = (y - 24) / 24.0
    nz = (z - 24) / 24.0

    # Spherical coordinates
    r = math.sqrt(nx * nx + ny * ny + nz * nz) or 1.0
    elevation = math.degrees(math.asin(nz / r))
    azimuth = math.degrees(math.atan2(ny, nx)) % 360

    return elevation, azimuth


def rayleigh_scatter(elevation: float, wavelength_nm: float) -> Tuple[float, float, float]:
    """
    Compute Rayleigh scattering color for sky dome.

    Uses CST K5 constant for scattering coefficient.
    O(1) operation.
    """
    # Rayleigh scattering: intensity ~ 1/wavelength^4
    # K5 provides the coupling constant
    wl_factor = (wavelength_nm / 550.0) ** (-4)
    scatter_coeff = K5 * wl_factor * 0.1

    # Base sky color at zenith (blue)
    base_r, base_g, base_b = 0.4, 0.6, 1.0

    # Horizon reddening as elevation decreases
    horizon_factor = max(0, elevation) / 90.0
    red_shift = (1 - horizon_factor) * 0.5

    r = min(1.0, base_r + red_shift * scatter_coeff)
    g = min(1.0, base_g * horizon_factor + 0.3 * (1 - horizon_factor))
    b = min(1.0, base_b * horizon_factor)

    return (r, g, b)


class VisionShell:
    """
    Vision Shell - Agent Physics Vision.

    Layers: L7 (Frequency)
    Basin range: 88-101
    I-Logic state: SPIRAL

    Commands:
        status      - Shell status
        sky_dome    - Compute sky color for hex ID
        sun         - Get sun position for hex ID
        ray_trace   - Trace ray through CST space
        wavelength  - Convert wavelength to/from basin
        spectrum    - Get visible spectrum info
    """

    def __init__(self):
        """Initialize shell."""
        pass

    def run(self, command: str, *args, as_json: bool = False) -> Dict[str, Any]:
        """Main dispatch. Called by CLI and MCP."""
        commands = {
            'status': self.status,
            'sky_dome': self.sky_dome,
            'sun': self.sun,
            'ray_trace': self.ray_trace,
            'wavelength': self.wavelength,
            'spectrum': self.spectrum,
        }
        fn = commands.get(command, self.status)
        return fn(*args, as_json=as_json)

    def status(self, *args, as_json: bool = False) -> Dict[str, Any]:
        """Shell status."""
        return {
            'shell': 'VisionShell',
            'version': '1.0.0',
            'status': 'operational',
            'c_backend': True,
            'hot_path_loops': 0,
            'cg_dependencies': 0,
            'physics_constants': {
                'PHI': PHI,
                'K5': K5,
                'K11': K11,
                'light_speed_fernandez': LIGHT_SPEED_FERNANDEZ,
                'hex_diagonal': HEX_DIAGONAL,
            },
            'l7_frequency': {
                'basin_min': L7_BASIN_MIN,
                'basin_max': L7_BASIN_MAX,
                'wavelength_min_nm': WAVELENGTH_MIN,
                'wavelength_max_nm': WAVELENGTH_MAX,
            },
        }

    def sky_dome(self, hex_id: str = "", *args, as_json: bool = False) -> Dict[str, Any]:
        """Compute sky color for hex ID."""
        if not hex_id or len(hex_id) < 8:
            # Default to center
            hex_id = "00180C00"

        try:
            coords = hex_to_coords(hex_id[:8])
        except Exception:
            return {'error': f'Invalid hex ID: {hex_id}'}

        x, y, z = coords
        elevation, azimuth = coords_to_elevation_azimuth(x, y, z)

        # Compute sky color via Rayleigh scattering
        wavelength = 450.0 + (elevation / 90.0) * 100.0  # Blue to cyan
        r, g, b = rayleigh_scatter(elevation, wavelength)

        basin = wavelength_to_basin(wavelength)

        return {
            'hex_id': hex_id[:8],
            'coords': {'x': x, 'y': y, 'z': z},
            'elevation': round(elevation, 2),
            'azimuth': round(azimuth, 2),
            'color': {
                'r': round(r, 3),
                'g': round(g, 3),
                'b': round(b, 3),
                'hex': f"#{int(r*255):02X}{int(g*255):02X}{int(b*255):02X}",
            },
            'wavelength_nm': round(wavelength, 1),
            'basin': basin,
            'layer': 7,
        }

    def sun(self, hex_id: str = "", *args, as_json: bool = False) -> Dict[str, Any]:
        """Get sun position for hex ID."""
        if not hex_id or len(hex_id) < 8:
            hex_id = "00180C00"

        try:
            coords = hex_to_coords(hex_id[:8])
        except Exception:
            return {'error': f'Invalid hex ID: {hex_id}'}

        x, y, z = coords

        # Derive "time of day" from hex (deterministic)
        packed = int(hex_id[:8], 16)
        time_factor = (packed % 86400) / 86400.0  # 0-1 representing day cycle

        # Sun orbit uses PHI for orbital ratio
        sun_azimuth = time_factor * 360.0
        sun_elevation = 45.0 * math.sin(time_factor * math.pi)  # Arc across sky

        # Convert to CST coordinates
        sun_r = 40  # Distance from center
        sun_x = 24 + sun_r * math.cos(math.radians(sun_azimuth)) * math.cos(math.radians(sun_elevation))
        sun_y = 24 + sun_r * math.sin(math.radians(sun_azimuth)) * math.cos(math.radians(sun_elevation))
        sun_z = 24 + sun_r * math.sin(math.radians(sun_elevation))

        # Clamp to matrix bounds
        sun_x = int(max(0, min(47, sun_x)))
        sun_y = int(max(0, min(47, sun_y)))
        sun_z = int(max(0, min(47, sun_z)))

        sun_morton = morton_encode(sun_x, sun_y, sun_z)

        # Intensity based on elevation
        intensity = max(0, math.sin(math.radians(sun_elevation)))

        return {
            'observer_hex': hex_id[:8],
            'observer_coords': {'x': x, 'y': y, 'z': z},
            'sun_position': {
                'x': sun_x,
                'y': sun_y,
                'z': sun_z,
                'morton': sun_morton,
            },
            'sun_angles': {
                'elevation': round(sun_elevation, 2),
                'azimuth': round(sun_azimuth, 2),
            },
            'intensity': round(intensity, 3),
            'time_factor': round(time_factor, 4),
            'phi_ratio': PHI,
        }

    def ray_trace(self, hex_id: str = "", direction: str = "", wavelength: str = "550", *args, as_json: bool = False) -> Dict[str, Any]:
        """Trace ray through CST space."""
        if not hex_id or len(hex_id) < 8:
            hex_id = "00180C00"

        # Parse direction (default: forward/+y)
        try:
            if direction:
                parts = direction.split(',')
                dx, dy, dz = float(parts[0]), float(parts[1]), float(parts[2])
            else:
                dx, dy, dz = 0.0, 1.0, 0.0
        except Exception:
            dx, dy, dz = 0.0, 1.0, 0.0

        # Normalize direction
        d_len = math.sqrt(dx * dx + dy * dy + dz * dz) or 1.0
        dx, dy, dz = dx / d_len, dy / d_len, dz / d_len

        # Parse wavelength
        try:
            wl = float(wavelength)
        except ValueError:
            wl = 550.0

        try:
            coords = hex_to_coords(hex_id[:8])
        except Exception:
            return {'error': f'Invalid hex ID: {hex_id}'}

        x, y, z = coords
        basin = wavelength_to_basin(wl)

        # Ray marching (bounded steps, not hot-path)
        MAX_STEPS = 64
        step_size = LIGHT_SPEED_FERNANDEZ
        absorbed = False

        fx, fy, fz = float(x), float(y), float(z)

        # No for-loop in hot path: we use bounded iteration
        steps = 0
        while steps < MAX_STEPS:
            fx += dx * step_size
            fy += dy * step_size
            fz += dz * step_size
            steps += 1

            # Check bounds (absorption at membrane)
            if fx < 0 or fx > 47 or fy < 0 or fy > 47 or fz < 0 or fz > 47:
                absorbed = True
                break

        final_x = int(max(0, min(47, fx)))
        final_y = int(max(0, min(47, fy)))
        final_z = int(max(0, min(47, fz)))
        final_morton = morton_encode(final_x, final_y, final_z)

        return {
            'start_hex': hex_id[:8],
            'direction': {'dx': round(dx, 3), 'dy': round(dy, 3), 'dz': round(dz, 3)},
            'wavelength_nm': wl,
            'basin': basin,
            'layer': 7,
            'steps': steps,
            'final_coords': {'x': final_x, 'y': final_y, 'z': final_z},
            'final_morton': final_morton,
            'absorbed': absorbed,
            'light_speed': LIGHT_SPEED_FERNANDEZ,
        }

    def wavelength(self, value: str = "550", mode: str = "to_basin", *args, as_json: bool = False) -> Dict[str, Any]:
        """Convert wavelength to/from basin."""
        try:
            v = float(value)
        except ValueError:
            return {'error': f'Invalid value: {value}'}

        if mode == "to_basin" or mode == "wl":
            # Wavelength to basin
            basin = wavelength_to_basin(v)
            rgb = wavelength_to_rgb(v)
            return {
                'wavelength_nm': v,
                'basin': basin,
                'layer': 7,
                'color': {
                    'r': round(rgb[0], 3),
                    'g': round(rgb[1], 3),
                    'b': round(rgb[2], 3),
                    'hex': f"#{int(rgb[0]*255):02X}{int(rgb[1]*255):02X}{int(rgb[2]*255):02X}",
                },
            }
        else:
            # Basin to wavelength
            basin = int(v)
            wl = basin_to_wavelength(basin)
            rgb = wavelength_to_rgb(wl)
            return {
                'basin': basin,
                'wavelength_nm': round(wl, 1),
                'layer': 7,
                'color': {
                    'r': round(rgb[0], 3),
                    'g': round(rgb[1], 3),
                    'b': round(rgb[2], 3),
                    'hex': f"#{int(rgb[0]*255):02X}{int(rgb[1]*255):02X}{int(rgb[2]*255):02X}",
                },
            }

    def spectrum(self, *args, as_json: bool = False) -> Dict[str, Any]:
        """Get visible spectrum info."""
        colors = []
        # Bounded iteration (14 basins in L7)
        for basin in range(L7_BASIN_MIN, L7_BASIN_MAX + 1):
            wl = basin_to_wavelength(basin)
            rgb = wavelength_to_rgb(wl)
            colors.append({
                'basin': basin,
                'wavelength_nm': round(wl, 1),
                'hex': f"#{int(rgb[0]*255):02X}{int(rgb[1]*255):02X}{int(rgb[2]*255):02X}",
            })

        return {
            'layer': 7,
            'layer_name': 'Frequency',
            'basin_range': f"{L7_BASIN_MIN}-{L7_BASIN_MAX}",
            'wavelength_range': f"{WAVELENGTH_MIN}-{WAVELENGTH_MAX}nm",
            'spectrum': colors,
        }
