"""
Condition Encoder Shell - Semantic Condition Encoding
HEX_ID: 0BF1DD04

Standalone version for cubesquare.

Encodes semantic conditions into ephemeral hex IDs using FML frequency-derived
wavelength mapping. I-Logic state is determined by GEOMETRY (basin position),
not keywords.

INPUTS:
    - Semantic primitive dict: {text, type, encoding}
    - Plain text strings
    - Condition type: state_condition, relationship, query, assertion, command

OUTPUTS:
    - EncodedCondition: hex_id, basin, wavelength, ilogic_state, ephemeral, primitive_text, primitive_type

FML SPECTRUM (131 basins):
    Basin 0-32   (violet/blue, 380-450nm) -> DIRECT     (stable core)
    Basin 33-65  (green/yellow, 450-550nm) -> SPIRAL    (transition, growth)
    Basin 66-98  (orange, 550-620nm)       -> ANTI_SPIRAL (reversal, decay)
    Basin 99-130 (red, 620-750nm)          -> INVERSE   (edge, instability)

Hex ID Format: EEBBIIII
    EE:   Ephemeral prefix (E0-EF, varies with basin high nibble)
    BB:   Basin byte (00-82 for basins 0-130)
    IIII: Unique counter/hash (varies per encode call)

NOTE: Hex IDs are NOT deterministic - same input produces different IDs
to ensure uniqueness of ephemeral tokens. Basin and state ARE deterministic.
"""

import hashlib
import time
from dataclasses import dataclass
from typing import Dict, Optional, List, Any
from enum import Enum

# Import from cubesquare core
from .._core.constants import PRIME_131

# Import I-Logic
from ..i_logic.states import ILogicState


# ==============================================================================
# CONSTANTS
# ==============================================================================

# FML Wavelength bounds (visible spectrum: 380-750nm)
LAMBDA_MIN = 380.0  # nm (violet)
LAMBDA_MAX = 750.0  # nm (red)
LAMBDA_RANGE = LAMBDA_MAX - LAMBDA_MIN

# Ephemeral token prefix
EPHEMERAL_PREFIX = 0xE0  # High byte marks ephemeral tokens

# Condition types
CONDITION_TYPES = ['state_condition', 'relationship', 'query', 'assertion', 'command']

# Default vocabulary size (for hash-based ranking)
DEFAULT_VOCAB_SIZE = 10000


# ==============================================================================
# I-LOGIC STATE ENUM (with name property for compatibility)
# ==============================================================================

class ILogicStateEnum(Enum):
    """I-Logic state enumeration."""
    DIRECT = 1
    INVERSE = -1
    SPIRAL = 2
    ANTI_SPIRAL = -2
    ABSORBED = 0

    @property
    def name(self) -> str:
        return self._name_


# ==============================================================================
# FREQUENCY FUNCTIONS
# ==============================================================================

def frequency_to_wavelength(rank: int, vocab_size: int) -> float:
    """
    Convert term frequency rank to wavelength.

    High frequency (common, low rank) -> short wavelength (violet)
    Low frequency (rare, high rank)   -> long wavelength (red)
    """
    if vocab_size <= 0:
        vocab_size = DEFAULT_VOCAB_SIZE

    # Normalize rank to 0-1
    normalized_rank = min(1.0, max(0.0, rank / vocab_size))

    # Map to wavelength (low rank = short wavelength)
    wavelength = LAMBDA_MIN + (normalized_rank * LAMBDA_RANGE)

    return wavelength


def wavelength_to_basin(wavelength: float) -> int:
    """
    Convert wavelength to basin number (0-130).

    Short wavelength -> low basin (violet/blue)
    Long wavelength  -> high basin (red)
    """
    # Clamp wavelength to valid range
    wavelength = max(LAMBDA_MIN, min(LAMBDA_MAX, wavelength))

    # Map to basin
    normalized = (wavelength - LAMBDA_MIN) / LAMBDA_RANGE
    basin = int(normalized * (PRIME_131 - 1))

    return max(0, min(PRIME_131 - 1, basin))


def basin_to_ilogic(basin: int) -> ILogicStateEnum:
    """
    Classify I-Logic state from basin position in FML spectrum.

    NO KEYWORDS. The wavelength IS the meaning.

    FML Spectrum (131 basins from Fernandez prime):
        Basin 0-32   (violet/blue, 380-450nm)  -> DIRECT     (stable core)
        Basin 33-65  (green/yellow, 450-550nm) -> SPIRAL     (transition, growth)
        Basin 66-98  (orange, 550-620nm)       -> ANTI_SPIRAL (reversal, decay)
        Basin 99-130 (red, 620-750nm)          -> INVERSE    (edge, instability)
    """
    basin_normalized = basin / 130.0

    if basin_normalized < 0.25:
        return ILogicStateEnum.DIRECT
    elif basin_normalized < 0.50:
        return ILogicStateEnum.SPIRAL
    elif basin_normalized < 0.75:
        return ILogicStateEnum.ANTI_SPIRAL
    else:
        return ILogicStateEnum.INVERSE


# ==============================================================================
# DATA STRUCTURES
# ==============================================================================

@dataclass
class EncodedCondition:
    """Result of encoding a semantic primitive into a condition token."""
    hex_id: str           # 8-char hex ID
    basin: int            # FML wavelength basin [0-130]
    wavelength: float     # Wavelength in nm
    ilogic_state: ILogicStateEnum
    ephemeral: bool       # True = dissolves after use
    primitive_text: str   # Original text
    primitive_type: str   # Condition type
    created_at: float     # Timestamp

    def to_dict(self) -> Dict:
        return {
            'hex_id': self.hex_id,
            'basin': self.basin,
            'wavelength': round(self.wavelength, 2),
            'ilogic_state': self.ilogic_state.name,
            'ephemeral': self.ephemeral,
            'primitive_text': self.primitive_text,
            'primitive_type': self.primitive_type,
            'created_at': self.created_at,
        }


# ==============================================================================
# CONDITION ENCODER
# ==============================================================================

class ConditionEncoder:
    """
    Encodes semantic primitives into condition hex IDs.

    Uses FML frequency-derived wavelength for basin assignment,
    NOT content hashing. I-Logic state comes from GEOMETRY (basin position).

    Usage:
        encoder = ConditionEncoder()
        condition = encoder.encode_text("stuck")
        print(condition.basin)       # -> 99+ (red zone)
        print(condition.ilogic_state) # -> INVERSE
    """

    def __init__(self, vocab_size: int = DEFAULT_VOCAB_SIZE, term_ranks: Dict[str, int] = None):
        """
        Initialize encoder.

        Args:
            vocab_size: Size of vocabulary for frequency normalization
            term_ranks: Optional dict mapping terms to frequency ranks (lower = more common)
        """
        self.vocab_size = vocab_size
        self.term_ranks = term_ranks or {}
        self._counter = 0
        self._last_ms = 0

    def _get_term_rank(self, term: str) -> int:
        """Get frequency rank for a term."""
        term_lower = term.lower()

        if term_lower in self.term_ranks:
            return self.term_ranks[term_lower]

        # Unknown term: hash to get a consistent pseudo-rank
        # Unknown terms are treated as rare (high rank = long wavelength = INVERSE zone)
        hash_val = int(hashlib.md5(term_lower.encode()).hexdigest()[:8], 16)
        return (hash_val % (self.vocab_size // 2)) + (self.vocab_size // 2)

    def _get_term_wavelength(self, term: str) -> float:
        """
        Get wavelength for a term based on its frequency rank.

        High frequency (common) -> short wavelength (violet)
        Low frequency (rare)    -> long wavelength (red)
        """
        rank = self._get_term_rank(term)
        return frequency_to_wavelength(rank, self.vocab_size)

    def _classify_ilogic_state(self, text: str, primitive_type: str, encoding: Dict,
                                basin: int = -1, wavelength: float = -1) -> ILogicStateEnum:
        """
        Classify I-Logic state - GEOMETRY FIRST, encoding hints as override.

        Priority:
        1. Explicit encoding hints (from CST layer analysis)
        2. Geometric position (basin/wavelength)
        3. Never keywords - that's translation, not decomposition
        """
        # Check for explicit state hints in encoding (from CST analysis)
        if encoding:
            k11_trend = encoding.get('k11_trend', '')
            if k11_trend == 'negative':
                return ILogicStateEnum.INVERSE
            elif k11_trend == 'positive':
                return ILogicStateEnum.DIRECT
            elif k11_trend == 'oscillating':
                return ILogicStateEnum.SPIRAL

            # Check for explicit ilogic in encoding
            if 'ilogic' in encoding:
                ilogic_name = encoding['ilogic'].upper()
                try:
                    return ILogicStateEnum[ilogic_name]
                except KeyError:
                    pass

        # Use geometry if we have basin info
        if basin >= 0:
            return basin_to_ilogic(basin)

        # Fallback: compute wavelength from text and use geometry
        wavelength = self._get_term_wavelength(text)
        basin = wavelength_to_basin(wavelength)
        return basin_to_ilogic(basin)

    def _generate_hex_id(self, text: str, basin: int, wavelength: float) -> str:
        """
        Generate a unique hex ID for the condition.

        Format: EEBBIIII
            EE = Ephemeral prefix (E0-EF, encodes basin high nibble)
            BB = Basin byte (00-82) + sub-basin discriminator
            IIII = Unique counter + text hash (collision chain)
        """
        # Get unique counter
        current_ms = int(time.time() * 1000)
        if current_ms == self._last_ms:
            self._counter += 1
        else:
            self._counter = 0
            self._last_ms = current_ms

        # Build hex ID
        prefix = EPHEMERAL_PREFIX + (basin >> 4)  # E0-E8 based on basin high nibble

        # Sub-basin discriminator
        basin_width = LAMBDA_RANGE / PRIME_131
        wavelength_in_basin = (wavelength - LAMBDA_MIN) % basin_width
        sub_basin = int((wavelength_in_basin / basin_width) * 256) & 0xFF

        basin_byte = (basin & 0x7F) | ((sub_basin >> 7) << 7)

        # Text hash + counter for uniqueness
        text_hash = int(hashlib.md5(text.encode()).hexdigest()[:4], 16)
        unique_part = ((text_hash ^ (sub_basin << 8)) + self._counter) & 0xFFFF

        return f"{prefix:02X}{basin_byte:02X}{unique_part:04X}"

    def encode(self, primitive: Dict) -> EncodedCondition:
        """
        Encode a semantic primitive into a condition token.

        Args:
            primitive: Dict with keys:
                - text: The condition text (e.g., "stuck", "waiting")
                - type: One of CONDITION_TYPES
                - encoding: Optional dict with hints (srl_delta, k11_trend, etc.)
                - resolved_hex: Optional pre-resolved hex ID (bypasses encoding)

        Returns:
            EncodedCondition with hex ID, basin, wavelength, and I-Logic state.
        """
        text = primitive.get('text', '')
        ptype = primitive.get('type', 'state_condition')
        encoding = primitive.get('encoding', {})

        # Check for pre-resolved hex
        if primitive.get('resolved_hex'):
            return EncodedCondition(
                hex_id=primitive['resolved_hex'],
                basin=-1,  # Unknown (persisted token)
                wavelength=-1,
                ilogic_state=ILogicStateEnum.DIRECT,
                ephemeral=False,
                primitive_text=text,
                primitive_type=ptype,
                created_at=time.time(),
            )

        # Compute wavelength from text frequency
        wavelength = self._get_term_wavelength(text)
        basin = wavelength_to_basin(wavelength)

        # Classify I-Logic state FROM GEOMETRY (basin position)
        ilogic_state = self._classify_ilogic_state(text, ptype, encoding, basin, wavelength)

        # Generate hex ID
        hex_id = self._generate_hex_id(text, basin, wavelength)

        return EncodedCondition(
            hex_id=hex_id,
            basin=basin,
            wavelength=wavelength,
            ilogic_state=ilogic_state,
            ephemeral=True,
            primitive_text=text,
            primitive_type=ptype,
            created_at=time.time(),
        )

    def encode_batch(self, primitives: List[Dict]) -> List[EncodedCondition]:
        """Encode multiple primitives."""
        return [self.encode(p) for p in primitives]

    def encode_text(self, text: str, ptype: str = 'state_condition') -> EncodedCondition:
        """Convenience method to encode plain text."""
        return self.encode({'text': text, 'type': ptype})


# ==============================================================================
# CONVENIENCE FUNCTIONS
# ==============================================================================

_encoder: Optional[ConditionEncoder] = None


def get_encoder() -> ConditionEncoder:
    """Get or create singleton encoder instance."""
    global _encoder
    if _encoder is None:
        _encoder = ConditionEncoder()
    return _encoder


def encode_condition(text: str, ptype: str = 'state_condition') -> EncodedCondition:
    """Convenience function to encode a condition."""
    return get_encoder().encode_text(text, ptype)


def encode_primitive(primitive: Dict) -> EncodedCondition:
    """Convenience function to encode a primitive dict."""
    return get_encoder().encode(primitive)


def get_basin_zone(basin: int) -> str:
    """Get FML spectrum zone for a basin."""
    basin_norm = basin / 130.0
    if basin_norm < 0.25:
        return 'VIOLET (stable)'
    elif basin_norm < 0.50:
        return 'GREEN (growth)'
    elif basin_norm < 0.75:
        return 'ORANGE (decay)'
    else:
        return 'RED (instability)'


def get_ilogic_for_condition(text: str) -> ILogicStateEnum:
    """Get I-Logic state for a condition text."""
    condition = encode_condition(text)
    return condition.ilogic_state


# Add run method to ConditionEncoder
def _condition_encoder_run(self, command: str, *args, as_json: bool = False):
    """Run a shell command (CLI interface)."""
    import json
    commands = {
        'status': lambda: {
            'hex_id': '0BF1DD04',
            'vocab_size': self.vocab_size,
            'term_ranks_count': len(self.term_ranks),
            'condition_types': CONDITION_TYPES,
        },
        'encode': lambda: self.encode_text(args[0] if args else '', args[1] if len(args) > 1 else 'state_condition').to_dict(),
        'batch': lambda: [c.to_dict() for c in self.encode_batch([{'text': t, 'type': 'state_condition'} for t in args])],
        'ilogic': lambda: {'text': args[0] if args else '', 'state': get_ilogic_for_condition(args[0] if args else '').name},
        'zone': lambda: {'basin': int(args[0]) if args else 0, 'zone': get_basin_zone(int(args[0]) if args else 0)},
    }
    if command in commands:
        result = commands[command]()
        if as_json and isinstance(result, (dict, list)):
            return json.dumps(result, indent=2, default=str)
        return result
    return {'error': f'Unknown command: {command}', 'available': list(commands.keys())}

# Attach to class
ConditionEncoder.run = _condition_encoder_run
