/* eslint-disable no-use-before-define */

const _ = require('lodash');

const COMMON_TYPES = [
    'users', 'departments', 'groups', 'organizations', 'sessions',
];

const ENTITY_TYPE_MAP = {
    users: 'user',
    departments: 'department',
    groups: 'group',
    organizations: 'organization',
    sessions: 'session',
};

const PRIVATE_KEYS = [
    'auth', 'headers', 'tvm_tickets', 'user_ticket',
];

function merge(target, source) {
    if (!target) {
        target = {};
    }

    const keys = _.union(_.keys(target), _.keys(source));

    keys.forEach(key => {
        let mergeable = false;

        if (target[key] === undefined) {
            mergeable = true;
        } else if (target[key] instanceof Array) {
            mergeable = source[key] instanceof Array && source[key].length;
        } else {
            mergeable = source[key] !== undefined && source[key] !== null;
        }

        if (mergeable) {
            target[key] = source[key];
        }
    });

    return target;
}

function include(index, item) {
    if (item && item.id !== undefined) {
        item.id = String(item.id);
        index[item.id] = merge(index[item.id], item);
    }

    return index;
}

function isObject(x) {
    return (typeof x === 'object') && (x !== null) && !(x instanceof Array);
}

function isObjectArray(x) {
    return (x instanceof Array) && isObject(x[0]);
}

function toNullableString(x) {
    return x === null || x === undefined ? x : String(x);
}

function isEmptyDepartment(department) {
    const departmentKeys = Object.keys(department) || [];

    return !departmentKeys.length || (departmentKeys.length === 1 && departmentKeys[0] === 'id');
}

function preprocessUser(user, storage) {
    user.id = toNullableString(user.id);

    if (isObjectArray(user.groups)) {
        user.groups = user.groups
            // оставляем только нетехнические 'generic' группы
            .filter(group => group.type === 'generic')
            .map(group => {
                include(storage.groups, preprocessGroup(group, storage));

                return group.id;
            });
    }

    if (isObject(user.department)) {
        include(storage.departments, preprocessDepartment(user.department, storage));
        user.department = String(user.department.id);
    }

    if (user.department_id !== undefined) {
        user.department = toNullableString(user.department_id);
        delete user.department_id;
    }

    return user;
}

function preprocessDepartment(department, storage) {
    if (!department || isEmptyDepartment(department)) {
        return null;
    }

    department.id = toNullableString(department.id);

    if (isObjectArray(department.parents)) {
        department.parents = department.parents
            .map(parent => {
                include(storage.departments, preprocessDepartment(parent, storage));

                return parent.id;
            });
    }

    if (isObject(department.head)) {
        include(storage.users, preprocessUser(department.head, storage));
        department.head = department.head.id;
    }

    if (isObject(department.parent)) {
        department.parent = toNullableString(department.parent.id);
    }

    if (department.parent_id !== undefined) {
        department.parent = toNullableString(department.parent_id);
        delete department.parent_id;
    }

    return department;
}

function preprocessGroup(group, storage) {
    group.id = toNullableString(group.id);

    if (isObject(group.author)) {
        include(storage.users, preprocessUser(group.author, storage));
        group.author = group.author.id;
    }

    if (isObjectArray(group.members) && group.members[0].object) {
        group.members = preprocessMixedEntityList(group.members, storage);
    }

    if (isObjectArray(group.admins)) {
        group.admins = group.admins
            .map(admin => {
                include(storage.users, preprocessUser(admin, storage));

                return admin.id;
            });
    }

    if (group.author_id !== undefined) {
        group.author = toNullableString(group.author_id);
        delete group.author_id;
    }

    return group;
}

function preprocessMixedEntityList(list, storage) {
    if (!list || !list.length) {
        return list;
    }

    return list.map(item => {
        const type = item.type || item.object_type;
        const itemData = item.object;

        switch (type) {
            case 'user':
                include(storage.users,
                    preprocessUser(itemData, storage));
                break;
            case 'department':
                include(storage.departments,
                    preprocessDepartment(itemData, storage));
                break;
            case 'group':
                include(storage.groups,
                    preprocessGroup(itemData, storage));
                break;
        }

        const result = { id: toNullableString(itemData.id), type };

        if (item.author) {
            include(storage.users, preprocessUser(item.author, storage));

            result.author_id = toNullableString(item.author.id);
        }

        if (item.created_at) {
            result.created_at = item.created_at;
        }

        if (item.comment) {
            result.comment = item.comment;
        }

        return result;
    });
}

function preprocessOrganization(org, storage) {
    org.id = toNullableString(org.id);

    if (org.admin_id !== undefined) {
        org.admin = toNullableString(org.admin_id);
        delete org.admin_id;
    }

    const currentOrgId = _.get(storage, 'state.organization');

    // удостовериваемся, что в индексы попадут данные только текущей организации
    if ((!currentOrgId || currentOrgId === org.id) && isObject(org.head)) {
        include(storage.users, preprocessUser(org.head, storage));
        org.head = org.head.id;
    }

    return org;
}

const preprocessors = {
    users: preprocessUser,
    departments: preprocessDepartment,
    groups: preprocessGroup,
    organizations: preprocessOrganization,
};

function preprocessList(data, index) {
    // пропускаем пустые данные и уже приведенные списки
    if (!data || data.meta || (data.type && data.order)) {
        return data;
    }

    const output = _.pick(data, ['query', 'type']);
    const meta = _.pick(data, ['page', 'pages', 'total', 'per_page']);

    output.order = [];
    output.meta = meta;

    let list;

    if (data instanceof Array) {
        list = data;
    } else if (data && (data.result instanceof Array)) {
        list = data.result;
    }

    if (list) {
        list.forEach(item => {
            include(index, item);
            output.order.push(item.id);
        });
    }

    return output;
}

function preprocessCommonEntities(data) {
    let sessionOrder;

    if (isObjectArray(data.sessions)) {
        sessionOrder = data.sessions.map(item => String(item.id));
    }

    if (!(data.sources instanceof Array)) {
        data.sources = [];
    }

    // создаём индексы сущностей
    // перекладываем сущности в соответствующие им индексы
    COMMON_TYPES.forEach(type => {
        const index = {};

        if (data[type] instanceof Array) {
            data[type].forEach(item => {
                include(index, item);
                if (type !== 'sessions') {
                    data.sources.push({ id: item.id, type: ENTITY_TYPE_MAP[type] });
                }
            });
        }

        if (!isObject(data[type])) {
            data[type] = index;
        }
    });

    if (!data.lists) {
        data.lists = [];
    }

    if (data.lists instanceof Array) {
        data.lists.forEach((item, index) => {
            data.lists[index] = preprocessList(item, data[item.type]);
        });
        // отдельно добавляем список сессий, чтобы сохранить их порядок
        if (sessionOrder && !_.find(data.lists, { type: 'sessions' })) {
            data.lists.push({ type: 'sessions', order: sessionOrder });
        }
    }

    if (data.currentUser) {
        include(data.users, preprocessUser(data.currentUser, data));
    }

    // упорядочиваем данные в списках;
    // к этому этапу индексы всех сущностей должны быть готовы, поэтому
    // упорядочивание проводим в отдельном проходе
    COMMON_TYPES.forEach(type => {
        const preprocessItem = preprocessors[type];

        if (preprocessItem) {
            _.each(data[type], item => preprocessItem(item, data));
        }
    });

    return data;
}

function preprocessSubscriptionData(data) {
    const subscribers = _.get(data, 'licenses.subscribers');
    const licensesRequests = _.get(data, 'licenses.requests');
    const preview = _.get(data, 'licenses.preview');

    if (subscribers) {
        Object.keys(subscribers).forEach(serviceId => {
            subscribers[serviceId] = preprocessMixedEntityList(
                subscribers[serviceId], data
            );
        });
    }

    if (licensesRequests) {
        Object.keys(licensesRequests).forEach(serviceId => {
            licensesRequests[serviceId] = preprocessMixedEntityList(
                licensesRequests[serviceId], data
            );
        });
    }

    if (preview) {
        Object.keys(preview).forEach(serviceId => {
            (preview[serviceId] || []).forEach(item => preprocessUser(item, data));
        });
    }

    return data;
}

function finalizePreprocessing(data) {
    // убираем незаполненные индексы
    COMMON_TYPES.forEach(type => {
        if (_.isEmpty(data[type])) {
            delete data[type];
        }
    });

    return data;
}

function preprocessMixedResponse(data, options = {}) {
    preprocessCommonEntities(data);
    preprocessSubscriptionData(data);
    finalizePreprocessing(data);

    if (options.removePrivateKeys) {
        PRIVATE_KEYS.forEach(key => {
            delete data[key];
        });
    }

    return data;
}

module.exports = {
    preprocessMixedResponse,
};
