"""cssl_iterate.py - CSSL iterate() builtin and IterChain fluent API

v4.9.17: First-class iterate() builtin for clean collection construction.

Usage in CSSL (no import needed — built into the language):

    // Range
    vector<int> numbers = iterate(0, 100);            // [0, 1, ..., 99]
    vector<int> evens   = iterate(0, 100, 2);         // [0, 2, 4, ..., 98]
    vector<int> numbers = iterate(min(0), max(100));  // same as iterate(0, 100)

    // Collection wrap
    string text = iterate(allnames["max"]["intro"]);  // traverse any value
    vector<string> chars = iterate("hello");          // ["h","e","l","l","o"]

    // Named-param modes
    vector<string> ch   = iterate(chars="hello");     // char list
    vector<string> ks   = iterate(keys=myDict);       // dict keys
    vector<dynamic> vs  = iterate(values=myDict);     // dict values
    vector<string> rep  = iterate(repeat=3, value="hi"); // ["hi","hi","hi"]

    // Linspace (evenly spaced floats)
    vector<float> grid  = iterate(from_=0.0, to=1.0, count=5);
    // → [0.0, 0.25, 0.5, 0.75, 1.0]

    // Fluent chain
    vector<int> result  = iterate(1, 50).filter("x % 2 == 0").map("x * x").limit(5);
    string upper        = iterate("hello").map("x.upper()").join();
    vector<int> sorted  = iterate(myList).sort(reverse=true).unique();
    vector<string> keys = iterate(myDict).skip(2).limit(5);
"""
from __future__ import annotations

import math as _math
from typing import Any, Dict, Iterable, Iterator, List, Optional, Union

# ── Save Python builtins before module scope shadow risk ──────────────────────
_py_sum = sum
_py_min = min
_py_max = max
_py_zip = zip
_py_sorted = sorted
_py_reversed = reversed
_py_enumerate = enumerate
_py_range = range


# ─────────────────────────────────────────────────────────────────────────────
# IterChain
# ─────────────────────────────────────────────────────────────────────────────

class IterChain:
    """Fluent iterator / collection builder returned by iterate().

    Behaves like a list for CSSL typed assignment and foreach iteration.
    Provides chainable transformation methods.

    All transformation methods return a new IterChain — the original is
    never modified (immutable pipeline pattern).
    """

    def __init__(self, data: Iterable[Any]) -> None:
        self._data: List[Any] = list(data)

    # ── Core list protocol ────────────────────────────────────────────────────

    def __iter__(self) -> Iterator[Any]:
        return iter(self._data)

    def __len__(self) -> int:
        return len(self._data)

    def __getitem__(self, idx: Any) -> Any:
        return self._data[idx]

    def __contains__(self, item: Any) -> bool:
        return item in self._data

    def __bool__(self) -> bool:
        return bool(self._data)

    def __repr__(self) -> str:
        return f'IterChain({self._data!r})'

    def __str__(self) -> str:
        # Smart: char list → joined string so `string x = iterate("hi")` = "hi"
        if self._data and all(isinstance(x, str) and len(x) == 1 for x in self._data):
            return ''.join(self._data)
        return str(self._data)

    def __eq__(self, other: Any) -> bool:
        if isinstance(other, IterChain):
            return self._data == other._data
        if isinstance(other, list):
            return self._data == other
        return NotImplemented

    # ── Expression evaluators ─────────────────────────────────────────────────

    _EVAL_GLOBALS: Dict[str, Any] = {
        '__builtins__': {},
        'abs': abs, 'len': len, 'str': str, 'int': int, 'float': float,
        'round': round, 'min': _py_min, 'max': _py_max, 'sum': _py_sum,
        'bool': bool, 'list': list, 'range': _py_range, 'math': _math,
    }

    @classmethod
    def _eval(cls, fn: Any, x: Any) -> Any:
        """Evaluate fn on element x. fn may be a callable or expression string."""
        if callable(fn):
            return fn(x)
        if isinstance(fn, str):
            try:
                return eval(fn, cls._EVAL_GLOBALS, {'x': x})  # noqa: S307
            except Exception:
                return x
        return x

    @classmethod
    def _pred(cls, fn: Any, x: Any) -> bool:
        """Evaluate predicate fn on x, returning bool."""
        if callable(fn):
            return bool(fn(x))
        if isinstance(fn, str):
            try:
                return bool(eval(fn, cls._EVAL_GLOBALS, {'x': x}))  # noqa: S307
            except Exception:
                return True
        return bool(fn)

    # ── Transformations ───────────────────────────────────────────────────────

    def filter(self, fn: Any) -> 'IterChain':
        """Keep only elements where fn(x) is truthy.

        Args:
            fn: Callable or expression string with 'x' as the element.

        Example:
            iterate(0, 20).filter("x % 2 == 0")       // even numbers
            iterate(names).filter("len(x) > 3")        // names longer than 3
        """
        return IterChain(x for x in self._data if self._pred(fn, x))

    def map(self, fn: Any) -> 'IterChain':
        """Transform each element via fn(x).

        Args:
            fn: Callable or expression string with 'x' as the element.

        Example:
            iterate(0, 5).map("x * x")                // [0, 1, 4, 9, 16]
            iterate("hello").map("x.upper()").join()  // "HELLO"
        """
        return IterChain(self._eval(fn, x) for x in self._data)

    def sort(self, reverse: bool = False, key: Any = None) -> 'IterChain':
        """Sort elements.

        Args:
            reverse: Sort descending if true.
            key:     Optional key function or expression string.

        Example:
            iterate([3,1,2]).sort()                    // [1, 2, 3]
            iterate(names).sort(reverse=true)          // Z → A
            iterate(names).sort(key="len(x)")         // by length
        """
        key_fn = (lambda x: self._eval(key, x)) if key is not None else None
        return IterChain(_py_sorted(self._data, key=key_fn, reverse=bool(reverse)))

    def unique(self) -> 'IterChain':
        """Remove duplicate elements, preserving insertion order.

        Example:
            iterate([1, 2, 2, 3, 1]).unique()         // [1, 2, 3]
        """
        seen: List[Any] = []
        result: List[Any] = []
        for x in self._data:
            key = x if isinstance(x, (int, float, bool, str, type(None))) else id(x)
            if key not in seen:
                seen.append(key)
                result.append(x)
        return IterChain(result)

    def reverse(self) -> 'IterChain':
        """Reverse the order of elements.

        Example:
            iterate(0, 5).reverse()                   // [4, 3, 2, 1, 0]
        """
        return IterChain(_py_reversed(self._data))

    def limit(self, n: int) -> 'IterChain':
        """Keep only the first n elements.

        Args:
            n: Maximum number of elements to keep.

        Example:
            iterate(0, 1000).filter("x % 7 == 0").limit(5)  // [0,7,14,21,28]
        """
        return IterChain(self._data[:int(n)])

    def skip(self, n: int) -> 'IterChain':
        """Skip the first n elements.

        Args:
            n: Number of elements to skip.

        Example:
            iterate(0, 10).skip(3)                    // [3, 4, 5, 6, 7, 8, 9]
        """
        return IterChain(self._data[int(n):])

    def flatten(self) -> 'IterChain':
        """Flatten one level of nesting.

        Example:
            iterate([[1,2],[3,4],[5]]).flatten()      // [1, 2, 3, 4, 5]
        """
        result: List[Any] = []
        for x in self._data:
            if isinstance(x, (list, IterChain)):
                result.extend(x)
            else:
                result.append(x)
        return IterChain(result)

    def zip_with(self, other: Any) -> 'IterChain':
        """Zip with another collection into [a, b] pairs.

        Args:
            other: Any iterable collection.

        Example:
            iterate(0, 3).zip_with(["a","b","c"])    // [[0,"a"],[1,"b"],[2,"c"]]
        """
        other_list = list(other) if not isinstance(other, list) else other
        return IterChain([list(pair) for pair in _py_zip(self._data, other_list)])

    def with_index(self, start: int = 0) -> 'IterChain':
        """Add index to each element as [index, value] pairs.

        Args:
            start: Starting index (default 0).

        Example:
            iterate(["a","b","c"]).with_index()      // [[0,"a"],[1,"b"],[2,"c"]]
            iterate(["a","b","c"]).with_index(1)     // [[1,"a"],[2,"b"],[3,"c"]]
        """
        return IterChain([[i + int(start), x] for i, x in _py_enumerate(self._data)])

    def chunk(self, size: int) -> 'IterChain':
        """Split into sub-lists of size n.

        Args:
            size: Chunk size.

        Example:
            iterate(0, 9).chunk(3)                   // [[0,1,2],[3,4,5],[6,7,8]]
        """
        size = int(size)
        if size <= 0:
            return IterChain([])
        return IterChain(
            [self._data[i:i + size] for i in _py_range(0, len(self._data), size)]
        )

    def group_by(self, fn: Any) -> Dict[str, List[Any]]:
        """Group elements by key into a dict.

        Args:
            fn: Callable or expression string returning a group key.

        Example:
            iterate(0, 10).group_by("x % 2 == 0 and 'even' or 'odd'")
            // {"True": [0,2,4,6,8], "False": [1,3,5,7,9]}
        """
        result: Dict[str, List[Any]] = {}
        for x in self._data:
            key = str(self._eval(fn, x))
            if key not in result:
                result[key] = []
            result[key].append(x)
        return result

    def window(self, size: int, step: int = 1) -> 'IterChain':
        """Sliding window over elements.

        Args:
            size: Window size.
            step: Step between windows (default 1).

        Example:
            iterate(0, 5).window(3)   // [[0,1,2],[1,2,3],[2,3,4]]
        """
        size = int(size)
        step = int(step)
        if size <= 0 or step <= 0:
            return IterChain([])
        return IterChain(
            [self._data[i:i + size]
             for i in _py_range(0, len(self._data) - size + 1, step)]
        )

    def interleave(self, other: Any) -> 'IterChain':
        """Interleave elements with another collection.

        Example:
            iterate([1,2,3]).interleave([10,20,30])  // [1,10,2,20,3,30]
        """
        other_list = list(other)
        result: List[Any] = []
        for pair in _py_zip(self._data, other_list):
            result.extend(pair)
        # Append leftovers
        n = min(len(self._data), len(other_list))
        result.extend(self._data[n:])
        result.extend(other_list[n:])
        return IterChain(result)

    # ── Aggregation ───────────────────────────────────────────────────────────

    def sum(self) -> float:
        """Sum of all numeric elements."""
        try:
            return float(_py_sum(self._data))
        except (TypeError, ValueError):
            return 0.0

    def avg(self) -> float:
        """Average of all numeric elements."""
        if not self._data:
            return 0.0
        try:
            return float(_py_sum(self._data)) / len(self._data)
        except (TypeError, ValueError):
            return 0.0

    def min(self) -> Any:
        """Minimum element."""
        return _py_min(self._data) if self._data else None

    def max(self) -> Any:
        """Maximum element."""
        return _py_max(self._data) if self._data else None

    def count(self) -> int:
        """Number of elements."""
        return len(self._data)

    def first(self, default: Any = None) -> Any:
        """First element, or default if empty."""
        return self._data[0] if self._data else default

    def last(self, default: Any = None) -> Any:
        """Last element, or default if empty."""
        return self._data[-1] if self._data else default

    def contains(self, value: Any) -> bool:
        """True if value exists in the collection."""
        return value in self._data

    def find(self, fn: Any) -> Any:
        """Return first element where fn(x) is truthy, or null.

        Args:
            fn: Callable or expression string.

        Example:
            iterate(names).find("x.startswith('A')")
        """
        for x in self._data:
            if self._pred(fn, x):
                return x
        return None

    def index_of(self, value: Any) -> int:
        """Return index of first occurrence of value, or -1 if not found."""
        try:
            return self._data.index(value)
        except ValueError:
            return -1

    def count_where(self, fn: Any) -> int:
        """Count elements where fn(x) is truthy.

        Example:
            iterate(0, 10).count_where("x % 2 == 0")  // 5
        """
        return _py_sum(1 for x in self._data if self._pred(fn, x))

    def all(self, fn: Any) -> bool:
        """True if fn(x) is truthy for all elements.

        Example:
            iterate(0, 10).all("x >= 0")   // true
        """
        return all(self._pred(fn, x) for x in self._data)

    def any(self, fn: Any) -> bool:
        """True if fn(x) is truthy for at least one element.

        Example:
            iterate(numbers).any("x > 50")
        """
        return any(self._pred(fn, x) for x in self._data)

    # ── Output ────────────────────────────────────────────────────────────────

    def join(self, sep: str = '') -> str:
        """Join all elements into a single string.

        Args:
            sep: Separator (default '').

        Example:
            iterate("hello").map("x.upper()").join()   // "HELLO"
            iterate(0, 5).map("str(x)").join(", ")     // "0, 1, 2, 3, 4"
        """
        return str(sep).join(str(x) for x in self._data)

    def to_list(self) -> list:
        """Materialize as a plain Python list."""
        return list(self._data)

    def to_dict(self) -> dict:
        """Convert [key, value] pairs to a dict.

        Example:
            iterate(0, 3).with_index().to_dict()  // {0:0, 1:1, 2:2}
        """
        result = {}
        for item in self._data:
            if isinstance(item, (list, IterChain)) and len(item) >= 2:
                result[item[0]] = item[1]
        return result


# ─────────────────────────────────────────────────────────────────────────────
# Factory function
# ─────────────────────────────────────────────────────────────────────────────

def build_iterate(*args: Any, **kwargs: Any) -> IterChain:
    """Build an IterChain from arguments.

    Modes:
        iterate(collection)                      → wrap any iterable value
        iterate(start, end)                      → integer range [start, end)
        iterate(start, end, step)                → integer range with step
        iterate(min(0), max(100))                → same as iterate(0, 100)
        iterate(chars="hello")                   → ['h','e','l','l','o']
        iterate(keys=myDict)                     → list of dict keys
        iterate(values=myDict)                   → list of dict values
        iterate(repeat=3, value="hi")            → ["hi","hi","hi"]
        iterate(from_=0.0, to=1.0, count=5)      → [0.0, 0.25, 0.5, 0.75, 1.0]
    """
    # ── Named-param modes ────────────────────────────────────────────────────

    if 'chars' in kwargs:
        return IterChain(list(str(kwargs['chars'])))

    if 'keys' in kwargs:
        d = kwargs['keys']
        return IterChain(list(d.keys()) if isinstance(d, dict) else [])

    if 'values' in kwargs:
        d = kwargs['values']
        return IterChain(list(d.values()) if isinstance(d, dict) else [])

    if 'repeat' in kwargs:
        n = int(kwargs['repeat'])
        val = kwargs.get('value', None)
        return IterChain([val] * n)

    if 'from_' in kwargs or 'to' in kwargs or kwargs.get('linspace'):
        a = float(kwargs.get('from_', 0.0))
        b = float(kwargs.get('to', 1.0))
        n = int(kwargs.get('count', 10))
        if n <= 0:
            return IterChain([])
        if n == 1:
            return IterChain([a])
        step = (b - a) / (n - 1)
        return IterChain([round(a + i * step, 10) for i in _py_range(n)])

    # ── Positional modes ─────────────────────────────────────────────────────

    if len(args) == 0:
        return IterChain([])

    if len(args) == 1:
        val = args[0]
        if isinstance(val, IterChain):
            return IterChain(val._data)
        if isinstance(val, str):
            return IterChain(list(val))
        if isinstance(val, (list, tuple)):
            return IterChain(list(val))
        if isinstance(val, dict):
            return IterChain(list(val.values()))
        # Scalar: single-element list
        return IterChain([val])

    # 2-3 positional args → integer range
    try:
        start = int(args[0])
        end = int(args[1])
        step_val = int(args[2]) if len(args) >= 3 else int(kwargs.get('step', 1))
        if step_val == 0:
            step_val = 1
        return IterChain(list(_py_range(start, end, step_val)))
    except (TypeError, ValueError):
        # Non-integer args: treat as zip
        return IterChain([list(row) for row in _py_zip(*args)])
