"""Metaclass and base class for LLM-powered dynamic code generation."""

from __future__ import annotations

import inspect
import re
import sys
import threading
from typing import Any, Optional, TYPE_CHECKING

from scry_run.console import (
    status, info, success, warning, error,
    generating, generated, using_cached, err_console
)
from scry_run.packages import install_packages, get_installed_packages

# Thread-local guard to prevent infinite recursion during repr() in context building
# When this is active, LLMMethodProxy.__repr__ returns a safe string instead of resolving
_context_build_guard = threading.local()


if TYPE_CHECKING:
    from scry_run.cache import ScryCache
    from scry_run.generator import CodeGenerator


# =============================================================================
# Dunder (Magic Method) Configuration
# =============================================================================

# All known Python dunder methods from the data model
ALL_KNOWN_DUNDERS = {
    # Basic customization
    '__new__', '__init__', '__del__',
    '__repr__', '__str__', '__bytes__', '__format__',

    # Rich comparison
    '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__',
    '__hash__', '__bool__',

    # Attribute access
    '__getattr__', '__getattribute__', '__setattr__', '__delattr__', '__dir__',

    # Descriptors
    '__get__', '__set__', '__delete__', '__set_name__',

    # Class creation
    '__init_subclass__', '__prepare__', '__class_getitem__',

    # Instance and subclass checks
    '__instancecheck__', '__subclasscheck__',

    # Callable objects
    '__call__',

    # Container emulation
    '__len__', '__length_hint__', '__getitem__', '__setitem__', '__delitem__',
    '__missing__', '__iter__', '__next__', '__reversed__', '__contains__',

    # Numeric types - binary operations
    '__add__', '__sub__', '__mul__', '__matmul__', '__truediv__', '__floordiv__',
    '__mod__', '__divmod__', '__pow__', '__lshift__', '__rshift__',
    '__and__', '__xor__', '__or__',

    # Numeric types - reflected (swapped) operations
    '__radd__', '__rsub__', '__rmul__', '__rmatmul__', '__rtruediv__',
    '__rfloordiv__', '__rmod__', '__rdivmod__', '__rpow__',
    '__rlshift__', '__rrshift__', '__rand__', '__rxor__', '__ror__',

    # Numeric types - in-place operations
    '__iadd__', '__isub__', '__imul__', '__imatmul__', '__itruediv__',
    '__ifloordiv__', '__imod__', '__ipow__', '__ilshift__', '__irshift__',
    '__iand__', '__ixor__', '__ior__',

    # Unary operations
    '__neg__', '__pos__', '__abs__', '__invert__',

    # Type conversion
    '__complex__', '__int__', '__float__', '__index__',
    '__round__', '__trunc__', '__floor__', '__ceil__',

    # Context managers
    '__enter__', '__exit__',

    # Async context managers and coroutines
    '__aenter__', '__aexit__', '__await__', '__aiter__', '__anext__',

    # Buffer protocol
    '__buffer__', '__release_buffer__',

    # Pickling
    '__getnewargs_ex__', '__getnewargs__', '__getstate__', '__setstate__',
    '__reduce__', '__reduce_ex__',

    # Copy
    '__copy__', '__deepcopy__',

    # Introspection
    '__sizeof__', '__fspath__',

    # Match statement (3.10+)
    '__match_args__',
}

# Dunders that should NEVER be auto-generated (would break Python or scry-run)
FORBIDDEN_DUNDERS = {
    '__getattribute__',  # Would break all attribute lookup
    '__setattr__',       # Would break attribute assignment
    '__delattr__',       # Would break attribute deletion
    '__getattr__',       # Already used by ScryClass for lazy generation
    '__class__',         # Would break type system
    '__dict__',          # Would break attribute storage
    '__init_subclass__', # Would break class creation hooks
    '__prepare__',       # Would break metaclass protocol
    '__mro__',           # Would break method resolution order
    '__new__',           # Handled specially, dangerous to auto-generate
    '__init__',          # Handled specially by metaclass __call__
    '__del__',           # Destructor - dangerous to auto-generate
    '__subclasshook__',  # ABC hook - dangerous
    '__instancecheck__', # isinstance() - dangerous
    '__subclasscheck__', # issubclass() - dangerous
    # Note: __repr__ and __str__ are now generatable with recursion guard
}

# Dunders that are safe to lazily generate via trampolines
GENERATABLE_DUNDERS = ALL_KNOWN_DUNDERS - FORBIDDEN_DUNDERS

# Thread-local guard to prevent recursion when generating __repr__/__str__
_generation_guard = threading.local()

# Per-attribute generation locks to prevent concurrent generation of the same attribute
# Key: (class_name, attr_name), Value: threading.Lock
_generation_locks: dict[tuple[str, str], threading.Lock] = {}
_generation_locks_lock = threading.Lock()  # Lock for accessing the locks dict


def _get_generation_lock(class_name: str, attr_name: str) -> threading.Lock:
    """Get or create a lock for generating a specific class attribute."""
    key = (class_name, attr_name)
    with _generation_locks_lock:
        if key not in _generation_locks:
            _generation_locks[key] = threading.Lock()
        return _generation_locks[key]

# Metadata for known dunders to provide better generation context
DUNDER_INFO = {
    # String representation
    '__str__': {
        'signature': 'def __str__(self) -> str',
        'returns': 'str',
        'purpose': 'Human-readable string representation',
        'description': 'Called by str(obj) and print(obj). Return a friendly, readable string.',
    },
    '__repr__': {
        'signature': 'def __repr__(self) -> str',
        'returns': 'str',
        'purpose': 'Unambiguous string representation',
        'description': 'Called by repr(obj) and in REPL. Ideally returns valid Python to recreate the object.',
    },
    '__bytes__': {
        'signature': 'def __bytes__(self) -> bytes',
        'returns': 'bytes',
        'purpose': 'Byte representation',
        'description': 'Called by bytes(obj). Return a bytes representation.',
    },
    '__format__': {
        'signature': 'def __format__(self, format_spec: str) -> str',
        'returns': 'str',
        'purpose': 'Custom formatting',
        'description': 'Called by format(obj, spec) and f-strings. Return formatted string.',
    },

    # Comparison
    '__lt__': {
        'signature': 'def __lt__(self, other) -> bool',
        'returns': 'bool',
        'purpose': 'Less than comparison',
        'description': 'Called by obj < other. Return True if self is less than other.',
    },
    '__le__': {
        'signature': 'def __le__(self, other) -> bool',
        'returns': 'bool',
        'purpose': 'Less than or equal comparison',
        'description': 'Called by obj <= other. Return True if self is less than or equal to other.',
    },
    '__eq__': {
        'signature': 'def __eq__(self, other) -> bool',
        'returns': 'bool',
        'purpose': 'Equality comparison',
        'description': 'Called by obj == other. Return True if equal. Should be consistent with __hash__.',
    },
    '__ne__': {
        'signature': 'def __ne__(self, other) -> bool',
        'returns': 'bool',
        'purpose': 'Inequality comparison',
        'description': 'Called by obj != other. Return True if not equal.',
    },
    '__gt__': {
        'signature': 'def __gt__(self, other) -> bool',
        'returns': 'bool',
        'purpose': 'Greater than comparison',
        'description': 'Called by obj > other. Return True if self is greater than other.',
    },
    '__ge__': {
        'signature': 'def __ge__(self, other) -> bool',
        'returns': 'bool',
        'purpose': 'Greater than or equal comparison',
        'description': 'Called by obj >= other. Return True if self is greater than or equal to other.',
    },
    '__hash__': {
        'signature': 'def __hash__(self) -> int',
        'returns': 'int',
        'purpose': 'Hash value for dict keys and sets',
        'description': 'Called by hash(obj). Return integer hash. Must be consistent with __eq__.',
    },
    '__bool__': {
        'signature': 'def __bool__(self) -> bool',
        'returns': 'bool',
        'purpose': 'Truth value testing',
        'description': 'Called by bool(obj) and in if statements. Return True or False.',
    },

    # Container methods
    '__len__': {
        'signature': 'def __len__(self) -> int',
        'returns': 'int',
        'purpose': 'Return the length/count of items',
        'description': 'Called by len(obj). Return non-negative integer count.',
    },
    '__length_hint__': {
        'signature': 'def __length_hint__(self) -> int',
        'returns': 'int',
        'purpose': 'Estimated length hint',
        'description': 'Called by operator.length_hint(). Return estimated length.',
    },
    '__getitem__': {
        'signature': 'def __getitem__(self, key)',
        'returns': 'Any',
        'purpose': 'Get item by key/index',
        'description': 'Called by obj[key]. Return the item at the given key or index.',
    },
    '__setitem__': {
        'signature': 'def __setitem__(self, key, value) -> None',
        'returns': 'None',
        'purpose': 'Set item by key/index',
        'description': 'Called by obj[key] = value. Store value at the given key or index.',
    },
    '__delitem__': {
        'signature': 'def __delitem__(self, key) -> None',
        'returns': 'None',
        'purpose': 'Delete item by key/index',
        'description': 'Called by del obj[key]. Remove the item at the given key or index.',
    },
    '__missing__': {
        'signature': 'def __missing__(self, key)',
        'returns': 'Any',
        'purpose': 'Handle missing dict keys',
        'description': 'Called by dict subclasses when key is not found. Return default or raise KeyError.',
    },
    '__iter__': {
        'signature': 'def __iter__(self)',
        'returns': 'Iterator',
        'purpose': 'Return an iterator',
        'description': 'Called by iter(obj) and for loops. Return an iterator object.',
    },
    '__next__': {
        'signature': 'def __next__(self)',
        'returns': 'Any',
        'purpose': 'Get next item from iterator',
        'description': 'Called by next(obj). Return next item or raise StopIteration.',
    },
    '__reversed__': {
        'signature': 'def __reversed__(self)',
        'returns': 'Iterator',
        'purpose': 'Return reversed iterator',
        'description': 'Called by reversed(obj). Return iterator over items in reverse order.',
    },
    '__contains__': {
        'signature': 'def __contains__(self, item) -> bool',
        'returns': 'bool',
        'purpose': 'Membership test',
        'description': 'Called by "item in obj". Return True if item is contained.',
    },

    # Callable
    '__call__': {
        'signature': 'def __call__(self, *args, **kwargs)',
        'returns': 'Any',
        'purpose': 'Make instance callable like a function',
        'description': 'Called by obj(*args, **kwargs). Makes the instance work like a function.',
    },

    # Context managers
    '__enter__': {
        'signature': 'def __enter__(self)',
        'returns': 'Any',
        'purpose': 'Context manager entry',
        'description': 'Called at start of "with obj as x:" block. Return value is assigned to x.',
    },
    '__exit__': {
        'signature': 'def __exit__(self, exc_type, exc_val, exc_tb)',
        'returns': 'bool | None',
        'purpose': 'Context manager exit',
        'description': 'Called at end of with block. Return True to suppress exceptions.',
    },

    # Async context managers
    '__aenter__': {
        'signature': 'async def __aenter__(self)',
        'returns': 'Any',
        'purpose': 'Async context manager entry',
        'description': 'Called at start of "async with obj as x:" block.',
    },
    '__aexit__': {
        'signature': 'async def __aexit__(self, exc_type, exc_val, exc_tb)',
        'returns': 'bool | None',
        'purpose': 'Async context manager exit',
        'description': 'Called at end of async with block.',
    },
    '__aiter__': {
        'signature': 'def __aiter__(self)',
        'returns': 'AsyncIterator',
        'purpose': 'Return async iterator',
        'description': 'Called by "async for x in obj". Return async iterator.',
    },
    '__anext__': {
        'signature': 'async def __anext__(self)',
        'returns': 'Any',
        'purpose': 'Get next item from async iterator',
        'description': 'Called by async iteration. Return next item or raise StopAsyncIteration.',
    },
    '__await__': {
        'signature': 'def __await__(self)',
        'returns': 'Iterator',
        'purpose': 'Make object awaitable',
        'description': 'Called by "await obj". Return iterator for await protocol.',
    },

    # Numeric - binary
    '__add__': {
        'signature': 'def __add__(self, other)',
        'returns': 'Any',
        'purpose': 'Addition',
        'description': 'Called by obj + other. Return the sum.',
    },
    '__sub__': {
        'signature': 'def __sub__(self, other)',
        'returns': 'Any',
        'purpose': 'Subtraction',
        'description': 'Called by obj - other. Return the difference.',
    },
    '__mul__': {
        'signature': 'def __mul__(self, other)',
        'returns': 'Any',
        'purpose': 'Multiplication',
        'description': 'Called by obj * other. Return the product.',
    },
    '__matmul__': {
        'signature': 'def __matmul__(self, other)',
        'returns': 'Any',
        'purpose': 'Matrix multiplication',
        'description': 'Called by obj @ other. Return matrix product.',
    },
    '__truediv__': {
        'signature': 'def __truediv__(self, other)',
        'returns': 'Any',
        'purpose': 'True division',
        'description': 'Called by obj / other. Return quotient.',
    },
    '__floordiv__': {
        'signature': 'def __floordiv__(self, other)',
        'returns': 'Any',
        'purpose': 'Floor division',
        'description': 'Called by obj // other. Return floor of quotient.',
    },
    '__mod__': {
        'signature': 'def __mod__(self, other)',
        'returns': 'Any',
        'purpose': 'Modulo',
        'description': 'Called by obj % other. Return remainder.',
    },
    '__divmod__': {
        'signature': 'def __divmod__(self, other)',
        'returns': 'tuple',
        'purpose': 'Division with remainder',
        'description': 'Called by divmod(obj, other). Return (quotient, remainder).',
    },
    '__pow__': {
        'signature': 'def __pow__(self, other, mod=None)',
        'returns': 'Any',
        'purpose': 'Exponentiation',
        'description': 'Called by obj ** other or pow(). Return power.',
    },
    '__lshift__': {
        'signature': 'def __lshift__(self, other)',
        'returns': 'Any',
        'purpose': 'Left bit shift',
        'description': 'Called by obj << other. Return shifted value.',
    },
    '__rshift__': {
        'signature': 'def __rshift__(self, other)',
        'returns': 'Any',
        'purpose': 'Right bit shift',
        'description': 'Called by obj >> other. Return shifted value.',
    },
    '__and__': {
        'signature': 'def __and__(self, other)',
        'returns': 'Any',
        'purpose': 'Bitwise AND',
        'description': 'Called by obj & other. Return bitwise AND.',
    },
    '__xor__': {
        'signature': 'def __xor__(self, other)',
        'returns': 'Any',
        'purpose': 'Bitwise XOR',
        'description': 'Called by obj ^ other. Return bitwise XOR.',
    },
    '__or__': {
        'signature': 'def __or__(self, other)',
        'returns': 'Any',
        'purpose': 'Bitwise OR',
        'description': 'Called by obj | other. Return bitwise OR.',
    },

    # Numeric - reflected (when left operand doesn't support the operation)
    '__radd__': {
        'signature': 'def __radd__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected addition',
        'description': 'Called by other + obj when other lacks __add__.',
    },
    '__rsub__': {
        'signature': 'def __rsub__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected subtraction',
        'description': 'Called by other - obj when other lacks __sub__.',
    },
    '__rmul__': {
        'signature': 'def __rmul__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected multiplication',
        'description': 'Called by other * obj when other lacks __mul__.',
    },
    '__rmatmul__': {
        'signature': 'def __rmatmul__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected matrix multiplication',
        'description': 'Called by other @ obj when other lacks __matmul__.',
    },
    '__rtruediv__': {
        'signature': 'def __rtruediv__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected true division',
        'description': 'Called by other / obj when other lacks __truediv__.',
    },
    '__rfloordiv__': {
        'signature': 'def __rfloordiv__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected floor division',
        'description': 'Called by other // obj when other lacks __floordiv__.',
    },
    '__rmod__': {
        'signature': 'def __rmod__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected modulo',
        'description': 'Called by other % obj when other lacks __mod__.',
    },
    '__rdivmod__': {
        'signature': 'def __rdivmod__(self, other)',
        'returns': 'tuple',
        'purpose': 'Reflected divmod',
        'description': 'Called by divmod(other, obj) when other lacks __divmod__.',
    },
    '__rpow__': {
        'signature': 'def __rpow__(self, other, mod=None)',
        'returns': 'Any',
        'purpose': 'Reflected power',
        'description': 'Called by other ** obj when other lacks __pow__.',
    },
    '__rlshift__': {
        'signature': 'def __rlshift__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected left shift',
        'description': 'Called by other << obj when other lacks __lshift__.',
    },
    '__rrshift__': {
        'signature': 'def __rrshift__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected right shift',
        'description': 'Called by other >> obj when other lacks __rshift__.',
    },
    '__rand__': {
        'signature': 'def __rand__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected bitwise AND',
        'description': 'Called by other & obj when other lacks __and__.',
    },
    '__rxor__': {
        'signature': 'def __rxor__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected bitwise XOR',
        'description': 'Called by other ^ obj when other lacks __xor__.',
    },
    '__ror__': {
        'signature': 'def __ror__(self, other)',
        'returns': 'Any',
        'purpose': 'Reflected bitwise OR',
        'description': 'Called by other | obj when other lacks __or__.',
    },

    # Numeric - in-place
    '__iadd__': {
        'signature': 'def __iadd__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place addition',
        'description': 'Called by obj += other. Modify self and return self.',
    },
    '__isub__': {
        'signature': 'def __isub__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place subtraction',
        'description': 'Called by obj -= other. Modify self and return self.',
    },
    '__imul__': {
        'signature': 'def __imul__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place multiplication',
        'description': 'Called by obj *= other. Modify self and return self.',
    },
    '__imatmul__': {
        'signature': 'def __imatmul__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place matrix multiplication',
        'description': 'Called by obj @= other. Modify self and return self.',
    },
    '__itruediv__': {
        'signature': 'def __itruediv__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place true division',
        'description': 'Called by obj /= other. Modify self and return self.',
    },
    '__ifloordiv__': {
        'signature': 'def __ifloordiv__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place floor division',
        'description': 'Called by obj //= other. Modify self and return self.',
    },
    '__imod__': {
        'signature': 'def __imod__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place modulo',
        'description': 'Called by obj %= other. Modify self and return self.',
    },
    '__ipow__': {
        'signature': 'def __ipow__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place power',
        'description': 'Called by obj **= other. Modify self and return self.',
    },
    '__ilshift__': {
        'signature': 'def __ilshift__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place left shift',
        'description': 'Called by obj <<= other. Modify self and return self.',
    },
    '__irshift__': {
        'signature': 'def __irshift__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place right shift',
        'description': 'Called by obj >>= other. Modify self and return self.',
    },
    '__iand__': {
        'signature': 'def __iand__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place bitwise AND',
        'description': 'Called by obj &= other. Modify self and return self.',
    },
    '__ixor__': {
        'signature': 'def __ixor__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place bitwise XOR',
        'description': 'Called by obj ^= other. Modify self and return self.',
    },
    '__ior__': {
        'signature': 'def __ior__(self, other)',
        'returns': 'Self',
        'purpose': 'In-place bitwise OR',
        'description': 'Called by obj |= other. Modify self and return self.',
    },

    # Unary operations
    '__neg__': {
        'signature': 'def __neg__(self)',
        'returns': 'Any',
        'purpose': 'Unary negation',
        'description': 'Called by -obj. Return negated value.',
    },
    '__pos__': {
        'signature': 'def __pos__(self)',
        'returns': 'Any',
        'purpose': 'Unary positive',
        'description': 'Called by +obj. Return positive value.',
    },
    '__abs__': {
        'signature': 'def __abs__(self)',
        'returns': 'Any',
        'purpose': 'Absolute value',
        'description': 'Called by abs(obj). Return absolute value.',
    },
    '__invert__': {
        'signature': 'def __invert__(self)',
        'returns': 'Any',
        'purpose': 'Bitwise inversion',
        'description': 'Called by ~obj. Return bitwise NOT.',
    },

    # Type conversion
    '__complex__': {
        'signature': 'def __complex__(self) -> complex',
        'returns': 'complex',
        'purpose': 'Complex number conversion',
        'description': 'Called by complex(obj). Return complex number.',
    },
    '__int__': {
        'signature': 'def __int__(self) -> int',
        'returns': 'int',
        'purpose': 'Integer conversion',
        'description': 'Called by int(obj). Return integer.',
    },
    '__float__': {
        'signature': 'def __float__(self) -> float',
        'returns': 'float',
        'purpose': 'Float conversion',
        'description': 'Called by float(obj). Return float.',
    },
    '__index__': {
        'signature': 'def __index__(self) -> int',
        'returns': 'int',
        'purpose': 'Lossless integer conversion',
        'description': 'Called when integer is needed (slicing, bin(), hex(), oct()). Must return exact int.',
    },
    '__round__': {
        'signature': 'def __round__(self, ndigits=None)',
        'returns': 'Any',
        'purpose': 'Rounding',
        'description': 'Called by round(obj, ndigits). Return rounded value.',
    },
    '__trunc__': {
        'signature': 'def __trunc__(self)',
        'returns': 'int',
        'purpose': 'Truncation',
        'description': 'Called by math.trunc(obj). Return truncated integer.',
    },
    '__floor__': {
        'signature': 'def __floor__(self)',
        'returns': 'int',
        'purpose': 'Floor',
        'description': 'Called by math.floor(obj). Return floor integer.',
    },
    '__ceil__': {
        'signature': 'def __ceil__(self)',
        'returns': 'int',
        'purpose': 'Ceiling',
        'description': 'Called by math.ceil(obj). Return ceiling integer.',
    },

    # Descriptors
    '__get__': {
        'signature': 'def __get__(self, obj, objtype=None)',
        'returns': 'Any',
        'purpose': 'Descriptor get',
        'description': 'Called when descriptor attribute is accessed. Return value.',
    },
    '__set__': {
        'signature': 'def __set__(self, obj, value) -> None',
        'returns': 'None',
        'purpose': 'Descriptor set',
        'description': 'Called when descriptor attribute is assigned. Set value.',
    },
    '__delete__': {
        'signature': 'def __delete__(self, obj) -> None',
        'returns': 'None',
        'purpose': 'Descriptor delete',
        'description': 'Called when descriptor attribute is deleted.',
    },
    '__set_name__': {
        'signature': 'def __set_name__(self, owner, name) -> None',
        'returns': 'None',
        'purpose': 'Descriptor naming',
        'description': 'Called at class creation time with the attribute name.',
    },

    # Attribute access
    '__dir__': {
        'signature': 'def __dir__(self) -> list',
        'returns': 'list',
        'purpose': 'List attributes',
        'description': 'Called by dir(obj). Return list of attribute names.',
    },

    # Introspection
    '__sizeof__': {
        'signature': 'def __sizeof__(self) -> int',
        'returns': 'int',
        'purpose': 'Size in bytes',
        'description': 'Called by sys.getsizeof(obj). Return memory size in bytes.',
    },
    '__fspath__': {
        'signature': 'def __fspath__(self) -> str | bytes',
        'returns': 'str | bytes',
        'purpose': 'Filesystem path',
        'description': 'Called by os.fspath(obj). Return filesystem path.',
    },

    # Pickling
    '__getstate__': {
        'signature': 'def __getstate__(self)',
        'returns': 'Any',
        'purpose': 'Pickle state',
        'description': 'Called by pickle. Return object state for serialization.',
    },
    '__setstate__': {
        'signature': 'def __setstate__(self, state) -> None',
        'returns': 'None',
        'purpose': 'Restore pickle state',
        'description': 'Called by pickle. Restore state from serialization.',
    },
    '__reduce__': {
        'signature': 'def __reduce__(self)',
        'returns': 'tuple',
        'purpose': 'Pickle reduction',
        'description': 'Called by pickle. Return (callable, args) to recreate object.',
    },
    '__reduce_ex__': {
        'signature': 'def __reduce_ex__(self, protocol)',
        'returns': 'tuple',
        'purpose': 'Pickle reduction with protocol',
        'description': 'Called by pickle with protocol version.',
    },

    # Copy
    '__copy__': {
        'signature': 'def __copy__(self)',
        'returns': 'Self',
        'purpose': 'Shallow copy',
        'description': 'Called by copy.copy(obj). Return shallow copy.',
    },
    '__deepcopy__': {
        'signature': 'def __deepcopy__(self, memo)',
        'returns': 'Self',
        'purpose': 'Deep copy',
        'description': 'Called by copy.deepcopy(obj). Return deep copy.',
    },

    # Class-level
    '__class_getitem__': {
        'signature': 'def __class_getitem__(cls, item)',
        'returns': 'Any',
        'purpose': 'Generic type parameterization',
        'description': 'Called by Class[item]. Return parameterized generic.',
    },
}


# =============================================================================
# Dunder Trampoline System
# =============================================================================

def _get_dunder_info(dunder_name: str) -> dict | None:
    """Get metadata about a known dunder method."""
    return DUNDER_INFO.get(dunder_name)


def _build_dunder_context(dunder_name: str, args: tuple, kwargs: dict) -> str:
    """Build context for dunder generation, even for unknown ones."""
    info = _get_dunder_info(dunder_name)

    args_desc = []
    if args:
        args_desc.append(f"Positional arguments: {repr(args)}")
    if kwargs:
        args_desc.append(f"Keyword arguments: {repr(kwargs)}")

    context = f"""
## Magic Method Generation

You are generating the magic method: `{dunder_name}`

**What happened:**
This method was called with:
{chr(10).join(args_desc) if args_desc else "No arguments (besides self)"}

**What you need to do:**
"""

    if info:
        context += f"""
{info['description']}

**Required signature**: `{info['signature']}`
**Expected return type**: `{info['returns']}`
**Purpose**: {info['purpose']}
"""
    else:
        # Unknown dunder - provide general guidance
        context += f"""
This is an UNKNOWN or CUSTOM magic method. Based on the name `{dunder_name}`:

1. It's a dunder method (surrounded by double underscores)
2. It was called with {len(args)} argument(s): {repr(args) if args else 'none'}
3. You should infer the purpose from:
   - The name itself (what does "{dunder_name[2:-2]}" suggest?)
   - The class docstring and context
   - The arguments passed

**Signature**: Based on the call, use: `def {dunder_name}(self{', ' + ', '.join(f'arg{i}' for i in range(len(args))) if args else ''})`
**Return**: Return an appropriate value based on what this method should do
"""

    context += f"""

**Critical requirements:**
1. Use the EXACT name: `{dunder_name}`
2. Don't call this method recursively (e.g., don't call str(self) inside __str__)
3. Follow Python's magic method protocols
4. Be consistent with the class's purpose (read the docstring!)

Reference: https://docs.python.org/3/reference/datamodel.html#special-method-names
"""

    return context


def _make_dunder_trampoline(dunder_name: str):
    """Create a trampoline function that lazily generates a magic method on first call.

    The trampoline is installed on the class at class-creation time. When called:
    1. Check if we've already generated and cached the real implementation
    2. If not, generate it using the LLM
    3. Replace the trampoline with the real method on the class
    4. Call the real method with the original arguments

    This works because Python looks up magic methods on type(obj), not obj itself.
    By injecting the trampoline at class creation time, it's found by Python's
    special method lookup, and then lazily generates the real implementation.
    """

    def trampoline(self, *args, **kwargs):
        """Trampoline that generates the real dunder on first invocation."""
        # Get the actual class (handles inheritance correctly)
        actual_cls = type(self)

        # Recursion guard for __repr__/__str__ to prevent infinite loops
        # during generation (when logging/debugging calls repr())
        if dunder_name in ('__repr__', '__str__'):
            guard_key = (id(self), dunder_name)
            active_guards = getattr(_generation_guard, 'active', set())
            if guard_key in active_guards:
                # Recursion detected - return safe placeholder
                return f"<{actual_cls.__name__} (generating...)>"
            # Mark as active
            if not hasattr(_generation_guard, 'active'):
                _generation_guard.active = set()
            _generation_guard.active.add(guard_key)

        # Check if LLM generation is disabled
        if not getattr(actual_cls, '_llm_enabled', True):
            if dunder_name in ('__repr__', '__str__'):
                _generation_guard.active.discard((id(self), dunder_name))
            raise TypeError(f"'{actual_cls.__name__}' object does not support {dunder_name}")

        # Check cache first
        cache = actual_cls._get_cache()
        cached = cache.get(actual_cls.__name__, dunder_name)

        if cached:
            # Already generated, execute it
            quiet = getattr(actual_cls, '_llm_quiet', False)
            if not quiet:
                using_cached(actual_cls.__name__, dunder_name)
            real_method = actual_cls._execute_code(
                cached.code,
                dunder_name,
                cached.dependencies
            )
        else:
            # Need to generate it
            quiet = getattr(actual_cls, '_llm_quiet', False)
            if not quiet:
                info(f"{dunder_name} called on {actual_cls.__name__} - generating...")

            # Build context with signature info
            context_hint = _build_dunder_context(dunder_name, args, kwargs)

            # Generate using standard flow
            real_method = actual_cls._llm_generate(
                dunder_name,
                is_classmethod=False,
                additional_context=context_hint
            )

        # Replace the trampoline with the real method on the class
        # This ensures subsequent calls go directly to the real implementation
        setattr(actual_cls, dunder_name, real_method)

        # Clear recursion guard for __repr__/__str__
        if dunder_name in ('__repr__', '__str__'):
            _generation_guard.active.discard((id(self), dunder_name))

        # Now call the real method
        return real_method(self, *args, **kwargs)

    # Set helpful attributes for introspection
    trampoline.__name__ = dunder_name
    trampoline.__qualname__ = f"ScryClass.{dunder_name}"
    trampoline._llm_is_trampoline = True
    trampoline._llm_dunder_name = dunder_name

    return trampoline


def _has_meaningful_parent_implementation(cls, dunder_name: str, llm_class: type) -> bool:
    """Check if a dunder is inherited from a meaningful parent class.

    Returns True if the dunder is defined in a parent class other than:
    - object (Python's base object)
    - ScryClass (our base class)

    This prevents us from overwriting user-defined dunders from parent classes.
    """
    for base in cls.__mro__[1:]:  # Skip cls itself
        if base is object:
            continue
        if base is llm_class:
            continue
        if dunder_name in base.__dict__:
            return True
    return False


# =============================================================================
# GenerationProxy (for non-dunder attributes)
# =============================================================================

class GenerationProxy:
    """Lazy proxy that triggers generation upon use (call or attribute access)."""

    def __init__(self, cls_or_inst: Any, name: str, is_classmethod: bool):
        self._target = cls_or_inst
        self._name = name
        self._is_classmethod = is_classmethod
        self._cls = cls_or_inst if is_classmethod else type(cls_or_inst)

    def __call__(self, *args, **kwargs) -> Any:
        """Called when the proxy is used as a function."""
        # We now know the call arguments, so we can generate the method with this context!

        # Describe args for the prompt
        args_desc = []
        if args:
            args_desc.append(f"Positional args: {repr(args)}")
        if kwargs:
            args_desc.append(f"Keyword args: {repr(kwargs)}")

        # Include instance attributes in context (for instance methods)
        instance_attrs_desc = ""
        if not self._is_classmethod and hasattr(self._target, '__dict__'):
            instance_attrs = {}
            for k, v in self._target.__dict__.items():
                if not k.startswith('_llm_'):
                    # Truncate long repr values
                    v_repr = repr(v)
                    if len(v_repr) > 100:
                        v_repr = v_repr[:100] + "..."
                    instance_attrs[k] = v_repr
            if instance_attrs:
                instance_attrs_desc = f"\n\nInstance attributes (self.__dict__):\n{instance_attrs}"

        call_context = f"""
## Runtime Call Context

The attribute `{self._name}` is being CALLED with these arguments:
{chr(10).join(args_desc)}{instance_attrs_desc}

Requirements:
1. Generate a METHOD (not property)
2. Ensure signature matches these arguments
3. If arguments are ambiguous, use *args, **kwargs
"""

        quiet = getattr(self._cls, '_llm_quiet', False)
        if not quiet:
            info(f"Calling {self._cls.__name__}.{self._name}(...) - resolving...")

        MAX_RETRIES = 3
        last_error = None
        current_context = call_context
        previous_errors: list[str] = []  # Track all errors for accumulated feedback

        for attempt in range(MAX_RETRIES + 1):
            # Generate code
            code_obj = self._cls._llm_generate(
                self._name,
                is_classmethod=self._is_classmethod,
                additional_context=current_context
            )

            # Bind to instance/class
            bound_obj = self._bind_and_cache(code_obj)

            # Try to execute
            try:
                return bound_obj(*args, **kwargs)
            except Exception as e:
                import traceback

                # Check for signature mismatch (TypeError)
                is_signature_error = isinstance(e, TypeError) and ("argument" in str(e) or "takes" in str(e))

                # Check if the error originated from the generated code
                # We look for the specific filename pattern we used in _execute_code
                generated_filename = f"<generated: {self._cls.__name__}.{self._name}>"
                is_generated_error = False
                if e.__traceback__:
                    for frame in traceback.extract_tb(e.__traceback__):
                        if frame.filename == generated_filename:
                            is_generated_error = True
                            break

                if (is_signature_error or is_generated_error) and attempt < MAX_RETRIES:
                    if not quiet:
                        reason = f"Signature mismatch: {e}" if is_signature_error else f"Runtime error: {type(e).__name__}: {e}"
                        warning(f"{reason}. Regenerating (attempt {attempt + 1}/{MAX_RETRIES})...")


                    # Invalidate cache so _llm_generate actually generates new code
                    self._cls._get_cache().prune(self._cls.__name__, self._name)

                    # Track this error
                    if is_signature_error:
                        previous_errors.append(f"Attempt #{attempt + 1} - TypeError (signature mismatch): {e}")
                    else:
                        previous_errors.append(f"Attempt #{attempt + 1} - {type(e).__name__}: {e}")

                    # Build accumulated error feedback
                    errors_list = chr(10).join(f"  {err}" for err in previous_errors)
                    error_feedback = f"""
## CRITICAL - PREVIOUS GENERATION FAILED

This is generation attempt #{attempt + 2}. Previous attempt(s) caused errors:

{errors_list}

The code was called with:
{chr(10).join(args_desc) if args_desc else "No arguments ()"}

You MUST analyze ALL previous errors and generate FIXED code that:
1. Has the correct signature matching how it is called
2. Handles edge cases that caused runtime exceptions
3. Does NOT repeat the same mistakes
"""

                    current_context = call_context + error_feedback
                    continue

                # If we get here, it's either not a retryable error OR retries exhausted.
                # Print helpful debugging info if not already improved.
                if not getattr(e, "_scry_run_handled", False):
                    error(f"Unhandled exception in generated code: {self._cls.__name__}.{self._name}")
                    warning(f"To inspect: scry-run cache show {self._cls.__name__} {self._name}")
                    warning(f"To remove:  scry-run cache rm {self._cls.__name__}.{self._name}")
                    e._scry_run_handled = True

                raise

        # If we exhausted retries (loop completed normally without return)
        # Wait, if we 'continue' on attempt == MAX_RETRIES-1?
        # Range is range(MAX_RETRIES + 1) -> 0, 1, 2, 3.
        # If attempt 3 fails, `attempt < 3` is False. We fall through to raise.
        # So we never exit the loop normally unless we return.
        # The code below is unreachable if logic is correct?
        # Actually if `pass` was used... but here we `raise`.
        pass

    def _bind_and_cache(self, code_obj: Any) -> Any:
        """Bind the generated object to the instance/class and replace the proxy."""
        if self._is_classmethod:
            setattr(self._target, self._name, code_obj)
            return code_obj
        else:
            # Instance access
            if isinstance(code_obj, property):
                 # Properties MUST be set on the class to work as descriptors
                 setattr(type(self._target), self._name, code_obj)
                 # Now access the property on the instance to trigger the getter and return the value
                 try:
                     return getattr(self._target, self._name)
                 except Exception as e:
                     if not getattr(e, "_scry_run_handled", False):
                        error(f"Unhandled exception in generated property: {self._cls.__name__}.{self._name}")
                        warning(f"To inspect: scry-run cache show {self._cls.__name__} {self._name}")
                        warning(f"To remove:  scry-run cache rm {self._cls.__name__}.{self._name}")
                        e._scry_run_handled = True
                     raise
            elif callable(code_obj):
                # Methods should be set on the class, not the instance.
                # This ensures they work as descriptors (bound methods) and can overwrite
                # existing (broken) properties on the class.
                setattr(self._cls, self._name, code_obj)
                # Return the bound method accessed from the instance
                return getattr(self._target, self._name)
            else:
                # Regular value (not a method or property)
                object.__setattr__(self._target, self._name, code_obj)
                return code_obj

    def _resolve(self) -> Any:
        """Resolve the proxy for non-call usage (properties)."""
        quiet = getattr(self._cls, '_llm_quiet', False)
        if not quiet:
            info(f"Accessing {self._cls.__name__}.{self._name} - resolving...")

        # Include instance attributes in context (for instance properties)
        instance_attrs_desc = ""
        if not self._is_classmethod and hasattr(self._target, '__dict__'):
            instance_attrs = {}
            for k, v in self._target.__dict__.items():
                if not k.startswith('_llm_'):
                    v_repr = repr(v)
                    if len(v_repr) > 100:
                        v_repr = v_repr[:100] + "..."
                    instance_attrs[k] = v_repr
            if instance_attrs:
                instance_attrs_desc = f"\n\nInstance attributes (self.__dict__):\n{instance_attrs}"

        context = f"## Runtime Context\n\nAttribute accessed as a value (property), NOT called as a function.{instance_attrs_desc}"

        code_obj = self._cls._llm_generate(
            self._name,
            is_classmethod=self._is_classmethod,
            additional_context=context
        )
        return self._bind_and_cache(code_obj)

    # Magic methods to trigger resolution on value usage
    def __str__(self): return str(self._resolve())
    def __repr__(self):
        # Prevent infinite recursion if we are building runtime context
        if getattr(_context_build_guard, 'active', False):
            return f"<Unresolved Proxy: {self._cls.__name__}.{self._name}>"
        return repr(self._resolve())

    def __bool__(self): return bool(self._resolve())
    def __int__(self): return int(self._resolve())
    def __float__(self): return float(self._resolve())
    def __add__(self, other): return self._resolve() + other
    def __radd__(self, other): return other + self._resolve()
    def __sub__(self, other): return self._resolve() - other
    def __mul__(self, other): return self._resolve() * other
    def __getitem__(self, item): return self._resolve()[item]
    def __iter__(self): return iter(self._resolve())
    def __getattr__(self, name): return getattr(self._resolve(), name)
    def __eq__(self, other): return self._resolve() == other
    def __hash__(self): return hash(self._resolve())


# =============================================================================
# Metaclass
# =============================================================================

class ScryMeta(type):
    """Metaclass that intercepts attribute access and object creation.

    Capabilities:
    1. __new__: Injects dunder trampolines at class creation time
    2. __call__: Generates __init__ when creating objects if not defined
    3. __getattr__: Generates missing class-level attributes/methods
    4. __getattribute__: Catches explicit access to unknown dunders

    When a class-level attribute is not found, this metaclass will
    attempt to generate the code using an LLM and cache it.
    """

    def __new__(mcs, name, bases, namespace, **kwargs):
        """Create a new class and inject dunder trampolines."""
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)

        # Only inject trampolines if LLM is enabled for this class
        # (check if it will be enabled, default is True)
        if namespace.get('_llm_enabled', True):
            mcs._inject_dunder_trampolines(cls)

        return cls

    @staticmethod
    def _inject_dunder_trampolines(cls):
        """Inject trampoline methods for all generatable dunders.

        Only inject if:
        1. The class inherits from ScryClass (has LLM infrastructure)
        2. The dunder doesn't already exist in this class's __dict__
        3. The dunder isn't inherited with a custom implementation from a real parent
        """
        # We need to import ScryClass here to avoid circular reference
        # at module load time. The class will be defined below.
        llm_class = None
        for base in cls.__mro__:
            if base.__name__ == 'ScryClass' and base is not cls:
                llm_class = base
                break

        # Don't inject trampolines if this class doesn't inherit from ScryClass
        # (it lacks the necessary infrastructure like _get_cache, _llm_generate, etc.)
        if llm_class is None:
            return

        for dunder_name in GENERATABLE_DUNDERS:
            # Skip if this class explicitly defines it
            if dunder_name in cls.__dict__:
                continue

            # Skip if meaningfully inherited (not from object/ScryClass)
            if llm_class and _has_meaningful_parent_implementation(cls, dunder_name, llm_class):
                continue

            # Inject the trampoline
            trampoline = _make_dunder_trampoline(dunder_name)
            setattr(cls, dunder_name, trampoline)

    def __call__(cls, *args, **kwargs):
        """Intercept object creation to generate __init__ if needed.

        This allows the LLM to generate constructors based on the class
        docstring and any arguments passed to the constructor.
        """
        # Check if __init__ is defined in the class itself (not inherited from ScryClass/object)
        has_custom_init = '__init__' in cls.__dict__

        # Check if LLM generation is enabled
        llm_enabled = getattr(cls, '_llm_enabled', True)

        # Generate __init__ if not custom-defined (even without args)
        if not has_custom_init and llm_enabled:
            quiet = getattr(cls, '_llm_quiet', False)
            if not quiet:
                if args or kwargs:
                    info(f"Creating {cls.__name__} with args - generating __init__...")
                else:
                    info(f"Creating {cls.__name__} - generating __init__...")

            # Generate __init__ using the LLM
            init_code = cls._llm_generate_init(args, kwargs)

            # Set it on the class
            setattr(cls, '__init__', init_code)

        # Now create the instance normally
        return super().__call__(*args, **kwargs)

    def __getattr__(cls, name: str) -> Any:
        """Called when a class-level attribute is not found.

        Args:
            name: Name of the missing attribute

        Returns:
            A GenerationProxy that triggers generation on use
        """
        # Check if this class has LLM infrastructure (inherits from ScryClass)
        # We check __dict__ directly to avoid recursion through __getattr__
        has_llm_infrastructure = any(
            '_get_cache' in base.__dict__ and '_llm_generate' in base.__dict__
            for base in cls.__mro__
        )

        # For dunders, trampolines should have been injected at class creation.
        # If we get here for a dunder, it means:
        # 1. It's a forbidden dunder, or
        # 2. The class doesn't inherit from ScryClass, or
        # 3. Something went wrong
        if name.startswith("__") and name.endswith("__"):
            if name in FORBIDDEN_DUNDERS:
                raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
            # If class doesn't have LLM infrastructure, don't try to generate
            if not has_llm_infrastructure:
                raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
            # For generatable dunders that somehow weren't injected, inject now
            if name in GENERATABLE_DUNDERS:
                trampoline = _make_dunder_trampoline(name)
                setattr(cls, name, trampoline)
                return trampoline
            # Unknown dunder - try to inject a trampoline anyway
            if getattr(cls, "_llm_enabled", True):
                info(f"Unknown dunder {name} accessed - injecting trampoline...")
                trampoline = _make_dunder_trampoline(name)
                setattr(cls, name, trampoline)
                return trampoline
            raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")

        # Skip our internal config namespace
        if name.startswith("_llm_"):
            raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")

        # Skip known Python internals
        if name.startswith(("_abc_", "_v_")):
            raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")

        # Check if LLM generation is enabled
        if not getattr(cls, "_llm_enabled", True):
            raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")

        return GenerationProxy(cls, name, is_classmethod=True)


# =============================================================================
# Base Class
# =============================================================================

class ScryClass(metaclass=ScryMeta):
    """Base class for LLM-powered dynamic code generation.

    Inherit from this class to enable automatic code generation for
    missing methods and properties. The LLM will use the class docstring,
    existing methods, and the full codebase as context.

    Features:
    - Instance methods: app.method() generates method if missing
    - Class methods: MyClass.method() generates class method if missing
    - Constructor: MyClass(args) generates __init__ if missing and args provided
    - Magic methods: str(obj), len(obj), obj[key], etc. generate dunders

    Example:
        ```python
        class TodoApp(ScryClass):
            '''A minimal web todo app with task management.'''
            pass

        app = TodoApp()
        # This will trigger LLM generation of add_task method
        app.add_task("Buy groceries")

        # Magic methods work too!
        print(app)  # Generates __str__
        len(app)    # Generates __len__
        ```

    Configuration:
        - _llm_enabled: Set to False to disable LLM generation
        - _llm_cache: Custom ScryCache instance
        - _llm_generator: Custom CodeGenerator instance
        - _llm_use_full_context: Whether to use full codebase context (default: True)
        - _llm_quiet: Set to True to suppress generation messages
    """

    # Class-level configuration
    _llm_enabled: bool = True
    _llm_cache: Optional["ScryCache"] = None
    _llm_generator: Optional["CodeGenerator"] = None
    _llm_model: Optional[str] = None  # Model override (e.g., "opus" for Claude)
    _llm_backend: Optional[str] = None  # Backend override (e.g., "frozen" to prevent generation)
    _llm_use_full_context: bool = True
    _llm_quiet: bool = False  # Set to True to suppress generation messages

    # Note: __repr__ and __str__ are now generated via trampolines with recursion guard

    def __getattr__(self, name: str) -> Any:
        """Called when an instance-level attribute is not found.

        Args:
            name: Name of the missing attribute

        Returns:
            The generated and cached attribute
        """
        # For dunders, trampolines should handle them at the class level.
        # If we get here for a dunder, something is wrong or it's forbidden.
        if name.startswith("__") and name.endswith("__"):
            if name in FORBIDDEN_DUNDERS:
                raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
            # This shouldn't normally happen (trampolines should catch it)
            # but we can try generating it anyway for robustness
            warning(f"{name} accessed via __getattr__ (trampoline missing?)")

        # Skip our internal config namespace
        if name.startswith("_llm_"):
            raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

        # Skip known Python internals
        if name.startswith(("_abc_", "_v_")):
            raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

        # Check if LLM generation is enabled
        if not getattr(type(self), "_llm_enabled", True):
            raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

        return GenerationProxy(self, name, is_classmethod=False)

    @classmethod
    def _get_cache(cls) -> "ScryCache":
        """Get or create the cache instance.

        Cache is stored as cache.json next to the source file that defines the class.
        """
        if cls._llm_cache is None:
            from pathlib import Path
            from scry_run.cache import ScryCache

            # Find cache.json next to the source file
            try:
                source_file = Path(inspect.getfile(cls))
                cache_path = source_file.parent / "cache.json"
            except (OSError, TypeError):
                # Fallback to current directory
                cache_path = Path.cwd() / "cache.json"

            # Enable read-only mode if the backend is frozen
            read_only = cls._llm_backend == "frozen"
            cls._llm_cache = ScryCache(cache_path, read_only=read_only)
        return cls._llm_cache


    @classmethod
    def _get_app_dir(cls) -> Path | None:
        """Get the app directory for this class, if running in an app context."""
        from pathlib import Path
        try:
            source_file = Path(inspect.getfile(cls))
            # Check if this looks like an app directory (has .venv or is under ~/.scry-run/apps/)
            app_dir = source_file.parent
            if (app_dir / ".venv").exists():
                return app_dir
            if ".scry-run/apps/" in str(app_dir):
                return app_dir
        except (OSError, TypeError):
            pass
        return None

    @classmethod
    def _get_generator(cls) -> "CodeGenerator":
        """Get or create the generator instance."""
        if cls._llm_generator is None:
            from scry_run.generator import CodeGenerator
            cls._llm_generator = CodeGenerator(
                model=cls._llm_model,
                backend=cls._llm_backend,
            )
        return cls._llm_generator

    @classmethod
    def _get_class_source(cls) -> str:
        """Get the source code of this class."""
        try:
            return inspect.getsource(cls)
        except (OSError, TypeError):
            # Fallback: construct minimal representation
            lines = [f"class {cls.__name__}:"]
            if cls.__doc__:
                lines.append(f'    """{cls.__doc__}"""')

            # List existing methods
            for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
                if not name.startswith("_"):
                    sig = inspect.signature(method)
                    lines.append(f"    def {name}{sig}: ...")

            return "\n".join(lines)

    @classmethod
    def _llm_generate(
        cls,
        attr_name: str,
        is_classmethod: bool = False,
        additional_context: Optional[str] = None
    ) -> Any:
        """Generate code for a missing attribute using LLM.

        Args:
            attr_name: Name of the attribute to generate
            is_classmethod: Whether this is a class-level attribute
            additional_context: Optional extra context (e.g. runtime arguments)

        Returns:
            The generated code object (function, property, etc.)
        """
        from scry_run.logging import get_logger
        logger = get_logger()

        # Set up app-specific logging if in app context
        app_dir = cls._get_app_dir()
        if app_dir:
            logger.set_app_context(app_dir)

        cache = cls._get_cache()
        class_name = cls.__name__

        logger.info(f"LLM generate: {class_name}.{attr_name} (is_classmethod={is_classmethod})")

        # Check cache first (fast path, no lock needed)
        # We always check cache. If the cached code is invalid (e.g. signature mismatch),
        # the caller (GenerationProxy) handles TypeError, invalidates cache, and retries.
        cached = cache.get(class_name, attr_name)
        if cached:
            logger.info(f"Cache HIT: {class_name}.{attr_name}")
            logger.debug(f"Cached code for {class_name}.{attr_name}", f"CODE:\n{cached.code}\n\nDEPENDENCIES:\n{cached.dependencies}")
            if not cls._llm_quiet:
                using_cached(class_name, attr_name)
            return cls._execute_code(cached.code, attr_name, cached.dependencies)

        # Cache miss - acquire per-attribute lock to prevent concurrent generation
        # Multiple threads might hit cache miss simultaneously; only one should generate
        generation_lock = _get_generation_lock(class_name, attr_name)
        with generation_lock:
            # Double-check cache after acquiring lock (another thread may have filled it)
            cached = cache.get(class_name, attr_name)
            if cached:
                logger.info(f"Cache HIT (after lock): {class_name}.{attr_name}")
                if not cls._llm_quiet:
                    using_cached(class_name, attr_name)
                return cls._execute_code(cached.code, attr_name, cached.dependencies)

            logger.info(f"Cache MISS: {class_name}.{attr_name} - generating...")

            # Generate new code
            if not cls._llm_quiet:
                generating(class_name, attr_name)
            generator = cls._get_generator()

            # Capture runtime context (call stack, variables)
            # Capture runtime context (call stack, variables)
            from scry_run.context import ContextBuilder
            
            # Set guard to prevent repr() on proxies from triggering recursive generation
            _context_build_guard.active = True
            try:
                runtime_context = ContextBuilder.build_runtime_context()
            finally:
                _context_build_guard.active = False


            if additional_context:
                runtime_context = additional_context + "\n\n" + runtime_context

            # Log truncated runtime context
            if len(runtime_context) > 1200:
                ctx_preview = runtime_context[:500] + f"\n\n... [{len(runtime_context) - 1000} chars truncated] ...\n\n" + runtime_context[-500:]
            else:
                ctx_preview = runtime_context
            logger.debug(f"Runtime context for {class_name}.{attr_name}", f"CONTEXT:\n{ctx_preview}")


            if cls._llm_use_full_context:
                builder = ContextBuilder()
                context = builder.build_context(
                    class_name=class_name,
                    attr_name=attr_name,
                    class_source=cls._get_class_source(),
                )
                # Append runtime context
                context = context + "\n\n" + runtime_context
            else:
                builder = ContextBuilder()
                context = builder.build_minimal_context(
                    class_source=cls._get_class_source(),
                    class_name=class_name,
                    attr_name=attr_name,
                    additional_context=runtime_context,
                )

            # Log truncated context
            if len(context) > 1200:
                context_preview = context[:500] + f"\n\n... [{len(context) - 1000} chars truncated] ...\n\n" + context[-500:]
            else:
                context_preview = context
            logger.debug(f"Full context for {class_name}.{attr_name} ({len(context)} chars)", f"CONTEXT:\n{context_preview}")

            # Get app directory for package management
            app_dir = cls._get_app_dir()
            installed_packages = get_installed_packages(app_dir) if app_dir else None

            result = generator.generate(
                context=context,
                class_name=class_name,
                attr_name=attr_name,
                is_classmethod=is_classmethod,
                installed_packages=installed_packages,
            )

            logger.info(f"Generation complete: {class_name}.{attr_name} (type={result.code_type})")
            logger.debug(f"Generated result for {class_name}.{attr_name}", f"CODE:\n{result.code}\n\nDOCSTRING:\n{result.docstring}\n\nDEPENDENCIES:\n{result.dependencies}")

            # Install any required packages
            if result.packages and app_dir:
                install_packages(app_dir, result.packages)

            # Cache the result
            cache.set(
                class_name=class_name,
                attr_name=attr_name,
                code=result.code,
                code_type=result.code_type,
                docstring=result.docstring,
                dependencies=result.dependencies,
                packages=result.packages,
            )

            if not cls._llm_quiet:
                generated(class_name, attr_name)

            return cls._execute_code(result.code, attr_name, result.dependencies)


    @classmethod
    def _llm_generate_init(cls, args: tuple, kwargs: dict) -> Any:
        """Generate __init__ method based on constructor arguments.

        This is called by the metaclass __call__ when creating an object
        with arguments but no custom __init__ defined.

        Args:
            args: Positional arguments passed to constructor
            kwargs: Keyword arguments passed to constructor

        Returns:
            The generated __init__ function
        """
        cache = cls._get_cache()
        class_name = cls.__name__

        # Check cache first
        cached = cache.get(class_name, "__init__")
        if cached:
            if not cls._llm_quiet:
                using_cached(class_name, "__init__")
            return cls._execute_code(cached.code, "__init__", cached.dependencies)

        # Generate new __init__
        if not cls._llm_quiet:
            generating(class_name, "__init__")

        generator = cls._get_generator()

        # Build context with constructor arguments info
        from scry_run.context import ContextBuilder

        # Describe the constructor call
        if args or kwargs:
            args_desc = []
            if args:
                args_desc.append(f"Positional args: {repr(args)}")
            if kwargs:
                args_desc.append(f"Keyword args: {repr(kwargs)}")
            constructor_context = f"""
## Constructor Call Context

This __init__ is being generated because the class was instantiated with arguments:
{chr(10).join(args_desc)}

The __init__ should:
1. Accept these arguments (infer parameter names from values/keys)
2. Store them as instance attributes (self.xxx = xxx)
3. Initialize any other state the class needs based on its docstring
"""
        else:
            constructor_context = """
## Constructor Call Context

This __init__ is being generated for a no-argument construction: `MyClass()`.

Based on the class docstring, initialize sensible default state:
1. Create any instance attributes the class needs to function
2. Set reasonable default values based on the class's purpose
3. If the class represents a collection/container, initialize it as empty
4. If the class needs configuration, use sensible defaults
"""

        if cls._llm_use_full_context:
            builder = ContextBuilder()
            context = builder.build_context(
                class_name=class_name,
                attr_name="__init__",
                class_source=cls._get_class_source(),
            )
            context = context + "\n\n" + constructor_context
        else:
            builder = ContextBuilder()
            context = builder.build_minimal_context(
                class_source=cls._get_class_source(),
                class_name=class_name,
                attr_name="__init__",
                additional_context=constructor_context,
            )

        # Get app directory for package management
        app_dir = cls._get_app_dir()
        installed_packages = get_installed_packages(app_dir) if app_dir else None

        result = generator.generate(
            context=context,
            class_name=class_name,
            attr_name="__init__",
            is_classmethod=False,
            installed_packages=installed_packages,
        )

        # Install any required packages
        if result.packages and app_dir:
            install_packages(app_dir, result.packages)

        # Cache the result
        cache.set(
            class_name=class_name,
            attr_name="__init__",
            code=result.code,
            code_type=result.code_type,
            docstring=result.docstring,
            dependencies=result.dependencies,
            packages=result.packages,
        )

        if not cls._llm_quiet:
            generated(class_name, "__init__")

        return cls._execute_code(result.code, "__init__", result.dependencies)

    @classmethod
    def _execute_code(
        cls,
        code: str,
        attr_name: str,
        dependencies: list[str],
    ) -> Any:
        """Execute generated code and extract the attribute.

        Args:
            code: The Python code to execute
            attr_name: Name of the attribute being generated
            dependencies: List of import statements

        Returns:
            The extracted code object
        """
        # Build execution namespace with imports
        namespace: dict[str, Any] = {}

        # Execute imports
        for dep in dependencies:
            try:
                exec(dep, namespace)
            except Exception:
                pass  # Ignore import errors, let the main code fail if needed

        # Execute the generated code
        # Compile with descriptive filename for clearer stack traces
        filename = f"<generated: {cls.__name__}.{attr_name}>"
        compiled = compile(code, filename, "exec")
        exec(compiled, namespace)

        # Extract the generated attribute
        obj = None
        if attr_name in namespace:
            obj = namespace[attr_name]
        else:
            # Try sanitized name (replace invalid chars with _)
            sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', attr_name)
            if sanitized in namespace:
                obj = namespace[sanitized]
            else:
                # If exact/sanitized name not found, look for functions defined in the generated block
                # We identify them by checking if their code object's filename matches what we compiled
                for name, o in namespace.items():
                    # Check for function with matching filename
                    if hasattr(o, "__code__") and getattr(o.__code__, "co_filename", "") == filename:
                         obj = o
                         break

                # Fallback: look for the first callable that isn't a dunder
                if obj is None:
                    for name, o in namespace.items():
                        if name.startswith("__") and name.endswith("__"):
                            continue
                        # Avoid picking up imports (which we don't have a perfect way to distinguish
                        # without the filename check above, but this is a last resort)
                        if callable(o) and not name.startswith("_"):
                            obj = o
                            break

        if obj is None:
            raise RuntimeError(f"Generated code did not define '{attr_name}'")

        # Watermark the object as generated
        marker_name = "_llm_is_generated"

        def mark(target: Any) -> None:
            try:
                setattr(target, marker_name, True)
                setattr(target, "_llm_source", f"{cls.__name__}.{attr_name}")
            except (AttributeError, TypeError):
                # Cannot mark some objects (e.g. built-ins, but generated code is unlikely to be built-in)
                pass

        if isinstance(obj, property):
             if obj.fget: mark(obj.fget)
             if obj.fset: mark(obj.fset)
             if obj.fdel: mark(obj.fdel)
        elif isinstance(obj, (classmethod, staticmethod)):
            mark(obj.__func__)
        else:
            mark(obj)

        return obj

    @classmethod
    def scry_export_cache(cls, output_path: Optional[str] = None) -> dict:
        """Export the cache for this class.

        Args:
            output_path: If provided, write to this file as Python code

        Returns:
            Dictionary of cached entries
        """
        cache = cls._get_cache()

        if output_path:
            cache.export_to_file(output_path)

        return cache.export()

    @classmethod
    def scry_prune_cache(
        cls,
        attr_name: Optional[str] = None,
    ) -> int:
        """Prune cache entries.

        Args:
            attr_name: If provided, only prune this specific attribute

        Returns:
            Number of entries pruned
        """
        cache = cls._get_cache()
        return cache.prune(class_name=cls.__name__, attr_name=attr_name)

    @classmethod
    def scry_disable(cls) -> None:
        """Disable LLM generation for this class."""
        cls._llm_enabled = False

    @classmethod
    def scry_enable(cls) -> None:
        """Enable LLM generation for this class."""
        cls._llm_enabled = True
