"""Basin Population Strategy -- Semantic Layer on Deterministic Coordinates

Populates the 131 Fernandez basins with semantic meaning.
Each basin gets a domain, representative terms, and I-Logic state
derived from its layer position.

ARCHITECTURE:
    Content -> Morton coords -> basin (mod 131) -> layer (basin * 9 // 131 + 1)
    
    This module adds MEANING on top:
    - Layer-derived semantic domains (Structure, Meaning, Location, ...)
    - Term collection per basin (discovers what content clusters where)
    - Zipf frequency -> wavelength -> basin mapping (from basin_dict)
    - Integration with explain pipeline (basin context, not UNKNOWN)

USAGE:
    from cubesquare.population import BasinPopulator

    pop = BasinPopulator()
    pop.populate(["/src/auth/login.py", "/src/db/query.py", ...])
    info = pop.get_basin(42)
    print(info.domain, info.terms, info.layer_name)

NO PYTHON LOOPS in hot path. Population is setup (bounded), not hot path.
Basin lookup is O(1) after population.
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
import re
import math

from ._core.constants import PRIME_131, LAYER_BASINS, LAYER_NAMES, K5, K11, PHI
from ._core import compute_basin, morton_encode, morton_decode
from .i_logic.states import ILogicState


# =========================================================================
# LAYER SEMANTIC DEFINITIONS (from CLAUDE.md architecture)
# =========================================================================

LAYER_DOMAINS = {
    1: ("Structure",    "Hierarchy, tree traversal, file navigation, geometry"),
    2: ("Meaning",      "Concept clustering, topic modeling, translation, semantics"),
    3: ("Location",     "Morton code, spatial proximity, GIS, coordinates"),
    4: ("Operation",    "API, LOD, summarization, abstraction levels, functions"),
    5: ("Probability",  "Sampling, QTS, load balancing, Pareto, uncertainty"),
    6: ("State",        "Affect mapping, beat sync, I-Logic states, emotion"),
    7: ("Timing",       "Light spectrum, color space, wavelength, frequency"),
    8: ("Force",        "Polarity, wave modulation, error stabilization, EM"),
    9: ("Boundary",     "K11 QRS, transmission/reflection, coherence, membrane"),
}

# SRL states per layer (from CLAUDE.md layer table)
LAYER_SRL_STATES = {
    1: ILogicState.DIRECT,
    2: ILogicState.DIRECT,
    3: ILogicState.SPIRAL,
    4: ILogicState.DIRECT,
    5: ILogicState.INVERSE,      # CONVERGENT maps to INVERSE
    6: ILogicState.INVERSE,
    7: ILogicState.SPIRAL,
    8: ILogicState.INVERSE,      # CONVERGENT maps to INVERSE
    9: ILogicState.INVERSE,
}

# Seed vocabulary -- common terms with approximate Zipf ranks
# High rank = rare (red/high basin), low rank = common (violet/low basin)
SEED_VOCABULARY = {
    # L1 Structure (basins 0-14) -- most common structural terms
    "file": 1, "path": 2, "dir": 3, "root": 4, "node": 5,
    "tree": 6, "parent": 7, "child": 8, "depth": 9, "branch": 10,
    "folder": 11, "index": 12, "link": 13, "graph": 14, "edge": 15,
    # L2 Semantic (basins 15-29)
    "name": 16, "type": 17, "class": 18, "method": 19, "function": 20,
    "variable": 21, "value": 22, "key": 23, "data": 24, "model": 25,
    "schema": 26, "category": 27, "concept": 28, "meaning": 29, "context": 30,
    # L3 Spatial (basins 30-43)
    "position": 31, "coord": 32, "location": 33, "region": 34, "area": 35,
    "distance": 36, "neighbor": 37, "grid": 38, "cell": 39, "zone": 40,
    "map": 41, "tile": 42, "sector": 43, "proximity": 44,
    # L4 Functional (basins 44-58)
    "api": 45, "endpoint": 46, "request": 47, "response": 48, "handler": 49,
    "route": 50, "controller": 51, "service": 52, "module": 53, "plugin": 54,
    "action": 55, "command": 56, "task": 57, "pipeline": 58, "process": 59,
    # L5 Probabilistic (basins 59-72)
    "random": 60, "sample": 61, "probability": 62, "confidence": 63, "score": 64,
    "weight": 65, "threshold": 66, "distribution": 67, "entropy": 68, "variance": 69,
    "estimate": 70, "predict": 71, "likelihood": 72, "bias": 73,
    # L6 Emotional/State (basins 73-87)
    "status": 74, "state": 75, "error": 76, "warning": 77, "success": 78,
    "failure": 79, "pending": 80, "active": 81, "inactive": 82, "blocked": 83,
    "ready": 84, "complete": 85, "critical": 86, "stable": 87, "volatile": 88,
    # L7 Frequency (basins 88-101)
    "event": 89, "trigger": 90, "signal": 91, "pulse": 92, "cycle": 93,
    "interval": 94, "timer": 95, "schedule": 96, "frequency": 97, "clock": 98,
    "heartbeat": 99, "tick": 100, "wave": 101, "oscillation": 102,
    # L8 Electromagnetic (basins 102-116)
    "force": 103, "field": 104, "energy": 105, "power": 106, "current": 107,
    "voltage": 108, "resistance": 109, "charge": 110, "flux": 111, "potential": 112,
    "amplitude": 113, "phase": 114, "polarity": 115, "dipole": 116,
    # L9 Membrane (basins 117-130)
    "boundary": 117, "limit": 118, "threshold": 119, "filter": 120, "gate": 121,
    "barrier": 122, "membrane": 123, "interface": 124, "bridge": 125, "tunnel": 126,
    "portal": 127, "edge": 128, "perimeter": 129, "horizon": 130,
}


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

@dataclass
class BasinSemantic:
    """Semantic profile for a single basin (0-130)."""
    basin_id: int
    layer: int
    layer_name: str
    domain: str
    domain_description: str
    terms: List[str] = field(default_factory=list)
    term_count: int = 0
    i_logic_state: complex = ILogicState.DIRECT
    wavelength: float = 0.0       # Center wavelength (380-750nm)
    density: float = 0.0          # Term density vs expected uniform

    def to_dict(self) -> dict:
        return {
            "basin_id": self.basin_id,
            "layer": self.layer,
            "layer_name": self.layer_name,
            "domain": self.domain,
            "terms": self.terms[:20],
            "term_count": self.term_count,
            "i_logic_state": ILogicState.name(self.i_logic_state),
            "wavelength_nm": round(self.wavelength, 1),
            "density": round(self.density, 3),
        }


@dataclass
class PopulationSummary:
    """Summary of basin population state."""
    total_basins: int = PRIME_131
    populated_basins: int = 0
    total_terms: int = 0
    coverage: float = 0.0
    layer_summaries: Dict[int, dict] = field(default_factory=dict)

    def to_dict(self) -> dict:
        return {
            "total_basins": self.total_basins,
            "populated": self.populated_basins,
            "total_terms": self.total_terms,
            "coverage": round(self.coverage, 3),
            "layers": self.layer_summaries,
        }


# =========================================================================
# BASIN POPULATOR
# =========================================================================

class BasinPopulator:
    """Populates 131 basins with semantic meaning from content.

    The populator discovers what content clusters in which basins
    by running content through the deterministic Morton -> basin pipeline.
    Basin semantics EMERGE from content, not from arbitrary assignment.

    Usage:
        pop = BasinPopulator()
        # Populate from content paths
        pop.populate(["/src/auth/login.py", "/lib/math/vector.py", ...])
        # Query basin meaning
        info = pop.get_basin(42)
        print(info.domain, info.terms)
        # Find where a term lives
        basin = pop.find_term("authentication")
    """

    def __init__(self):
        self._basins: Dict[int, BasinSemantic] = {}
        self._term_index: Dict[str, int] = {}
        self._initialize()

    def _initialize(self):
        """Initialize all 131 basins with layer-derived semantics."""
        for basin_id in range(PRIME_131):
            layer = min(basin_id * 9 // PRIME_131 + 1, 9)
            layer_name = LAYER_NAMES.get(layer, f"L{layer}")
            domain_name, domain_desc = LAYER_DOMAINS.get(layer, ("Unknown", ""))
            # Wavelength: 380nm (violet, basin 0) -> 750nm (red, basin 130)
            wavelength = 380.0 + (basin_id / 130.0) * 370.0

            self._basins[basin_id] = BasinSemantic(
                basin_id=basin_id,
                layer=layer,
                layer_name=layer_name,
                domain=domain_name,
                domain_description=domain_desc,
                wavelength=wavelength,
                i_logic_state=LAYER_SRL_STATES.get(layer, ILogicState.DIRECT),
            )

        # Seed basins with vocabulary
        self._seed_vocabulary()

    def _seed_vocabulary(self):
        """Seed basins with the default vocabulary using Zipf -> wavelength -> basin."""
        vocab_size = len(SEED_VOCABULARY)
        for term, rank in SEED_VOCABULARY.items():
            # Zipf wavelength: high rank (rare) -> long wavelength (red/high basin)
            # low rank (common) -> short wavelength (violet/low basin)
            wavelength = 380.0 + (rank / vocab_size) * 370.0
            basin_id = int((wavelength - 380.0) / 370.0 * 130.0)
            basin_id = max(0, min(130, basin_id))

            if basin_id in self._basins:
                self._basins[basin_id].terms.append(term)
                self._basins[basin_id].term_count += 1
                self._term_index[term] = basin_id

    def populate(self, contents: List[str]) -> Dict[int, int]:
        """Populate basins from content paths/strings.

        Runs each content string through CSTMemory.write() to get
        deterministic basin assignment, then extracts terms and
        indexes them by basin.

        Args:
            contents: List of content strings (paths, descriptions, etc.)

        Returns:
            Dict mapping basin_id -> number of tokens that landed there.
        """
        from .memory import CSTMemory
        mem = CSTMemory()
        basin_counts: Dict[int, int] = {}

        # Write all content -- CSTMemory handles Morton encoding
        tokens = [mem.write(content) for content in contents]

        # Index terms by basin
        for i, token in enumerate(tokens):
            basin = token.basin
            basin_counts[basin] = basin_counts.get(basin, 0) + 1

            if basin in self._basins:
                semantic = self._basins[basin]
                terms = self._extract_terms(contents[i])
                # Deduplicate while preserving order
                existing = set(semantic.terms)
                new_terms = [t for t in terms if t not in existing]
                semantic.terms.extend(new_terms)
                semantic.term_count += 1

                for term in new_terms:
                    self._term_index[term] = basin

        # Update density (how full vs expected uniform)
        expected = len(contents) / PRIME_131 if contents else 1.0
        for semantic in self._basins.values():
            semantic.density = semantic.term_count / expected if expected > 0 else 0.0

        return basin_counts

    def _extract_terms(self, text: str) -> List[str]:
        """Extract meaningful terms from text. Setup path, not hot path."""
        parts = re.split(r'[/\\._\-\s:]+', text.lower())
        stopwords = {
            'the', 'a', 'an', 'is', 'in', 'on', 'at', 'to', 'for', 'of',
            'and', 'or', 'py', 'js', 'ts', 'md', 'src', 'test', 'tests',
            'lib', 'bin', 'var', 'tmp', 'usr', 'etc',
        }
        return [p for p in parts if len(p) > 2 and p not in stopwords]

    # -----------------------------------------------------------------
    # QUERY API (all O(1) after population)
    # -----------------------------------------------------------------

    def get_basin(self, basin_id: int) -> Optional[BasinSemantic]:
        """Get semantic info for a basin. O(1)."""
        return self._basins.get(basin_id)

    def find_term(self, term: str) -> Optional[int]:
        """Find which basin a term is indexed in. O(1)."""
        return self._term_index.get(term.lower())

    def get_context(self, basin_id: int) -> str:
        """Get human-readable context string for a basin.

        This is what explain() should show instead of UNKNOWN.
        """
        semantic = self._basins.get(basin_id)
        if not semantic:
            return "UNKNOWN"
        parts = [f"L{semantic.layer} {semantic.domain}"]
        if semantic.terms:
            parts.append(f"terms=[{', '.join(semantic.terms[:5])}]")
        parts.append(f"wl={semantic.wavelength:.0f}nm")
        parts.append(f"state={ILogicState.name(semantic.i_logic_state)}")
        return " | ".join(parts)

    def get_populated_basins(self) -> List[int]:
        """Return basin IDs that have content (beyond seed vocab)."""
        return [b for b, s in self._basins.items() if s.term_count > 0]

    def get_layer_summary(self, layer: int) -> dict:
        """Summarize all basins in a layer."""
        basins = [s for s in self._basins.values() if s.layer == layer]
        total_terms = sum(s.term_count for s in basins)
        all_terms = []
        for s in basins:
            all_terms.extend(s.terms[:5])
        domain_name, domain_desc = LAYER_DOMAINS.get(layer, ("Unknown", ""))
        basin_range = (basins[0].basin_id, basins[-1].basin_id) if basins else (0, 0)
        return {
            "layer": layer,
            "name": domain_name,
            "description": domain_desc,
            "basin_count": len(basins),
            "basin_range": basin_range,
            "total_terms": total_terms,
            "sample_terms": all_terms[:15],
            "i_logic_state": ILogicState.name(
                LAYER_SRL_STATES.get(layer, ILogicState.DIRECT)
            ),
        }

    def summary(self) -> PopulationSummary:
        """Full population summary."""
        populated = self.get_populated_basins()
        total_terms = sum(s.term_count for s in self._basins.values())
        layer_sums = {}
        for i in range(1, 10):
            layer_sums[i] = self.get_layer_summary(i)
        return PopulationSummary(
            total_basins=PRIME_131,
            populated_basins=len(populated),
            total_terms=total_terms,
            coverage=len(populated) / PRIME_131,
            layer_summaries=layer_sums,
        )

    # -----------------------------------------------------------------
    # WAVELENGTH API (Zipf frequency <-> basin position)
    # -----------------------------------------------------------------

    def wavelength_to_basin(self, wavelength_nm: float) -> int:
        """Convert wavelength (380-750nm) to basin (0-130). O(1)."""
        normalized = (wavelength_nm - 380.0) / 370.0
        return max(0, min(130, int(normalized * 130.0)))

    def basin_to_wavelength(self, basin_id: int) -> float:
        """Convert basin (0-130) to center wavelength (380-750nm). O(1)."""
        return 380.0 + (basin_id / 130.0) * 370.0

    def frequency_rank_to_basin(self, rank: int, vocab_size: int) -> int:
        """Convert Zipf rank to basin via wavelength. O(1).

        Low rank (common word) -> short wavelength (violet) -> low basin
        High rank (rare word) -> long wavelength (red) -> high basin
        """
        wavelength = 380.0 + (rank / max(vocab_size, 1)) * 370.0
        return self.wavelength_to_basin(wavelength)
