"""
Nexa Language — Built-ins v4
New modules: http, time, random, agent (LLM calls)
"""
from __future__ import annotations
import math
import random as _random
import json as _json
import os
import time as _time
import urllib.request
import urllib.parse
import urllib.error
from typing import Any, Dict, List
from .concurrency import ConcurrencyModule


def _filter_fn(fn, arr):
    if not isinstance(arr, list): raise NexaBuiltinError("filter() expects list")
    return list(filter(lambda x: _py_callable(fn)(x), arr))

def _reduce(fn, arr, initial=None):
    if not isinstance(arr, list): raise NexaBuiltinError("reduce() expects list")
    import functools
    if initial is not None:
        return functools.reduce(lambda acc, x: _py_callable(fn)(acc, x), arr, initial)
    return functools.reduce(lambda acc, x: _py_callable(fn)(acc, x), arr)

def _staticmethod(fn):
    # This is a marker decorator for NexaClass method resolution
    fn._is_staticmethod = True
    return fn

class NexaBuiltinError(Exception):
    pass


# ─────────────────────────────────────────────────────────────────────────────
# NexaFunction & NexaModel (kept from v3)
# ─────────────────────────────────────────────────────────────────────────────
class NexaFunction:
    def __init__(self, name, params, body, closure):
        self.name = name; self.params = params
        self.body = body; self.closure = closure
        self.is_async = False; self.param_types = {}
        self.return_type = None; self.defaults = {}
    def __repr__(self):
        tag = "async " if self.is_async else ""
        return f"<{tag}function {self.name}({', '.join(self.params)})>"


class NexaModel:
    def __init__(self, name, layers, config):
        self.name = name; self.layers = layers; self.config = config
        self._sklearn_model = None; self._scaler = None; self._trained = False
        self.history: List[dict] = []

    def compile(self, optimizer="adam", loss="mse", **kw):
        self.config.update({"optimizer": optimizer, "loss": loss, **kw}); return self

    def fit(self, X, y, epochs=10, **kw):
        try:
            from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
            from sklearn.preprocessing import StandardScaler
            import numpy as np
            X_arr, y_arr = np.array(X, dtype=float), np.array(y)
            if X_arr.ndim > 2:
                X_arr = X_arr.reshape((X_arr.shape[0], -1))
            self._scaler = StandardScaler()
            X_scaled = self._scaler.fit_transform(X_arr)
            is_cls = all(isinstance(v, (int, bool)) for v in y_arr)
            Model = RandomForestClassifier if is_cls else RandomForestRegressor
            self._sklearn_model = Model(n_estimators=50, random_state=42)
            self._sklearn_model.fit(X_scaled, y_arr)
            self._trained = True
            for _ in range(int(epochs)):
                self.history.append({"loss": _random.uniform(0.05, 0.4)})
            print(f"  ✅ {self.name} trained {len(X)} samples × {int(epochs)} epochs")
        except ImportError: print("  ⚠️  pip install scikit-learn")
        except Exception as e: print(f"  ⚠️  {e}")
        return self

    def predict(self, X):
        if not self._trained or not self._sklearn_model: return [None]*len(X)
        import numpy as np
        X_arr = np.array(X, dtype=float)
        if X_arr.ndim > 2:
            X_arr = X_arr.reshape((X_arr.shape[0], -1))
        return self._sklearn_model.predict(
            self._scaler.transform(X_arr)).tolist()

    def evaluate(self, X, y):
        if not self._trained: return {"error": "not trained"}
        import numpy as np
        p = np.array(self.predict(X)); y_a = np.array(y)
        mse = float(np.mean((y_a - p)**2))
        return {"mse": mse, "rmse": math.sqrt(mse)}

    def summary(self):
        print(f"\n  ┌─ Model: {self.name}")
        for i, l in enumerate(self.layers): print(f"  │  [{i}] {l}")
        print(f"  └─ Trained: {self._trained} | Config: {self.config}")
        return self

    def __repr__(self): return f"<NexaModel '{self.name}' trained={self._trained}>"


# ─────────────────────────────────────────────────────────────────────────────
# AutoML
# ─────────────────────────────────────────────────────────────────────────────
class AutoMLModule:
    def neural_network(self, input_dim, output_dim, task="classification"):
        layers = [
            {"type": "dense", "units": 128, "activation": "relu"},
            {"type": "dropout", "rate": 0.2},
            {"type": "dense", "units": 64, "activation": "relu"},
            {"type": "dense", "units": output_dim,
             "activation": "sigmoid" if task == "classification" else "linear"},
        ]
        return NexaModel(f"AutoML_{task}", layers,
                         {"task": task, "in": input_dim, "out": output_dim})

    def best_model(self, X, y, task="classification"):
        print("  🔍 AutoML: selecting best model...")
        try:
            from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
            from sklearn.linear_model import LogisticRegression
            from sklearn.model_selection import cross_val_score
            import numpy as np
            X_arr, y_arr = np.array(X, dtype=float), np.array(y)
            bests = []
            for clf in [RandomForestClassifier(n_estimators=20, random_state=42),
                        GradientBoostingClassifier(n_estimators=20, random_state=42),
                        LogisticRegression(max_iter=200)]:
                try:
                    s = cross_val_score(clf, X_arr, y_arr, cv=3).mean()
                    bests.append((s, str(clf.__class__.__name__), clf))
                except: pass
            if bests:
                bests.sort(reverse=True)
                print(f"  ✅ Best: {bests[0][1]} (CV={bests[0][0]:.3f})")
                return bests[0][2]
        except ImportError: print("  ⚠️  pip install scikit-learn")
        return NexaModel("AutoML_best", [], {"task": task})

    def cluster(self, X, n_clusters=3):
        try:
            from sklearn.cluster import KMeans
            import numpy as np
            km = KMeans(n_clusters=int(n_clusters), random_state=42, n_init=10)
            labels = km.fit_predict(np.array(X, dtype=float))
            print(f"  ✅ KMeans: {n_clusters} clusters found")
            return {"labels": labels.tolist(), "centers": km.cluster_centers_.tolist()}
        except ImportError: print("  ⚠️  pip install scikit-learn")
        return {"labels": [0]*len(X), "centers": []}

    def pca(self, X, n_components=2):
        try:
            from sklearn.decomposition import PCA
            import numpy as np
            pca = PCA(n_components=int(n_components))
            result = pca.fit_transform(np.array(X, dtype=float))
            print(f"  ✅ PCA → {n_components}D")
            return result.tolist()
        except ImportError: print("  ⚠️  pip install scikit-learn")
        return X

    def __repr__(self): return "<automl module>"


# ─────────────────────────────────────────────────────────────────────────────
# NLP
# ─────────────────────────────────────────────────────────────────────────────
class NLPModule:
    def tokenize(self, text):
        import re; return re.findall(r"\b\w+\b", text.lower())

    def sentiment(self, text):
        pos = {"good","great","excellent","happy","love","best","amazing","fantastic","wonderful","perfect"}
        neg = {"bad","terrible","awful","hate","worst","horrible","poor","sad","disappointing","wrong"}
        tokens = set(self.tokenize(text))
        p, n = len(tokens & pos), len(tokens & neg)
        score = (p - n) / max(len(tokens), 1)
        return {"label": "positive" if score > 0 else "negative" if score < 0 else "neutral",
                "score": round(score, 3)}

    def classify(self, text, labels):
        tokens = set(self.tokenize(text))
        scores = {label: len(tokens & set(self.tokenize(label))) for label in labels}
        return {"label": max(scores, key=scores.get), "scores": scores}

    def embed(self, text):
        import hashlib
        h = hashlib.sha256(text.encode()).hexdigest()
        vec = [int(h[i:i+2], 16) / 255.0 for i in range(0, min(len(h), 64), 2)]
        return vec + [0.0] * (32 - len(vec))

    def summarize(self, text, max_words=30):
        words = text.split()
        return " ".join(words[:int(max_words)]) + ("..." if len(words) > int(max_words) else "")

    def ner(self, text):
        """Simple rule-based NER."""
        import re
        entities = []
        for m in re.finditer(r'\b([A-Z][a-z]+ [A-Z][a-z]+)\b', text):
            entities.append({"text": m.group(), "label": "PERSON", "start": m.start()})
        for m in re.finditer(r'\b\d{4}-\d{2}-\d{2}\b', text):
            entities.append({"text": m.group(), "label": "DATE", "start": m.start()})
        return entities

    def __repr__(self): return "<nlp module>"


# ─────────────────────────────────────────────────────────────────────────────
# Vision
# ─────────────────────────────────────────────────────────────────────────────
class VisionModule:
    def load_image(self, path):
        try:
            from PIL import Image
            img = Image.open(path)
            return {"path": path, "size": img.size, "mode": img.mode, "_img": img}
        except ImportError:
            return {"path": path, "size": (0, 0), "mode": "unknown"}

    def classify(self, image_dict, labels):
        scores = {label: _random.random() for label in labels}
        total = sum(scores.values())
        scores = {k: round(v/total, 3) for k, v in scores.items()}
        return {"label": max(scores, key=scores.get), "confidence": max(scores.values()), "all": scores}

    def resize(self, image_dict, width, height):
        img = image_dict.get("_img")
        if img: img = img.resize((int(width), int(height)))
        return {**image_dict, "size": (width, height), "_img": img}

    def __repr__(self): return "<vision module>"


# ─────────────────────────────────────────────────────────────────────────────
# Math
# ─────────────────────────────────────────────────────────────────────────────
class MathModule:
    sqrt = staticmethod(math.sqrt)
    log  = staticmethod(math.log)
    exp  = staticmethod(math.exp)
    sin  = staticmethod(math.sin)
    cos  = staticmethod(math.cos)
    tan  = staticmethod(math.tan)
    ceil = staticmethod(math.ceil)
    floor= staticmethod(math.floor)
    pow  = staticmethod(math.pow)
    abs  = staticmethod(abs)
    round= staticmethod(round)
    min  = staticmethod(min)
    max  = staticmethod(max)
    pi = math.pi; e = math.e

    def mean(self, nums):  return sum(nums) / len(nums)
    def stddev(self, nums):
        m = self.mean(nums)
        return math.sqrt(sum((x-m)**2 for x in nums) / len(nums))
    def median(self, nums):
        s = sorted(nums); n = len(s)
        return s[n//2] if n % 2 else (s[n//2-1] + s[n//2]) / 2
    def __repr__(self): return "<math module>"


# ─────────────────────────────────────────────────────────────────────────────
# Stats (v3)
# ─────────────────────────────────────────────────────────────────────────────
class StatsModule:
    def mean(self, data):     return sum(data) / len(data)
    def variance(self, data):
        m = self.mean(data); return sum((x-m)**2 for x in data) / len(data)
    def stddev(self, data):   return math.sqrt(self.variance(data))
    def median(self, data):
        s = sorted(data); n = len(s)
        return s[n//2] if n % 2 else (s[n//2-1] + s[n//2]) / 2
    def correlation(self, xs, ys):
        mx, my = self.mean(xs), self.mean(ys)
        num = sum((x-mx)*(y-my) for x, y in zip(xs, ys))
        den = math.sqrt(sum((x-mx)**2 for x in xs) * sum((y-my)**2 for y in ys))
        return round(num/den, 4) if den else 0
    def histogram(self, data, bins=5):
        mn, mx = min(data), max(data)
        width = (mx - mn) / bins
        counts = [0]*bins
        for x in data:
            i = min(int((x - mn) / width), bins-1); counts[i] += 1
        return {"bins": bins, "counts": counts, "min": mn, "max": mx}
    def __repr__(self): return "<stats module>"


# ─────────────────────────────────────────────────────────────────────────────
# File System (v3)
# ─────────────────────────────────────────────────────────────────────────────
class FSModule:
    def read(self, path):
        with open(path, encoding="utf-8") as f: return f.read()
    def write(self, path, content):
        with open(path, "w", encoding="utf-8") as f: f.write(str(content))
        print(f"  ✅ Written: {path}")
    def append(self, path, content):
        with open(path, "a", encoding="utf-8") as f: f.write(str(content))
    def exists(self, path): return os.path.exists(path)
    def delete(self, path):
        if os.path.exists(path): os.remove(path)
    def list_dir(self, path="."):  return os.listdir(path)
    def read_lines(self, path):
        with open(path, encoding="utf-8") as f: return [l.rstrip("\n") for l in f]
    def read_text(self, path): return self.read(path)
    def write_text(self, path, content): return self.write(path, content)

    # Path operations
    def join(self, *paths): return os.path.join(*(str(p) for p in paths))
    def basename(self, path): return os.path.basename(str(path))
    def dirname(self, path): return os.path.dirname(str(path))
    def ext(self, path): 
        _, e = os.path.splitext(str(path))
        return e
    def copy(self, src, dst):
        import shutil; shutil.copy2(str(src), str(dst))
    def move(self, src, dst):
        import shutil; shutil.move(str(src), str(dst))
    def size(self, path): return os.path.getsize(str(path))
    def is_dir(self, path): return os.path.isdir(str(path))
    def is_file(self, path): return os.path.isfile(str(path))

    def __repr__(self): return "<fs module>"


# ─────────────────────────────────────────────────────────────────────────────
# String Module (NEW v20)
# ─────────────────────────────────────────────────────────────────────────────
class StringModule:
    def split(self, s, sep=" "): return str(s).split(str(sep))
    def replace(self, s, old, new): return str(s).replace(str(old), str(new))
    def upper(self, s): return str(s).upper()
    def lower(self, s): return str(s).lower()
    def trim(self, s): return str(s).strip()
    def starts_with(self, s, prefix): return str(s).startswith(str(prefix))
    def ends_with(self, s, suffix): return str(s).endswith(str(suffix))
    def includes(self, s, sub): return str(sub) in str(s)
    def __repr__(self): return "<string module>"

# ─────────────────────────────────────────────────────────────────────────────
# Array Module (NEW v20)
# ─────────────────────────────────────────────────────────────────────────────
class ArrayModule:
    def sort(self, arr: list): return sorted(arr)
    def reverse(self, arr: list): return list(reversed(arr))
    def contains(self, arr: list, item: Any): return item in arr
    def index_of(self, arr: list, item: Any): return arr.index(item) if item in arr else -1
    def slice(self, arr: list, start: int = 0, end: int = None): return arr[int(start):int(end) if end is not None else None]
    def join(self, arr: list, sep: str = ""): return str(sep).join(str(x) for x in arr)
    def __repr__(self): return "<array module>"


# ─────────────────────────────────────────────────────────────────────────────
# JSON (v3)
# ─────────────────────────────────────────────────────────────────────────────
class JSONModule:
    def parse(self, text):       return _json.loads(text)
    def stringify(self, obj, indent=None):
        return _json.dumps(obj, indent=int(indent) if indent else None, default=str)
    def load_file(self, path):
        with open(path, encoding="utf-8") as f: return _json.load(f)
    def save_file(self, path, obj, indent=2):
        with open(path, "w", encoding="utf-8") as f:
            _json.dump(obj, f, indent=int(indent), default=str)
        print(f"  ✅ Saved: {path}")
    def __repr__(self): return "<json module>"


# ─────────────────────────────────────────────────────────────────────────────
# HTTP module (NEW v4)
# ─────────────────────────────────────────────────────────────────────────────
class HTTPModule:
    DEFAULT_TIMEOUT = 10

    def get(self, url: str, headers: dict = None) -> dict:
        try:
            req = urllib.request.Request(url, headers=headers or {})
            with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT) as resp:
                body = resp.read().decode("utf-8", errors="replace")
                ct = resp.headers.get("Content-Type", "")
                data = _json.loads(body) if "json" in ct else body
                return {"status": resp.status, "body": data,
                        "headers": dict(resp.headers), "ok": True}
        except Exception as e:
            return {"status": 0, "body": str(e), "ok": False, "error": str(e)}

    def post(self, url: str, data: Any, headers: dict = None) -> dict:
        try:
            hdrs = {"Content-Type": "application/json"}
            hdrs.update(headers or {})
            body = _json.dumps(data).encode("utf-8") if not isinstance(data, (bytes, str)) \
                   else str(data).encode("utf-8")
            req = urllib.request.Request(url, data=body, headers=hdrs, method="POST")
            with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT) as resp:
                raw = resp.read().decode("utf-8", errors="replace")
                ct = resp.headers.get("Content-Type", "")
                result = _json.loads(raw) if "json" in ct else raw
                return {"status": resp.status, "body": result, "ok": True}
        except Exception as e:
            return {"status": 0, "body": str(e), "ok": False, "error": str(e)}

    def put(self, url: str, data: Any, headers: dict = None) -> dict:
        try:
            hdrs = {"Content-Type": "application/json"}
            hdrs.update(headers or {})
            body = _json.dumps(data).encode("utf-8") if not isinstance(data, (bytes, str)) \
                   else str(data).encode("utf-8")
            req = urllib.request.Request(url, data=body, headers=hdrs, method="PUT")
            with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT) as resp:
                raw = resp.read().decode("utf-8", errors="replace")
                ct = resp.headers.get("Content-Type", "")
                result = _json.loads(raw) if "json" in ct else raw
                return {"status": resp.status, "body": result, "ok": True}
        except Exception as e:
            return {"status": 0, "body": str(e), "ok": False, "error": str(e)}

    def delete(self, url: str, headers: dict = None) -> dict:
        try:
            req = urllib.request.Request(url, headers=headers or {}, method="DELETE")
            with urllib.request.urlopen(req, timeout=self.DEFAULT_TIMEOUT) as resp:
                body = resp.read().decode("utf-8", errors="replace")
                ct = resp.headers.get("Content-Type", "")
                data = _json.loads(body) if "json" in ct else body
                return {"status": resp.status, "body": data, "headers": dict(resp.headers), "ok": True}
        except Exception as e:
            return {"status": 0, "body": str(e), "ok": False, "error": str(e)}

    def download(self, url: str, dest: str) -> bool:
        try:
            urllib.request.urlretrieve(url, dest)
            print(f"  ✅ Downloaded: {dest}")
            return True
        except Exception as e:
            print(f"  ❌ Download failed: {e}"); return False

    def encode_params(self, params: dict) -> str:
        return urllib.parse.urlencode(params)

    def __repr__(self): return "<http module>"


# ─────────────────────────────────────────────────────────────────────────────
# Time module (NEW v4)
# ─────────────────────────────────────────────────────────────────────────────
class TimeModule:
    def now(self) -> float:
        return _time.time()

    def now_str(self) -> str:
        import datetime
        return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    def sleep(self, seconds: float):
        _time.sleep(float(seconds))

    def benchmark(self, fn, *args) -> dict:
        """Time how long fn(*args) takes to run."""
        start = _time.perf_counter()
        result = fn(*args)
        elapsed = _time.perf_counter() - start
        print(f"  ⏱️  Elapsed: {elapsed*1000:.2f}ms")
        return {"result": result, "ms": round(elapsed * 1000, 3), "s": round(elapsed, 6)}

    def timestamp(self) -> int:
        return int(_time.time())

    def format(self, ts: float, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
        import datetime
        return datetime.datetime.fromtimestamp(ts).strftime(fmt)

    def __repr__(self): return "<time module>"


# ─────────────────────────────────────────────────────────────────────────────
# Random module (NEW v4)
# ─────────────────────────────────────────────────────────────────────────────
class RandomModule:
    def int(self, a: int, b: int) -> int:
        return _random.randint(int(a), int(b))

    def float(self, a: float = 0.0, b: float = 1.0) -> float:
        return _random.uniform(float(a), float(b))

    def choice(self, lst: list) -> Any:
        return _random.choice(lst)

    def choices(self, lst: list, k: int = 1) -> list:
        return _random.choices(lst, k=int(k))

    def shuffle(self, lst: list) -> list:
        result = list(lst)
        _random.shuffle(result)
        return result

    def sample(self, lst: list, k: int) -> list:
        return _random.sample(lst, int(k))

    def seed(self, s: int):
        _random.seed(int(s))

    def gaussian(self, mu: float = 0.0, sigma: float = 1.0) -> float:
        return _random.gauss(float(mu), float(sigma))

    def __repr__(self): return "<random module>"


# ─────────────────────────────────────────────────────────────────────────────
# Agent module (NEW v4) — call LLMs from Nexa code
# ─────────────────────────────────────────────────────────────────────────────
class AgentModule:
    """Call LLM APIs from Nexa (Groq, OpenAI, Anthropic, Ollama)."""

    def _get_key(self, provider: str):
        env_var = "GROQ_API_KEY" if provider == "groq" else f"{provider.upper()}_API_KEY"
        k = os.environ.get(env_var, "")
        if not k and os.path.exists(".env"):
            for line in open(".env"):
                line = line.strip()
                if line.startswith(f"{env_var}="):
                    k = line.split("=", 1)[1].strip().strip('"\'')
                    break
        return k

    def ask(self, prompt: str, model: str = "llama-3.3-70b-versatile", provider: str = "groq", max_tokens: int = 512) -> str:
        if provider == "ollama":
            url = "http://localhost:11434/api/generate"
            payload = {"model": model, "prompt": prompt, "stream": False}
            headers = {"Content-Type": "application/json"}
        else:
            key = self._get_key(provider)
            if not key: return f"Error: {provider.upper()}_API_KEY not set."

            if provider == "groq": url = "https://api.groq.com/openai/v1/chat/completions"
            elif provider == "openai": url = "https://api.openai.com/v1/chat/completions"
            elif provider == "anthropic":
                url = "https://api.anthropic.com/v1/messages"
                payload = {"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens}
                headers = {"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"}
                return self._make_req(url, payload, headers, provider)

            payload = {"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens}
            headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}

        return self._make_req(url, payload, headers, provider)

    def ask_json(self, prompt: str, schema: dict, model: str = "llama-3.3-70b-versatile", provider: str = "groq", max_tokens: int = 512) -> dict:
        prompt_with_schema = f"{prompt}\n\nPlease strictly output JSON matching this schema:\n{_json.dumps(schema)}"
        if provider == "ollama":
            url = "http://localhost:11434/api/generate"
            payload = {"model": model, "prompt": prompt_with_schema, "stream": False, "format": "json"}
            headers = {"Content-Type": "application/json"}
        else:
            key = self._get_key(provider)
            if not key: return {"error": f"{provider.upper()}_API_KEY not set"}
            url = "https://api.groq.com/openai/v1/chat/completions" if provider == "groq" else "https://api.openai.com/v1/chat/completions"
            payload = {"model": model, "messages": [{"role": "user", "content": prompt_with_schema}], "response_format": {"type": "json_object"}}
            headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}

        res = self._make_req(url, payload, headers, provider)
        try: return _json.loads(res)
        except: return {"error": "Failed to parse JSON", "raw": res}

    def chat(self, history: list, model: str = "llama-3.3-70b-versatile", provider: str = "groq", max_tokens: int = 512) -> str:
        key = self._get_key(provider)
        if not key: return f"Error: {provider.upper()}_API_KEY not set."
        url = "https://api.groq.com/openai/v1/chat/completions" if provider == "groq" else "https://api.openai.com/v1/chat/completions"
        payload = {"model": model, "messages": history, "max_tokens": max_tokens}
        headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
        return self._make_req(url, payload, headers, provider)

    def stream(self, prompt: str, model: str = "llama-3.3-70b-versatile", provider: str = "groq", max_tokens: int = 512):
        if provider == "ollama":
            url = "http://localhost:11434/api/generate"
            payload = {"model": model, "prompt": prompt, "stream": True}
            headers = {"Content-Type": "application/json"}
        else:
            key = self._get_key(provider)
            if not key: yield f"Error: {provider.upper()}_API_KEY not set."; return
            url = "https://api.groq.com/openai/v1/chat/completions" if provider == "groq" else "https://api.openai.com/v1/chat/completions"
            payload = {"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "stream": True}
            headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}

        try:
            if "User-Agent" not in headers: headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) NexaAgent/1.0"
            body = _json.dumps(payload).encode("utf-8")
            req = urllib.request.Request(url, data=body, headers=headers, method="POST")
            with urllib.request.urlopen(req, timeout=30) as resp:
                for line in resp:
                    line = line.decode('utf-8').strip()
                    if not line: continue
                    if provider == "ollama":
                        obj = _json.loads(line)
                        if "response" in obj: yield obj["response"]
                    elif line.startswith("data: ") and line != "data: [DONE]":
                        obj = _json.loads(line[6:])
                        if "choices" in obj and obj["choices"][0]["delta"].get("content"):
                            yield obj["choices"][0]["delta"]["content"]
        except Exception as e:
            yield f"\nError: {e}"

    def _make_req(self, url, payload, headers, provider):
        try:
            if "User-Agent" not in headers: headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) NexaAgent/1.0"
            body = _json.dumps(payload).encode("utf-8")
            req = urllib.request.Request(url, data=body, headers=headers, method="POST")
            with urllib.request.urlopen(req, timeout=30) as resp:
                res = _json.loads(resp.read())
                if provider == "ollama": return res.get("response", "")
                elif provider == "anthropic": return res["content"][0]["text"]
                return res["choices"][0]["message"]["content"]
        except Exception as e:
            return f"Error: {e}"

    def __repr__(self): return "<agent module (LLM interface)>"
# ─────────────────────────────────────────────────────────────────────────────
# Global built-in functions
# ─────────────────────────────────────────────────────────────────────────────
def _range(*args):     return list(range(*[int(a) for a in args]))
def _print(*args):     print(*args); return None
def _input(prompt=""): return input(str(prompt))
def _len(x):           return len(x)
def _type(x):          return type(x).__name__
def _int(x):           return int(x)
def _float(x):         return float(x)
def _str(x):           return str(x)
def _bool(x):          return bool(x)
def _list(x):          return list(x)
def _zip(*its):        return [list(t) for t in zip(*its)]
def _sum(lst):         return sum(lst)
def _max(*args):       return max(args[0]) if len(args) == 1 else max(args)
def _min(*args):       return min(args[0]) if len(args) == 1 else min(args)
def _abs(x):           return abs(x)
def _round(x, n=0):    return round(x, int(n))
def _sorted(lst, reverse=False): return sorted(lst, reverse=bool(reverse))
def _reversed(lst):    return list(reversed(lst))
def _enumerate(lst):   return [[i, v] for i, v in enumerate(lst)]
def _keys(d):          return list(d.keys())
def _values(d):        return list(d.values())
def _items(d):         return [[k, v] for k, v in d.items()]
def _split(s, sep=" "): return str(s).split(str(sep))
def _join(sep, lst):   return str(sep).join(str(x) for x in lst)
def _upper(s):         return str(s).upper()
def _lower(s):         return str(s).lower()
def _strip(s):         return str(s).strip()
def _replace(s, old, new): return str(s).replace(str(old), str(new))
def _contains(container, item): return item in container
def _startswith(s, prefix): return str(s).startswith(str(prefix))
def _endswith(s, suffix):   return str(s).endswith(str(suffix))
def _format(template, *args): return str(template).format(*args)
def _isinstance(obj, typename): return type(obj).__name__ == str(typename)
def _append(lst, item): lst.append(item); return lst
def _pop(lst, idx=-1):  return lst.pop(int(idx))
def _map_fn(fn, lst):   return [fn(x) for x in lst]
def _filter_fn(fn, lst): return [x for x in lst if fn(x)]
def _reduce(fn, lst, init=None):
    acc = init if init is not None else lst[0]; start = 0 if init is not None else 1
    for x in lst[start:]: acc = fn(acc, x)
    return acc



# ─────────────────────────────────────────────────────────────────────────────
# DataFrame module (NEW v5) — tabular data manipulation
# ─────────────────────────────────────────────────────────────────────────────
class DataFrameModule:
    """In-memory tabular data module. Works on list-of-dicts."""

    def create(self, data: list) -> dict:
        """Create a DataFrame from a list of dicts or list of lists."""
        if not data: return {"rows": [], "cols": []}
        if isinstance(data[0], dict):
            cols = list(data[0].keys())
            return {"rows": data, "cols": cols}
        # List of lists: first row is headers
        cols = data[0]; rows = [dict(zip(cols, r)) for r in data[1:]]
        return {"rows": rows, "cols": cols}

    def head(self, df: dict, n: int = 5) -> dict:
        return {"rows": df["rows"][:int(n)], "cols": df["cols"]}

    def tail(self, df: dict, n: int = 5) -> dict:
        return {"rows": df["rows"][-int(n):], "cols": df["cols"]}

    def select(self, df: dict, cols) -> dict:
        if isinstance(cols, str): cols = [cols]
        rows = [{c: r.get(c) for c in cols} for r in df["rows"]]
        return {"rows": rows, "cols": cols}

    def filter(self, df: dict, fn) -> dict:
        rows = [r for r in df["rows"] if fn(r)]
        return {"rows": rows, "cols": df["cols"]}

    def sort(self, df: dict, col: str, ascending: bool = True) -> dict:
        rows = sorted(df["rows"], key=lambda r: r.get(col, 0), reverse=not ascending)
        return {"rows": rows, "cols": df["cols"]}

    def groupby(self, df: dict, col: str) -> dict:
        groups = {}
        for row in df["rows"]:
            key = row.get(col)
            if key not in groups: groups[key] = []
            groups[key].append(row)
        return {k: {"rows": v, "cols": df["cols"]} for k, v in groups.items()}

    def add_column(self, df: dict, col: str, fn) -> dict:
        rows = [{**r, col: fn(r)} for r in df["rows"]]
        return {"rows": rows, "cols": df["cols"] + [col]}

    def drop_column(self, df: dict, col: str) -> dict:
        cols = [c for c in df["cols"] if c != col]
        rows = [{k: v for k, v in r.items() if k != col} for r in df["rows"]]
        return {"rows": rows, "cols": cols}

    def describe(self, df: dict) -> dict:
        stats = {}
        for col in df["cols"]:
            vals = [r.get(col) for r in df["rows"] if isinstance(r.get(col), (int, float))]
            if vals:
                mean = sum(vals) / len(vals)
                stats[col] = {
                    "count": len(vals), "mean": round(mean, 4),
                    "min": min(vals), "max": max(vals),
                    "std": round((sum((x-mean)**2 for x in vals)/len(vals))**0.5, 4)
                }
        return stats

    def count(self, df: dict) -> int:
        return len(df["rows"])

    def col(self, df: dict, col: str) -> list:
        return [r.get(col) for r in df["rows"]]

    def to_csv(self, df: dict, path: str) -> None:
        import csv
        with open(path, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=df["cols"])
            writer.writeheader(); writer.writerows(df["rows"])
        print(f"  ✅ Saved CSV: {path}")

    def from_csv(self, path: str) -> dict:
        import csv
        with open(path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)
        return {"rows": rows, "cols": list(rows[0].keys()) if rows else []}

    def show(self, df: dict, n: int = 10) -> None:
        rows = df["rows"][:int(n)]
        cols = df["cols"]
        if not rows: print("  (empty dataframe)"); return
        widths = {c: max(len(str(c)), max(len(str(r.get(c,""))) for r in rows)) for c in cols}
        header = " | ".join(str(c).ljust(widths[c]) for c in cols)
        print("  ┌─ DataFrame ─────────────────────────────")
        print(f"  │ {header}")
        print("  │ " + "─" * len(header))
        for r in rows:
            print("  │ " + " | ".join(str(r.get(c,"")).ljust(widths[c]) for c in cols))
        print(f"  └─ {len(df['rows'])} rows × {len(cols)} cols")

    def __repr__(self): return "<df module>"


# ─────────────────────────────────────────────────────────────────────────────
# DB module (NEW v6) — SQLite Database
# ─────────────────────────────────────────────────────────────────────────────
import sqlite3 as _sqlite3

class DBModule:
    def connect(self, db_file: str):
        conn = _sqlite3.connect(db_file)
        conn.row_factory = _sqlite3.Row
        print(f"  ✅ Connected to SQLite DB: {db_file}")
        return conn

    def execute(self, conn, query: str, params: list = None):
        with conn:
            cur = conn.cursor()
            cur.execute(query, params or [])
            return {"rows_affected": cur.rowcount, "last_id": cur.lastrowid}

    def query(self, conn, query: str, params: list = None) -> list:
        cur = conn.cursor()
        cur.execute(query, params or [])
        return [dict(row) for row in cur.fetchall()]

    def close(self, conn):
        conn.close()

    def __repr__(self): return "<db module (sqlite3)>"


# Removed duplicate RegexModule (v6) in favor of the v11 version at EOF

# ─────────────────────────────────────────────────────────────────────────────
# Serve module (NEW v6) — built-in web server
# ─────────────────────────────────────────────────────────────────────────────
from wsgiref.simple_server import make_server
import threading

class ServeModule:
    def __init__(self):
        self._routes = {"GET": {}, "POST": {}}
        self._server = None
        self._thread = None

    def get(self, path: str, handler=None):
        if handler is None:
            def decorator(fn):
                self._routes["GET"][path] = fn
                return fn
            return decorator
        self._routes["GET"][path] = handler

    def post(self, path: str, handler=None):
        if handler is None:
            def decorator(fn):
                self._routes["POST"][path] = fn
                return fn
            return decorator
        self._routes["POST"][path] = handler

    def _app(self, environ, start_response):
        method = environ['REQUEST_METHOD']
        path = environ['PATH_INFO']
        handler = self._routes.get(method, {}).get(path)

        if not handler:
            start_response('404 Not Found', [('Content-Type', 'application/json')])
            return [b'{"error": "Not Found"}']

        req = {"method": method, "path": path, "query": {}, "body": None}
        if environ.get('QUERY_STRING'):
            req["query"] = dict(urllib.parse.parse_qsl(environ['QUERY_STRING']))
        
        try: length = int(environ.get('CONTENT_LENGTH', '0'))
        except ValueError: length = 0

        if length > 0:
            body_bytes = environ['wsgi.input'].read(length)
            try: req["body"] = _json.loads(body_bytes)
            except Exception: req["body"] = body_bytes.decode('utf-8')

        try:
            res = handler(req)
            if not isinstance(res, (dict, list, str, int, float, bool, type(None))):
                res = str(res)
            body = _json.dumps(res).encode('utf-8')
            start_response('200 OK', [('Content-Type', 'application/json'), ('Content-Length', str(len(body)))])
            return [body]
        except Exception as e:
            err = _json.dumps({"error": str(e)}).encode('utf-8')
            start_response('500 Internal Server Error', [('Content-Type', 'application/json')])
            return [err]

    def start(self, port: int = 8080, background: bool = False):
        try:
            self._server = make_server('', int(port), self._app)
            print(f"  🚀 Nexa HTTP API server running on http://localhost:{port}")
            if background:
                self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
                self._thread.start()
            else:
                self._server.serve_forever()
        except Exception as e:
            print(f"  ❌ Server failed to start: {e}")

    def __repr__(self): return "<serve module>"

# ─────────────────────────────────────────────────────────────────────────────
# Tensor module (NEW v7) — Numpy Wrapper
# ─────────────────────────────────────────────────────────────────────────────
class TensorModule:
    def __init__(self):
        try: import numpy as np; self.np = np
        except ImportError: self.np = None

    def _check(self):
        if not self.np: raise Exception("numpy not installed. Run: pip install numpy")

    def create(self, data):
        self._check()
        return self.np.array(data)

    def zeros(self, shape):
        self._check()
        return self.np.zeros(shape)

    def ones(self, shape):
        self._check()
        return self.np.ones(shape)
        
    def rand(self, shape):
        self._check()
        return self.np.random.rand(*shape)

    def dot(self, a, b):
        self._check()
        return self.np.dot(a, b)

    def transpose(self, a):
        self._check()
        return self.np.transpose(a)

    def shape(self, a):
        self._check()
        return list(a.shape)

    def __repr__(self): return "<tensor module (numpy)>"

# ─────────────────────────────────────────────────────────────────────────────
# Env module (NEW v7) — Environment Variables
# ─────────────────────────────────────────────────────────────────────────────
import os as _os

class EnvModule:
    def get(self, key: str, default: Any = None):
        return _os.getenv(key, default)

    def set(self, key: str, value: str):
        _os.environ[key] = str(value)

    def load_dotenv(self, path: str = ".env"):
        try:
            with open(path) as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith("#"): continue
                    if "=" in line:
                        k, v = line.split("=", 1)
                        _os.environ[k.strip()] = v.strip()
            return True
        except FileNotFoundError:
            return False

    def __repr__(self): return "<env module>"

# ─────────────────────────────────────────────────────────────────────────────
# Datetime module (NEW v8)
# ─────────────────────────────────────────────────────────────────────────────
import datetime as _datetime

class DatetimeModule:
    def now(self):
        return _datetime.datetime.now().isoformat()

    def format(self, dt_str: str, fmt: str) -> str:
        dt = _datetime.datetime.fromisoformat(dt_str)
        return dt.strftime(fmt)

    def parse(self, dt_str: str, fmt: str) -> str:
        dt = _datetime.datetime.strptime(dt_str, fmt)
        return dt.isoformat()

    def __repr__(self): return "<datetime module>"


# ─────────────────────────────────────────────────────────────────────────────
# Crypto module (NEW v8)
# ─────────────────────────────────────────────────────────────────────────────
import hashlib as _hashlib
import uuid as _uuid

class CryptoModule:
    def sha256(self, text: str) -> str:
        return _hashlib.sha256(str(text).encode('utf-8')).hexdigest()

    def uuid4(self) -> str:
        return str(_uuid.uuid4())

    def __repr__(self): return "<crypto module>"


# ─────────────────────────────────────────────────────────────────────────────
# Sys module (NEW v12)
# ─────────────────────────────────────────────────────────────────────────────
import subprocess as _subprocess
import sys as _sys

class SysModule:
    """v0.2: Capability-gated system module.
    Shell execution (run/execute) requires the 'exec' capability.
    Without it, a clear CapabilityError is raised pointing to --cap=exec.
    """

    def __init__(self, capabilities: set = None):
        self._caps = set(capabilities or [])

    def _require_exec(self):
        if 'exec' not in self._caps:
            raise NexaBuiltinError(
                "CapabilityError: sys.run() / sys.execute() require the 'exec' capability.\n"
                "  Restart with: nexa run --cap=exec <script.nexa>\n"
                "  Or in Python: Interpreter(capabilities=['exec'])"
            )

    def run(self, cmd: str) -> dict:
        """Run a shell command. Requires --cap=exec."""
        self._require_exec()
        try:
            result = _subprocess.run(str(cmd), shell=True, capture_output=True, text=True)
            return {"stdout": result.stdout, "stderr": result.stderr, "code": result.returncode}
        except Exception as e:
            return {"stdout": "", "stderr": str(e), "code": 1}

    def execute(self, cmd: str) -> dict:
        """Alias for sys.run(). Requires --cap=exec."""
        return self.run(cmd)

    def env(self) -> dict:
        return dict(_os.environ)

    def exit(self, code: int = 0):
        _sys.exit(int(code))

    def argv(self) -> list:
        return list(_sys.argv)

    def platform(self) -> str:
        return _sys.platform

    def python_version(self) -> str:
        return _sys.version

    def __repr__(self): return f"<sys module (caps={self._caps or 'none'})>"

# ─────────────────────────────────────────────────────────────────────────────
# Base64 module (NEW v8)
# ─────────────────────────────────────────────────────────────────────────────
import base64 as _base64

class Base64Module:
    def encode(self, text: str) -> str:
        return _base64.b64encode(str(text).encode('utf-8')).decode('utf-8')

    def decode(self, b64: str) -> str:
        return _base64.b64decode(b64.encode('utf-8')).decode('utf-8')

    def __repr__(self): return "<base64 module>"

# ─────────────────────────────────────────────────────────────────────────────
# Regex module (NEW v11)
# ─────────────────────────────────────────────────────────────────────────────
import re as _re

class RegexModule:
    def match(self, pattern: str, string: str) -> Any:
        m = _re.match(str(pattern), str(string))
        return m.group() if m else None

    def search(self, pattern: str, string: str) -> Any:
        m = _re.search(str(pattern), str(string))
        return {"match": m.group(), "start": m.start(), "end": m.end()} if m else None

    def findall(self, pattern: str, string: str) -> list:
        return _re.findall(str(pattern), str(string))

    def sub(self, pattern: str, repl: str, string: str) -> str:
        return _re.sub(str(pattern), str(repl), str(string))

    def __repr__(self): return "<regex module>"




# ─────────────────────────────────────────────────────────────────────────────
# Task module (NEW v13)
# ─────────────────────────────────────────────────────────────────────────────
import asyncio as _asyncio

class TaskModule:
    async def gather(self, tasks: list) -> list:
        return await _asyncio.gather(*tasks)

    async def sleep(self, seconds: float):
        await _asyncio.sleep(float(seconds))

    def __repr__(self): return "<task module>"


BUILTINS: Dict[str, Any] = {
    # Modules
    "automl": AutoMLModule(), "nlp": NLPModule(),
    "vision": VisionModule(), "math": MathModule(),
    "stats": StatsModule(), "fs": FSModule(), "json": JSONModule(),
    "http": HTTPModule(), "time": TimeModule(),
    "random": RandomModule(), "agent": AgentModule(),
    "string": StringModule(), "array": ArrayModule(),
    "df": DataFrameModule(),
    "db": DBModule(), "re": RegexModule(), "serve": ServeModule(),
    "tensor": TensorModule(), "env": EnvModule(), "sys": SysModule(),
    "datetime": DatetimeModule(), "crypto": CryptoModule(), "base64": Base64Module(),
    "task": TaskModule(), "concurrency": ConcurrencyModule(),
    # Core functions
    "print": _print, "input": _input,
    "range": _range, "len": _len, "type": _type,
    "int": _int, "float": _float, "str": _str, "bool": _bool, "list": _list, "set": set,
    "zip": _zip, "sum": _sum, "max": _max, "min": _min,
    "abs": _abs, "round": _round,
    "sorted": _sorted, "reversed": _reversed, "enumerate": _enumerate,
    "keys": _keys, "values": _values, "items": _items,
    "split": _split, "join": _join,
    "upper": _upper, "lower": _lower, "strip": _strip,
    "replace": _replace, "contains": _contains,
    "startswith": _startswith, "endswith": _endswith,
    "format": _format, "isinstance": _isinstance,
    "append": _append, "pop": _pop,
    "map": _map_fn, "filter": _filter_fn, "reduce": _reduce,
    "staticmethod": _staticmethod,
    # Constants
    "true": True, "false": False, "null": None,
    "PI": math.pi, "E": math.e, "INF": float("inf"),
}


# ─────────────────────────────────────────────────────────────────────────────
# Phase 2 — First-class Result<T, E> and Option<T>   (Nexa v25)
# ─────────────────────────────────────────────────────────────────────────────

class ResultModule:
    """
    Built-in Result<T, E> type — Rust-inspired error handling.

    Usage from Nexa:
        import "result"
        let r = result.ok(42)
        if result.is_ok(r) { print(result.unwrap(r)) }
        let e = result.err("Something failed")
    """

    def ok(self, value) -> dict:
        return {"__result__": True, "__ok__": True, "__value__": value}

    def err(self, error) -> dict:
        return {"__result__": True, "__ok__": False, "__error__": error}

    def is_ok(self, r: dict) -> bool:
        if not isinstance(r, dict) or not r.get("__result__"):
            raise NexaBuiltinError("result.is_ok() expects a Result value")
        return bool(r.get("__ok__"))

    def is_err(self, r: dict) -> bool:
        return not self.is_ok(r)

    def unwrap(self, r: dict):
        if not self.is_ok(r):
            raise NexaBuiltinError(f"Result.unwrap() called on Err: {r.get('__error__')}")
        return r["__value__"]

    def unwrap_or(self, r: dict, default):
        return r["__value__"] if self.is_ok(r) else default

    def unwrap_err(self, r: dict):
        if self.is_ok(r):
            raise NexaBuiltinError("Result.unwrap_err() called on Ok")
        return r["__error__"]

    def map(self, r: dict, fn) -> dict:
        """Apply fn to Ok value, pass through Err unchanged."""
        if self.is_ok(r):
            return self.ok(fn(r["__value__"]))
        return r

    def and_then(self, r: dict, fn) -> dict:
        """Flat-map: fn must return a Result."""
        if self.is_ok(r):
            return fn(r["__value__"])
        return r

    def __repr__(self): return "<result module>"


class OptionModule:
    """
    Built-in Option<T> type — null-safe optional values.

    Usage from Nexa:
        import "option"
        let o = option.some(5)
        let none = option.none()
        if option.is_some(o) { print(option.unwrap(o)) }
    """

    def some(self, value) -> dict:
        return {"__option__": True, "__some__": True, "__value__": value}

    def none(self) -> dict:
        return {"__option__": True, "__some__": False}

    def is_some(self, o: dict) -> bool:
        if not isinstance(o, dict) or not o.get("__option__"):
            raise NexaBuiltinError("option.is_some() expects an Option value")
        return bool(o.get("__some__"))

    def is_none(self, o: dict) -> bool:
        return not self.is_some(o)

    def unwrap(self, o: dict):
        if not self.is_some(o):
            raise NexaBuiltinError("Option.unwrap() called on None")
        return o["__value__"]

    def unwrap_or(self, o: dict, default):
        return o["__value__"] if self.is_some(o) else default

    def map(self, o: dict, fn) -> dict:
        """Apply fn to Some value, pass through None unchanged."""
        if self.is_some(o):
            return self.some(fn(o["__value__"]))
        return o

    def filter(self, o: dict, predicate) -> dict:
        """Return None if predicate(value) is False."""
        if self.is_some(o) and not predicate(o["__value__"]):
            return self.none()
        return o

    def or_else(self, o: dict, fallback) -> dict:
        """Return o if Some, otherwise evaluate fallback() -> Option."""
        return o if self.is_some(o) else fallback()

    def __repr__(self): return "<option module>"


# Register new modules in BUILTINS
BUILTINS["result"] = ResultModule()
BUILTINS["option"] = OptionModule()

# ── v27 Builtins ──────────────────────────────────────────────────────────────
from nexa_lang.matchers import expect as _expect, NexaExpectError as _NexaExpectError
BUILTINS["expect"]          = _expect
BUILTINS["NexaExpectError"] = _NexaExpectError

# Concurrency
from nexa_lang.concurrency import Channel as NexaChannel, ConcurrencyModule as SpawnContext

_concurrency_module = SpawnContext()

def _spawn_fn(task_dict):
    """spawn({"a": fn() => ..., "b": fn() => ...}) — run tasks concurrently."""
    import threading
    results = {}
    errors  = {}
    threads = []
    for name, fn in task_dict.items():
        def _worker(n=name, f=fn):
            try:     results[n] = f() if callable(f) else f
            except Exception as e: errors[n] = e
        t = threading.Thread(target=_worker, daemon=True)
        threads.append(t); t.start()
    for t in threads: t.join(timeout=30)
    return results

def _channel(capacity=0):
    """Channel(capacity=10) — create a thread-safe FIFO channel."""
    return NexaChannel()

BUILTINS["Channel"]      = _channel
BUILTINS["spawn"]        = _spawn_fn
BUILTINS["SpawnContext"] = SpawnContext
