import bottle
import updater
import logging
import traceback
import os
import socket

from optparse import OptionParser
from helpers import get_machine_id, assert_node, WrongMachineID
from helpers import get_commit_version, generate_new_device_map, updates_api_wrapper, reload_node_daemon, reload_device_daemon


app = bottle.Bottle()
STATIC_DIR = "./static"

##################
# Bottle framework
##################

@app.get('/favicon.ico')
def get_favicon():
    assert_node(is_node)
    return server_static('/img/favicon.ico')

@app.route('/static/<filepath:path>')
def server_static(filepath):
    assert_node(is_node)
    return bottle.static_file(filepath, root=STATIC_DIR)

@app.route('/')
def index():
    assert_node(is_node)
    return bottle.static_file('index.html', root=STATIC_DIR)


##########################
## UPDATE API
# All devices and node:
###########################

@app.get('/device/<action>/<id>')
def device(action, id):
    """
    Control update state / get info about a node or device

    :param action: what to do
    :return:
    """
    if id != device_id:
        raise WrongMachineID("Not the same ID")
    
    try:
        if action == 'check_update':
            local_commit, origin_commit = ethoscope_updater.get_local_and_origin_commits()
            up_to_date = local_commit == origin_commit
            # @pepelisu you can get
            #data["local_commit"]["id"] -> a34fac...
            #data["local_commit"]["date"] -> 2015-01-24 12:23:00
            return {"up_to_date":up_to_date,
                    "local_commit":get_commit_version(local_commit),
                    "origin_commit":get_commit_version(origin_commit)
                    }
        if action == 'active_branch':
            return {"active_branch": str(ethoscope_updater.active_branch())}
            
        if action == 'available_branches':
            return {"available_branches": str(ethoscope_updater.available_branches())}


        if action == 'update':
            old_commit, _= ethoscope_updater.get_local_and_origin_commits()
            ethoscope_updater.update_active_branch()
            new_commit, _= ethoscope_updater.get_local_and_origin_commits()

            return {"old_commit":get_commit_version(old_commit),
                    "new_commit":get_commit_version(new_commit)
                    }
        if action == "restart_daemon":
            if is_node:
                reload_node_daemon()
            else:
                reload_device_daemon()
        else:
            raise UnexpectedAction()

    except Exception as e:
        logging.error(traceback.format_exc())
        return {'error': traceback.format_exc()}


@app.post('/device/<action>/<id>')
def change_branch(action, id):

    if id != device_id:
        raise WrongMachineID
    
    #todo
    try:
        data = bottle.request.json
        branch = data['new_branch']
        if action == 'change_branch':
            ethoscope_updater.change_branch(branch)

        return {"new_branch": branch}

    except Exception as e:
        logging.error(traceback.format_exc())
        return {'error': traceback.format_exc()}
        
@app.get('/id')
def name():
    try:
        return {"id": device_id}
    except Exception as e:
        return {'error':traceback.format_exc()}


###############################
## UPDATES API
# Node only functions
###############################


@app.get('/bare/<action>')
def bare(action):
    try:
        assert_node(is_node)
        if action == 'update':
            #out format looks like  {branch:up_to_date}. e.g. out["dev"]=True
            out = bare_repo_updater.update_all_visible_branches()
            return out
        elif action == 'discover_branches':
            out = bare_repo_updater.discover_branches()
            return out
        else:
            raise UnexpectedAction()

    except Exception as e:
        logging.error(traceback.format_exc())
        return {'error': traceback.format_exc()}



@app.get('/node_info')
def node_info():#, device):
    try:
        assert_node(is_node)
        host = bottle.request.get_header('host')
        return {'ip': "http://{}".format(host),
                'status': "NA",
                "id": "node"}

    except Exception as e:
        logging.error(e)
        return {'error': traceback.format_exc()}

@app.get('/devices')
def scan_subnet():
    try:
        assert_node(is_node)
        devices_map = generate_new_device_map()
        return devices_map
        
    except Exception as e:
        logging.error("Unexpected exception when scanning for devices:")
        logging.error(traceback.format_exc())
        return {'error': traceback.format_exc()}



@app.post('/group/<what>')
def group(what):
    try:
        responses = []
        data = bottle.request.json
        if what == "update":
            for device in data["devices"]:
                response = updates_api_wrapper(device['ip'], device['id'], what='device/update')
                responses.append(response)
            for device in data["devices"]:
                response = updates_api_wrapper(device['ip'], device['id'], what='device/restart_daemon')
                responses.append(response)
        elif what == "swBranch":
            for device in data["devices"]:
                data_one_dev = {'new_branch': device['new_branch']}
                response = updates_api_wrapper(device['ip'], device['id'], what='device/change_branch', data=data_one_dev)
                responses.append(response)
            for device in data["devices"]:
                response = updates_api_wrapper(device['ip'], device['id'], what='device/restart_daemon')
                responses.append(response)
        elif what == "restart":
            for device in data["devices"]:
                response = updates_api_wrapper(device['ip'], device['id'], what='device/restart_daemon')
                responses.append(response)
        return {'response':responses}

    except Exception as e:
        logging.error("Unexpected exception when updating devices:")
        logging.error(traceback.format_exc())
        return {'error': traceback.format_exc()}

def close(exit_status=0):
    logging.info("Closing server")
    os._exit(exit_status)

#======================================================================================================================#
#############
### CLASSS TO BE REMOVED IF BOTTLE CHANGES TO 0.13
############
class CherootServer(bottle.ServerAdapter):
    def run(self, handler): # pragma: no cover
        from cheroot import wsgi
        from cheroot.ssl import builtin
        self.options['bind_addr'] = (self.host, self.port)
        self.options['wsgi_app'] = handler
        certfile = self.options.pop('certfile', None)
        keyfile = self.options.pop('keyfile', None)
        chainfile = self.options.pop('chainfile', None)
        server = wsgi.Server(**self.options)
        if certfile and keyfile:
            server.ssl_adapter = builtin.BuiltinSSLAdapter(
                    certfile, keyfile, chainfile)
        try:
            server.start()
        finally:
            server.stop()
#############

if __name__ == '__main__':
    """
    The same server runs on both the ethoscope and the node.
    If no -b flag is passed specifying the location of the bare repo, then
    we assume that we are on an ethoscope, otherwise we assume we run on the node
    
    URLs available on the node are:
    /
    /bare/<action>
    /node_info
    /devices
    /group/<what>
    
    URLs available on the ethoscope are:
    /device/<action>/<id> (POST and GET)
    /id
    
    """

    logging.getLogger().setLevel(logging.INFO)
    parser = OptionParser()

    parser.add_option("-g", "--git-local-repo", dest="local_repo", help="Route to local repository to update")
    parser.add_option("-b", "--bare-repo", dest="bare_repo", default=None, help="Route to bare repository")
    parser.add_option("-r", "--router-ip", dest="router_ip", default="192.169.123.254", help="the ip of the router in your setup")
    parser.add_option("-p", "--port", default=8888, dest="port", help="The port to run the server on. Default 8888")
    parser.add_option("-D", "--debug", dest="debug", default=False, help="Set DEBUG mode ON", action="store_true")

    (options, args) = parser.parse_args()

    option_dict = vars(options)
    local_repo = option_dict["local_repo"]
    bare_repo = option_dict["bare_repo"]
    PORT = option_dict["port"]
    DEBUG = option_dict["debug"]

    if not local_repo:
        raise Exception("You must specify the location of the GIT repo to update using the -g or --git-local-repo flags.")


    ethoscope_updater = updater.DeviceUpdater(local_repo)

    #Here we decide if we are running on an ethoscope or a node
    if bare_repo is not None:
        bare_repo_updater = updater.BareRepoUpdater(bare_repo)
        is_node = True
        device_id = "node"

    else:
        bare_repo_updater = None
        is_node = False
        device_id = get_machine_id()

    try:
        ####### TO be remove when bottle changes to version 0.13
        server = "cherrypy"
        try:
            from cherrypy import wsgiserver
        except:
            # Trick bottle to think that cheroot is actulay cherrypy server adds the pacth to BOTTLE
            bottle.server_names["cherrypy"] = CherootServer(host='0.0.0.0', port=PORT)
            logging.warning("Cherrypy version is bigger than 9, we have to change to cheroot server")
            pass
        #########
        bottle.run(app, host='0.0.0.0', port=PORT, debug=DEBUG, server='cherrypy')

    except KeyboardInterrupt:
        logging.info("Stopping update server cleanly")
        pass

    except socket.error as e:
        logging.error(traceback.format_exc())
        logging.error("Port %i is probably not accessible for you. Maybe use another one e.g.`-p 8000`" % port)

    except Exception as e:
        logging.error(traceback.format_exc())
        close(1)
 
    finally:
        close()