import arrow
import bson
import datetime

from mongo_interface.util.config import GenConfig
from mongo_interface.util.mongo_utils import DbConnection


def update_smpte_flm_include_id(flm_dict: dict = {}, clear_all_id_keys_generated: bool = False,
                                lookup_collection=None, config: dict = None):
    """ Take a SMPTE FLM and a list of keywords and apply bson objectIDs to the corresponding elements

    :param dict flm_dict: the FLM
    :param bool clear_all_id_keys_generated: to remove all of the keys that would have been generated by this
    :param string lookup_collection: database name for the lookup table
    :param dict config: override configuration data
    :return: None or the initial flm_dict
    :rtype None or dict
    """

    # Review first it has the lookup db, if it does not have it return the same element
    if not lookup_collection:
        return flm_dict

    # Create connection to lookup Db.
    config = GenConfig(config=config)
    db = DbConnection(config['flm-db'])
    db_conn = db.conn()
    lookup_db_conn = db_conn[lookup_collection]

    stack = [flm_dict]
    while stack:
        o = stack.pop()
        if isinstance(o, dict):
            for key, val in o.items():
                # if o[key] is not None and type(o[key]) is list:
                if val is not None and type(val) is list:
                    for item in val:
                        if type(item) is dict:
                            keyword = key.split('List')[0].lower()
                            new_key = f'_{keyword}_id'
                            # override for same schema used
                            if keyword == 'nonsecuritydevice':
                                new_key = '_device_id'
                            if clear_all_id_keys_generated:
                                if new_key in item:
                                    item.pop(new_key, None)
                            else:
                                if new_key not in item:
                                    default_id = str(bson.ObjectId())
                                    if keyword in ['device']:
                                        default_id = update_device(item=item, default_id=default_id,
                                                                   lookup_db=lookup_db_conn)
                                    item[new_key] = default_id
                            stack.append(item)
                        elif type(item) is list:
                            stack.extend(item)
                elif type(val) is dict:
                    stack.append(val)
                else:
                    o[key] = cleanup_date_fields(key, val)
        elif isinstance(o, list):
            stack.extend(o)


def cleanup_date_fields(key, value):
    """ Date fields should not contain microseconds

    :param str key: element name ie. last_modified
    :param str value:  string representation of a date/datetime
    :return:  datetime object without microseconds
    :rtype: datetime
    """
    if key in ['last_modified', 'created_date']:
        return arrow.get(value).floor('second').datetime
    else:
        return value


def update_device(item: dict = [], default_id=None, lookup_db=None, matching_trusted_device_id=None):
    """ Updates a device with the appropriate unique id and places it into a device lookup table

    :param dict item: device object
    :param str default_id: string representation of a bson.ObjectId
    :param lookup_db: mongodb client connection to a specific database
    :param str matching_trusted_device_id: unique identifier for a device and it's active certificate
    :return: the unique id for the device
    :rtype: string
    """

    found_trusted_id = None
    doc_id = None

    make = item['Manufacturer']
    model = item['ModelNumber']
    serial = item['DeviceSerial']
    d_type = item['DeviceTypeID']

    lookup_match = lookup_db.device_lookup.find({'$and': [{'Manufacturer': make},
                                                          {'ModelNumber': model},
                                                          {'DeviceSerial': serial},
                                                          {'DeviceTypeID': d_type}]}
                                                )

    found_matches = lookup_match.count()
    if found_matches == 1:
        for l in lookup_match:
            default_id = l['_device_id']
            doc_id = l['_id']
            found_trusted_id = l['trusted_device_id']
    elif found_matches > 1:
        raise Exception(f'More than one match for a given make: {make},'
                        ' model: {model}, serial: {serial}, type: {d_type}')

    if not found_matches:
        insert_doc = {
            '_device_id': default_id,
            'Manufacturer': make,
            'ModelNumber': model,
            'DeviceSerial': serial,
            'DeviceTypeID': d_type,
            'trusted_device_id': matching_trusted_device_id,
            'update_date': datetime.datetime.utcnow(),
            'create_date': datetime.datetime.utcnow()
        }
        lookup_db.insert_one(insert_doc)
    elif found_matches == 1:
        if found_trusted_id == None:
            lookup_db.update_one({'_id': bson.ObjectId(doc_id)}, {'$set': {
                'trusted_device_id': matching_trusted_device_id,
                'update_date': datetime.datetime.utcnow()
            }})

        elif matching_trusted_device_id and found_trusted_id and found_trusted_id != matching_trusted_device_id:
            raise Exception(
                f'Unable to process, the trusted device id ({found_trusted_id}) for {make} {model} {serial} already exists and cannot be altered to be {matching_trusted_device_id}')

    return default_id


def update_ids_in_all_flms(config: dict = None) -> None:
    """ Update all FLMs such that all objects in lists are given unique identifiers

    :param dict config: override configuration
    :return: None
    """
    config = GenConfig(config=config)
    db = DbConnection(config['flm-db'])
    db_conn = db.conn()
    flm_connection = db_conn['smpte_flm']

    # Get all FLM data
    flms_to_process = flm_connection.find({})

    # Process all the documents
    for x, data in enumerate(flms_to_process):
        update_smpte_flm_include_id(flm_dict=data['flm'], lookup_collection='device_lookup', config=config)
        flm_connection.update_one({'_id': data['_id']}, {"$set": data}, upsert=False)
