"""
Gizmo Shell — Spherical Gizmo Transformation System (p - p³√(p³))
=================================================================

Maps the complete 3D gizmo from the inverse transformation into the
CST coordinate system.  Every grid cell has a local gizmo; every agent
carries its own gizmo state (C, fz).  This shell exposes both and lets
them talk to each other.

Mathematical Core
-----------------
From a single position P we extract:
  • Translation  — P_center as origin
  • Rotation     — Complete 3×3 [Normal, Tangent, Bitangent] frame
  • Scale        — Distance from P_center (= |P|)
  • Volume       — Transformation-space bounds (width × height × depth)

The inverse transformation  p - p³√(p³)  tells you how "warped" space
is at that point.  Positive gizmo value → attractor.  Negative → repulsor.
Near-zero → saddle.

Integration with CST
--------------------
  Basin ← morton(x,y,z) % 131
  Layer ← basin // 15 + 1
  Compass → N=L3(Spatial) S=L1(Geometric) E=L2(Semantic) W=L5(Probabilistic)
  Agent state (C, fz) → gizmo contextualization → alignment / steering / context

Use Cases
---------
  UC-G01  Local probe      — Evaluate gizmo at any (x,y,z) coordinate
  UC-G02  Inverse normal   — Get the p - p³√(p³) direction vector
  UC-G03  P-center         — Translation origin from gizmo topology
  UC-G04  Full transform   — [N,T,B] rotation + scale + volume
  UC-G05  Rotation frame   — Pitch/Yaw/Roll axes from [N,T,B]
  UC-G06  Volume bounds    — Transformation-space bounding box
  UC-G07  Scale handles    — Distance-based scaling along each axis
  UC-G08  Agent context    — Agent gizmo vs local gizmo interaction
  UC-G09  Compass bearing  — Which layer quadrant the gizmo points toward
  UC-G10  Field map        — Gizmo field across a basin or layer range
  UC-G11  Agent message    — Send gizmo-contextualized message to an agent
  UC-G12  Validate frame   — Verify [N,T,B] orthogonality + handedness
  UC-G13  Batch probe      — Evaluate multiple coordinates at once
  UC-G14  Basin gizmo      — Average gizmo statistics for a basin
  UC-G15  Compare agents   — Compare two agents' gizmo states

Layers Touched
--------------
  L1 (Geometric)     — Frame extraction, [N,T,B] orthogonality
  L2 (Semantic)      — Meaning of gizmo context (ATTRACTOR/REPULSOR/SADDLE)
  L3 (Spatial)       — Grid coordinate → gizmo field
  L5 (Probabilistic) — Compass W direction, Pareto boundary
  L6 (Emotional)     — Agent state interaction
  L9 (Membrane)      — Volume bounds as boundary condition

Shell: cubesquare shell gizmo <command> [args...]
MCP:   gizmo_shell { command: "...", args: "..." }
"""

import json
import math
import cmath
from typing import Dict, Any, List, Tuple, Optional
from .._core import PHI, K5, K11, PRIME_131
from .._core.morton import encode as morton_encode
from ..primitives.basin import compute as compute_basin
from ..memory import CSTMemory
from ..layers.base import Token


# ── Constants ────────────────────────────────────────────────────────

GRID = 48
CENTER = GRID // 2  # 24
EPSILON = 0.001     # Gradient step for tangent computation

# Compass: cardinal layers
COMPASS = {
    'N': {'layer': 3, 'name': 'Spatial',        'basin_range': (30, 43)},
    'S': {'layer': 1, 'name': 'Geometric',      'basin_range': (0, 14)},
    'E': {'layer': 2, 'name': 'Semantic',        'basin_range': (15, 29)},
    'W': {'layer': 5, 'name': 'Probabilistic',   'basin_range': (59, 72)},
}

# Volume sign thresholds
ATTRACTOR_THRESHOLD = 0.01
REPULSOR_THRESHOLD = -0.01


# ── Core Math (ported from spherical_gizmo HTML) ─────────────────────

def _sign(x: float) -> float:
    """Sign function: -1, 0, or +1."""
    if x > 0:
        return 1.0
    elif x < 0:
        return -1.0
    return 0.0


def _vec_len(v: Tuple[float, float, float]) -> float:
    return math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)


def _normalize(v: Tuple[float, float, float]) -> Tuple[float, float, float]:
    length = _vec_len(v)
    if length < 1e-12:
        return (0.0, 0.0, 0.0)
    return (v[0] / length, v[1] / length, v[2] / length)


def _cross(a: Tuple[float, float, float],
           b: Tuple[float, float, float]) -> Tuple[float, float, float]:
    return (
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0],
    )


def _dot(a: Tuple[float, float, float],
         b: Tuple[float, float, float]) -> float:
    return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]


def inverse_normal(px: float, py: float, pz: float) -> Tuple[float, float, float]:
    """
    Calculate the inverse normal at position P.

    The inverse transformation:  p - p³√(p³)
    Returns the normalized direction vector.

    Exact port of calculateInverseNormal() from the spherical gizmo.
    """
    # p³ (signed cube)
    p3x = abs(px)**3 * _sign(px)
    p3y = abs(py)**3 * _sign(py)
    p3z = abs(pz)**3 * _sign(pz)

    # √(|p|³) magnitude term (with epsilon to prevent div-by-zero)
    mx = math.sqrt(abs(px)**3 + 1e-6)
    my = math.sqrt(abs(py)**3 + 1e-6)
    mz = math.sqrt(abs(pz)**3 + 1e-6)

    # Inverse attractor = p³ · √(p³)
    ix = p3x * mx
    iy = p3y * my
    iz = p3z * mz

    # Normal direction = p - inverse_attractor
    nx = px - ix
    ny = py - iy
    nz = pz - iz

    return _normalize((nx, ny, nz))


def p_center(px: float, py: float, pz: float) -> Tuple[float, float, float]:
    """
    Calculate P_center — the translation origin from the gizmo.

    P_center = P + inverse_attractor * 0.3

    Exact port of calculatePCenter() from the spherical gizmo.
    """
    p3x = abs(px)**3 * _sign(px)
    p3y = abs(py)**3 * _sign(py)
    p3z = abs(pz)**3 * _sign(pz)

    mx = math.sqrt(abs(px)**3 + 1e-6)
    my = math.sqrt(abs(py)**3 + 1e-6)
    mz = math.sqrt(abs(pz)**3 + 1e-6)

    ix = p3x * mx
    iy = p3y * my
    iz = p3z * mz

    return (
        px + ix * 0.3,
        py + iy * 0.3,
        pz + iz * 0.3,
    )


def complete_transformation(px: float, py: float, pz: float) -> Dict[str, Any]:
    """
    Calculate the complete transformation from position P.

    Returns:
        translation: P_center (3-vector)
        rotation:    [Normal, Tangent, Bitangent] frame
        scale:       |P| distance
        volume:      {width, height, depth} bounding box

    Exact port of calculateCompleteTransformation() from the spherical gizmo.
    """
    eps = EPSILON

    # Normal at P
    normal = inverse_normal(px, py, pz)

    # Tangent via gradient of the normal field
    # ∂N/∂x
    nx_plus = inverse_normal(px + eps, py, pz)
    nx_minus = inverse_normal(px - eps, py, pz)
    grad_x = (
        (nx_plus[0] - nx_minus[0]) / (2 * eps),
        (nx_plus[1] - nx_minus[1]) / (2 * eps),
        (nx_plus[2] - nx_minus[2]) / (2 * eps),
    )

    # ∂N/∂y
    ny_plus = inverse_normal(px, py + eps, pz)
    ny_minus = inverse_normal(px, py - eps, pz)
    grad_y = (
        (ny_plus[0] - ny_minus[0]) / (2 * eps),
        (ny_plus[1] - ny_minus[1]) / (2 * eps),
        (ny_plus[2] - ny_minus[2]) / (2 * eps),
    )

    # Primary tangent (combine gradients)
    raw_tangent = _normalize((
        grad_x[0] + grad_y[0] * 0.707,
        grad_x[1] + grad_y[1] * 0.707,
        grad_x[2] + grad_y[2] * 0.707,
    ))

    # Gram-Schmidt: make tangent perpendicular to normal
    proj = _dot(raw_tangent, normal)
    tangent = _normalize((
        raw_tangent[0] - proj * normal[0],
        raw_tangent[1] - proj * normal[1],
        raw_tangent[2] - proj * normal[2],
    ))

    # Bitangent = N × T
    bitangent = _normalize(_cross(normal, tangent))

    # P_center and scale
    center = p_center(px, py, pz)
    scale = math.sqrt(px**2 + py**2 + pz**2)

    # Volume bounds
    volume_w = abs(px - center[0]) * 2
    volume_h = abs(py - center[1]) * 2
    volume_d = abs(pz - center[2]) * 2

    return {
        'translation': center,
        'rotation': {
            'normal': normal,
            'tangent': tangent,
            'bitangent': bitangent,
        },
        'scale': scale,
        'volume': {
            'width': volume_w,
            'height': volume_h,
            'depth': volume_d,
        },
    }


def gizmo_value(px: float, py: float, pz: float) -> float:
    """
    Scalar gizmo value at position P.

    Computes |p - p³√(p³)| — the magnitude of the inverse transformation.
    This tells you whether the point is an attractor (+), repulsor (-), or saddle (~0).
    """
    r = math.sqrt(px**2 + py**2 + pz**2)
    if r < 1e-12:
        return 0.0
    p = r / CENTER if CENTER > 0 else r
    p3 = p ** 3
    sqrt_p3 = math.sqrt(abs(p3) + 1e-12)
    return p - p3 * sqrt_p3


def gizmo_context(gval: float) -> str:
    """Classify gizmo value as ATTRACTOR, REPULSOR, or SADDLE."""
    if gval > ATTRACTOR_THRESHOLD:
        return 'ATTRACTOR'
    elif gval < REPULSOR_THRESHOLD:
        return 'REPULSOR'
    return 'SADDLE'


def compass_direction(px: float, py: float, pz: float) -> str:
    """
    Compute compass bearing from position relative to grid center.

    Compass model:
      N = L3 (Spatial)        → +z from center
      S = L1 (Geometric)      → -z from center
      E = L2 (Semantic)       → +x from center
      W = L5 (Probabilistic)  → -x from center
    """
    dx = px - CENTER
    dz = pz - CENTER
    if abs(dz) >= abs(dx):
        return 'N' if dz >= 0 else 'S'
    return 'E' if dx >= 0 else 'W'


def validate_frame(normal: Tuple[float, float, float],
                   tangent: Tuple[float, float, float],
                   bitangent: Tuple[float, float, float]) -> Dict[str, Any]:
    """
    Validate [N, T, B] frame for orthogonality and handedness.

    Returns:
        orthogonal: bool     — all dot products ≈ 0
        right_handed: bool   — N × T ≈ B
        dot_NT: float        — N · T (should be 0)
        dot_NB: float        — N · B (should be 0)
        dot_TB: float        — T · B (should be 0)
        handedness: float    — det([N,T,B]) (should be +1)
    """
    dot_nt = _dot(normal, tangent)
    dot_nb = _dot(normal, bitangent)
    dot_tb = _dot(tangent, bitangent)
    cross_nt = _cross(normal, tangent)
    handedness = _dot(cross_nt, bitangent)

    tol = 1e-6
    orthogonal = (abs(dot_nt) < tol and abs(dot_nb) < tol and abs(dot_tb) < tol)
    right_handed = abs(handedness - 1.0) < tol

    return {
        'orthogonal': orthogonal,
        'right_handed': right_handed,
        'dot_NT': dot_nt,
        'dot_NB': dot_nb,
        'dot_TB': dot_tb,
        'handedness': handedness,
    }


def agent_gizmo_interaction(agent_C: float, agent_fz: complex,
                            local_gval: float,
                            local_normal: Tuple[float, float, float],
                            correction_scale: Optional[float] = None) -> Dict[str, Any]:
    """
    Compute interaction between agent gizmo state and local gizmo.

    Agent carries (C, fz) → agent direction = phase(fz)
    Local field has gizmo_value and normal.
    Interaction = alignment (dot) + steering (cross).

    Args:
        agent_C:          |C| magnitude of the agent's Fernandez constant
        agent_fz:         F(z,C) complex state of the agent
        local_gval:       scalar gizmo value at local position
        local_normal:     unit normal at local position
        correction_scale: multiplier for REPULSOR correction angle.
                          Default 0.5.  Exploration passes P*P (normalization)
                          so outer-shell agents get amplified steering.

    Returns:
        alignment:        float  — dot(agent_dir, local_normal) in [-1, 1]
        steer_magnitude:  float  — |cross(agent_dir, local_normal)|
        context:          str    — ATTRACTOR / REPULSOR / SADDLE
        correction_angle: float  — radians to steer perpendicular to repulsion
        normalization:    float  — P*P = N
        agent_phase_deg:  float  — agent direction in degrees
    """
    # Agent direction from F(z,C) phase
    phase = cmath.phase(agent_fz) if abs(agent_fz) > 1e-12 else 0.0
    agent_dir = (math.cos(phase), math.sin(phase), 0.0)

    # Alignment
    alignment = _dot(agent_dir, local_normal)
    steer = _vec_len(_cross(agent_dir, local_normal))

    # Context from local gizmo value
    ctx = gizmo_context(local_gval)

    # Correction angle: in REPULSOR zones, steer perpendicular
    # Scale defaults to 0.5; exploration passes P*P for radius-dependent steering
    scale = correction_scale if correction_scale is not None else 0.5
    correction = 0.0
    if ctx == 'REPULSOR' and steer > 1e-6:
        correction = math.atan2(steer, abs(alignment)) * scale
    elif ctx == 'SADDLE' and abs(alignment) < 0.1:
        correction = math.pi / 6  # 30° nudge away from saddle

    # P*P = N normalization for negative volumes
    normalization = agent_C * agent_C if agent_C != 0 else 1.0

    return {
        'alignment': alignment,
        'steer_magnitude': steer,
        'context': ctx,
        'correction_angle': correction,
        'normalization': normalization,
        'agent_phase_deg': math.degrees(phase) % 360,
    }


# ── Shell Class ──────────────────────────────────────────────────────

class GizmoShell:
    """
    Gizmo Shell — Spherical Gizmo Transformation System.

    The inverse transformation p - p³√(p³) defines a complete 3D
    transformation gizmo at every coordinate.  This shell provides
    the math, the queries, the validation, and the agent communication
    bridge.

    Commands:
        status          — System status and capability summary
        probe           — Evaluate gizmo at a single (x,y,z) coordinate
        inverse_normal  — Get the p - p³√(p³) direction at (x,y,z)
        p_center        — Translation origin from gizmo at (x,y,z)
        transform       — Full [N,T,B] rotation + scale + volume at (x,y,z)
        rotation        — Extract rotation axes (pitch/yaw/roll)
        volume          — Volume bounds at (x,y,z)
        scale           — Scale factor at (x,y,z)
        compass         — Compass bearing (N/S/E/W → layers) at (x,y,z)
        validate        — Verify [N,T,B] orthogonality + handedness
        field           — Gizmo field map across a layer or basin range
        basin_gizmo     — Average gizmo stats for a specific basin
        agent_context   — Agent vs local gizmo interaction
        batch_probe     — Evaluate gizmo at multiple coordinates
        compare_agents  — Compare two agents' gizmo states at a position
        message         — Send gizmo-contextualized message to an agent
        use_cases       — List all use cases with descriptions
    """

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

        Mailbox is a CSTMemory instance — messages are tokens.
        The gizmo context rides in token.layer_data, the basin
        routes the message, and the hex_id IS the identity.
        No JSON serialization in the communication path.
        """
        self.grid = GRID
        self.center = CENTER
        self._mailbox_mem = CSTMemory()  # Messages ARE tokens

    # ── Status ───────────────────────────────────────────────────────

    def status(self, as_json: bool = False) -> Dict[str, Any]:
        """Get Gizmo system status."""
        result = {
            'system': 'Gizmo Shell — Spherical Gizmo Transformation (p - p³√(p³))',
            'version': '1.0.0',
            'grid': self.grid,
            'center': self.center,
            'equation': 'p - p³√(p³)',
            'components': {
                'translation': 'P_center = P + inverse_attractor × 0.3',
                'rotation': '[Normal, Tangent, Bitangent] via Gram-Schmidt',
                'scale': '|P| distance from origin',
                'volume': '2 × |P - P_center| per axis',
            },
            'compass': {
                'N': 'L3 Spatial (basin 30-43)',
                'S': 'L1 Geometric (basin 0-14)',
                'E': 'L2 Semantic (basin 15-29)',
                'W': 'L5 Probabilistic (basin 59-72)',
            },
            'contexts': {
                'ATTRACTOR': f'gizmo_value > {ATTRACTOR_THRESHOLD}',
                'REPULSOR': f'gizmo_value < {REPULSOR_THRESHOLD}',
                'SADDLE': f'|gizmo_value| <= {ATTRACTOR_THRESHOLD}',
            },
            'agent_mailbox': len(self._mailbox_mem),
            'use_cases': 15,
            'layers_touched': ['L1', 'L2', 'L3', 'L5', 'L6', 'L9'],
            'status': 'operational',
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G01: Local Probe ──────────────────────────────────────────

    def probe(self, x: int, y: int, z: int, as_json: bool = False) -> Dict[str, Any]:
        """
        Evaluate the gizmo at a single grid coordinate.

        UC-G01: Returns gizmo_value, context, compass, basin, layer,
        inverse normal, P_center, volume sign, and normalization (P*P=N).
        """
        fx, fy, fz = float(x), float(y), float(z)

        gval = gizmo_value(fx, fy, fz)
        ctx = gizmo_context(gval)
        compass = compass_direction(fx, fy, fz)
        normal = inverse_normal(fx, fy, fz)
        center = p_center(fx, fy, fz)

        # CST mapping
        m = morton_encode(x % GRID, y % GRID, z % GRID)
        b = compute_basin(x % GRID, y % GRID, z % GRID)
        layer = min(b // 15 + 1, 9)

        # P*P = N normalization
        r = math.sqrt(fx**2 + fy**2 + fz**2)
        p = r / CENTER if CENTER > 0 else 0
        normalization = p * p

        # Volume sign
        if gval > ATTRACTOR_THRESHOLD:
            vol_sign = 'positive'
        elif gval < REPULSOR_THRESHOLD:
            vol_sign = 'negative'
        else:
            vol_sign = 'zero'

        result = {
            'position': (x, y, z),
            'gizmo_value': round(gval, 8),
            'context': ctx,
            'compass': compass,
            'compass_layer': COMPASS[compass],
            'volume_sign': vol_sign,
            'normalization_PxP': round(normalization, 8),
            'inverse_normal': tuple(round(v, 8) for v in normal),
            'p_center': tuple(round(v, 6) for v in center),
            'morton': m,
            'hex_id': f'{m:08X}',
            'basin': b,
            'layer': layer,
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G02: Inverse Normal ───────────────────────────────────────

    def cmd_inverse_normal(self, x: int, y: int, z: int,
                           as_json: bool = False) -> Dict[str, Any]:
        """UC-G02: Get the p - p³√(p³) direction vector at (x,y,z)."""
        fx, fy, fz = float(x), float(y), float(z)
        normal = inverse_normal(fx, fy, fz)
        gval = gizmo_value(fx, fy, fz)

        result = {
            'position': (x, y, z),
            'inverse_normal': tuple(round(v, 8) for v in normal),
            'magnitude': round(_vec_len(normal), 8),
            'gizmo_value': round(gval, 8),
            'equation': 'p - p³√(p³)',
            'description': 'Direction of the inverse attractor field',
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G03: P-Center ─────────────────────────────────────────────

    def cmd_p_center(self, x: int, y: int, z: int,
                     as_json: bool = False) -> Dict[str, Any]:
        """UC-G03: Translation origin from gizmo topology."""
        fx, fy, fz = float(x), float(y), float(z)
        center = p_center(fx, fy, fz)
        offset = (center[0] - fx, center[1] - fy, center[2] - fz)

        result = {
            'position': (x, y, z),
            'p_center': tuple(round(v, 6) for v in center),
            'offset': tuple(round(v, 6) for v in offset),
            'offset_magnitude': round(_vec_len(offset), 6),
            'equation': 'P + inverse_attractor × 0.3',
            'identity': 'P - P = 0 (at P_center, self-reference collapses)',
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G04: Full Transform ───────────────────────────────────────

    def cmd_transform(self, x: int, y: int, z: int,
                      as_json: bool = False) -> Dict[str, Any]:
        """UC-G04: Full [N,T,B] rotation + scale + volume at (x,y,z)."""
        fx, fy, fz = float(x), float(y), float(z)
        t = complete_transformation(fx, fy, fz)
        gval = gizmo_value(fx, fy, fz)

        # Validate frame
        n = t['rotation']['normal']
        tg = t['rotation']['tangent']
        bt = t['rotation']['bitangent']
        v = validate_frame(n, tg, bt)

        result = {
            'position': (x, y, z),
            'gizmo_value': round(gval, 8),
            'context': gizmo_context(gval),
            'translation': tuple(round(c, 6) for c in t['translation']),
            'rotation': {
                'normal':    tuple(round(c, 8) for c in n),
                'tangent':   tuple(round(c, 8) for c in tg),
                'bitangent': tuple(round(c, 8) for c in bt),
            },
            'rotation_matrix': [
                [round(tg[0], 8), round(tg[1], 8), round(tg[2], 8), round(t['translation'][0], 6)],
                [round(bt[0], 8), round(bt[1], 8), round(bt[2], 8), round(t['translation'][1], 6)],
                [round(n[0],  8), round(n[1],  8), round(n[2],  8), round(t['translation'][2], 6)],
                [0.0, 0.0, 0.0, 1.0],
            ],
            'scale': round(t['scale'], 6),
            'volume': {k: round(v2, 6) for k, v2 in t['volume'].items()},
            'frame_valid': v,
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G05: Rotation Frame ───────────────────────────────────────

    def cmd_rotation(self, x: int, y: int, z: int,
                     as_json: bool = False) -> Dict[str, Any]:
        """UC-G05: Extract rotation axes (pitch/yaw/roll) from [N,T,B]."""
        fx, fy, fz = float(x), float(y), float(z)
        t = complete_transformation(fx, fy, fz)

        result = {
            'position': (x, y, z),
            'pitch_axis': {
                'label': 'Tangent',
                'vector': tuple(round(c, 8) for c in t['rotation']['tangent']),
                'description': 'Rotation around tangent axis',
            },
            'yaw_axis': {
                'label': 'Normal',
                'vector': tuple(round(c, 8) for c in t['rotation']['normal']),
                'description': 'Rotation around normal axis',
            },
            'roll_axis': {
                'label': 'Bitangent',
                'vector': tuple(round(c, 8) for c in t['rotation']['bitangent']),
                'description': 'Rotation around bitangent axis',
            },
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G06: Volume Bounds ────────────────────────────────────────

    def cmd_volume(self, x: int, y: int, z: int,
                   as_json: bool = False) -> Dict[str, Any]:
        """UC-G06: Transformation-space bounding box at (x,y,z)."""
        fx, fy, fz = float(x), float(y), float(z)
        t = complete_transformation(fx, fy, fz)

        vol = t['volume']
        total_vol = vol['width'] * vol['height'] * vol['depth']
        gval = gizmo_value(fx, fy, fz)

        result = {
            'position': (x, y, z),
            'volume': {k: round(v, 6) for k, v in vol.items()},
            'total_volume': round(total_vol, 6),
            'volume_sign': 'positive' if gval > ATTRACTOR_THRESHOLD else (
                'negative' if gval < REPULSOR_THRESHOLD else 'zero'),
            'p_center': tuple(round(c, 6) for c in t['translation']),
            'description': 'Bounds of the inverse transformation space',
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G07: Scale ────────────────────────────────────────────────

    def cmd_scale(self, x: int, y: int, z: int,
                  as_json: bool = False) -> Dict[str, Any]:
        """UC-G07: Distance-based scale along each axis."""
        fx, fy, fz = float(x), float(y), float(z)
        t = complete_transformation(fx, fy, fz)

        result = {
            'position': (x, y, z),
            'uniform_scale': round(t['scale'], 8),
            'scale_x': round(t['volume']['width'] / 2, 8),
            'scale_y': round(t['volume']['height'] / 2, 8),
            'scale_z': round(t['volume']['depth'] / 2, 8),
            'identity': 'P/P = 1 (self-reference preserves scale)',
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G08: Agent Context ────────────────────────────────────────

    def agent_context(self, x: int, y: int, z: int,
                      agent_C: float = 0.25, agent_fz_real: float = 1.0,
                      agent_fz_imag: float = 0.0,
                      as_json: bool = False) -> Dict[str, Any]:
        """
        UC-G08: Compare agent gizmo state against local field at (x,y,z).

        Agent state = (C, fz) where fz is the Fernandez iteration complex state.
        Local field = inverse transformation at the coordinate.

        Returns alignment, steering, context, and correction angle.
        """
        fx, fy, fz_pos = float(x), float(y), float(z)
        local_gval = gizmo_value(fx, fy, fz_pos)
        local_n = inverse_normal(fx, fy, fz_pos)
        agent_fz = complex(agent_fz_real, agent_fz_imag)

        interaction = agent_gizmo_interaction(agent_C, agent_fz, local_gval, local_n)

        # CST mapping
        m = morton_encode(x % GRID, y % GRID, z % GRID)
        b = compute_basin(x % GRID, y % GRID, z % GRID)
        layer = min(b // 15 + 1, 9)
        compass = compass_direction(fx, fy, fz_pos)

        result = {
            'position': (x, y, z),
            'agent': {
                'C_magnitude': agent_C,
                'fz_state': {'real': agent_fz_real, 'imag': agent_fz_imag},
                'fz_phase_deg': interaction['agent_phase_deg'],
            },
            'local_field': {
                'gizmo_value': round(local_gval, 8),
                'inverse_normal': tuple(round(v, 8) for v in local_n),
                'context': interaction['context'],
            },
            'interaction': {
                'alignment': round(interaction['alignment'], 8),
                'steer_magnitude': round(interaction['steer_magnitude'], 8),
                'correction_angle_rad': round(interaction['correction_angle'], 8),
                'correction_angle_deg': round(math.degrees(interaction['correction_angle']), 4),
                'normalization_PxP': round(interaction['normalization'], 8),
            },
            'cst': {
                'morton': m,
                'hex_id': f'{m:08X}',
                'basin': b,
                'layer': layer,
                'compass': compass,
                'compass_layer': COMPASS[compass],
            },
            'recommendation': (
                'PENETRATE — agent should steer through repulsive zone'
                if interaction['context'] == 'REPULSOR' else
                'FOLLOW — agent aligns with attractor field'
                if interaction['context'] == 'ATTRACTOR' else
                'NUDGE — agent at saddle point, apply 30° correction'
            ),
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G09: Compass Bearing ──────────────────────────────────────

    def cmd_compass(self, x: int, y: int, z: int,
                    as_json: bool = False) -> Dict[str, Any]:
        """UC-G09: Compass bearing → layer quadrant."""
        fx, fy, fz = float(x), float(y), float(z)
        direction = compass_direction(fx, fy, fz)

        b = compute_basin(x % GRID, y % GRID, z % GRID)
        actual_layer = min(b // 15 + 1, 9)

        result = {
            'position': (x, y, z),
            'compass': direction,
            'compass_layer': COMPASS[direction],
            'actual_basin': b,
            'actual_layer': actual_layer,
            'model': 'N=L3(Spatial) S=L1(Geometric) E=L2(Semantic) W=L5(Probabilistic)',
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G10: Field Map ────────────────────────────────────────────

    def field(self, layer: int = 0, basin: int = -1,
              as_json: bool = False) -> Dict[str, Any]:
        """
        UC-G10: Map the gizmo field across a layer or basin range.

        Scans the grid and aggregates gizmo statistics.
        If layer > 0: scan cells in that layer's basin range.
        If basin >= 0: scan cells that map to that specific basin.
        Otherwise: scan the full grid (sampled).

        NO PYTHON LOOPS — grid coords generated, filtered, and mapped functionally.
        """
        # Determine scan range
        step = 4 if (layer == 0 and basin < 0) else 2

        # Generate all grid coordinates as flat list — one computation
        coords = list(range(0, GRID, step))

        # Build (x, y, z, basin, layer) tuples — cartesian product via comprehension
        # This is the coordinate generation step, not a hot-path loop
        cells = [
            (gx, gy, gz, compute_basin(gx, gy, gz))
            for gx in coords for gy in coords for gz in coords
        ]

        # Annotate with layer
        cells = [(x, y, z, b, min(b // 15 + 1, 9)) for x, y, z, b in cells]

        # Filter in one pass — functional, not iterative
        if layer > 0:
            cells = list(filter(lambda c: c[4] == layer, cells))
        if basin >= 0:
            cells = list(filter(lambda c: c[3] == basin, cells))

        # Map gizmo computation across all cells at once
        evaluated = list(map(
            lambda c: (c, gizmo_value(float(c[0]), float(c[1]), float(c[2]))),
            cells
        ))

        # Classify — map, don't loop
        classified = list(map(
            lambda ev: (ev[0], ev[1], gizmo_context(ev[1])),
            evaluated
        ))

        # Aggregate — reduce
        cells_scanned = len(classified)
        attractor_count = sum(1 for _, _, ctx in classified if ctx == 'ATTRACTOR')
        repulsor_count = sum(1 for _, _, ctx in classified if ctx == 'REPULSOR')
        saddle_count = cells_scanned - attractor_count - repulsor_count
        total_gizmo = sum(gv for _, gv, _ in classified)
        avg_gizmo = total_gizmo / max(cells_scanned, 1)

        # Sample — slice, don't loop
        sample = list(map(
            lambda item: {
                'position': (item[0][0], item[0][1], item[0][2]),
                'basin': item[0][3],
                'layer': item[0][4],
                'gizmo_value': round(item[1], 8),
                'context': item[2],
            },
            classified[:10]
        ))

        scope = f'layer={layer}' if layer > 0 else (
            f'basin={basin}' if basin >= 0 else 'full_grid (sampled)')

        result = {
            'scope': scope,
            'cells_scanned': cells_scanned,
            'distribution': {
                'ATTRACTOR': attractor_count,
                'REPULSOR': repulsor_count,
                'SADDLE': saddle_count,
            },
            'average_gizmo_value': round(avg_gizmo, 8),
            'dominant_context': (
                'ATTRACTOR' if attractor_count >= max(repulsor_count, saddle_count) else
                'REPULSOR' if repulsor_count >= saddle_count else 'SADDLE'
            ),
            'sample': sample,
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G11: Agent Message ────────────────────────────────────────

    def message(self, agent_name: str, content: str,
                x: int = 24, y: int = 24, z: int = 24,
                as_json: bool = False) -> Dict[str, Any]:
        """
        UC-G11: Send a gizmo-contextualized message to an agent.

        The message IS a token.  The gizmo context rides in layer_data.
        The basin routes it.  The hex_id identifies it.  No JSON needed
        in the communication path — the coordinate IS the meaning.

        gizmo_value flips the semantics:
          positive → ATTRACTOR → content pulls agent in
          negative → REPULSOR  → content pushes agent away
          zero     → SADDLE    → decision point
        """
        fx, fy, fz = float(x), float(y), float(z)
        gval = gizmo_value(fx, fy, fz)
        ctx = gizmo_context(gval)
        compass = compass_direction(fx, fy, fz)

        # The message path encodes: who, what, where
        # The token's basin/layer/hex_id do the rest — no JSON fields needed
        msg_path = f"gizmo:msg:{agent_name}:{content}:{x},{y},{z}"
        token = self._mailbox_mem.write(msg_path)

        # Gizmo context rides in layer_data — it's part of the token, not a JSON blob
        token.layer_data['gizmo_value'] = round(gval, 8)
        token.layer_data['context'] = ctx
        token.layer_data['compass'] = compass
        token.layer_data['agent'] = agent_name
        token.layer_data['content'] = content
        token.layer_data['position'] = (x, y, z)

        result = {
            'delivered': True,
            'token': token.hex_id,
            'to': agent_name,
            'basin': token.basin,
            'layer': token.layer,
            'gizmo_context': ctx,
            'compass': compass,
            'gizmo_value': round(gval, 8),
            'mailbox_size': len(self._mailbox_mem),
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G12: Validate Frame ───────────────────────────────────────

    def cmd_validate(self, x: int, y: int, z: int,
                     as_json: bool = False) -> Dict[str, Any]:
        """UC-G12: Verify [N,T,B] orthogonality + handedness at (x,y,z)."""
        fx, fy, fz = float(x), float(y), float(z)
        t = complete_transformation(fx, fy, fz)
        n = t['rotation']['normal']
        tg = t['rotation']['tangent']
        bt = t['rotation']['bitangent']
        v = validate_frame(n, tg, bt)

        result = {
            'position': (x, y, z),
            'frame': {
                'normal': tuple(round(c, 8) for c in n),
                'tangent': tuple(round(c, 8) for c in tg),
                'bitangent': tuple(round(c, 8) for c in bt),
            },
            'validation': v,
            'passed': v['orthogonal'] and v['right_handed'],
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G13: Batch Probe ──────────────────────────────────────────

    def batch_probe(self, coords_str: str,
                    as_json: bool = False) -> Dict[str, Any]:
        """
        UC-G13: Evaluate gizmo at multiple coordinates.

        coords_str format: "x1,y1,z1;x2,y2,z2;..." (semicolon-separated triples)
        NO PYTHON LOOPS — the batch IS the computation.
        """
        triples = [t.strip().split(',') for t in coords_str.split(';') if t.strip()]
        results = list(map(
            lambda parts: self.probe(int(parts[0]), int(parts[1]), int(parts[2]))
                          if len(parts) == 3 else {'error': f'Invalid: {parts}'},
            triples
        ))

        result = {
            'count': len(results),
            'probes': results,
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── Batch Validate ───────────────────────────────────────────────

    def batch_validate(self, coords_str: str,
                       as_json: bool = False) -> Dict[str, Any]:
        """
        Validate [N,T,B] frames at multiple coordinates in one call.

        coords_str format: "x1,y1,z1;x2,y2,z2;..."
        Returns all validations — no Python loop needed by caller.
        """
        triples = [t.strip().split(',') for t in coords_str.split(';') if t.strip()]
        results = list(map(
            lambda parts: self.cmd_validate(int(parts[0]), int(parts[1]), int(parts[2]))
                          if len(parts) == 3 else {'error': f'Invalid: {parts}'},
            triples
        ))

        all_passed = all(r.get('passed', False) for r in results if 'error' not in r)

        result = {
            'count': len(results),
            'all_passed': all_passed,
            'validations': results,
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── Batch Field (all layers at once) ─────────────────────────────

    def batch_field(self, as_json: bool = False) -> Dict[str, Any]:
        """
        Scan gizmo field across ALL 9 layers in one call.

        Returns per-layer distribution — no Python loop needed by caller.
        """
        layers = list(map(lambda l: {'layer': l, **self.field(layer=l)}, range(1, 10)))

        result = {
            'layers_scanned': 9,
            'layers': layers,
            'all_have_cells': all(l['cells_scanned'] > 0 for l in layers),
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── UC-G14: Basin Gizmo ──────────────────────────────────────────

    def basin_gizmo(self, basin_id: int,
                    as_json: bool = False) -> Dict[str, Any]:
        """UC-G14: Average gizmo statistics for a specific basin."""
        return self.field(basin=basin_id, as_json=as_json)

    # ── UC-G15: Compare Agents ───────────────────────────────────────

    def compare_agents(self, x: int, y: int, z: int,
                       agent_a_C: float, agent_a_fz_real: float, agent_a_fz_imag: float,
                       agent_b_C: float, agent_b_fz_real: float, agent_b_fz_imag: float,
                       as_json: bool = False) -> Dict[str, Any]:
        """UC-G15: Compare two agents' gizmo states at a position."""
        ctx_a = self.agent_context(x, y, z, agent_a_C, agent_a_fz_real, agent_a_fz_imag)
        ctx_b = self.agent_context(x, y, z, agent_b_C, agent_b_fz_real, agent_b_fz_imag)

        result = {
            'position': (x, y, z),
            'agent_a': {
                'C': agent_a_C,
                'alignment': ctx_a['interaction']['alignment'],
                'context': ctx_a['local_field']['context'],
                'correction_deg': ctx_a['interaction']['correction_angle_deg'],
            },
            'agent_b': {
                'C': agent_b_C,
                'alignment': ctx_b['interaction']['alignment'],
                'context': ctx_b['local_field']['context'],
                'correction_deg': ctx_b['interaction']['correction_angle_deg'],
            },
            'alignment_diff': round(
                abs(ctx_a['interaction']['alignment'] - ctx_b['interaction']['alignment']), 8),
            'same_context': ctx_a['local_field']['context'] == ctx_b['local_field']['context'],
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── Use Cases ────────────────────────────────────────────────────

    def use_cases(self, as_json: bool = False) -> Dict[str, Any]:
        """List all use cases with descriptions."""
        cases = {
            'UC-G01': {'command': 'probe',          'description': 'Evaluate gizmo at (x,y,z)'},
            'UC-G02': {'command': 'inverse_normal', 'description': 'p - p³√(p³) direction vector'},
            'UC-G03': {'command': 'p_center',       'description': 'Translation origin from gizmo'},
            'UC-G04': {'command': 'transform',      'description': 'Full [N,T,B] + scale + volume'},
            'UC-G05': {'command': 'rotation',       'description': 'Pitch/Yaw/Roll axes from [N,T,B]'},
            'UC-G06': {'command': 'volume',         'description': 'Transformation-space bounds'},
            'UC-G07': {'command': 'scale',          'description': 'Distance-based scale per axis'},
            'UC-G08': {'command': 'agent_context',  'description': 'Agent vs local gizmo interaction'},
            'UC-G09': {'command': 'compass',        'description': 'Compass bearing → layer quadrant'},
            'UC-G10': {'command': 'field',          'description': 'Gizmo field map across layer/basin'},
            'UC-G11': {'command': 'message',        'description': 'Send gizmo-contextualized message'},
            'UC-G12': {'command': 'validate',       'description': 'Verify [N,T,B] frame integrity'},
            'UC-G13': {'command': 'batch_probe',    'description': 'Multi-coordinate probe'},
            'UC-G14': {'command': 'basin_gizmo',    'description': 'Basin-level gizmo statistics'},
            'UC-G15': {'command': 'compare_agents', 'description': 'Compare two agent gizmo states'},
        }
        result = {'use_cases': cases, 'total': len(cases)}
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── Mailbox ──────────────────────────────────────────────────────

    def mailbox(self, agent_name: str = '', as_json: bool = False) -> Dict[str, Any]:
        """Read the agent mailbox — messages are tokens in CSTMemory.

        If agent_name is provided, queries for that agent's messages.
        Otherwise returns all messages.  The query is O(1) — the
        coordinate IS the meaning, no JSON parsing needed.
        """
        if agent_name:
            tokens = self._mailbox_mem.query(f"gizmo:msg:{agent_name}", limit=100)
        else:
            tokens = list(self._mailbox_mem)

        # The tokens ARE the messages — layer_data carries the gizmo context
        messages = list(map(
            lambda t: {
                'token': t.hex_id,
                'to': t.layer_data.get('agent', ''),
                'content': t.layer_data.get('content', ''),
                'basin': t.basin,
                'layer': t.layer,
                'gizmo_context': t.layer_data.get('context', ''),
                'gizmo_value': t.layer_data.get('gizmo_value', 0.0),
                'compass': t.layer_data.get('compass', ''),
                'position': t.layer_data.get('position', (0, 0, 0)),
            },
            tokens
        ))

        result = {
            'messages': messages,
            'count': len(messages),
        }
        if as_json:
            return json.dumps(result, indent=2)
        return result

    # ── Universal Dispatch ───────────────────────────────────────────

    def run(self, command: str, *args, as_json: bool = False) -> Any:
        """Run a Gizmo shell command."""

        def _parse_xyz(args, start=0):
            """Parse x,y,z from args starting at index `start`."""
            if len(args) > start + 2:
                return int(args[start]), int(args[start+1]), int(args[start+2])
            return CENTER, CENTER, CENTER  # Default to grid center

        commands = {
            'status':         lambda: self.status(as_json),
            'probe':          lambda: self.probe(*_parse_xyz(args), as_json=as_json),
            'inverse_normal': lambda: self.cmd_inverse_normal(*_parse_xyz(args), as_json=as_json),
            'p_center':       lambda: self.cmd_p_center(*_parse_xyz(args), as_json=as_json),
            'transform':      lambda: self.cmd_transform(*_parse_xyz(args), as_json=as_json),
            'rotation':       lambda: self.cmd_rotation(*_parse_xyz(args), as_json=as_json),
            'volume':         lambda: self.cmd_volume(*_parse_xyz(args), as_json=as_json),
            'scale':          lambda: self.cmd_scale(*_parse_xyz(args), as_json=as_json),
            'compass':        lambda: self.cmd_compass(*_parse_xyz(args), as_json=as_json),
            'validate':       lambda: self.cmd_validate(*_parse_xyz(args), as_json=as_json),
            'field':          lambda: self.field(
                layer=int(args[0]) if args else 0,
                basin=int(args[1]) if len(args) > 1 else -1,
                as_json=as_json,
            ),
            'basin_gizmo':    lambda: self.basin_gizmo(
                basin_id=int(args[0]) if args else 0,
                as_json=as_json,
            ),
            'agent_context':  lambda: self.agent_context(
                *_parse_xyz(args),
                agent_C=float(args[3]) if len(args) > 3 else 0.25,
                agent_fz_real=float(args[4]) if len(args) > 4 else 1.0,
                agent_fz_imag=float(args[5]) if len(args) > 5 else 0.0,
                as_json=as_json,
            ),
            'batch_probe':    lambda: self.batch_probe(
                coords_str=args[0] if args else '24,24,24',
                as_json=as_json,
            ),
            'batch_validate': lambda: self.batch_validate(
                coords_str=args[0] if args else '24,24,24',
                as_json=as_json,
            ),
            'batch_field':    lambda: self.batch_field(as_json=as_json),
            'compare_agents': lambda: self.compare_agents(
                *_parse_xyz(args),
                agent_a_C=float(args[3]) if len(args) > 3 else 0.1,
                agent_a_fz_real=float(args[4]) if len(args) > 4 else 1.0,
                agent_a_fz_imag=float(args[5]) if len(args) > 5 else 0.0,
                agent_b_C=float(args[6]) if len(args) > 6 else 0.4,
                agent_b_fz_real=float(args[7]) if len(args) > 7 else 0.5,
                agent_b_fz_imag=float(args[8]) if len(args) > 8 else 0.5,
                as_json=as_json,
            ),
            'message':        lambda: self.message(
                agent_name=args[0] if args else 'Healer',
                content=args[1] if len(args) > 1 else 'probe',
                x=int(args[2]) if len(args) > 2 else CENTER,
                y=int(args[3]) if len(args) > 3 else CENTER,
                z=int(args[4]) if len(args) > 4 else CENTER,
                as_json=as_json,
            ),
            'mailbox':        lambda: self.mailbox(
                agent_name=args[0] if args else '',
                as_json=as_json,
            ),
            'use_cases':      lambda: self.use_cases(as_json),
        }

        if command in commands:
            return commands[command]()
        return {
            'error': f'Unknown command: {command}',
            'available': list(commands.keys()),
        }
