import threading
import cloudpickle as pickle
import json
from typing import Dict, Any
from .exceptions import *
from .funcs import exec_and_return, str_to_int
import ast, inspect


class _CodeGenerator:
    '''Basic system engine for code generation.'''
    def __init__(self):
        self._code = []
        self._cache = 0
    def _new_line(self, text: str): 
        self._code.append(text)
        self._cache += 1
        return self
    def _gen_temp_var(self, k: int = 0):
        var_name = f'__tmp_{self._cache - k}'
        self._cache += 1
        return var_name
    def _gen_id(self, k: int = 0):
        id = f'_id{len(self._code)+self._cache+k}{self._cache * (k+1)}{k}'
        self._cache += 1
        return id
    def _comentary(self, *args):
        self._new_line(f'"""{"\n".join(args)}"""')







class Framer(_CodeGenerator):
    '''
# Framer

Main context manager class: Framer.

For system.
    '''
    def __init__(self):
        super().__init__()
        self._vars = {}
        self._aliases = {}
        self._types = {}
        self._lock = threading.RLock()
        self._new_code_line = self._new_line
    
    def var(self, name:str, value):
        self._vars[name] = value
        return self

    def op(self, a, b, 
           operator: str = '+'):
        result_var = self._gen_temp_var()
        res = f'{a} {operator} {b}'
        code_line = f'{result_var} = {res}'
        self._code.append(code_line)
        return result_var
    
    def execute(self, algoritm=1, globals=None):
        try:
            final_code = "\n".join(self._code)
            local_scope = self._vars.copy()
            if globals is None:
                globals = {}
            exec(final_code, globals if globals != 'locals' else local_scope, local_scope)
            if algoritm == 1:
                return_var = "__frame_return_value"
                return local_scope.get(return_var)
            else:
                return local_scope
        except Exception as e:
            error_msg = f"\nError in frame execution: {e}. \nCode for execution:\n\n{final_code}"
            raise FrameExecutionError(error_msg)
    
    def get_thread_safe(self, name):
        with self._lock: return self._vars.get(self._aliases[name])

    def __enter__(self):
        System.last_framer = System.framer
        System.framer = self
        return self
    def __exit__(self, *args, **kwargs): pass








class SystemClass:
    '''
# System

System context class: System.

Has main [{framer}: Framer], [{last_framer}: Framer] and [{framers}: dict] self parameters.

For system.'''
    def __init__(self):
        self.framer = Framer()
        self.last_framer = self.framer
        self.framers = {'basic': Framer(), 'temp': Framer()}
    
    @classmethod
    def get_global(cls):
        if not hasattr(cls, '_global'):
            cls._global = cls()
        return cls._global

System = SystemClass().get_global()







class SystemOP:
    '''
    # System operations
    Basic simple api for [System] and [Framer].
    '''
    def match(condition: str, 
              true_block: str, 
              false_block: str = None, 
              framer: Framer | None = None):
        'Simple condition blok for frame.'
        framer = System.framer if framer == None else framer
        framer._comentary('condition block', condition)
        cache = framer._gen_id(len(framer._aliases) + len(framer._code))
        Var(f'__condition_temp{cache}', condition, with_eval=True, framer=framer)
        framer._new_code_line(f'if __condition_temp{cache}:')
        new_true_block = ''
        for i in true_block.split('\n'):
            if i.strip() != '': new_true_block += '\n    ' + i
        framer._new_code_line(f'    {new_true_block}')
        if false_block:
            framer._new_code_line('else:')
            new_false_block = ''
            for i in false_block.split('\n'):
                if i.strip() != '': new_false_block += '\n    ' + i
            framer._new_code_line(f'    {new_false_block}')
    def to_last():
        s = System.framer
        System.framer = System.last_framer
        System.last_framer = s








class Var:
    '''
# Variable

Abstraction api class for [Framer] and [System].

### Args:

- {name}: str - name of variable.
- {value}: Any - value of variable.
- {type}: str - type hint for debug in code.
- {to_repr}: bool - if true, value in variable will be repr(value).
- {with_eval}: bool | str - if true, value in variable will be ```f'eval({repr(value)})'```, if 'literal' value in variable will be ```f'ast.literal_eval({repr(value)})'``` (literal works only after import ast)
- {framer}: Framer | None - Framer object.
- {check_for_constant}: bool - check is variable constant if true
- {add_to_code}: bool - add variable to code in format `var: type = val` if true, else pass
- {not_add_if_exist}: bool - create new variable if variable {name} is not created, else pass


### Example: 
```
ctx = Framer() # creating context
Var('x', 10, framer = ctx) # setting variable
```
    '''
    def __init__(self, 
                 name: str, 
                 value, 
                 type: str = 'int', 
                 to_repr: bool = True, 
                 with_eval: bool | str = False,
                 framer: Framer | None | str = 'System',
                 check_for_constant: bool = True,
                 add_to_code: bool = True,
                 not_add_if_exist: bool = False):
        framer: Framer = Framer() if framer == None else System.framer if isinstance(framer, str) and framer.lower().strip() == 'system' else framer
        param_name = framer._gen_temp_var()
        self.name = param_name
        self.value = value
        to_repr = True if with_eval else to_repr
        val = repr(value) if to_repr else value
        val = f'eval({val})' if with_eval == True else f'ast.literal_eval({val})' if with_eval == 'literal' else val
        with framer._lock:
            def add(type):
                if name in framer._types and check_for_constant:
                    if framer._types[name] == 'const':
                        raise VariableTypeError(f'Variable already declared with type [{type}]. It cannot be overwritten.')
                framer.var(param_name, value)
                framer._types[name] = type
                type = type.replace("const", "int")
                if add_to_code: framer._new_line(f'{name}: {type} = {val}')
                framer._aliases[name] = param_name
            if not_add_if_exist:
                cond1 = self.value in list(framer._aliases.keys())
                cond2 = self.value in list(framer._types.keys())
                if not (cond1 and cond2): add(type)
            else: add(type)
        self.framer = framer







def Get(name: str, 
        framer: Framer | None | str = 'System'):
    '''Get variable by {name} from {framer} method.'''
    framer: Framer = Framer() if framer == None else System.framer if isinstance(framer, str) and framer.lower().strip() == 'system' else framer
    return framer.get_thread_safe(name)









class Return:
    '''
# Return
 
Method to set variable to return with Exec() method.

### Args: 
- {value}: Var - Variable for return (object).
- {framer}: Framer | None - Framer object.

### Example: 
Code:
```
with Frame() as f: # creating context
    # setting variables
    x = Var('x', 10)
    y = Var('y', 50)
    res = Var('test', Get('x') * Get('y')) 
    Return(res) # setting variable to return
print('result:', Exec()) # executing code
```
Output:
```
result: 500
```'''
    def __init__(self, 
                 value: Var, 
                 framer: Framer | None | str = 'System'):
        framer: Framer = Framer() if framer == None else System.framer if isinstance(framer, str) and framer.lower().strip() == 'system' else framer
        return_var = "__frame_return_value"
        try: framer.var(return_var, f'{framer._vars.get(value.name)}')
        except AttributeError:
            raise FramerError(f'Exception in atribute parsing. \nObject [{value}, {type(value)}] has no atribute .name to create return. \nPlease, use [value] declaration like [`res = Var(...); Return(res, ...)`].')
        self.framer = framer









class Code:
    '''
# Code append

Method to append code in Framer.

### Args:
- {code}: str - code for paste to framer.
- {framer}: Framer | None - framer object.

### Example:
Code:
```
with Frame() as f:
    x = Var('x', 10)
    y = Var('y', 50)
    Code('result = x + y')
    Var('test', Get('x') * Get('y')) 
    Var('res', 'test + result', with_eval=True)
print('result:', exec_and_return(f.compile(), 'res'))
```
Output:
```
result: 560
```'''
    def __init__(self, 
                 code: str, 
                 framer: Framer | None | str = 'System'):
        framer: Framer = Framer() if framer == None else System.framer if isinstance(framer, str) and framer.lower().strip() == 'system' else framer
        framer._new_code_line(code)
        self._code = code
        self.framer = framer









def Exec(framer: Framer | None | str = 'System', algoritm: int = 1, globals: dict | str = 'locals'):
    '''
## Execution of [Frame] method.
## Args: 
- {framer}: Framer | None | str - Framer object.
- {algoritm}: int - returning all local_scope if 2, else only result var.
- {globals}: dict | str - globals for execution.
    '''
    framer: Framer = Framer() if framer == None else System.framer if isinstance(framer, str) and framer.lower().strip() == 'system' else framer
    with framer._lock:
        return framer.execute(algoritm, globals)
    








class Frame:
    '''
# Frame
Key abstraction api for all [Framer] and [System] methods.

(framer in functions is Frame.framer)

You can use with to create context, and call [Frame] object like `frame()` to get framer.

### Args of initialization:
- {framer}: str | Framer = 'new' - framer context object for frame.
- {safemode}: bool - if safemode true, Exec method will be is not available.
- {name}: str - framer name in [System.framers]
- {save_while_exit}: bool - if true, while will be called [__exit__], context will be automaticly saved.
- {save_args}: list - list of args [name, format] for method save.
- {add_ast}: bool - import ast to frame and var.with_eval - literal available if true.

### Example usage:
Code:
```
with Frame(safemode=False) as f:
    f.Var('x', 10)
    f.Var('y', 50)
    fOp.match('x > y', 'print("x bigger")', 'print("y bigger")')
    f.Var('test', f['x'] * f['y']) 
    f.Exec(2, True)
print('result:', f[''])
```
Output:
```
y bigger
result: 500
```'''
    def __init__(self, 
                 framer: str | Framer = 'new', 
                 safemode: bool = True, 
                 name: str = 'f1',
                 save_while_exit: bool = False,
                 save_args: list = ['ctx', 'pickle'], 
                 add_ast: bool = False):
        self._ast_available = add_ast
        if add_ast: self.Code('import ast')
        self.__saving = [save_while_exit, save_args]
        self.framer: Framer = Framer() if framer == 'new' else framer
        self.__safemode = safemode
        self._name = name
        System.framers[name] = self.framer
    def Sys(self) -> SystemOP: 
        '''Return [SystemOp] class.'''
        return SystemOP
    def System(self) -> SystemClass: 
        '''Return [SystemClass] class.'''
        return System
    def set_System(self, new_sys: SystemClass = SystemClass()) -> SystemClass:
        '''Set system class'''
        global System
        System = new_sys
        return self
    def Var(self, 
            name: str, 
            value, 
            type: str = 'int', 
            to_repr: bool = True,
            with_eval: bool = False,
            check_for_constant: bool = False,
            add_to_code: bool = True,
            not_add_if_exist: bool = False) -> Var:
        '''
# Variable

Set variable function. Abstraction for [Var] class.

### Args:
- {name}: str - name of variable.
- {value}: Any - value of variable.
- {type}: str - type hint for debug in code.
- {to_repr}: bool - if true, value in variable will be repr(value).
- {with_eval}: bool | str - if true, value in variable will be ```f'eval({repr(value)})'```, if 'literal' value in variable will be ```f'ast.literal_eval({repr(value)})'``` (literal works only after import ast)
- {check_for_constant}: bool - check is variable constant if true
- {add_to_code}: bool - add variable to code in format `var: type = val` if true, else pass
- {not_add_if_exist}: bool - create new variable if variable {name} is not created, else pass
```
        '''
        if with_eval == 'literal':
            raise FrameApiError('Ast.literal_eval is not avilable. ')
        return Var(name = name, 
                   value = value, 
                   type = type, 
                   to_repr = to_repr, 
                   with_eval = with_eval, 
                   framer = self.framer, 
                   check_for_constant = check_for_constant,
                   add_to_code = add_to_code,
                   not_add_if_exist = not_add_if_exist)
    def Get(self, name: str) -> Any: 
        '''Get variable by {name}.'''
        return Get(name, self.framer)
    def Return(self, name: Var) -> Return: 
        '''Set of variable name to return.'''
        return Return(name, self.framer)
    def Code(self, code: str, comentary: bool = True, *comentaries) -> Code:
        '''
## Append code to frame.
## Args: 
- {code}: str - code for append to framer.code.
- {comentary}: bool - add debug comentary to code if true.
- {*comentaries} - strings for append to debug comentary.
        '''
        if comentary:
            self.framer._comentary('code section', f'Framer: {self._name}', f'Safemode: {self.__safemode}', *comentaries)
        return Code(code, self.framer)
    def Exec(self, algoritm: int = 1, add_localscope_to_vars: bool = False, globals: dict | str = 'locals') -> Any:
        '''
## Executing code of frame.
## Args:
- {algoritm}: int[1|2] - return variable, choised by [Return] method if 1, return full localscope if 2.
- {add_localscope_to_vars}: bool - add all localscope after execution to code and variables if true.
- {globals}: dict | str['locals'] - globals for execution. `globals = locals` if {globals} is 'locals'.
        '''
        if not self.__safemode: 
            res = Exec(self.framer, algoritm if not add_localscope_to_vars else 2, globals=globals)
            if add_localscope_to_vars == False: return res
            else: 
                self.add_to_code_localscope(res)
                return res
        else: raise FrameApiError('Exec is not avialable in safemode.')
    def add_to_code_localscope(self, data: dict) -> 'Frame':
        '''
        Add variables from {data} to system vars.
        '''
        for i in data: 
            val = data[i]
            cond1 = not isinstance(val, object)
            blocked = i in ('__builtins__') # not allowed vars
            if not blocked:    
                if cond1 and i != '__builtins__': 
                    self.Var(i, val, to_repr=True)
                else: 
                    self.Var(i, val, to_repr=False, 
                            add_to_code=False, 
                            not_add_if_exist= True)
        return self
    def compile(self) -> str: 
        '''Get full code of frame.'''
        return '\n'.join(self.framer._code)
    def reset(self) -> 'Frame': 
        '''Recreate framer.'''
        self.framer = Framer()
        return self
    def register(self, name: str = None):
        'Register any construction (class/function) to frame decortor.'
        def decorator(func):
            func_name = name or func.__name__
            try:
                # geting source code
                source_code = inspect.getsource(func)
                lines = source_code.split('\n')[1:]
                tabbed = False
                can_edit_tabs = True
                tabs = 0
                cleaned_code = []
                # adaptive cleaning tabs
                for i in lines:
                    if i.startswith('   '): 
                        t = 0
                        for local_i in i: 
                            if local_i == ' ':
                                t += 1
                            if t % 4 == 0: 
                                if can_edit_tabs: tabs = t/4
                            tabbed = True
                        can_edit_tabs = False
                    if not i.startswith('   '): tabbed = False; can_edit_tabs = False
                    if tabbed: 
                        tabs_c = int(4 * tabs)
                        cleaned_code.append(i[tabs_c:])
                    else: 
                        cleaned_code.append(i)
                # adding to code
                cleaned_code = '\n'.join(cleaned_code)
                type = cleaned_code.split(" ")[0]
                self.Code(f"\n# Registred construction: {type} {func_name}\n{cleaned_code}", True, f'Registring [{type} {func_name}] construcntion.')
            except Exception as e:
                print(f"Warning: Could not register source code for {func_name}: \n{e}")
                self.Var(func_name, func, to_repr=False)
            return func
        return decorator
    def save(self, filename: str, format: str = 'pickle') -> 'Frame':
        '''
        ## Saving frame to file.
        ### Args:
        - {filename}: str - file name
        - {format}: str - saving format ('pickle' or 'json')
        '''
        data = self.data
        try:
            if format == 'pickle':
                with open(filename, 'wb') as f: pickle.dump(data, f)
            elif format == 'json':
                json_data = self._serealize('json')
                with open(filename, 'w', encoding='utf-8') as f: 
                    json.dump(json_data, f, indent=2, ensure_ascii=False)
            else: raise FrameApiError(f"Unsupported format: {format}")
            return self
        except Exception as e:
            raise FrameApiError(f"Save failed: {e}")

    def load(self, filename: str = 'ctx', format: str = 'pickle', local_safemode: bool = True) -> 'Frame':
        '''
        ## Loading frame from file.
        ### Args:
        - {filename}: str - file name
        - {format}: str - loading format ('pickle' or 'json')
        '''
        try:
            if format == 'pickle':
                with open(filename, 'rb') as f: data = pickle.load(f)
            elif format == 'json':
                with open(filename, 'r', encoding='utf-8') as f: data = json.load(f)
                restored_vars = {}
                for k, v in data['framer']['_vars'].items():
                    try: restored_vars[k] = ast.literal_eval(v)
                    except: 
                        try:
                            if not self.__safemode or not local_safemode: 
                                restored_vars[k] = eval(v)
                            else: restored_vars[k] = v
                        except Exception as e: 
                            raise FrameApiError(f'Error in parsing parameters: {e}')
                data['framer']['_vars'] = restored_vars
            else: raise FrameApiError(f"Unsupported format: {format}")
            self._load_data(data)
            return self
        except Exception as e:
            raise FrameApiError(f"Load failed: {e}")
    @property
    def data(self) -> dict:
        '''Get all data from frame like dict (for serealization).'''
        return {
                'framer': {
                    '_code': self.framer._code,
                    '_vars': self.framer._vars,
                    '_aliases': self.framer._aliases,
                    '_types': self.framer._types
                },
                'saving': self.__saving,
                'safemode': self.__safemode,
                'name': self._name
                }

    def _serealize(self, form: str = 'pickle'):
        data = self.data
        json_data = {
            'framer': {
                '_code': data['framer']['_code'],
                '_vars': {k: str(v) for k, v in data['framer']['_vars'].items()},
                '_aliases': data['framer']['_aliases'],
                '_types': data['framer']['_types']
            },
            'safemode': data['safemode'],
            'saving': data['saving'],
            'name': data['name']
        }
        return data if form == 'pickle' else json_data
    def _load_data(self, data: dict):
        self.framer._code = data['framer']['_code']
        self.framer._vars = data['framer']['_vars'] 
        self.framer._aliases = data['framer']['_aliases']
        self.framer._types = data['framer']['_types']
        self.__safemode = data['safemode']
        self.__saving = data['saving']
        self._name = data['name']
        return self
    def _get_safemode(self): return self.__safemode
    def __enter__(self): 
        self.framer.__enter__()
        return self
    def __exit__(self, *args, **kwargs): 
        if self.__saving[0]: self.save(*self.__saving[1])
    def __call__(self, *args, **kwds):
        return self.framer
    def __getitem__(self, index):
        return self.Get(index)









Var('name', 'frame_concept', framer=System.framers['basic'])

if __name__ == '__main__':
    with Frame() as f:
        x = Var('x', 10)
        y = Var('y', 50)
        SystemOP.match('x > y', 'print("x bigger")', 'print("y bigger")')
        res = Var('test', Get('x') * Get('y')) 
        Return(res)  
    print(Exec())  # → 500
    with Frame() as f:
        x = Var('x', 10)
        y = Var('y', 50)
        res = Var('res', 'x + y', with_eval=True)
        Return(res)
    print(Exec())
    code = f.compile()
    print(exec_and_return(code, 'res')) # 60
    with Frame() as f:
        x = Var('x', 10)
        y = Var('y', 50)
        Code('result = x + y')
        Var('test', Get('x') * Get('y')) 
        Var('res', 'test + result', with_eval=True)
    print(exec_and_return(f.compile(), 'res')) # 560
    with Frame(save_while_exit=True, save_args=['ctx.json', 'json']) as f:
        f.Var('x', 10)
        f.Var('y', 50)
        SystemOP.match('x > y', 'print("x bigger")', 'print("y bigger")')
        f.Var('test', Get('x') * Get('y')) 
        print(f['test'], '========')
        @f.register()
        def test(): 
            print('testing')
        @f.register()
        class Test():
            hello = 'World'
            pass
    with Frame().load('ctx.json', format='json') as f:
        code = f.compile()
        print('result:', exec_and_return(code, 'test'))
    print('\n\n======= CODE =======')
    print(code)
    with Frame(safemode=False) as f:
        f.Code('test = 10 + 2')
        f.Code('test2 = "test"')
        f.Exec(2, True)
        print(f['test']) # 12

    with Frame(safemode=False) as f:
        f.Code('''
a = 10
b = 20  
c = a + b
d = (a + b) + c
        ''')
        f.Exec(2, True)  # ← ВСЕ переменные автоматически становятся частью фрейма!
        print(f['c'])  # 30
        print(f['d'])  # результат 60


    # Пример: динамическое создание API
    with Frame(safemode=False) as f:
        f.Code('''
def create_multiplier(n):
    return lambda x: x * n
        ''')
        f.Code('test = 0.3')
        f.Exec(2, True)  # Захватываем функцию create_multiplier
        print(f['create_multiplier'](18)(13))  # 234 - ЖИВАЯ ФУНКЦИЯ!
        print(f['test'], type(f['test'])) # 0.3, float - тип не утерян!

    with Frame(safemode=False) as api_factory:
        api_factory.Code('''
def create_rest_endpoint(path, handler):
    return {"path": path, "handler": handler, "method": "GET"}
    
users_endpoint = create_rest_endpoint("/users", lambda req: {"users": []})
posts_endpoint = create_rest_endpoint("/posts", lambda req: {"posts": []})
        ''')
        api_factory.Exec(2, True)
        
        # Теперь endpoints - живые объекты в фрейме!
        print(api_factory['users_endpoint']['handler'](10))  # Работает!


    with Frame(safemode=False) as meta:
        meta.Code('''
class DynamicClass:
    def __init__(self, name):
        self.name = name
        
    @classmethod
    def create_variant(cls, suffix):
        return type(f"{cls.__name__}_{suffix}", (cls,), {})
    
variant_factory = DynamicClass.create_variant
        ''')
        meta.Exec(2, True)
        
        # Динамическое создание классов становится тривиальным
        NewClass = meta['variant_factory']('Special')
        instance = NewClass("test")
        print(instance, NewClass)

    # Интерактивный Python tutor
    with Frame(safemode=False) as lesson:
        lesson.Code('''
# Урок по функциям
def factorial(n):
    return 1 if n <= 1 else n * factorial(n-1)
        ''')
        lesson.Exec(add_localscope_to_vars= True, globals='locals')
        
        # Студенты могут исследовать результаты
        print("Факториалы:", lesson['factorial'](4))
    
    with Frame(safemode= False) as database:
        database.Code('''
tables = {}
def create_table(name, schema):
    tables[name] = {'schema': schema, 'data': []}
    
def insert_into(table, record):
    tables[table]['data'].append(record)
        ''')
        database.Exec(2, True)  # Захватываем все функции и переменные
        database['create_table']('test', {'users': []})
        print(database['tables']) # {'test': {'schema': {'users': []}, 'data': []}}
    
    with Frame(safemode=False) as database:
        database.Code('''
tables = {}
def create_table(name, schema):
    tables[name] = {'schema': schema, 'data': []}
    
def insert_into(table, record):
    tables[table]['data'].append(record)
    
def query(table, condition):
    return [record for record in tables[table]['data'] if condition(record)]
        ''')
        database.Exec(2, True)  # Захватываем все функции и переменные
        
    # Теперь database['tables'] и database['insert_into'] - живые объекты!
    database['create_table']('users', {'name': '', 'age': 0})
    database['insert_into']('users', {'name': 'John', 'age': 30})
    results = database['query']('users', lambda user: user['age'] > 25)
    database.Code(f'result = {results}')
    database.Exec(2, True)
    print(database['result']) # [{'name': 'John', 'age': 30}]

    '''
 y bigger
500
x + y
60
560
500 ========
y bigger
result: <function test at 0x102ec7a00>


======= CODE =======
import ast
x: int = 10
y: int = 50
"""condition block
x > y"""
__condition_temp_id15356: int = eval('x > y')
if __condition_temp_id15356:
    
    print("x bigger")
else:
    
    print("y bigger")
test: int = 500
"""code section
Framer: f1
Safemode: True
Registring [def test] construcntion."""

# Registred construction: def test
def test(): 
    print('testing')

"""code section
Framer: f1
Safemode: True
Registring [class Test] construcntion."""

# Registred construction: class Test
class Test():
    hello = 'World'
    pass

12
30
60
234
0.3 <class 'float'>
{'users': []}
<DynamicClass_Special object at 0x102e44050> <class 'DynamicClass_Special'>
Факториалы: 24
{'test': {'schema': {'users': []}, 'data': []}}
[{'name': 'John', 'age': 30}]   

    '''