#
#    Copyright (c) 2017 Tom Keffer <tkeffer@gmail.com>
#
#    See the file LICENSE.txt for your full rights.
#
"""Database-related classes and functions"""

import influxdb

class DatabaseError(Exception):
    """Database error"""
    def __init__(self, message, description=None):
        Exception.__init__(self, message)
        self.description = description
        

class ServerError(DatabaseError):
    """Can't connect to server"""
    

class ClientError(DatabaseError):
    """Malformed query or other problem"""
    

class NotFoundError(DatabaseError):
    """Requested item not found."""


def guard(fn):
    """Decorator function that catches InfluxDB exceptions 
    and converts them to WeeRT exceptions."""

    def guarded_fn(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except influxdb.exceptions.InfluxDBClientError as e:
            raise ClientError(str(e))
        except influxdb.exceptions.InfluxDBServerError as e:
            raise ServerError(str(e))
            
    return guarded_fn


class Database(object):
    """Represents a connection to a particular InfluxDB database"""
    
    def __init__(self, client, database_name=None):
        self.client = client
        self.database_name = database_name or client._database
        
    @staticmethod
    def from_config(influx_config):
        client = influxdb.InfluxDBClient(**influx_config)
        return Database(client, influx_config.get('database', 'weert'))
    
    def init(self):
        self.client.create_database(self.database_name)
        
    @guard
    def drop_database(self):
        self.client.drop_database(self.database_name)

    @guard
    def get_measurement(self, measurement):
        """Get information about a particular measurement"""
        
        response = self.client.request(
            url="query",
            method='GET',
            params={"q":"SHOW SERIES FROM {}".format(measurement), "db":self.database_name},
            data=None,
            expected_response_code=200
        )
        results = self._check_response(response)
#         results = response.json()['results']
#         if 'error' in results[0]:
#             raise ClientError(results[0]['error'])
        # There's a lot indexing and keying in here. Guard against the server returning something
        # I'm not aware of:
        try:
            return [x[0] for x in results[0]['series'][0]['values']]
        except (IndexError, KeyError):
            raise NotFoundError("Measurement {} not found".format(measurement))

    @guard
    def delete_measurement(self, measurement):
        response = self.client.request(
            url="query",
            method='POST',
            params={"q":"DROP MEASUREMENT {}".format(measurement), "db":self.database_name},
            data=None,
            expected_response_code=200
        )
        data = response.json()
        if 'error' in data['results'][0]:
            raise NotFoundError("Non-existent measurement {}".format(measurement), description=data['results'][0]['error'])
        
        
    @guard
    def get_packet(self, measurement, timestamp, platform_name=None, stream_name=None):
        """Get a particular packet from a measurement"""
        query_string = u"SELECT * FROM {} WHERE time={:d}".format(measurement, timestamp)
        if platform_name:
            query_string += u" AND platform_name='{}'".format(platform_name)
        if stream_name:
            query_string += u" AND stream_name='{}'".format(stream_name)
        result = self.client.query(query_string, epoch='ns')
        # Package up the results as a list.
        y = list([unflatten_packet(packet, measurement) \
                  for packet in result.get_points(measurement=measurement)])
        return y

    @guard
    def get_packets(self, measurement, platform_name=None, stream_name=None,
                   start_time=None, stop_time=None,
                   limit=None, sort_direction='asc'):
        if start_time:
            if stop_time:
                query_string = u"SELECT * FROM {} WHERE time > {:d} AND time <= {:d}"\
                    .format(measurement, start_time, stop_time,)
            else:
                query_string = u"SELECT * FROM {} WHERE time > {:d}"\
                    .format(measurement, start_time)
        else:
            if stop_time:
                query_string = u"SELECT * FROM {} WHERE time <= {:d}"\
                    .format(measurement, stop_time)
            else:
                query_string = u"SELECT * FROM {}"\
                    .format(measurement)
                    
        if platform_name:
            if u"WHERE" in query_string:
                query_string += u" AND platform_name = '{}'".format(platform_name)
            else:
                query_string += u" WHERE platform_name = '{}'".format(platform_name)
        if stream_name:
            if u"WHERE" in query_string:
                query_string += u" AND stream_name = '{}'".format(stream_name)
            else:
                query_string += u" WHERE stream_name = '{}'".format(stream_name)
        query_string += u" ORDER BY time {}".format(sort_direction)
        if limit:
            query_string += u" LIMIT {}".format(limit)

        result = self.client.query(query_string, epoch='ns')
        # Package up the results as a list.
        y = list([unflatten_packet(packet, measurement) \
                  for packet in result.get_points(measurement=measurement)])
        return y
    
    @guard
    def post_packet(self, deep_packet):
        """Post a deep packet to the database.""" 

        if deep_packet is None:
            raise ClientError("Missing packet")

        # Make sure the time is an integer (if it's a string, 
        # it will raise an OverflowError unless converted first):
        try:
            deep_packet["time"] = int(deep_packet["time"])
        except ValueError as e:
            raise ClientError("Malformed or missing time", description=str(e))
        except KeyError:
            # Missing key is fine: the server will add one
            pass

        # Add the wrapper:
        point = {"points": [deep_packet]}

        # If the client sends a malformed packet, it can result in a ValueError or AttributeError.
        try:
            # The database must be sent as an undocumented param.
            self.client.write(point, params={'db': self.database_name})
        except (ValueError, AttributeError) as e:
            raise ClientError("Malformed post", description=str(e))
            
        return deep_packet
    
    @staticmethod
    def _check_response(response):
        results = response.json()['results']
        if 'error' in results[0]:
            if results[0]['error'].find('not found') != -1:
                raise NotFoundError(results[0]['error'])
            raise ClientError(results[0]['error'])
        return results

def create_packet(measurement, platform_name, stream_name, timestamp, fields):
    """Create a deep packet from a set of parameters"""
    packet = {u'time': timestamp,
              u'measurement' : measurement,
              u'tags': {u'platform_name' : platform_name, u'stream_name': stream_name},
              u'fields': fields
              }
    return packet

def deep_packet(weewx_packet, measurement, platform_name, stream_name):
    """Construct a "deep packet" from a weewx-style packet."
    
    This is a destructive version! It will change the input packet!
    """
    out = {u'time': weewx_packet.pop('time'),
           u'measurement' : measurement,
           u'tags' : {u'platform_name' : platform_name, u'stream_name' : stream_name},
           u'fields': weewx_packet
           }
    return out

def unflatten_packet(packet, measurement):
    """Convert what the flat packet returned from the influxdb-python API back into a deep packet
    
    This is a destructive version! It will change the input packet!
    """
    return deep_packet(packet, measurement, packet.pop('platform_name'), packet.pop('stream_name'))
    
def write_point_to_database(client, deep_packet):
    """Write a data point directly to the database."""
    json_body = {
        "points": [deep_packet]
    }
    response = client.write(json_body, params={'db': client._database})
    return response
