"""
All the API endpoints for controling processes and tunnels via SSH
"""
import json
from flask import session, redirect, request, Response, make_response
from flask_restful import Resource
from flask import render_template
import flask_restful

from . import api, app
from .sshwrapper import Ssh, SshAgentException, SftpPermissionException, SftpException, SshCtrlException, SshExecException
from .tunnelstat import SSHSession
import subprocess

class AppParamsException(Exception):
    pass

class GetCert(Resource):
    """
    This class is necessary because I'm not reconfiguring
    SSHAuthZ to support CORS, but the TES does support CORS
    """

    def post(self):
        """
        takes a public key, returns to the SPA a certificate
        """
        import logging
        logger = logging.getLogger()
        try:
            data = request.get_json()
            response = {'cert':GetCert.get_cert(data['token'], data['pubkey'], data['signing_url'])}
            return response
        except:
            import traceback
            logger.error('Failed to get certificate')
            logger.error(traceback.format_exc())
            flask_restful.abort(400,message="an error occured generating your certificate")

    @staticmethod
    def get_cert(access_token, pub_key, url):
        """
        Sign a pub key into a cert
        """
        import requests
        import logging
        logger = logging.getLogger()
        sess = requests.Session()
        headers = {"Authorization":"Bearer %s"%access_token}
        data = {"public_key":pub_key}
        resp = sess.post(url, json=data, headers=headers, verify=False)
        data = resp.json()
        return data['certificate']


class SSHAgent(Resource):
    def post(self):
        import logging
        logger = logging.getLogger()
        try:
            session.permanent = True
            from .tunnelstat import SSHSession
            sshsess = SSHSession.get_sshsession()
            data = request.get_json()
            sshsess.add_keycert(key=data['key'],cert=data['cert'])
            sshsess.refresh()
            return "OK"
        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            logger.error('failed to add ssh key to the agent')
            flask_restful.abort(500,message="failed to add the ssh key to the agent")

    def get(self):
        from .tunnelstat import SSHSession
        import logging
        logger = logging.getLogger()
        try:
            sshsess = SSHSession.get_sshsession()
            if sshsess.socket == None:
                return [] # The agent hasn't even been started yet
            return sshsess.get_cert_contents()
        except Exception as e:
            import traceback
            logger.error('SSHAgent.get: Exception {}'.format(e))
            logger.error(traceback.format_exc())
            flask_restful.abort(500,message="failed to query the ssh-agent")


    def delete(self):
        from .tunnelstat import SSHSession
        try:
            sshsess = SSHSession.get_sshsession()
            sshsess.kill()
            SSHSession.remove_sshsession()
            session.pop('sshsessid', None)
            return []
        except Exception as e:
            import traceback
            logger.error(traceback.format_exc())
            flask_restful.abort(500,message="failed to shut down ssh-agent")

def get_conn_params():
    """
    Return parameters relating to the backend compute service
    Retrieve them from the query string
    """
    import logging
    logger = logging.getLogger()
    identitystr = request.args.get('identity')
    identityparams = json.loads(identitystr)
    try:
        interfacestr = request.args.get('interface')
        interfaceparams = json.loads(interfacestr)
    except:
        interfaceparams = {}
        pass
    try:
        pathstr = request.args.get('path')
        pathparams = json.loads(pathstr)
        cdstr = request.args.get('cd')
        cdparams = json.loads(cdstr)
    except:
        pathparams = None
        cdparams = None
    try:
        appstr = request.args.get('app')
        appparams = json.loads(appstr)
    except:
        appparams = {}
    try:
        appinstancestr = request.args.get('appinstance')
        appinstanceparams = json.loads(appinstancestr)
    except:
        appinstanceparams = {}
    params = {}

    params['identity'] = identityparams
    params['interface'] = interfaceparams
    params['app'] = appparams
    params['appinstance'] = appinstanceparams


    params.update(interfaceparams)
    params['user'] = identityparams['username']
    params['host'] = identityparams['site']['host']
    params['path'] = pathparams
    params['cd'] = cdparams

    return params



class TunnelstatEP(Resource):
    """
    Endpoints used by the WS proxy
    The Frontend should never call this
    """

    def get(self, authtok):
        """
        given an authtoken, return the port of the tunnels
        """
        import logging
        logger = logging.getLogger()
        from . import sshsessions
        from flask import session
        port = None

        try:
            for (sessid,sshsess) in sshsessions.items():
                for (tok,port) in sshsess.port.items():
                    if tok == authtok:
                        return port
        except:
            logger.error("exception in TunnelstatEP.get")
            import traceback
            logger.error(traceback.format_exc())
        return None

class ContactUs(Resource):
    def post(self):
        import tempfile
        data = request.get_json()
        f = tempfile.NamedTemporaryFile(mode='w+b',dir=app.config['MESSAGES'],delete=False)
        f.write(json.dumps(data).encode())
        f.close()
        return

def wrap_execute(sess, host, user, cmd, bastion=None,stdin=None, sshport="22", bastionsshport="22"):
    """
    This function is supposed to interpret all the possible exceptions from Ssh.execute and generate
    appropriate HTTP errors (both status codes and messages)

    There are a range of situations:
    it might raise an SshExecException (non-zero return code)
    it might return valid data or not
    it might return stderr or not
    the data on stderr might be valid json or not.
    """
    import logging
    logger = logging.getLogger()
    try:
        res = Ssh.execute(sess,  host, user, cmd, bastion, stdin, sshport, bastionsshport)
        if not (res['stderr'] == '' or res['stderr'] is None or res['stderr'] == b''):
            logger.error(res['stderr'])
            #flask_restful.abort(400, message=res['stderr'].decode())
            return apiabort(400, message=res['stderr'].decode(), sess=sess)

        try:
            if res['stdout'].decode() != '':
                data = json.loads(res['stdout'].decode())
                return data
            else:
                return None
        except json.decoder.JSONDecodeError:
            return None
            #return apiabort(500, data = {'stdout':res['stdout'].decode(),'stderr':res['stderr'].decode()})
        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            return apiabort(400, message=e, sess=sess)

    except SshCtrlException as e:
        return apiabort(401,message="{} {}".format(host,e), sess=sess, logdata = json.dumps({'user':user,'cmd':cmd,'host':host}))
    # This exception is raised if the remove command had a non-zero return code
    except SshExecException as e:
        return apiabort(400, message="{}".format(e), sess=sess)
    # Any other exceptions. This should never be reached.
    except subprocess.TimeoutExpired as e:
        return apiabort(504, message="{}".format(e), sess=sess)
    except Exception as e:
        import traceback
        logger.error('JobStat.get: Exception {}'.format(e), sess=sess)
        logger.error(traceback.format_exc())
        return apiabort(500,message="{}".format(e), sess=sess)

class JobStat(Resource):
    """
    endpoints to return info on jobs on the backend compute Resource
    """
    def get(self):
        """
        get info on the job from the backend
        """
        import logging
        logger = logging.getLogger()
        try:
            sshsess = SSHSession.get_sshsession()
            sshsess.refresh()
        except Exception as e:
            return apiabort(500, message="{}".format(e))
        try:
            cmd = json.loads(request.args.get('statcmd'))
            host = json.loads(request.args.get('host'))
            user = json.loads(request.args.get('username'))
        except (TypeError, KeyError) as e:
            import traceback
            return apiabort(400, message="Missing required parameter {}\n{}".format(e,traceback.format_exc()), sess=sshsess)

        rv = wrap_execute(sshsess, host=host, user=user, cmd=cmd)
        return rv

class MkDir(Resource):
    def post(self):
        import logging
        logger = logging.getLogger()
        try:
            data = request.get_json()
            params = get_conn_params()
            sshsess = SSHSession.get_sshsession()
            site = params['identity']['site']
            if 'dtnport' in site:
                sshport = site['dtnport']
            else:
                sshport = "22"
            try:
                Ssh.sftpmkdir(sshsess, host=params['identity']['site']['host'],
                               user=params['identity']['username'], path=params['path'],name=data['name'], sshport=sshport)
            except SftpPermissionException as e:
                #return json.dumps({'message':"You don't have permission to make a directory there"}), 403
                #flask_restful.abort(403,message="You don't have permission to make a directory there")
                return apiabort(403,message="You don't have permission to make a directory there")
            except SftpException as e:
                #return json.dumps({'message':"Something went wrong making that directory"}), 500
                #flask_restful.abort(500,message="Something went wrong making that directory")
                return apiabort(500,message="Something went wrong making that directory")
            except Exception as e:
                import traceback
                logger.error(traceback.format_exc())
                #flask_restful.abort(500,message="Something went wrong creating that directory, probably a bug")
                return apiabort(500,message="Something went wrong creating that directory, probably a bug")

            return
        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            #flask_restful.abort(500,message="mkdir failed in some unexpected way")
            return apiabort(500,message="mkdir failed in some unexpected way")

class DirList(Resource):
    def get(self):
        import logging
        logger = logging.getLogger()
        try:
            params = get_conn_params()
            sshsess = SSHSession.get_sshsession()
            site = params['identity']['site']
            if 'dtnport' in site:
                sshport = site['dtnport']
            else:
                sshport = "22"
            path = params['path']
            cd = params['cd']
            if path == "":
                path = "."
            if cd == "":
                cd = "."
            try:
                if 'lscmd' in site and site['lscmd'] is not None and site['lscmd'] != "":
                    res = Ssh.execute(sshsess, host=params['identity']['site']['host'], user=params['identity']['username'],
                                        sshport=sshport,
                                        cmd="{} {} {}".format(site['lscmd'],path,cd))
                    try:
                        dirls = json.loads(res['stdout'].decode())
                    except:
                        #return json.dumps({'message':"You don't have permission to view that directory"}), 401
                        #flask_restful.abort(401,message="You don't have permission to view that directory")
                        return apiabort(403,message="You don't have permission to view that directory")
                else:
                    dirls = Ssh.sftpls(sshsess, host=params['identity']['site']['host'],
                                       user=params['identity']['username'], path=params['path'],changepath=params['cd'], sshport=sshport)
                return dirls
            except SshCtrlException as e:
                if ("{}".format(e) != ''):
                    return apiabort(400,message="{}".format(e))
                else:
                    # This is the most common error, if the user disconnected from the network and the api server cleaned up all the agents and control sockets
                    return apiabort(401)
        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            #flask_restful.abort(500,message="dirlist failed in some unexpected way")
            return apiabort(500,message="dirlist failed in some unexpected way")

class JobCancel(Resource):
    """
    Terminate a job on the compute backend
    """
    def delete(self, jobid):
        """
        Terminate a job on the backend
        """
        try:
            params = get_conn_params()
            sshsess = SSHSession.get_sshsession()
            res = wrap_execute(sshsess, host=params['identity']['site']['host'], user=params['identity']['username'],
                                  cmd=params['interface']['cancelcmd'].format(jobid=jobid))
            return res

        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            #return json.dumps({'message':"jobcancel failed in some unexpected way"}), 500
            #flask_restful.abort(500,message="jobcancel failed in some unexpected way")
            return apiabort(500,message="jobcancel failed in some unexpected way", sess=sshsess)



class JobSubmit(Resource):
    """
    Class dealing the starting a new job on the compute backend
    """
    def post(self):
        """starting a job is a post, since it changes the state of the backend"""
        import logging
        logger=logging.getLogger()
        try:
            params = get_conn_params()
            sshsess = SSHSession.get_sshsession()
            data = request.get_json()
            try:
                script = data['app']['startscript'].format(**data)
            except Exception as e:
                import traceback
                logger.error(e)
                logger.error(traceback.format_exc())
                logger.error('formating data')
                logger.error(data)
                logger.error('end formating data')
                logger.error('body')
                logger.error(request.data)
                logger.error('end body')
                #return json.dumps({'message':'Incomplete job information was passed to the backend.'}), 400
                #flask_restful.abort(400, message='Incomplete job information was passed to the backend.')
                return apiabort(400, message='Incomplete job information was passed to the backend.', sess=sshsess)

            res = wrap_execute(sshsess, host=params['identity']['site']['host'], user=params['identity']['username'],
                          cmd=params['interface']['submitcmd'], stdin=script)
            return res
        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            #return json.dumps({'message':"JobSubmission failed in some unexpected way"}), 500
            #flask_restful.abort(500,message="JobSubmission failed in some unexpected way")
            return apiabort(500,message="JobSubmission failed in some unexpected way", sess=sshsess)

def gen_authtok():
    """
    generate a random string suitable for an auth token stored in a cookie
    """
    import random
    import string
    import logging
    logger=logging.getLogger()
    return ''.join(random.SystemRandom().choice(string.ascii_uppercase +
                                                string.digits) for _ in range(16))

def apiabort(code, message=None, data = None, sess = None, logdata = None):
    import logging
    import traceback
    from flask import g
    import time
    logger = logging.getLogger('apiabort')
    logger.error("request time {}".format(time.time() - g.start))
    if sess is not None:
        logger.error("certs {}".format(sess.get_certs()))
    logger.error(logdata)
    logger.error("{} {} {}".format(code,data,message))
    logger.error("{}".format(traceback.format_exc()))
    if data is not None:
        return data, code
    else:
        return {'message':message}, code

class AppUrl(Resource):
    def get(self):
        import logging
        try:
            logger = logging.getLogger()
            appdef = json.loads(request.args.get('app'))
            inst = json.loads(request.args.get('appinst'))
            inst['twsproxy']='{twsproxy}'
            inst['twsproxyauth'] = request.cookies.get('twsproxyauth')
            logger.debug('twsproxyauth {} {}'.format(inst['twsproxyauth'],request.cookies.get('twsproxyauth')))
            url = "{}/{}".format("{twsproxy}",appdef['client']['redir'].format(**inst))
            return url
        except Exception as e:
            import traceback
            logger.error('AppUrl failed')
            logger.error(e)
            logger.error(traceback.format_exc())
            #flask_restful.abort(500,message="AppUrl failed in some unexpected way")
            return apiabort(500,message="AppUrl failed in some unexpected way")

class SetTWSProxyAuth(Resource):
    def get(self, authtok):
        import urllib.parse
        import logging
        logger = logging.getLogger()
        url = urllib.parse.unquote(request.args.get('redirect'))
        logger.debug('SetTWSProxyAuth will redirect to {}'.format(url))
        response = make_response(redirect(url))
        response.set_cookie('twsproxyauth', authtok, secure=True)
        return response


class Ping(Resource):
    def get(self):
        return None, 201

class AppLaunch(Resource):
    def get(self):
        import logging
        logger = logging.getLogger()
        try:
            appdef = json.loads(request.args.get('app'))
            inst = json.loads(request.args.get('appinst'))
            cmd = "{}".format(appdef['client']['cmd'].format(**inst)).split()
            import subprocess
            try:
                logger.debug('launching subprocess')
                p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
                (stdout,stderr) = p.communicate()
                logger.debug('subprocess completed')
                if p.returncode != 0:
                    if stderr != "":
                        msg = stderr.decode()
                    else:
                        msg = "Unable to start the program {}".format(appdef['client']['cmd'])
                    return apiabort(500,message=msg)
            except FileNotFoundError:
                return apiabort(500,message="Unable to find the program {}".format(appdef['client']['cmd']))
                pass

        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            #flask_restful.abort(500,message="AppUrl failed in some unexpected way")
            return apiabort(500,message="AppLaunc failed in some unexpected way")
        logger.debug('application launched, returning')
        return None, 200

class AppInstance(Resource):
    def get(self, username, loginhost, batchhost, jobid):
        """Run a command to get things like password and port number
        command is passed as a query string"""
        import logging
        import hashlib
        logger=logging.getLogger()
        events = logging.getLogger('connection_events')
        sshsess = SSHSession.get_sshsession()
        sessionId = hashlib.sha1(sshsess.sshsessid.encode()).hexdigest()
        try:
            paramscmd = json.loads(request.args.get('cmd')).format(jobid=jobid)
        except json.decoder.JSONDecodeError:
            return apiabort(400, message="No command was sent to the API server")
        try:
            rv = wrap_execute(sshsess, host=batchhost, bastion=loginhost, user=username, cmd=paramscmd)
            events.info('connect {}'.format(json.dumps({'user':username, 'jobid':jobid, 'loginhost': loginhost, 'sessid':sessionId})))
            return rv
        except Exception as e:
            import traceback
            logger.error('AppInstance failed')
            logger.error(e)
            logger.error(traceback.format_exc())
            #flask_restful.abort(500,message="AppUrl failed in some unexpected way")
            return apiabort(500,message="AppInstance failed in some unexpected way")

class CreateTunnel(Resource):
    @staticmethod
    def validate_connect_params(port, username, host, batchhost):
        try:
            intport = int(port)
        except Exception as e:
            return False
        if ' ' in username or '\n' in username: # This really needs more validation
            return False
        if ' ' in host or '\n' in host: # This really needs more validation
            return False
        return True

    def post(self,username,loginhost,batchhost):
        """
        Create a tunnel using established keys
        parameters for the tunnel (host username port etc)
        will be passed in the body
        """
        import logging
        import json
        logger = logging.getLogger()
        try:
            data = request.get_json()
            try:
                port = data['port']
            except KeyError as missingdata:
                raise AppParamsException("missing value for {} data contains {}".format(missingdata,data))
            if not CreateTunnel.validate_connect_params(port, username, loginhost, batchhost):
                raise AppParamsException("Invalid value: {} {} {} {}".
                                         format(username, loginhost, batchhost, port))
            if 'internalfirewall' in data:
                firewall = data['internalfirewall']
            else:
                firewall = False
            if 'localbind' in data:
                localbind = data['localbind']
            else:
                localbind = True
            sshsess = SSHSession.get_sshsession()
            if 'token' in data:
                authtok = data['token']
                logger.debug('using token for authenticating tunnel')
            else:
                authtok = gen_authtok()
            port,pids = Ssh.tunnel(sshsess, port=port, batchhost=batchhost,
                       user=username, host=loginhost,
                       internalfirewall=firewall,
                       localbind=localbind, authtok=authtok)
            response = make_response(json.dumps({'localport':port}),200)
            response.mime_type = 'application/json'
            response.set_cookie('twsproxyauth', authtok, secure=True)
            return response
        except AppParamsException as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            
            return apiabort(400,message="Unable to create the secure tunnel. Please try again.")

        except Exception as e:
            import traceback
            logger.error(e)
            logger.error(traceback.format_exc())
            #flask_restful.abort(500,message="CreateTunnel failed in some unexpected way")
            return apiabort(500,message="CreateTunnel failed in some unexpected way")



api.add_resource(TunnelstatEP, '/tunnelstat/<string:authtok>')
api.add_resource(GetCert, '/getcert')
api.add_resource(JobStat, '/stat')
api.add_resource(Ping, '/ping')
api.add_resource(JobCancel, '/cancel/<string:jobid>')
api.add_resource(SetTWSProxyAuth, '/settwsproxyauth/<string:authtok>')
api.add_resource(JobSubmit, '/submit')
api.add_resource(CreateTunnel, '/createtunnel/<string:username>/<string:loginhost>/<string:batchhost>')
api.add_resource(AppInstance, '/appinstance/<string:username>/<string:loginhost>/<string:batchhost>/<string:jobid>')
api.add_resource(AppUrl, '/appurl')
if 'ENABLELAUNCH' in app.config and app.config['ENABLELAUNCH']:
    api.add_resource(AppLaunch, '/applaunch')
api.add_resource(AppLaunch, '/applaunch')
api.add_resource(SSHAgent,'/sshagent')
api.add_resource(DirList,'/ls')
api.add_resource(MkDir,'/mkdir')
api.add_resource(ContactUs,'/contactus')