import os
import uuid
import json
import requests
import socket
import subprocess
import base64
from django.conf import settings
from django.shortcuts import render
from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
from django.contrib.auth.models import User
from django.shortcuts import redirect
from django.db.models import Q
from .models import Profile, LoginToken, Task, TaskStatuses, Container, Computing, KeyPair, Page
from .utils import send_email, format_exception, timezonize, os_shell, booleanize, get_task_tunnel_host
from .utils import get_task_proxy_host, random_username, setup_tunnel_and_proxy, finalize_user_creation
from .utils import sanitize_container_env_vars, get_or_create_container_from_repository
from .decorators import public_view, private_view
from .exceptions import ErrorMessage

# Setup logging
import logging
logger = logging.getLogger(__name__)


# Task cache
_task_cache = {}


#====================
#  Page views
#====================

@public_view
def login_view(request):

    data = {}
    
    # Set post login page
    post_login_page = request.COOKIES.get('post_login_redirect')
    if post_login_page is None:
        post_login_page = '/main'

    # If authenticated user reloads the main URL
    if request.method == 'GET' and request.user.is_authenticated:
        response = HttpResponseRedirect(post_login_page)
        response.delete_cookie('post_login_redirect')
        return response
    else:
        # If local auth disabled, just redirect to OIDC
        if settings.DISABLE_LOCAL_AUTH:
            return HttpResponseRedirect('/oidc/authenticate/')

    # If unauthenticated user tries to log in
    if request.method == 'POST':
        if not request.user.is_authenticated:
            username = request.POST.get('username')
            password = request.POST.get('password')
            # Use Django's machinery to attempt to see if the username/password
            # combination is valid - a User object is returned if it is.

            if "@" in username:
                # Get the username from the email
                try:
                    user = User.objects.get(email=username)
                    username = user.username
                except User.DoesNotExist:
                    if password:
                        raise ErrorMessage('Check email and password')
                    else:
                        # Return here, we don't want to give any hints about existing users
                        data['success'] = 'Ok, if we have your data you will receive a login link by email shortly.'
                        return render(request, 'success.html', {'data': data})

            if password:
                if user.profile.auth != 'local':
                    # This actually hides that the user cannot be authenticated using the local auth.
                    raise ErrorMessage('Check email and password')
                user = authenticate(username=username, password=password)
                if user:
                    login(request, user)
                    response = HttpResponseRedirect(post_login_page)
                    response.delete_cookie('post_login_redirect')
                    return response
                else:
                    raise ErrorMessage('Check email and password')
            else:

                # If empty password and local auth, send mail with login token
                if user.profile.auth == 'local':

                    logger.debug('Sending login token via mail to {}'.format(user.email))
    
                    token = uuid.uuid4()
    
                    # Create token or update if existent (and never used)
                    try:
                        loginToken = LoginToken.objects.get(user=user)
                    except LoginToken.DoesNotExist:
                        LoginToken.objects.create(user=user, token=token)
                    else:
                        loginToken.token = token
                        loginToken.save()
                    try:
                        send_email(to=user.email, subject='Rosetta login link', text='Hello,\n\nhere is your login link: https://{}/login/?token={}\n\nOnce logged in, you can go to "My Account" and change password (or just keep using the login link feature).\n\nThe Rosetta Team.'.format(settings.ROSETTA_HOST, token))
                    except Exception as e:
                        logger.error(format_exception(e))
                        raise ErrorMessage('Something went wrong. Please retry later.')
    
                    # Return here, we don't want to give any hints about existing users
                    data['success'] = 'Ok, if we have your data you will receive a login link by email shortly.'
                    return render(request, 'success.html', {'data': data})


        else:
            # This should never happen.
            # User tried to log-in while already logged in: log him out and then render the login
            logout(request)

    else:
        # If we are logging in through a token
        token = request.GET.get('token', None)

        if token:

            loginTokens = LoginToken.objects.filter(token=token)

            if not loginTokens:
                raise ErrorMessage('Token not valid or expired')


            if len(loginTokens) > 1:
                raise Exception('Consistency error: more than one user with the same login token ({})'.format(len(loginTokens)))

            # Use the first and only token (todo: use the objects.get and correctly handle its exceptions)
            loginToken = loginTokens[0]

            # Get the user from the table
            user = loginToken.user

            # Set auth backend
            user.backend = 'django.contrib.auth.backends.ModelBackend'

            # Ok, log in the user
            login(request, user)
            loginToken.delete()

            # Now redirect to site
            response = HttpResponseRedirect(post_login_page)
            response.delete_cookie('post_login_redirect')
            return response

    # All other cases, render the login page again with no other data than title
    return render(request, 'login.html', {'data': data})


@private_view
def logout_view(request):
    logout(request)
    return HttpResponseRedirect('/')


@public_view
def register_view(request):

    data = {}

    # If authenticated user reloads the main URL
    if request.method == 'GET' and request.user.is_authenticated:
        return HttpResponseRedirect('/main/')

    # If unauthenticated register if post
    if request.method == 'POST':
        if not request.user.is_authenticated:
            email    = request.POST.get('email')
            password = request.POST.get('password')
            invitation = request.POST.get('invitation')
            
            if settings.INVITATION_CODE:
                if invitation != settings.INVITATION_CODE:
                    raise ErrorMessage('Wrong invitation code')

            if '@' not in email:
                raise ErrorMessage('Detected invalid email address')
            
            # Register the user
            user = User.objects.create_user(random_username(), password=password, email=email)

            # Is this necessary?
            user.save()
            
            data['user'] = user

            finalize_user_creation(user)

            # Manually set the auth backend for the user
            user.backend = 'django.contrib.auth.backends.ModelBackend'
            login(request, user)
            
            data['status'] = 'activated'

    # All other cases, render the login page again with no other data than title
    return render(request, 'register.html', {'data': data})



@public_view
def entrypoint(request):
    return HttpResponseRedirect('/main/')



@public_view
def main_view(request):

    # Init data
    data = {}
    
    # Get custom home page if any
    try:
        page = Page.objects.get(id='main')
        data['page'] = page
    except Page.DoesNotExist:
        pass

    return render(request, 'main.html', {'data': data})


@public_view
def page_view(request, page_id):

    # Init data
    data = {}
    
    # Get the page
    try:
        page = Page.objects.get(id=page_id)
        data['page'] = page
    except Page.DoesNotExist:
        return HttpResponseNotFound('Page not found')

    return render(request, 'page.html', {'data': data})



#====================
# Account view
#====================

@private_view
def account(request):

    data={}
    data['user'] = request.user
    try:
        profile = Profile.objects.get(user=request.user)
    except Profile.DoesNotExist:
        profile = Profile.objects.create(user=request.user)
    data['profile'] = profile

    # Set values from POST and GET
    edit = request.POST.get('edit', None)
    if not edit:
        edit = request.GET.get('edit', None)
        data['edit'] = edit
    value = request.POST.get('value', None)

    # Fix None
    if value and value.upper() == 'NONE':
        value = None
    if edit and edit.upper() == 'NONE':
        edit = None

    # Set data.default_public_key
    with open(KeyPair.objects.get(user=request.user, default=True).public_key_file) as f:
        data['default_public_key'] = f.read()

    # Add computings (for extra confs)
    if request.user.profile.extra_confs:
        data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user))

    # Edit values
    if edit and value:
        try:
            logger.info('Setting "{}" to "{}"'.format(edit,value))

            # Timezone
            if edit=='timezone' and value:
                # Validate
                timezonize(value)
                profile.timezone = value
                profile.save()

            # Email
            elif edit=='email' and value:
                # If no local auth, you should never get here
                if request.user.profile.auth != 'local':
                    raise ErrorMessage('Cannot change password using an external authentication service')
                request.user.email=value
                request.user.save()

            # Password
            elif edit=='password' and value:
                # If no local auth, you should never get here
                if request.user.profile.auth != 'local':
                    raise ErrorMessage('Cannot change password using an external authentication service')
                request.user.set_password(value)
                request.user.save()

            # Generic property
            elif edit and value:
                raise Exception('Attribute to change is not valid')


        except Exception as e:
            logger.error(format_exception(e))
            data['error'] = 'The property "{}" does not exists or the value "{}" is not valid.'.format(edit, value)
            return render(request, 'error.html', {'data': data})

    # Lastly, do we have to remove an extra conf?
    
    delete_extra_conf_uuid = request.GET.get('delete_extra_conf_uuid', None)
    if delete_extra_conf_uuid:
        #logger.debug('Deleting extra conf "{}"'.format(delete_extra_conf_uuid))
        new_extra_confs = {}
        for extra_conf_uuid in profile.extra_confs:
            if extra_conf_uuid != delete_extra_conf_uuid:
                new_extra_confs[extra_conf_uuid] = profile.extra_confs[extra_conf_uuid]
        profile.extra_confs = new_extra_confs
        profile.save()
        return redirect('/account')
                
            
    
    return render(request, 'account.html', {'data': data})




#=========================
#  Tasks view
#=========================

def set_verified_status(task):
    # Chech status with ping
    if task.status == 'running':
        logger.debug('Task is running, check if startup completed')

        logger.debug('Trying to establish connection on: "{}:{}"'.format(task.interface_ip,task.interface_port))
        s = socket.socket()
        try:
            s.settimeout(1)
            s.connect((task.interface_ip, task.interface_port))
            # Not necessary, we just check that the container interfcae is up
            #if not s.recv(10):
            #    logger.debug('No data read from socket')
            #    raise Exception('Could not read any data from socket')
        except Exception as e:
            logger.debug('Could not connect to socket')
            task.verified_status = 'starting up...'
        else:
            task.verified_status = 'running'
        finally:
            s.close()
    else:
        task.verified_status = task.status

@private_view
def tasks(request):

    # Init data
    data={}
    data['user']  = request.user
    data['profile'] = Profile.objects.get(user=request.user)
    data['title'] = 'Tasks'

    # Get action if any
    action  = request.GET.get('action', None)
    uuid    = request.GET.get('uuid', None)
    fromlist = request.GET.get('fromlist', False)
    details = booleanize(request.GET.get('details', None))
    

    # Do we have to operate on a specific task?
    if uuid:

        try:
            
            # Get the task (raises if none available including no permission)
            try:
                task = Task.objects.get(user=request.user, uuid=uuid)
            except Task.DoesNotExist:
                raise ErrorMessage('Task does not exists or no access rights')
            
            set_verified_status(task)
            data['task'] = task
            
            #  Task actions
            if action=='delete':
                if task.status not in [TaskStatuses.stopped, TaskStatuses.exited]:
                    try:
                        task.computing.manager.stop_task(task)
                    except:
                        pass
                try:
                    # Get the task (raises if none available including no permission)
                    task = Task.objects.get(user=request.user, uuid=uuid)

                    # Re-remove proxy files before deleting the task itself just to be sure
                    try:
                        os.remove('/shared/etc_apache2_sites_enabled/{}.conf'.format(task.uuid))
                    except:
                        pass
                    try:
                        os.remove('/shared/etc_apache2_sites_enabled/{}.htpasswd'.format(task.uuid))
                    except:
                        pass
    
                    # Delete
                    task.delete()
                    
                    # Unset task
                    data['task'] = None    
    
                except Exception as e:
                    data['error'] = 'Error in deleting the task'
                    logger.error('Error in deleting task with uuid="{}": "{}"'.format(uuid, e))
                    return render(request, 'error.html', {'data': data})
                

    
            elif action=='stop': # or delete,a and if delete also remove object
                # Remove proxy files. Do it here or will cause issues when reloading the conf re-using ports of stopped tasks.
                try:
                    os.remove('/shared/etc_apache2_sites_enabled/{}.conf'.format(task.uuid))
                except:
                    pass
                try:
                    os.remove('/shared/etc_apache2_sites_enabled/{}.htpasswd'.format(task.uuid))
                except:
                    pass
                   
                task.computing.manager.stop_task(task)

        except Exception as e:
            data['error'] = 'Error in getting the task or performing the required action'
            logger.error('Error in getting the task with uuid="{}" or performing the required action: "{}"'.format(uuid, e))
            return render(request, 'error.html', {'data': data})

        # Ok, redirect if there was an action
        if action:
            if fromlist:
                return redirect('/tasks')
            else:
                if not task.uuid:
                    # it has just been deleted
                    return redirect('/tasks')
                else:
                    return redirect('/tasks/?uuid={}'.format(task.uuid))


    # Do we have to list all the tasks?
    if not uuid or (uuid and not details):

        # Get all tasks for list
        try:
            tasks = Task.objects.filter(user=request.user).order_by('created') 
        except Exception as e:
            data['error'] = 'Error in getting Tasks info'
            logger.error('Error in getting Virtual Devices: "{}"'.format(e))
            return render(request, 'error.html', {'data': data})
    
        # Update task statuses
        for task in tasks:
            task.update_status()
            set_verified_status(task)
        
        # Set task and tasks variables
        data['task']  = None   
        data['tasks'] = tasks

    return render(request, 'tasks.html', {'data': data})


#=========================
#  New task
#=========================

@private_view
def new_task(request):

    # Init data
    data={}
    data['user']    = request.user

    # Get task container helper function
    def get_task_container(request):
        task_container_uuid = request.POST.get('task_container_uuid', None)
        if not task_container_uuid:
            # At the second step the task uuid is set via a GET request 
            task_container_uuid = request.GET.get('task_container_uuid', None)
        try:
            task_container = Container.objects.get(uuid=task_container_uuid, user=None)
        except Container.DoesNotExist:
            try:
                task_container =  Container.objects.get(uuid=task_container_uuid, user=request.user)
            except Container.DoesNotExist:
                raise Exception('Consistency error, container with uuid "{}" does not exists or user "{}" does not have access rights'.format(task_container_uuid, request.user.email))
        return task_container

    # Get task computing helper function
    def get_task_computing(request):
        task_computing_uuid = request.POST.get('task_computing_uuid', None)
        try:
            task_computing = Computing.objects.get(uuid=task_computing_uuid, group=None)
        except Computing.DoesNotExist:
            try:
                task_computing =  Computing.objects.get(uuid=task_computing_uuid, group__user=request.user)
            except Computing.DoesNotExist:
                raise Exception('Consistency error, computing with uuid "{}" does not exists or user "{}" does not have access rights'.format(task_computing_uuid, request.user.email))        
        return task_computing

    # Get task name helper function
    def get_task_name(request):
        task_name = request.POST.get('task_name', None)
        if not task_name:
            raise ErrorMessage('Missing task name')
        return task_name

    # Get step if any, check both POST and GET
    step = request.POST.get('step', None)
    if not step:
        step = request.GET.get('step', None)
    

    # Handle the various steps
    if not step:
        
        # Step one is assumed: chose software container
        return HttpResponseRedirect('/software/?mode=new_task')
        
    elif step == 'two':
        
        # Get software container and arch
        data['task_container'] = get_task_container(request)

        # List all computing resources 
        data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user))
            
        data['step'] = 'two'
        data['next_step'] = 'three'

    elif step == 'three':

        # Get software container and arch
        data['task_container'] = get_task_container(request)

        # Get computing resource
        data['task_computing'] = get_task_computing(request)
        
        # Check that container required architecture is compatible with the computing resource
        # TODO: support setting the container engine/engine when creating the task
        # TODO: refactor and unroll this code
        if data['task_computing'].supported_archs is None: data['task_computing'].supported_archs=[]
        if data['task_computing'].emulated_archs is None: data['task_computing'].emulated_archs={}
        data['arch_emulation'] = False
        
        if data['task_container'].image_arch:
            if (data['task_container'].image_arch != data['task_computing'].arch) and (data['task_container'].image_arch not in data['task_computing'].supported_archs):

                # Does container engines/engines support emulated archs?
                if data['task_computing'].emulated_archs:
                                        
                    # For now by default our container engine is the first one
                    container_engine = data['task_computing'].container_engines[0]
                    
                    # Check for emulation against the engine
                    if container_engine in data['task_computing'].emulated_archs and data['task_container'].image_arch in data['task_computing'].emulated_archs[container_engine]:
                        data['arch_emulation'] = True

                    # Check for emulation against the engine
                    def get_engines(container_engine):
                        if not '[' in container_engine:
                            return None
                        else:
                            container_engines = container_engine.split('[')[1].replace(']','').split(',')
                            return container_engines
                        
                    for container_engine in get_engines(container_engine):
                        if container_engine in data['task_computing'].emulated_archs and data['task_container'].image_arch in data['task_computing'].emulated_archs[container_engine]:
                            data['arch_emulation'] = True

                    if not data['arch_emulation']:
                        raise ErrorMessage('This computing resource does not support architecture \'{}\' nor as native or emulated'.format(data['task_container'].image_arch))
                
                else:
                    raise ErrorMessage('This computing resource does not support architecture \'{}\' nor as native or emulated'.format(data['task_container'].image_arch))

        else:
            data['arch_auto_selection'] = True
            #raise ErrorMessage('Auto selecting architectures is not supported yet')

        # Generate random auth token        
        data['task_auth_token'] = str(uuid.uuid4())

        # Set current and next step
        data['step'] = 'three'
        data['next_step'] = 'last'


    elif step == 'last':

        # Get software container and arch
        data['task_container'] = get_task_container(request)

        # Get computing resource
        data['task_computing'] = get_task_computing(request)

        # Get task name
        data['task_name'] = get_task_name(request)

        # Generate the task uuid
        task_uuid = str(uuid.uuid4())

        # Create the task object
        task = Task(uuid      = task_uuid,
                    user      = request.user,
                    name      = data['task_name'],
                    status    = TaskStatuses.created,
                    container = data['task_container'],
                    computing = data['task_computing'])

        # Add auth
        task_auth_password = request.POST.get('task_auth_password', None)
        if task_auth_password and not request.user.profile.is_power_user:
            raise ErrorMessage('Sorry, only power users can set a custom task password.')
        task_auth_token = request.POST.get('task_auth_token', None)
        if task_auth_password:
            if task_auth_password != task_auth_token: # Just an extra check probably not much useful
                if not task_auth_password:
                    raise ErrorMessage('No task password set')
                if len(task_auth_password) < 6:
                    raise ErrorMessage('Task password must be at least 6 chars')
                task.password = task_auth_password # Not stored in the ORM model, just a temporary var.
        else:
            task.auth_token = task_auth_token # This is saved on the ORM model
            task.password = task_auth_token # Not stored

        # Any task requires the TCP tunnel for now
        task.requires_tcp_tunnel = True
        
        # Task access method
        access_method = request.POST.get('access_method', 'auto')
        if access_method and access_method != 'auto' and not request.user.profile.is_power_user:
            raise ErrorMessage('Sorry, only power users can set a task access method other than \'auto\'.')
        if access_method == 'auto':
            if task.container.interface_protocol in ['http','https']:
                task.requires_proxy      = True
                task.requires_proxy_auth = True
            else:
                task.requires_proxy      = False
                task.requires_proxy_auth = False                
        elif access_method == 'direct_tunnel':
            task.requires_proxy      = False
            task.requires_proxy_auth = False            
        elif access_method == 'https_proxy':
            task.requires_proxy      = True
            task.requires_proxy_auth = True
        else:
            raise ErrorMessage('Unknown access method "{}"'.format(access_method))

        # Computing options
        computing_options = {}
        
        # Container engine if any set
        container_engine = request.POST.get('container_engine', None)
        if container_engine:
            if not container_engine in data['task_computing'].container_engines:
                raise ErrorMessage('Unknown container engine "{}"'.format(container_engine))
            computing_options['container_engine'] = container_engine

        # CPUs, memory and partition if set 
        computing_cpus = request.POST.get('computing_cpus', None)
        computing_memory = request.POST.get('computing_memory', None)
        computing_partition = request.POST.get('computing_partition', None)
        
        if computing_cpus:
            try:
                int(computing_cpus)
            except:
                raise Exception('Cannot convert computing_cpus to int')
            computing_options['cpus'] = int(computing_cpus)

        if computing_memory:
            computing_options['memory'] = computing_memory

        if computing_partition:
            computing_options['partition'] = computing_partition        
        
        if computing_options:
            task.computing_options = computing_options
                    
        # Save the task before starting it, or the computing manager will not be able to work properly
        task.save()

        # Start the task
        try:
            task.computing.manager.start_task(task)
        except:
            # Delete the task if could not start it
            task.delete()
            
            # ..and re-raise
            raise

        # Ensure proxy conf directory exists
        if not os.path.exists('/shared/etc_apache2_sites_enabled'):
            os.makedirs('/shared/etc_apache2_sites_enabled')
    
        # Add here proxy auth file as we have the password
        if task.requires_proxy_auth:
            out = os_shell('ssh -o StrictHostKeyChecking=no proxy "cd /shared/etc_apache2_sites_enabled/ && htpasswd -bc {}.htpasswd {} {}"'.format(task.uuid, task.user.email, task.password), capture=True)
            if out.exit_code != 0:
                logger.error(out.stderr)
                raise ErrorMessage('Something went wrong when enabling proxy auth')   

        # Set step        
        data['step'] = 'created'


    return render(request, 'new_task.html', {'data': data})


#=========================
#  Task log
#=========================

@private_view
def task_log(request):

    # Init data
    data={}
    data['user']  = request.user
    data['profile'] = Profile.objects.get(user=request.user)
    data['title'] = 'Tasks'

    # Get uuid and refresh if any
    uuid    = request.GET.get('uuid', None)
    refresh = request.GET.get('refresh', None)

    if not uuid:
        return render(request, 'error.html', {'data': 'uuid not set'})

    # Get the task (raises if none available including no permission)
    task = Task.objects.get(user=request.user, uuid=uuid)

    # Set back task and refresh
    data['task']    = task 
    data['refresh'] = refresh

    # Get the log
    try:

        data['log'] = task.computing.manager.get_task_log(task)

    except Exception as e:
        data['error'] = 'Error in viewing task log'
        logger.error('Error in viewing task log with uuid="{}": "{}"'.format(uuid, e))
        raise

    return render(request, 'task_log.html', {'data': data})





#=========================
#  Software containers
#=========================

@private_view
def software(request):

    # Init data
    data={}
    data['user']    = request.user
    data['profile'] = Profile.objects.get(user=request.user)

    # Get action if any
    container_uuid = request.GET.get('container_uuid', None)
    container_family_id = request.GET.get('container_family_id', None)
    action = request.GET.get('action', None)
    details = booleanize(request.GET.get('details', False))


    # Get filter/search if any
    search_text   = request.POST.get('search_text', '')
    search_owner  = request.POST.get('search_owner', 'All')

    # Set back to page data
    data['search_owner'] = search_owner
    data['search_text']  = search_text
    data['details'] = details

    # Are we using this page as first step of a new task?
    data['mode'] = request.GET.get('mode', None)
    if not data['mode']:
        data['mode'] = request.POST.get('mode', None)


    # Do we have to operate on a specific container, or family of containers?
    if container_uuid:

        try:

            # Get the container (raises if none available including no permission)
            try:
                container = Container.objects.get(uuid=container_uuid)
            except Container.DoesNotExist:
                raise ErrorMessage('Container does not exists or no access rights')                
            if container.user and container.user != request.user:
                raise ErrorMessage('Container does not exists or no access rights')
            data['container'] = container

            # Container actions
            if action and action=='delete':

                # Delete
                container.delete()
                
                # Redirect
                return HttpResponseRedirect('/software')

        except Exception as e:
            data['error'] = 'Error in getting the software container or performing the required action'
            logger.error('Error in getting container with uuid="{}" or performing the required action: "{}"'.format(uuid, e))
            return render(request, 'error.html', {'data': data})
    
    else:
        # Ddo we have to operate on a container family?
        if container_family_id:
            
            # Get back name, registry and image from contsainer url
            container_name, container_registry, container_image_name = base64.b64decode(container_family_id.encode('utf8')).decode('utf8').split('\t')
          
            # get containers from the DB
            user_containers = Container.objects.filter(user=request.user, name=container_name, registry=container_registry, image_name=container_image_name)
            platform_containers = Container.objects.filter(user=None, name=container_name, registry=container_registry, image_name=container_image_name)
        
        else:    
            
            # Get containers (fitered by search term, or all)
            if search_text:
                search_query=(Q(name__icontains=search_text) | Q(description__icontains=search_text) | Q(image_name__icontains=search_text))
                user_containers = Container.objects.filter(search_query, user=request.user)
                platform_containers = Container.objects.filter(search_query, user=None)
            else:
                user_containers = Container.objects.filter(user=request.user)
                platform_containers = Container.objects.filter(user=None)
    
            
        # Ok, nilter by owner
        if search_owner != 'All':
            if search_owner == 'User':
                platform_containers =[]
            if search_owner == 'Platform':
                user_containers = []
                
        # Create all container list
        data['containers'] = list(user_containers) + list(platform_containers)
            
        # Merge containers with the same name, registry and image name
        data['container_families'] = {}
        
        # Container family support class
        class ContainerFamily(object):
        
            def __init__(self, id, name, registry, image_name):
                self.id = id
                self.name = name
                self.registry = registry
                self.image_name = image_name
                self.description = None
                self.members = []
                self.all_archs = []
                self.container_by_tags_by_arch = {} 
    
            def add(self, container):
                self.members.append(container)
    
                if not self.description:
                    self.description = container.description
    
                if not container.image_arch in self.all_archs:
                    self.all_archs.append(container.image_arch)
    
                if not container.image_arch in self.container_by_tags_by_arch:
                    self.container_by_tags_by_arch[container.image_arch]={}
                self.container_by_tags_by_arch[container.image_arch][container.image_tag] = container
                
                # Lastly, add the container to the "all tags"
                #if None not in self.container_by_tags_by_arch:
                #    self.container_by_tags_by_arch[None]={}
                #self.container_by_tags_by_arch[None][container.image_tag] = container
    
            
            @ property
            def color(self):
                try:
                    return self.members[0].color
                except IndexError:
                    return '#000000'
            
        # Populate container families
        for container in data['containers']:
            if container.family_id not in data['container_families']:
                data['container_families'][container.family_id] = ContainerFamily(container.family_id, container.name, container.registry, container.image_name)
            data['container_families'][container.family_id].add(container)
        # Finalize the families
        #for container.family_id in data['container_families']:
        #    if len(data['container_families'][container.family_id].all_archs) == 1:
        #        if data['container_families'][container.family_id].all_archs[0] != None:
        #            data['container_families'][container.family_id].container_by_tags_by_arch.pop(None)
                
    return render(request, 'software.html', {'data': data})



#=========================
#  Add software container
#=========================

@private_view
def add_software(request):

    # Init data
    data = {}
    data['user'] = request.user
    
    # Loop back the new container mode in the page to handle the switch
    data['new_container_from'] = request.GET.get('new_container_from', 'registry')

    # Container name if setting up a new container
    container_name = request.POST.get('container_name', None)

    if container_name:
        
        # How do we have to add this new container?
        new_container_from = request.POST.get('new_container_from', None)

        if new_container_from == 'registry':
    
            # Container description
            container_description = request.POST.get('container_description', None)
    
            # Container registry
            container_registry = request.POST.get('container_registry', None)
    
            # Container image name
            container_image_name = request.POST.get('container_image_name',None)
            
            # Container image tag
            container_image_tag = request.POST.get('container_image_tag', None)
    
            # Container image architecture
            container_image_arch = request.POST.get('container_image_arch', None)
    
            # Container image OS 
            container_image_os = request.POST.get('container_image_os', None)
    
            # Container image digest
            container_image_digest = request.POST.get('container_image_digest', None)
    
            # Container interface port
            container_interface_port = request.POST.get('container_interface_port', None) 
            if container_interface_port:       
                try:
                    container_interface_port = int(container_interface_port)
                except:
                    raise ErrorMessage('Invalid container port "{}"')
            else:
                container_interface_port = None
    
            # Container interface protocol 
            container_interface_protocol = request.POST.get('container_interface_protocol', None)
    
            if container_interface_protocol and not container_interface_protocol in ['http','https']:
                raise ErrorMessage('Sorry, only power users can add custom software containers with interface protocols other than \'http\' or \'https\'.')
    
            # Container interface transport 
            container_interface_transport = request.POST.get('container_interface_transport')
    
            # Capabilities
            container_supports_custom_interface_port = request.POST.get('container_supports_custom_interface_port', None)
            if container_supports_custom_interface_port and container_supports_custom_interface_port == 'True':
                container_supports_custom_interface_port = True
            else:
                container_supports_custom_interface_port = False
    
            container_supports_interface_auth = request.POST.get('container_supports_interface_auth', None)
            if container_supports_interface_auth and container_supports_interface_auth == 'True':
                container_supports_pass_auth = True
            else:
                container_supports_pass_auth = False
    
            # Environment variables
            container_env_vars = request.POST.get('container_env_vars', None)
            if container_env_vars:
                container_env_vars = sanitize_container_env_vars(json.loads(container_env_vars))
    
            # Log
            #logger.debug('Creating new container object with image="{}", type="{}", registry="{}", ports="{}"'.format(container_image, container_type, container_registry, container_ports))
    
            # Create
            Container.objects.create(user         = request.user,
                                     name         = container_name,
                                     description  = container_description,
                                     registry     = container_registry,
                                     image_name   = container_image_name,
                                     image_tag    = container_image_tag,
                                     image_arch   = container_image_arch,
                                     image_os     = container_image_os,
                                     image_digest = container_image_digest,
                                     interface_port      = container_interface_port,
                                     interface_protocol  = container_interface_protocol,
                                     interface_transport = container_interface_transport,
                                     supports_custom_interface_port = container_supports_custom_interface_port,
                                     supports_interface_auth = container_supports_pass_auth,
                                     env_vars = container_env_vars)
            
        elif new_container_from == 'repository':

            container_description = request.POST.get('container_description', None)
            
            repository_url = request.POST.get('repository_url', None)
            repository_tag = request.POST.get('repository_tag', 'HEAD')

            return HttpResponseRedirect('/import_repository/?repository_url={}&repository_tag={}&container_name={}&container_description={}'.format(repository_url,repository_tag,container_name,container_description))
        
            # The return type here is a container, not created
            #get_or_create_container_from_repository(request.user, repository_url, repository_tag=repository_tag, container_name=container_name, container_description=container_description)

        else:
            raise Exception('Unknown new container mode "{}"'.format(new_container_from)) 
        # Set added switch
        data['added'] = True

    return render(request, 'add_software.html', {'data': data})



#=========================
#  Computing resources
#=========================

@private_view
def computing(request):

    # Init data
    data={}
    data['user'] = request.user
    data['name'] = request.POST.get('name',None)

    # Get action/details if any
    uuid    = request.GET.get('uuid', None)
    action  = request.GET.get('action', None)
    details = booleanize(request.GET.get('details', None))
    computing_uuid = request.GET.get('uuid', None)
    data['details'] = details
    data['action'] = action
    
    if details and computing_uuid:
        try:
            data['computing'] = Computing.objects.get(uuid=computing_uuid, group__user=request.user)
        except Computing.DoesNotExist:
            data['computing'] = Computing.objects.get(uuid=computing_uuid, group=None)
    else:
        data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user))
        
    return render(request, 'computing.html', {'data': data})


#=========================
#  Storage
#=========================
@private_view
def storage(request):

    # Set data & render
    data = {}
    data['user'] = request.user
    
    return render(request, 'storage.html', {'data': data})




#=========================
#  Add profile conf
#=========================
 
@private_view
def add_profile_conf(request):
 
    # Init data
    data={}
    data['user']    = request.user
    
    # Set conf types we can add
    data['conf_types'] = ['computing_user'] #,'computing_custom_binds']
    
    # Process adding the new conf
    conf_type = request.POST.get('conf_type', None)
    if conf_type:
        data['conf_type'] = conf_type
        if conf_type in ['computing_user']:
            computing_uuid = request.POST.get('computing_uuid', None)
            if computing_uuid:
                try:
                    computing = Computing.objects.get(uuid=computing_uuid, group__user=request.user)
                except Computing.DoesNotExist:
                    computing = Computing.objects.get(uuid=computing_uuid, group=None)                
                data['computing'] = computing
                data['last_step'] = True
                value = request.POST.get('value', None)
                if value:
                    request.user.profile.add_extra_conf(conf_type=conf_type, object=computing, value=value)
                    # Now redirect to site
                    return HttpResponseRedirect('/account/')
            
            else:
                data['computings'] = list(Computing.objects.filter(group=None)) + list(Computing.objects.filter(group__user=request.user))
        else:
            raise ErrorMessage('Unknown conf type \'{}\''.format(conf_type))
    
 
    return render(request, 'add_profile_conf.html', {'data': data})


#=========================
#  Task connect
#=========================

@private_view
def task_connect(request):

    task_uuid = request.GET.get('uuid', None)
    if not task_uuid:
        raise ErrorMessage('Empty task uuid')


    # Get the task     
    task = Task.objects.get(uuid=task_uuid)
    
    if task.user != request.user:
        raise ErrorMessage('You do not have access to this task.')

    # Ensure that the tunnel and proxy are set up
    setup_tunnel_and_proxy(task)

    data ={}
    data['task'] = task
    
    return render(request, 'task_connect.html', {'data': data})


#===========================
# Direct connection handler
#===========================

@private_view
def direct_connection_handler(request, uuid):

    # Get the task     
    #task = Task.objects.get(uuid__startswith=short_uuid)
    task = Task.objects.get(uuid=uuid)

    if task.user != request.user:
        raise ErrorMessage('You do not have access to this task.')

    # Ensure that the tunnel and proxy are set up
    setup_tunnel_and_proxy(task)
    
    # Get task and tunnel proxy host
    task_proxy_host = get_task_proxy_host()
    task_tunnel_host = get_task_tunnel_host()

    # Redirect to the task through the tunnel    
    if task.requires_proxy:
        if task.requires_proxy_auth and task.auth_token:
            user = request.user.email
            password = task.auth_token
            redirect_string = 'https://{}:{}@{}:{}'.format(user, password, task_proxy_host, task.tcp_tunnel_port)        
        else:
            redirect_string = 'https://{}:{}'.format(task_proxy_host, task.tcp_tunnel_port)       
    else:
        redirect_string = '{}://{}:{}'.format(task.container.interface_protocol, task_tunnel_host, task.tcp_tunnel_port)
    
    logger.debug('Task direct connect redirect: "{}"'.format(redirect_string))
    return redirect(redirect_string)
        
    

#===========================
#  Sharable link handler
#===========================

@public_view
def sharable_link_handler(request, short_uuid):

    # Get the task (if the short uuid is not enough an error wil be raised)
    task = Task.objects.get(uuid__startswith=short_uuid)
    
    # First ensure that the tunnel and proxy are set up
    setup_tunnel_and_proxy(task)
    
    # Get task and tunnel proxy host
    task_proxy_host = get_task_proxy_host()
    task_tunnel_host = get_task_tunnel_host()

    # Redirect to the task through the tunnel    
    if task.requires_proxy:
        redirect_string = 'https://{}:{}'.format(task_proxy_host, task.tcp_tunnel_port)       
    else:
        redirect_string = '{}://{}:{}'.format(task.container.interface_protocol, task_tunnel_host, task.tcp_tunnel_port)
    
    logger.debug('Task sharable link connect redirect: "{}"'.format(redirect_string))
    return redirect(redirect_string)


#=========================
#  New Binder Task
#=========================

@private_view
def new_binder_task(request, repository):

    # Init data
    data={}
    data['user']  = request.user

    # Convert the Git repository as a Docker container
    logger.debug('Got a new Binder task request for repository "%s"', repository)
        
    # Set repository name/tag/url        
    repository_tag = repository.split('/')[-1]
    repository_url = repository.replace('/'+repository_tag, '')

    # I have no idea why the https:// of the repo part of the url gets transfrmed in https:/
    # Here i work around this, but TODO: understand what the hell is going on.
    if 'https:/' in repository_url and not 'https://' in repository_url:
        repository_url = repository_url.replace('https:/', 'https://')
    
    if not repository_tag:
        repository_tag='HEAD'

    data['repository_url'] = repository_url
    data['repository_tag'] = repository_tag
    
    data['mode'] = 'new_task' #new container

    # Render the import page. This will call an API, and when the import is done, it
    # will automatically redirect to the page "new_task/?step=two&task_container_uuid=..."
    return render(request, 'import_repository.html', {'data': data})


#=========================
#  Import repository
#=========================

@private_view
def import_repository(request):

    # Init data
    data={}
    data['user']  = request.user
    
    repository_url = request.GET.get('repository_url', None)
    # I have no idea why the https:// of the repo part of the url gets transfrmed in https:/
    # Here i work around this, but TODO: understand what the hell is going on.
    if 'https:/' in repository_url and not 'https://' in repository_url:
        repository_url = repository_url.replace('https:/', 'https://')
    
    repository_tag= request.GET.get('repository_tag', None)
    if not repository_tag:
        repository_tag='HEAD'
        
    data['repository_url'] = repository_url
    data['repository_tag'] = repository_tag

    data['container_name'] = request.GET.get('container_name', None)
    data['container_description'] = request.GET.get('container_description', None)

    data['mode'] = 'new_container'

    # Render the import page. This will call an API, and when the import is done, it
    # will automatically say "Ok, crrated, go to software".
    return render(request, 'import_repository.html', {'data': data})

