#!/usr/bin/env python
try:
    import simplejson as json
except ImportError:
    import json

from webob import Request, Response
from traceback import format_exc
from urllib import unquote_plus
import new
import re
import os

from clusto.drivers import Driver, IPManager
import clusto.script_helper
import clusto

from clusto.services.config import conf, get_logger
log = get_logger('clusto.http', 'INFO')

conf = clusto.script_helper.load_config(os.environ.get('CLUSTOCONFIG', '/etc/clusto/clusto.conf'))
clusto.connect(conf)


def unclusto(obj, prefetch_attrs=None):
    '''
    Convert an object to a representation that can be safely serialized into
    JSON.
    '''
    if type(obj) in (str, unicode, int) or obj is None:
        return obj
    if type(obj) in (list, dict):
        return json.dumps(obj)
    if isinstance(obj, clusto.Attribute):
        return {
            'key': obj.key,
            'value': unclusto(obj.value),
            'subkey': obj.subkey,
            'number': obj.number,
            'datatype': obj.datatype
        }
    if issubclass(obj.__class__, Driver):
        if not prefetch_attrs:
            return '/%s/%s' % (obj.type, obj.name)

        result = {
            'attrs': []
        }
        for prefetch_attr in prefetch_attrs:
            for attr in obj.attrs(**prefetch_attr):
                result['attrs'].append(unclusto(attr))
        return {'/%s/%s' % (obj.type, obj.name): result}
    return str(obj)


def dumps(request, obj, **kwargs):
    result = json.dumps(obj, indent=2, sort_keys=True, **kwargs)
    if 'callback' in request.params:
        callback = request.params['callback']
        result = '%s(%s)' % (callback, result)
        content_type = 'text/javascript'
    else:
        content_type = 'application/json'
    return Response(result, content_type=content_type, **kwargs)


def loads(request, obj):
    return json.loads(obj)


class EntityAPI(object):
    def __init__(self, obj):
        self.obj = obj
        self.url = '/%s/%s' % (self.obj.type, self.obj.name)

    def _dict(self):
        result = {
            'object': self.url,
            'driver': self.obj.driver,
        }

        attrs = []
        for x in self.obj.attrs():
            attrs.append(unclusto(x))
        result['attrs'] = attrs
        result['contents'] = [unclusto(x) for x in self.obj.contents()]
        result['parents'] = [unclusto(x) for x in self.obj.parents()]
        result['actions'] = [x for x in dir(self) if not x.startswith('_') and callable(getattr(self, x))]
        return result

    def addattr(self, request):
        '''
        Add an attribute to this object.

        Requires HTTP parameters "key" and "value"
        Optional parameters are "subkey", "number" and "datatype"
        where "datatype" could be "relation", "int" or string by default
        '''
        kwargs = dict(request.params.items())
        datatype = kwargs.get('datatype', None)
        kwargs.pop('datatype', None)

        if datatype == 'relation':
            try:
                kwargs['value'] = clusto.get_by_name(kwargs['value'].rsplit('/', 1)[1])
            except LookupError:
                return Response(status=400, body='400 Element Not Found\n')
        elif datatype == 'int':
            kwargs['value'] = int(kwargs['value'])
        else:
            kwargs['value'] = str(kwargs['value'])

        self.obj.add_attr(**kwargs)
        return self.show(request)

    def setattr(self, request):
        '''
        Set an attribute on this object.

        Requires HTTP parameters "key" and "value"
        Optional parameters are "subkey", "number" and "datatype"
        where "datatype" could be "relation", "int" or string by default
        '''
        kwargs = dict(request.params.items())
        datatype = kwargs.get('datatype', None)
        kwargs.pop('datatype', None)

        if datatype == 'relation':
            try:
                kwargs['value'] = clusto.get_by_name(kwargs['value'].rsplit('/', 1)[1])
            except LookupError:
                return Response(status=400, body='400 Element Not Found\n')
        elif datatype == 'int':
            kwargs['value'] = int(kwargs['value'])
        else:
            kwargs['value'] = str(kwargs['value'])

        if 'number' in kwargs:
            kwargs['number'] = int(kwargs['number'])
        self.obj.set_attr(**kwargs)
        return self.show(request)

    def delattr(self, request):
        '''
        Delete an attribute from this object.

        Requires HTTP parameter "key"
        '''
        self.obj.del_attrs(request.params['key'])
        return self.show(request)

    def attrs(self, request):
        '''
        Query attributes from this object.
        '''
        result = {
            'attrs': []
        }
        kwargs = dict(request.params.items())
        for attr in self.obj.attrs(**kwargs):
            result['attrs'].append(unclusto(attr))
        return dumps(request, result)

    def insert(self, request):
        '''
        Insert an object into this object

        Requires a "device" attribute, which is the absolute URL path to the
        object to be inserted.

        For example: /pool/examplepool/insert?object=/server/exampleserver
        '''
        device = request.params['object'].strip('/').split('/')[1]
        device = clusto.get_by_name(device)
        self.obj.insert(device)
        return self.show(request)

    def remove(self, request):
        '''
        Remove a device from this object.

        Requires a "device" attribute, which is the absolute URL path to the
        object to be removed.

        For example: /pool/examplepool/remove?object=/server/exampleserver
        '''
        device = request.params['object'].strip('/').split('/')[1]
        device = clusto.get_by_name(device)
        self.obj.remove(device)
        return self.show(request)

    def show(self, request, prefetch_attrs=None):
        '''
        Returns attributes and actions available for this object.
        '''
        result = {}
        result['object'] = self.url
        result['driver'] = self.obj.driver

        attrs = []
        for x in self.obj.attrs():
            attrs.append(unclusto(x))
        result['attrs'] = attrs
        result['contents'] = [unclusto(x, prefetch_attrs) for x in self.obj.contents()]
        result['parents'] = [unclusto(x, prefetch_attrs) for x in self.obj.parents()]
        result['actions'] = [x for x in dir(self) if not x.startswith('_') and callable(getattr(self, x))]

        return dumps(request, result)


class PortInfoAPI(EntityAPI):
    def ports(self, request):
        '''
        Returns a list of ports within this object and any information about
        connections between those ports and other devices.
        '''
        result = {}
        result['object'] = self.url
        result['ports'] = []
        for port in self.obj.port_info_tuples:
            porttype, portnum, otherobj, othernum = [unclusto(x) for x in port]
            result['ports'].append({
                'type': porttype,
                'num': portnum,
                'other': otherobj,
                'othernum': othernum,
            })

        return dumps(request, result)

    def set_port_attr(self, request):
        kwargs = {}
        params = request.params
        for arg in ('porttype', 'portnum', 'key', 'value'):
            if not arg in params:
                return Response(status=400, body='Required argument "%s" is missing\n' % arg)
            kwargs[arg] = params[arg]

        try:
            kwargs['portnum'] = int(kwargs['portnum'])
        except ValueError:
            return Response(status=400, body='portnum must be an integer\n')

        self.obj.set_port_attr(**kwargs)
        return Response(status=200)

    def get_port_attr(self, request):
        kwargs = {}
        params = request.params
        for arg in ('porttype', 'portnum', 'key'):
            if not arg in params:
                return Response(status=400, body='Required argument "%s" is missing\n' % arg)
            kwargs[str(arg)] = params[arg]

        try:
            kwargs['portnum'] = int(kwargs['portnum'])
        except ValueError:
            return Response(status=400, body='portnum must be an integer\n')

        return dumps(request, self.obj.get_port_attr(**kwargs))


class RackAPI(EntityAPI):
    def insert(self, request):
        '''
        Insert a device into a rack.

        This method is overridden to require a "ru" parameter in addition to
        "device"

        Example: /rack/examplerack/insert?object=/server/exampleserver&ru=6
        '''
        device = request.params['object'].strip('/').split('/')[1]
        device = clusto.get_by_name(device)
        self.obj.insert(device, int(request.params['ru']))
        return self.contents(request)


class ResourceAPI(EntityAPI):
    def allocate(self, request):
        '''
        Allocate a new object of the given type
        '''
        driver = clusto.DRIVERLIST[request.params['driver']]
        device = self.obj.allocate(driver)
        return dumps(request, unclusto(device), status=201)


class QueryAPI(object):
    @classmethod
    def get_entities(self, request):
        kwargs = {'attrs': []}
        for k, v in request.params.items():
            if k in ('format', 'callback'): continue
            v = loads(request, unquote_plus(v))
            kwargs[str(k)] = v

        attrs = []
        for attr in kwargs['attrs']:
            attrs.append(dict([(str(k), v) for k, v in attr.items()]))
        kwargs['attrs'] = attrs

        result = [unclusto(x) for x in clusto.get_entities(**kwargs)]
        return dumps(request, result)

    @classmethod
    def get_by_name(self, request):
        if not 'name' in request.params:
            return Response(status=400, body='400 Bad Request\nYou must specify a "name" parameter\n')
        name = request.params['name']
        obj = clusto.get_by_name(name)
        api = EntityAPI(obj)
        prefetch_attrs = None
        if 'prefetch_attrs' in request.params:
            # Prefetch all the matching attributes while pulling data
            # Prefetch format would like something like
            #   prefetch_attrs=[{'key': 'disk', 'subkey': 'make'}, {'key': 'system', 'subkey': 'version'}]
            prefetch_attrs = loads(request, request.params['prefetch_attrs'])
        return api.show(request, prefetch_attrs)

    @classmethod
    def get(self, request):
        if not 'name' in request.params:
            return Response(status=400, body='400 Bad Request\nYou must specifiy a "name" parameter\n')
        name = request.params['name']

        result = clusto.get(name)
        if result is None:
            return Response(status=404, body='404 Not Found\n')

        result = [EntityAPI(x)._dict() for x in result]
        return dumps(request, result)

    @classmethod
    def get_from_pools(self, request):
        if not 'pools' in request.params:
            return Response(status=400, body='400 Bad Request\nYou must specify at least one pool\n')
        pools = request.params['pools'].split(',')

        if 'types' in request.params:
            clusto_types = request.params['types'].split(',')
        else:
            clusto_types = None

        prefetch_attrs = None
        if 'prefetch_attrs' in request.params:
            # Prefetch all the matching attributes while pulling data
            # Prefetch format would like something like
            #   prefetch_attrs=[{'key': 'disk', 'subkey': 'make'}, {'key': 'system', 'subkey': 'version'}]
            prefetch_attrs = loads(request, request.params['prefetch_attrs'])

        result = [unclusto(x, prefetch_attrs) for x in clusto.get_from_pools(pools, clusto_types)]
        return dumps(request, result)

    @classmethod
    def get_ip_manager(self, request):
        if not 'ip' in request.params:
            return Response(status=400, body='400 Bad Request\nYou must specify an IP\n')

        try:
            ipman = IPManager.get_ip_manager(request.params['ip'])
        except:
            return Response(status=404, body='404 Not Found\n')
        return dumps(request, unclusto(ipman))


class ClustoApp(object):
    def __init__(self):
        self.urls = [
            ('^/favicon.ico$',
                self.notfound),
            ('^/search$',
                self.search),
            ('^/query/(?P<querytype>[a-z_]+)',
                self.query_delegate),
            ('^/(?P<objtype>\w+)/(?P<name>[-\w0-9]+)/(?P<action>\w+)',
                self.action_delegate),
            ('^/(?P<objtype>\w+)/(?P<name>[-\w0-9]+)',
                self.action_delegate),
            ('^/(?P<objtype>\w+)',
                self.types_delegate),
            ('^/',
                self.default_delegate),
        ]
        self.urls = [(re.compile(pattern), obj) for pattern, obj in self.urls]

        self.types = {
            'server': PortInfoAPI,
            'consoleserver': PortInfoAPI,
            'networkswitch': PortInfoAPI,
            'powerstrip': PortInfoAPI,
            'rack': RackAPI,
            'resource': ResourceAPI,
        }

    def default_delegate(self, request, match):
        types = ['/' + x for x in clusto.typelist.keys()]
        types.sort()
        return dumps(request, types)

    def types_delegate(self, request, match):
        objtype = match.groupdict()['objtype']
        result = []
        for obj in clusto.get_entities(clusto_types=(objtype,)):
            result.append(unclusto(obj))
        return dumps(request, result)

    def action_delegate(self, request, match):
        if request.method == 'GET':
            return self.get_action(request, match)

        if request.method == 'POST':
            return self.post_action(request, match)

        if request.method == 'DELETE':
            return self.delete_action(request, match)

        return Response(status=501, body='501 Not Implemented\n')

    def query_delegate(self, request, match):
        querytype = match.groupdict()['querytype']

        if hasattr(QueryAPI, querytype):
            method = getattr(QueryAPI, querytype)
            return method(request)
        else:
            return Response(status=400, body='400 Bad Request\nThere is no such query\n')

    def post_action(self, request, match):
        name = request.path_info.strip('/')
        if name.count('/') != 1:
            return Response(status=400, body='400 Bad Request\nYou may only create objects, not types or actions\n')
        objtype, objname = name.split('/', 1)

        try:
            obj = clusto.get_by_name(objname)
            if obj:
                return Response(status=409, body='409 Conflict\nObject already exists\n')
        except LookupError: pass

        obj = clusto.driverlist[objtype](objname)

        obj = EntityAPI(obj)
        response = obj.show(request)
        response.status = 201
        return response

    def delete_action(self, request, match):
        name = request.path_info.strip('/')
        if name.count('/') != 1:
            return Response(status=400, body='400 Bad Request\nYou may only delete objects, not types or actions\n')
        objtype, objname = name.split('/', 1)

        try:
            obj = clusto.get_by_name(objname)
        except LookupError:
            return Response(status=404, body='404 Not Found\n')

        clusto.delete_entity(obj.entity)
        return Response(status=200, body='200 OK\nObject deleted\n')

    def get_action(self, request, match):
        group = match.groupdict()
        try:
            obj = clusto.get_by_name(group['name'])
        except LookupError:
            return Response(status=404, body='404 Not Found\n')

        if obj.type != group['objtype']:
            response = Response(status=302)
            response.headers.add('Location', str('/%s/%s/%s' % (obj.type, obj.name, group.get('action', ''))))
            return response

        action = group.get('action', 'show')
        handler = self.types.get(group['objtype'], EntityAPI)
        if not obj:
            obj = clusto.get_by_name(group['name'])
        h = handler(obj)
        if hasattr(h, action):
            f = getattr(h, action)
            response = f(request)
        else:
            response = Response(status=404, body='404 Not Found\nInvalid action\n')
        return response

    def search(self, request, match):
        query = request.params.get('q', None)
        if not query:
            return Response(status=400, body='400 Bad Request\nNo query specified\n')

        result = []
        for obj in clusto.get_entities():
            if obj.name.find(query) != -1:
                result.append(unclusto(obj))
        return dumps(request, result)

    def notfound(self, request, match):
        return Response(status=404)

    def __call__(self, environ, start_response):
        request = Request(environ)
        response = Response(status=404, body='404 Not Found\nUnmatched URL\n')

        for pattern, handler in self.urls:
            match = pattern.match(request.path_info)
            if match:
                try:
                    response = handler(request, match)
                except:
                    response = Response(status=500, body=format_exc(),
                                        content_type='text/plain')
                break
        return response(environ, start_response)


application = ClustoApp()


if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('0.0.0.0', 9996, application)
    log.info('clusto httpd loaded using wsgiref (debug mode)')
    server.serve_forever()
