import hashlib

from context import get_json_value
from hotfolder_modules.util import add_to_json, InternalError, print_traceback, get_link_pool_id, dumpjs


def get_object_hash(obj):
    return hashlib.sha1(dumpjs(obj, ind=0, sort=True).encode('utf-8')).digest()


def search_objects_by_query(worker, query, user_id):
    # search objects by a query and return objects array from search result

    worker.logger.debug('[search_objects_by_query] => query: {}'.format(dumpjs(query)))

    objs = []

    try:
        connection = worker.get_connection('search')
        response = worker.manager.easydb_context.search(
            connection, 'user', user_id, query, False)
        if 'objects' in response:
            objs = response['objects']
    except Exception as e:
        raise InternalError('Could not perform search: {}'.format(e.message))
    finally:
        connection.close()

    worker.logger.debug('[search_objects_by_query] => found {} objects'.format(len(objs)))
    return objs


def create_new_object(worker, session, new_object, obj, eas_info):
    # creates and returns a new object to be imported. based on asset information, collection settings

    _new_object = None

    objecttype = obj.collection.objecttype
    new_tags = obj.collection.tags

    # Create Linked and Nested Objects necessary to map Meta Data
    if objecttype in new_object:
        _new_object = create_meta_data_objects(worker, session, new_object, obj, objecttype)
    else:
        _new_object = {}

    if _new_object is None:
        return None

    # Add mandatory default key/value sets
    add_to_json(_new_object, '_mask', obj.collection.mask_name)
    add_to_json(_new_object, '{0}._version'.format(objecttype), 1)
    add_to_json(_new_object, '{0}._pool.pool._id'.format(objecttype), obj.collection.pool_id)

    set_asset_fields(worker, session, _new_object, obj.collection, eas_info[0])

    # Add Tags set for collection
    tags = set()
    if len(new_tags) > 0:

        if '_tags' in new_object:
            for element in new_object['_tags']:
                tag_id = get_json_value(element, '_id')
                if tag_id is not None:
                    tags.add(int(tag_id))

        else:
            _new_object['_tags'] = []

        for tag_id in new_tags:
            if tag_id in tags:
                return None

            _new_object['_tags'].append({
                '_id': int(tag_id)
            })
            tags.add(tag_id)

    _new_object['__files'] = eas_info[2]

    worker.logger.debug('create new object: {}'.format(dumpjs(_new_object)))

    return _new_object


def format_asset(eas_ids, first_preferred=True):
    preferred = first_preferred
    asset = []

    for v in eas_ids:
        asset.append({
            '_id': v,
            'preferred': preferred
        })
        preferred = False

    return asset


def format_assets_in_nested(eas_ids, asset_column, is_reverse):
    sub_objs = []

    for eas_versions in eas_ids:
        sub_obj = {
            asset_column: []
        }

        if is_reverse:
            sub_obj['_id'] = None
            sub_obj['_version'] = 1

        preferred = True

        for version in eas_versions:
            sub_obj[asset_column].append({
                '_id': version,
                'preferred': preferred
            })
            preferred = False

        sub_objs.append(sub_obj)

    return sub_objs


def parse_sid_from_link(obj, link_key, worker, sids_for_search):
    link_obj = get_json_value(obj, link_key)
    if not isinstance(link_obj, dict):
        return
    worker.logger.debug('[collect_linked_objects] asset in link {}: {}'.format(link_key, dumpjs(link_obj)))
    if not '_system_object_id' in link_obj:
        return
    sid = link_obj['_system_object_id']
    if sid in worker.linked_objects_to_update:
        return
    sids_for_search.add(sid)


def collect_linked_objects(worker, session, objects_by_filename, collection):
    # collect linked objects with assets in the objects that need to be updated

    path_split = collection.eas_column_path.split('.')

    if len(path_split) < 3 or len(path_split) > 4:
        return

    sids_for_search = set()

    if len(path_split) == 3:
        # find linked objects in direct links in the search results
        link_key = '{}.{}'.format(collection.objecttype, path_split[0])
        link_ot = path_split[1]
        for obj in list(objects_by_filename.values()):
            parse_sid_from_link(obj, link_key, worker, sids_for_search)

    elif len(path_split) == 4:
        # find linked objects in direct links in the search results
        nested_key = '{}.{}'.format(collection.objecttype, path_split[0])
        # link_in_nested_key = '{}.{}'.format(path_split[1], path_split[2])
        link_in_nested_key = path_split[1]
        link_ot = path_split[2]
        for obj in list(objects_by_filename.values()):
            nested_table = get_json_value(obj, nested_key)
            if not isinstance(nested_table, list):
                continue
            for elem in nested_table:
                worker.logger.debug('nested: key to link: {}, elem: {}'.format(link_in_nested_key, dumpjs(elem)))
                # collect link in nested table elem at link_in_nested_key
                parse_sid_from_link(elem, link_in_nested_key, worker, sids_for_search)

    # search by _system_object_id
    worker.logger.debug('[collect_linked_objects] found {} linked objects'.format(len(sids_for_search)))
    if len(sids_for_search) < 1:
        return

    query = {
        'exclude_fields': [
            '_standard',
            '_owner',
            '_collection',
            '_score'
        ],
        'format': 'long',
        'generate_rights': False,
        'objecttypes': [
            link_ot
        ],
        'search': [
            {
                'type': 'in',
                'in': list(sids_for_search),
                'fields': [
                    '_system_object_id'
                ]
            }
        ]
    }

    result_objects = search_objects_by_query(worker, query, collection.user_id)
    if not isinstance(result_objects, list):
        return
    if len(result_objects) < 1:
        return

    # map linked objects by _system_object_id
    for obj in result_objects:
        if not '_system_object_id' in obj:
            continue
        sid = obj['_system_object_id']
        worker.linked_objects_to_update[sid] = (obj, False)


def set_asset_fields(worker, session, obj, collection, eas_ids, for_update=False):
    # Add link to files on eas Server. Either as Single assets or Asset-Versions or -Series
    # return True if the object itself needs to be reindexed (in case it is not a new object anyways)

    path_split = collection.eas_column_path.split('.')
    worker.logger.debug('[set_asset_fields] eas path: {}'.format(str(path_split)))

    reindex = False

    # handle update policy
    if for_update:
        worker.logger.debug('[set_asset_fields] update loaded object, update_policy: {}'.format(collection.update_policy))

    eas_versions = eas_ids[0]

    # Basic Asset Field
    # <table>.<asset>
    if len(path_split) == 1:

        asset_key = '{}.{}'.format(collection.objecttype, collection.eas_column_path)

        if not for_update:
            # create new asset(s) in the new object
            asset = format_asset(eas_versions)
            add_to_json(obj, asset_key, asset)

        else:
            # get the asset field and add/replace assets depending on the update policy
            asset = get_json_value(obj, asset_key)

            # for the existing asset, apply the update_policy
            if isinstance(asset, list):
                if collection.update_policy == collection.POLICY_CREATE_VERSION:
                    asset_versions = format_asset(eas_versions, False)
                    add_to_json(obj, asset_key, asset + asset_versions, worker.logger)
                elif collection.update_policy == collection.POLICY_CREATE_VERSION_PREFERRED:
                    # set all other asset version to non-preferred
                    for i in range(len(asset)):
                        asset[i]['preferred'] = False
                    asset_versions = format_asset(eas_versions, True)
                    add_to_json(obj, asset_key, asset + asset_versions, worker.logger)
                elif collection.update_policy == collection.POLICY_REPLACE:
                    # replace the existing asset
                    asset = format_asset(eas_versions)
                    add_to_json(obj, asset_key, asset)

                # invalid combination of eas column, update mode and update policy
                else:
                    raise InternalError('Invalid update policy {} for EAS-Path: {}'.format(
                        collection.update_policy, collection.eas_column_path))

            # set new asset value
            else:
                asset = format_asset(eas_versions)
                add_to_json(obj, asset_key, asset)

            # the updated object needs to be reindexed
            reindex = True

    # Nested Asset Field or Reverse Nested Objects
    # <table>._nested:nested_table.<asset>
    # <table>._reverse_nested:other_table.<asset>
    elif len(path_split) == 2:

        sub_obj_key = '{}.{}'.format(collection.objecttype, path_split[0])
        asset_column = path_split[1]
        is_reverse = path_split[0].startswith('_reverse_nested:')

        if not for_update:
            sub_objs = format_assets_in_nested(eas_ids, asset_column, is_reverse)
            add_to_json(obj, sub_obj_key, sub_objs)

        else:
            # get the asset fields in the nested/reverse nested table and add/replace assets depending on the update policy
            sub_objs = get_json_value(obj, sub_obj_key)

            # for the existing assets in the nested table, apply the update_policy
            if isinstance(sub_objs, list):
                new_asset_sub_objs = format_assets_in_nested(eas_ids, asset_column, is_reverse)

                # append asset to asset field in nested -> update object
                if collection.update_policy == collection.POLICY_CREATE_VERSION or collection.update_policy == collection.POLICY_CREATE_VERSION_PREFERRED:
                    add_to_json(obj, sub_obj_key, sub_objs + new_asset_sub_objs, worker.logger)

                # remove all assets from nested, add new asset as only asset in nested -> update object
                elif collection.update_policy == collection.POLICY_REPLACE:
                    add_to_json(obj, sub_obj_key, new_asset_sub_objs, worker.logger)

                elif collection.update_policy == collection.POLICY_REFUSE:
                    # if nested is not empty: error
                    if len(sub_objs) > 0:
                        raise InternalError('Update policy {}: refuse updating of non-empty nested table for EAS-Path: {}'.format(
                            collection.update_policy, collection.eas_column_path))
                    # append asset to asset field in nested, set new asset as preferred -> update object
                    else:
                        add_to_json(obj, sub_obj_key, new_asset_sub_objs, worker.logger)

                # unknown update policy
                else:
                    raise InternalError('Unknown update policy {} for EAS-Path in nested table: {}'.format(
                        collection.update_policy, collection.eas_column_path))

            # set new asset value
            else:
                sub_objs = format_assets_in_nested(eas_ids, asset_column, is_reverse)
                add_to_json(obj, sub_obj_key, sub_objs)

            # the updated object needs to be reindexed
            reindex = True

    # Asset within Linked Object
    # <table>.lk_other_table.<asset>
    elif len(path_split) == 3:
        link_key = '{}.{}'.format(collection.objecttype, path_split[0])
        link_ot = path_split[1]
        asset_field_in_link = path_split[2]
        worker.logger.debug('[set_asset_fields] link {}, ot: {}'.format(link_key, link_ot))

        if not for_update:
            add_to_json(
                obj,
                link_key,
                create_linked_object(
                    worker,
                    session,
                    eas_versions,
                    link_ot,
                    collection.datamodel,
                    asset_field_in_link,
                    obj,
                    get_link_pool_id(collection, link_ot, path_split[0])
                )
            )

        else:
            # get the asset fields in the nested/reverse nested table and add/replace assets depending on the update policy
            link_obj = get_json_value(obj, link_key)

            # for the existing assets in the nested table, apply the update_policy and update the linked object (the current object does not need to be updated)
            if isinstance(link_obj, dict):
                # check if the linked object is one of the loaded and updated objects
                sid = get_json_value(link_obj, '_system_object_id')
                if sid is not None and sid in worker.linked_objects_to_update:
                    _obj, updated = worker.linked_objects_to_update[sid]
                    if not updated:
                        worker.logger.debug('[set_asset_fields] policy {}. update linked object (_system_object_id: {}): {}'.format(
                            collection.update_policy, sid, dumpjs(_obj)))
                        update_linked_object(
                            worker,
                            session,
                            _obj,
                            collection,
                            eas_versions,
                            asset_field_in_link,
                            obj
                        )

    # Asset within each Linked Object in a Nested Field
    # <table>.lk_other_table._nested:other_nested_table.<asset>
    elif len(path_split) == 4:

        nested_key = '{}.{}'.format(collection.objecttype, path_split[0])
        link_in_nested_key = path_split[1]
        link_ot = path_split[2]
        asset_field_in_link = path_split[3]
        worker.logger.debug('[set_asset_fields] link {} in nested {}, ot: {}'.format(link_in_nested_key, nested_key, link_ot))

        new_linked_objects = create_nested_linked_objects(
            worker,
            session,
            collection,
            eas_ids,
            link_ot,
            collection.datamodel,
            asset_field_in_link,
            link_in_nested_key,
            obj,
            get_link_pool_id(collection, link_ot, nested_key + '.' + path_split[1]),
            path_split[0].startswith('_reverse_nested:')
        )

        if len(new_linked_objects) > 0:

            if not for_update:
                add_to_json(
                    obj,
                    nested_key,
                    new_linked_objects
                )

            else:
                # find linked objects in direct links in the search results
                nested_table = get_json_value(obj, nested_key)
                worker.logger.debug('[set_asset_fields] nested_table: {}'.format(dumpjs(nested_table)))
                if nested_table is None:
                    nested_table = []
                if isinstance(nested_table, list):

                    linked_objects = []
                    for o in nested_table:
                        linked_objects.append(o)

                    for o in new_linked_objects:
                        linked_objects.append(o)

                    add_to_json(
                        obj,
                        nested_key,
                        linked_objects
                    )

                    # the updated object needs to be reindexed
                    reindex = True

    else:
        raise InternalError('Incorrect EAS-Path: {}'.format('.'.join(path_split)))

    return reindex


def create_nested_linked_objects(worker, session, collection, eas_ids, objecttype, datamodel, eas_field, link_field, obj, pool_id, in_reverse=False):
    # Called if Main Object contains a list of linked Objects. For each linked Object create_linked_object() is called

    linked_objects = []

    for versions in eas_ids:
        obj = {
            link_field: create_linked_object(
                worker,
                session,
                versions,
                objecttype,
                datamodel,
                eas_field,
                obj,
                pool_id
            )
        }
        if in_reverse:
            obj['_version'] = 1

        linked_objects.append(obj)

    return linked_objects


def create_linked_object(worker, session, eas_versions, objecttype, datamodel, eas_field, obj, pool_id):
    # If files are uplaoaded to be part of a linked, rather then the main easydb-Object, these linked Objects must be first created here

    mask = None

    for mask_js in datamodel['maskset']['masks']:
        if mask_js['table_name_hint'] == objecttype:
            mask = mask_js['name']
            break

    if mask is None:
        raise Exception('could not get mask for linked object %s' % objecttype)

    new_object = {
        '_objecttype': objecttype,
        '_mask': mask,
        objecttype: {
            '_id': None,
            '_version': 1,
            eas_field: []
        }
    }

    if pool_id is not None:
        new_object[objecttype]['_pool'] = {
            'pool': {
                '_id': pool_id
            }
        }

    preferred = True

    for version in eas_versions:
        new_object[objecttype][eas_field].append({
            '_id': version,
            'preferred': preferred
        })
        preferred = False

    connection = worker.get_connection()
    try:
        worker.logger.info('Uploading linked Object: {}'.format(dumpjs(new_object)))
        ret = worker.manager.easydb_context.dbapi_import(
            connection,
            session,
            mask,
            [new_object],
            None
        )
        connection.commit()
        return ret[0]
    except Exception as e:
        print_traceback(e, logger=worker.logger)
        worker.handle_object_error(obj, e)
        return None
    finally:
        connection.close()

    return None


def update_linked_object(worker, session, link_obj, collection, eas_versions, eas_field, obj):

    worker.logger.debug('[update_linked_object] obj: {}'.format(dumpjs(link_obj)))

    new_object = link_obj

    mask = get_json_value(link_obj, '_mask')
    if mask is None:
        return None

    link_ot = get_json_value(link_obj, '_objecttype')
    if link_ot is None:
        return None

    version = get_json_value(link_obj, '{}._version'.format(link_ot))
    if not isinstance(version, int):
        return None
    add_to_json(new_object, '_version', version + 1)

    # get the asset field and add/replace assets depending on the update policy
    asset_key = '{}.{}'.format(link_ot, eas_field)
    asset = get_json_value(link_obj, asset_key)
    worker.logger.debug('[update_linked_object] asset: obj[{}]: {}'.format(asset_key, dumpjs(asset)))

    # for the existing asset, apply the update_policy
    if isinstance(asset, list):
        if collection.update_policy == collection.POLICY_CREATE_VERSION:
            add_to_json(new_object, asset_key, asset +
                        format_asset(eas_versions, False), worker.logger)
        elif collection.update_policy == collection.POLICY_CREATE_VERSION_PREFERRED:
            # set all other asset version to non-preferred
            for i in range(len(asset)):
                asset[i]['preferred'] = False
            add_to_json(new_object, asset_key, asset +
                        format_asset(eas_versions), worker.logger)
        elif collection.update_policy == collection.POLICY_REPLACE:
            # replace the existing asset
            add_to_json(new_object, asset_key, format_asset(eas_versions))

        # invalid combination of eas column, update mode and update policy
        else:
            raise InternalError('Invalid update policy {} for EAS-Path in linked object: {}'.format(
                collection.update_policy, collection.eas_column_path))

    # set new asset value
    else:
        add_to_json(new_object, asset_key, format_asset(eas_versions))

    connection = worker.get_connection()
    try:
        worker.logger.info('Uploading updated linked Object: {}'.format(dumpjs(new_object)))
        ret = worker.manager.easydb_context.dbapi_import(
            connection,
            session,
            mask,
            [new_object],
            None
        )
        connection.commit()
        return ret[0]
    except Exception as e:
        worker.handle_object_error(obj, e)
        return None
    finally:
        connection.close()

    return None


def create_meta_data_objects(worker, session, new_object, obj, objecttype):
    # If Metadata-Mapping defines Metadata to be mapped to linked objects, these have to be first created here

    _new_object = new_object

    for key in list(new_object[objecttype].keys()):
        entry = new_object[objecttype][key]

        # Create Objects for Nested Fields
        if key.startswith('_nested:'):
            for i in range(len(entry)):
                entry_dict = entry[i]

                for nested_key in list(entry_dict.keys()):
                    nested_entry = entry_dict[nested_key]

                    if isinstance(nested_entry, dict):
                        if '_mapped_metadata_created' in list(nested_entry.keys()):
                            if nested_entry['_mapped_metadata_created'] == True:
                                linked_mask = nested_entry['_mask']
                                linked_objecttype = nested_entry['_objecttype']
                                nested_entry[linked_objecttype]['_version'] = 1
                                nested_entry[linked_objecttype]['_id'] = None

                                linked_pool_id = get_link_pool_id(obj.collection, linked_objecttype, key)
                                if linked_pool_id is not None:
                                    add_to_json(nested_entry, '{0}._pool.pool._id'.format(
                                        linked_objecttype), linked_pool_id)

                                entry_hash = get_object_hash(nested_entry)
                                if not linked_objecttype in worker.created_objects:
                                    worker.created_objects[linked_objecttype] = {
                                    }

                                if entry_hash in worker.created_objects[linked_objecttype]:
                                    # the linked object has already been created, do not create a new object
                                    obj_id = worker.created_objects[linked_objecttype][entry_hash]
                                    _new_object[objecttype][key][i][nested_key][linked_objecttype]['_id'] = obj_id
                                    worker.logger.debug('Nested Meta-Data-Object already exists: {} (ID: {})'.format(dumpjs(nested_entry), obj_id))

                                else:
                                    connection = worker.get_connection()
                                    try:
                                        worker.logger.debug('Uploading nested Meta-Data-Object: {}'.format(dumpjs(nested_entry)))
                                        ret = worker.manager.easydb_context.dbapi_import(
                                            connection,
                                            session,
                                            linked_mask,
                                            [nested_entry],
                                            None
                                        )
                                        connection.commit()
                                    except Exception as e:
                                        worker.handle_object_error(obj, e)
                                        raise
                                    finally:
                                        connection.close()

                                    obj_id = ret[0][linked_objecttype]['_id']
                                    _new_object[objecttype][key][i][nested_key][linked_objecttype]['_id'] = obj_id
                                    worker.created_objects[linked_objecttype][entry_hash] = obj_id

        # Create Linked Objects
        else:
            if isinstance(entry, dict):
                if '_mapped_metadata_created' in list(entry.keys()):
                    if entry['_mapped_metadata_created'] == True:
                        linked_mask = entry['_mask']
                        linked_objecttype = entry['_objecttype']
                        entry[linked_objecttype]['_version'] = 1
                        entry[linked_objecttype]['_id'] = None

                        linked_pool_id = get_link_pool_id(obj.collection, linked_objecttype, key)
                        if linked_pool_id is not None:
                            add_to_json(entry, '{0}._pool.pool._id'.format(linked_objecttype), linked_pool_id)

                        entry_hash = get_object_hash(entry)
                        if not linked_objecttype in worker.created_objects:
                            worker.created_objects[linked_objecttype] = {}

                        if entry_hash in worker.created_objects[linked_objecttype]:
                            # the linked object has already been created, do not create a new object
                            obj_id = worker.created_objects[linked_objecttype][entry_hash]
                            _new_object[objecttype][key][linked_objecttype]['_id'] = obj_id
                            worker.logger.debug('Meta-Data-Object already exists: {} (ID: {})'.format(dumpjs(entry), obj_id))

                        else:
                            connection = worker.get_connection()
                            try:
                                worker.logger.debug('Uploading Meta-Data-Object: {}'.format(dumpjs(entry)))
                                ret = worker.manager.easydb_context.dbapi_import(
                                    connection,
                                    session,
                                    linked_mask,
                                    [entry],
                                    None
                                )
                                connection.commit()
                            except Exception as e:
                                worker.handle_object_error(obj, e)
                                raise
                            finally:
                                connection.close()

                            obj_id = ret[0][linked_objecttype]['_id']
                            _new_object[objecttype][key][linked_objecttype]['_id'] = obj_id
                            worker.created_objects[linked_objecttype][entry_hash] = obj_id

    return _new_object
