import logging
import datetime

from mongokit import Document, Connection, Database
from pymongo import ReadPreference, DESCENDING


logger = logging.getLogger(__name__)


class BenchmarkResult(Document):
    """
    This is where the structure of a benchmark result is defined,
    without filling in the required_fields of this
    class the entry will not be stored,
    similarly the type must match or the entry will not be stored.

    - branch: The name of the branch under which the execution happened.
    - buildnumber: The buildnumber or changelist at which the execution occured
    - entrytime: This is the time at which the execution result was
        placed into the database, it is autogenerated on entry
    - machinename: This is the machine name where the testcase was executed,
        names should contain A-Z,a-z,0-9 and hypens, and nothing more
    - metrics: This is the list of currently known metrics,
        to add more simply append a metric to the DB.
    - benchmarkname: This is the name of the testcase was run,
        it should be unique to your testcase.

    We're skipping validation to make mongokit behave more like pymongo (which
    is it's ancestor). This allows us to insert DB keys that are not explicitly
    part of it's defined structure below. For example, this allows us to insert
    a metric (key) that is not an fps, frametime or malloc.
    """
    use_dot_notation = True
    skip_validation = True

    structure = {
        'branch': basestring,
        'buildnumber': int,
        'entrytime': datetime.datetime,
        'machinename': basestring,
        'metrics': {
            'fps': list,
            'frametime': list,
            'malloc': list,
        },
        'benchmarkname': basestring
    }
    required_fields = [
        'branch', 'buildnumber', 'machinename', 'benchmarkname', 'entrytime'
    ]


def insert_benchmark_result(resultData, benchResult):
    """
    This method accepts a dict which must contain the following keys : type(values)
    buildnumber : int
    machinename: str
    benchmarkname : str
    and
    metrics.[metricname] : [[datetime.datetime.isoformat], value], [datetime.datetime.isoformat], value]...]
    where [metricname] can be the name of any metric that you wish to store.

    It is used to store Execution Results into the database.
    """
    requiredKeys = ['branch', 'buildnumber', 'machinename', 'benchmarkname',
                    'benchmarkname']
    for key in requiredKeys:
        if key not in resultData.keys():
            logger.error(
                'The key %s was missing while attempting to store a Benchmark Result' % key)
            raise KeyError(
                'The key %s was missing while attempting to store a Benchmark Result' % key)

    for key in resultData.keys():
        if 'metrics' in str(key)[:7]:
            break
    else:
        raise KeyError(
            'The metrics key was missing while attempting to store a Benchmark Result')

    result = benchResult()
    result['entrytime'] = datetime.datetime.now()
    for key in resultData:
        if key.startswith('metrics.'):
            result['metrics'][key[8:]] = resultData[key]
            continue
        result[key] = resultData[key]
    result.save()
    return result


def get_unique_values_for_key(keyname, connData):
    """
    Fetch distinct keys from the database
    :param keyname: The key to fetch
    :return: A list of results from the DB
    """
    coll = connData()[1]['benchresults']
    return coll.distinct(keyname)


def get_mongo_objs(host, port, dbname,
                   username=None, passwd=None):
    """
    :return: Database connection, database instance and collection instance.
    """
    connection = Connection(host=host, port=port,
                            read_preference=ReadPreference.NEAREST)
    connection.register([BenchmarkResult])
    benchresult = getattr(connection, dbname).benchresults.BenchmarkResult
    db = Database(connection, dbname)
    if username is not None:
        db.authenticate(username, password=passwd)
    return connection, db, benchresult


def create_mongo_objs_getter_from_config(cfg):
    def conndata():
        return get_mongo_objs(
            cfg.MONGO_HOST, cfg.MONGO_PORT,cfg.DB_NAME,
            cfg.MONGO_USER, cfg.MONGO_PASS)
    return conndata


def get_last_entry_time(getCollection):  # pragma: no cover
    coll = getCollection()[1]['benchresults']
    cursor = coll.find({}, {"entrytime": 1}, skip=0, limit=1,
                           sort=([('entrytime', DESCENDING)]))
    return cursor[0]['entrytime']


def append_timedelta_filter(baseFilter, daysToReturn):
    """
    If this function isnt called with daysToReturn it will return baseFilter
    unmodified

    If this function is called with days to return it will modify $match in
    baseFilter to include the number of days passed in

    :param baseFilter: a working list of filters that can be passed into
       pymongos aggregation framework
    :param daysToReturn: the number of days from now() to fetch results for
    """
    if not daysToReturn:
        return baseFilter

    #the earliest datetime to get data from
    earliest = datetime.datetime.now() - datetime.timedelta(days=daysToReturn)
    baseFilter[0]['$match'].update({"entrytime": {'$gte': earliest}})
    return baseFilter


def format_cursor_to_scatterjson(cursor, metric, calc):
    testcases = {}
    for item in cursor:
        tcname = item['benchmarkname']
        value = {
            'key': item['entrytime'],
            'value': item['AggregateData'][metric][str(calc)],
            'id': str(item['_id']),
            'changelist': item['buildnumber'],
            'branch': item['branch']}
        testcases.setdefault(tcname, []).append(value)
    return testcases


def drop_benchmark(benchmarkname, collection):
    collection.remove({'benchmarkname': benchmarkname})


def set_search_date_restriction(search, daysToReturn):
    earliest = datetime.datetime.now() - datetime.timedelta(days=daysToReturn)
    search["entrytime"] = {'$gte': earliest}
    return search
