import sys, os, shutil, json, base64
from slinn import version as slinn_version
import slinn

RED = '\u001b[31m'
GREEN = '\u001b[32m'
BLUE = '\u001b[34m'
RESET = '\u001b[0m'
BOLD = '\u001b[1m'
GRAY = '\u001b[38;2;127;127;127m'


def arg_parse(arg: str):
    return base64.urlsafe_b64decode(arg.removeprefix('b64@').encode()+b'==').decode() if arg.startswith('b64@') else arg

def splits(string: str, delimeters: list[str]=[' ', '\n'], quotes: list[str]=[]):
        result = ['']
        current_quote = ''
        for char in str(string):
            if char in quotes:
                if current_quote == '':
                    current_quote = char
                    continue
                elif current_quote == char:
                    current_quote = ''
                    continue
            if current_quote == '':
                if char in delimeters:
                    result.append('')
                else:
                    result[-1] += char
            else:
                result[-1] += char
        return result

def get_args(expecting, text):
    text = text.strip()
    if text == '':
        return {}
    args = {'not_used': []}
    Ds = ['\n', ' ']
    Qs = ['"', "'", '`']
    spls  = splits(text, Ds, Qs)
    i = 0
    while i < len(spls):
        arg = str(spls[i])
        try:
            if arg.strip().endswith('='):
                W = arg.strip().removesuffix('=').strip()
                expecting.pop(expecting.index(W))
                args[W] = arg_parse(str(splits[i + 1]))
                i += 2
                continue
            if len(splits(arg, ['='], Qs)) == 2:
                spl = splits(arg, ['='])
                W = str(spl[0])
                if W not in args.keys():
                    args[W] = arg_parse(str(spl[1]))
                    expecting.pop(expecting.index(W))
                else:
                    args[W] = [args[W]]
                    args[W].append(arg_parse(str(spl[1])))
                i += 1
                continue
        except ValueError:
            pass

        if len(expecting) == 0:
            args['not_used'].append(arg_parse(arg))
            i += 1
            continue

        E = expecting.pop(0)
        args[E] = arg_parse(arg)
        i += 1
    return args

def replace_all(text: str, sss: list[str] | str, ss2: str) -> str:
    for ss1 in sss:
        text = text.replace(ss1, ss2)
    return text

def add_quotes_to_list(lst: list[str]):
        for l in lst:
            yield f'\'{l}\''

def update():
    project = open('project.json', 'r')
    project_json = json.loads(project.read())
    project.close()
    if 'apps' not in project_json.keys():
        return print(f'Updated project.json')
    apps = []
    for app in project_json['apps']:
        if os.path.isdir(app):
            apps.append(app)
    project_json['apps'] = apps
    project = open('project.json', 'w')
    project.write(json.dumps(project_json, indent=4))
    project.close()
    return print(f'Updated project.json')

def main():
    if len(sys.argv) > 1:
        if sys.argv[1].lower() == 'run':
            from slinn import Server, Address

            def config():
                file = open('project.json')
                cfg = json.loads(file.read())
                file.close()
                return cfg
            
            def app_config(app):
                try:
                    cfg = {"debug": False}
                    file = open(f'{app}/config.json')
                    cfg.update(json.loads(file.read()))
                    file.close()
                    return cfg
                except FileNotFoundError:
                    print(f'{RED}{app}/config.json file not found{RESET}')
                    exit()


            def load_imports(apps, debug=False):
                imports = []
                for app in apps:
                    if not app_config(app)['debug'] or debug:
                        imports.append(f'import {app}')
                return imports


            def get_dispatchers(apps, debug=False):
                dispachers = []
                for app in apps:
                    if not app_config(app)['debug'] or debug:
                        dispachers.append(f'{app}.dp')
                return dispachers
            
            def app_reload(app):
                return f'global {app};{app} = importlib.reload({app});'
            
            print('Loading config...')
            cfg = config()
            debug = cfg["debug"] if "debug" in cfg.keys() else False
            apps = cfg['apps'] if 'apps' in cfg.keys() else []
            port = cfg['port'] if 'port' in cfg.keys() else 8080
            host = cfg['host'] if 'host' in cfg.keys() else ''
            timeout = float(cfg['timeout']) if 'timeout' in cfg.keys() else 0.03
            max_bytes_per_recieve = int(cfg['max_bytes_per_recieve']) if 'max_bytes_per_recieve' in cfg.keys() else 4096
            max_bytes = int(cfg['max_bytes']) if 'max_bytes' in cfg.keys() else 4294967296
            smart_navigation = cfg['smart_navigation'] if 'smart_navigation' in cfg.keys() else True
            ssl_fullchain, ssl_key = None, None
            if 'ssl' in cfg.keys() and 'fullchain' in cfg['ssl'].keys() and 'key' in cfg['ssl'].keys():
                ssl_fullchain = '"'+cfg['ssl']['fullchain']+'"' if cfg['ssl']['fullchain'] else None    
                ssl_key = '"'+cfg['ssl']['key']+'"' if cfg['ssl']['key'] else None
            dps = get_dispatchers(apps, debug)
            if dps == []:
                print(f'{RED}Dispatchers not found. Check your apps and ./project.json{RESET}')
                exit()
            global get_dir_checksum
            def get_dir_checksum(dir):
                import hashlib
                def get_dir_checksums(dir):
                    def md5(fname):
                        hash_md5 = hashlib.md5()
                        with open(fname, "rb") as f:
                            for chunk in iter(lambda: f.read(4096), b""):
                                hash_md5.update(chunk)
                        return hash_md5.hexdigest()
                    paths = os.listdir(dir)
                    checksums = []
                    for path in paths:
                        if os.path.isdir(path):
                            checksums += get_dir_checksums(dir+'/'+path)
                        elif path.endswith('.py'):
                            checksums.append(path+md5(dir+'/'+path))
                    return checksums
                return hashlib.md5(''.join([checksum for checksum in get_dir_checksums(dir)]).encode()).hexdigest()
            global checksum
            checksum = get_dir_checksum('.')

            reloader = """
def reloader(server):
    import importlib, traceback
    global checksum
    try:
        if checksum != get_dir_checksum('.'):
            checksum = get_dir_checksum('.')
            """
            for app in cfg['apps']:
                if not app_config(app)['debug'] or debug:
                    reloader += app_reload(app)
            reloader += """
            server.reload("""
            reloader += ",".join(dps)
            reloader += """)
    except Exception:
        print('During handling request, an exception has occured:')
        traceback.print_exc()
"""

            apps_info = []
            for app in cfg['apps']:
                if not app_config(app)['debug'] or debug:
                    apps_info.append(app)
                else:
                    apps_info.append('['+app+']')
            print(f'{GRAY}Apps: ' + ', '.join(apps_info))
            print('Debug mode ' + 'enabled' if debug else 'disabled')
            print('Smart navigation ' + ('enabled' if smart_navigation else 'disabled'))
            print(RESET)

            print('Starting server...')
            start = ';'.join(load_imports(apps, debug))+reloader+f'server=Server({",".join(dps)}, smart_navigation={smart_navigation}, ssl_fullchain={ssl_fullchain}, ssl_key={ssl_key}, timeout={timeout}, max_bytes_per_recieve={max_bytes_per_recieve}, max_bytes={max_bytes}, _func=reloader);server.listen(Address({port}, "{host}"))'
            exec(start)
        elif sys.argv[1].lower() == 'create':
            args = get_args(['name', 'host'], ' '.join(sys.argv[2:]))
            if 'name' not in args.keys():
                return print(f'{RED}The app`s name is not specified{RESET}')
            ensure_appname = replace_all(args['name'], '-&$#!@%^().,', '_')
            if os.path.isdir(ensure_appname):
                return print(f'{BLUE}The app named {args["name"]} exists{RESET}')
            if 'host' not in args.keys():
                print(f'{BLUE}Hosts were not specified')
            os.mkdir(ensure_appname)
            with open(f'{ensure_appname}/__init__.py', 'w') as f:
                data = """import sys, importlib
if '%appname%.app' not in sys.modules.keys():
    from %appname%.app import dp
else:
    dp = importlib.reload(sys.modules['%appname%.app']).dp
""".replace('%appname%', ensure_appname)
                f.write(data)
            with open(f'{ensure_appname}/app.py', 'w') as f:
                data = """from slinn import Dispatcher, Filter, HttpResponse
 
dp = Dispatcher(%hosts%)

# Write your code down here                         
""".replace('%appname%', ensure_appname).replace('%hosts%', '' if 'host' not in args.keys() else ', '.join(add_quotes_to_list(args['host'] if type(args['host']) == list else [args['host']])))
                f.write(data)
            with open(f'{ensure_appname}/config.json', 'w') as f:
                data = """
{
    "debug": false
}
"""
                f.write(data)
            fr = open('project.json', 'r')
            fj = json.loads(fr.read())
            fr.close()
            if 'apps' in fj.keys():
                fj['apps'].insert(0, ensure_appname)
            else:
                fj['apps'] = []
            fw = open('project.json', 'w')
            fw.write(json.dumps(fj, indent=4))
            fw.close()
            update()
            print(f'{GREEN}App successfully created{RESET}')
        elif sys.argv[1].lower() == 'delete':
            args = get_args(['name'], ' '.join(sys.argv[2:]))
            apppath = (args['path']+'?').replace('/?', '').replace('?', '') if 'path' in args.keys() else '.'
            if 'name' not in args.keys():
                return print(f'{RED}The app`s name is not specified{RESET}')
            ensure_appname = replace_all(args['name'], '-&$#!@%^().,', '_')
            if not os.path.isdir(ensure_appname):
                return print(f'{BLUE}The app named {args["name"]} does not exist{RESET}')
            if input(f'{RESET}Are you sure? (y/N) >>> ').lower() not in ['y', 'yes']:
                return print(f'{RESET}Aborted')
            shutil.rmtree(ensure_appname)
            if os.path.isdir(f'{apppath}/templates_data/{ensure_appname}'):
                shutil.rmtree(f'{apppath}/templates_data/{ensure_appname}')
            if len(os.listdir(f'{apppath}/templates_data')) == 0:
                shutil.rmtree(f'{apppath}/templates_data')
            update()
            return print(f'{GREEN}App successfully deleted{RESET}')
        elif sys.argv[1].lower() == 'update':
            return update()
        elif sys.argv[1].lower() == 'help':
            return print("""%BOLD%Slinn manager help page

Commands%RESET%:
    %cmd% run                                                              %GRAY%# Starts server%RESET%
    %cmd% create {app`s name} host=(host1) host=(host2)...                 %GRAY%# Creates a new app
        Example: %cmd% create localhost host=localhost host=127.0.0.1%RESET%
    %cmd% delete {app`s name} (project`s path)                             %GRAY%# Deletes an app
        Example: %cmd% delete localhost%RESET%
    %cmd% template {template`s name} (projects`s path)                     %GRAY%# Installs a template
        Example: %cmd% template firstrun%RESET%
    %cmd% update                                                           %GRAY%# Updates project.py%RESET%
    %cmd% migrate_app {app`s name}                                         %GRAY%# Migrates app(check slinn.guides.migration1xx2xx.migration1xx2xx)%RESET%
    %cmd% help                                                             %GRAY%# Prints this help%RESET%
    %cmd% version                                                          %GRAY%# Prints version of Slinn%RESET%
""".replace('%cmd%', f'py {sys.argv[0]}').replace('%GRAY%', GRAY).replace('%RESET%', RESET).replace('%BOLD%', BOLD))
        elif sys.argv[1].lower() == 'version':
            return print(slinn_version)
        elif sys.argv[1].lower() == 'template':
            args = get_args(['name', 'path'], ' '.join(sys.argv[2:]))
            apppath = (args['path']+'?').replace('/?', '').replace('?', '') if 'path' in args.keys() else '.'
            if 'name' not in args.keys():
                return print(f'{RED}Template name is not specified{RESET}')
            modulepath = os.path.abspath(slinn.__file__).replace('__init__.py', '')
            fr = open('project.json', 'r')
            fj = json.loads(fr.read())
            fr.close()
            if 'apps' in fj.keys():
                if args['name'] in fj['apps']:
                    return print(f'{BLUE}Template {args["name"]} has already installed{RESET}')
                fj['apps'].insert(0, args['name'])
            else:
                fj['apps'] = []
            fw = open('project.json', 'w')
            fw.write(json.dumps(fj, indent=4))
            fw.close()
            try:
                shutil.copytree(f'{modulepath}templates/{args["name"]}/', f'{apppath}/{args["name"]}', ignore=shutil.ignore_patterns('data'))
                try:
                    if not os.path.isdir(f'{apppath}/templates_data'):
                        os.mkdir(f'{apppath}/templates_data')
                    shutil.copytree(f'{modulepath}templates/{args["name"]}/data/', f'{apppath}/templates_data/{args["name"]}')
                except  FileExistsError:
                    pass
                except FileNotFoundError:
                    pass
                update()
                return print(f'{GREEN}Template {args["name"]} successfully installed{RESET}') 
            except FileExistsError:
                return print(f'{BLUE}Template {args["name"]} has already installed{RESET}')
            except FileNotFoundError:
                return print(f'{BLUE}Template {args["name"]} not found{RESET}')
        elif sys.argv[1].lower() == 'migrate_app':
            args = get_args(['name'], ' '.join(sys.argv[2:]))
            if 'name' not in args.keys():
                return print(f'{RED}The app`s name is not specified{RESET}')
            ensure_appname = replace_all(args['name'], '-&$#!@%^().,', '_')
            if not os.path.isdir(ensure_appname):
                return print(f'{BLUE}The app named {args["name"]} does not exist{RESET}')
            with open(f'{ensure_appname}/__init__.py', 'w') as f:
                data = """import sys, importlib
if '%appname%.app' not in sys.modules.keys():
    from %appname%.app import dp
else:
    dp = importlib.reload(sys.modules['%appname%.app']).dp
""".replace('%appname%', ensure_appname)
                f.write(data)
            with open(f'{ensure_appname}/config.json', 'w') as f:
                data = """
{
    "debug": false
}
"""
                f.write(data)
            print(f'{GREEN}App successfully migrated{RESET}')
        else:
            return print(f'{RED}Command {sys.argv[1].lower()} is not exists{RESET}')
    else:
        return print(f'{RED}Command was not specified{RESET}')

            
if __name__ == '__main__':
    main()