/* Copyright (C) 2016 NooBaa */
/**
 *
 * TIER_SERVER
 *
 */
'use strict';

const _ = require('lodash');
const config = require('../../../config');
const dbg = require('../../util/debug_module')(__filename);
const { RpcError } = require('../../rpc');
const size_utils = require('../../util/size_utils');
const Dispatcher = require('../notifications/dispatcher');
const nodes_client = require('../node_services/nodes_client');
const node_allocator = require('../node_services/node_allocator');
const system_store = require('./system_store').get_instance();
const chunk_config_utils = require('../utils/chunk_config_utils');
const SensitiveString = require('../../util/sensitive_string');

function new_tier_defaults(name, system_id, chunk_config, mirrors) {
    return {
        _id: system_store.new_system_store_id(),
        name: name,
        system: system_id,
        chunk_config,
        data_placement: 'SPREAD',
        mirrors,
    };
}

function new_policy_defaults(name, system_id, chunk_split_config, tiers_orders) {
    return {
        _id: system_store.new_system_store_id(),
        name: name,
        system: system_id,
        tiers: tiers_orders,
        chunk_split_config: _.defaults(chunk_split_config, {
            avg_chunk: config.CHUNK_SPLIT_AVG_CHUNK,
            delta_chunk: config.CHUNK_SPLIT_DELTA_CHUNK,
        }),
    };
}

/**
 *
 * CREATE_TIER
 *
 */
function create_tier(req) {
    const { attached_pools = [] } = req.rpc_params;
    const pools = attached_pools.map(pool_name =>
        req.system.pools_by_name[pool_name]
    );

    throw_on_invalid_pools_for_tier(pools);

    const changes = { insert: {} };
    const policy_pool_ids = pools.map(pool => pool._id);
    const chunk_config = chunk_config_utils.resolve_chunk_config(
        req.rpc_params.chunk_coder_config, req.account, req.system);
    if (!chunk_config._id) {
        chunk_config._id = system_store.new_system_store_id();
        changes.insert.chunk_configs = [chunk_config];
    }

    const mirrors = _convert_pools_to_data_placement_structure(policy_pool_ids, req.rpc_params.data_placement);

    check_tier_exists(req, req.rpc_params.name);
    const tier = new_tier_defaults(
        req.rpc_params.name,
        req.system._id,
        chunk_config._id,
        mirrors
    );
    if (req.rpc_params.data_placement) tier.data_placement = req.rpc_params.data_placement;
    changes.insert.tiers = [tier];

    dbg.log0('Creating new tier', tier);
    return system_store.make_changes(changes)
        .then(function() {
            req.load_auth();
            const created_tier = find_tier_by_name(req);
            return get_tier_info(created_tier);
        });
}



/**
 *
 * READ_TIER
 *
 */
function read_tier(req) {
    const tier = find_tier_by_name(req);
    const pool_names = [];

    _.each(tier.mirrors, object => {
        _.each(object.spread_pools, pool => {
            pool_names.push((pool && pool.name) || '');
        });
    });

    return Promise.all([
            nodes_client.instance().aggregate_nodes_by_pool(_.compact(pool_names), req.system._id),
            nodes_client.instance().aggregate_data_free_by_tier([String(tier._id)], req.system._id)
        ])
        .then(function(res) {
            let nodes_aggregate_pool = res[0];
            let available_to_upload = res[1];
            return get_tier_info(tier, nodes_aggregate_pool, { mirror_storage: available_to_upload[String(tier._id)] });
        });
}


function _convert_pools_to_data_placement_structure(pool_ids, data_placement) {
    let mirrors = [];
    if (data_placement === 'MIRROR') {
        _.forEach(pool_ids, pool_id => mirrors.push({
            _id: system_store.new_system_store_id(),
            spread_pools: [pool_id]
        }));
    } else {
        mirrors.push({
            _id: system_store.new_system_store_id(),
            spread_pools: pool_ids
        });
    }
    return mirrors;
}


/**
 *
 * UPDATE_TIER
 *
 */
function update_tier(req) {
    const { attached_pools = [] } = req.rpc_params;
    throw_on_invalid_pools_for_tier(attached_pools.map(pool_name =>
        req.system.pools_by_name[pool_name]
    ));

    const tier = find_tier_by_name(req);
    const updates = {
        _id: tier._id
    };
    const changes = {
        insert: {},
        update: { tiers: [updates] },
    };


    if (req.rpc_params.new_name) {
        updates.name = req.rpc_params.new_name;
    }

    let chunk_config_changed = false;
    if (req.rpc_params.chunk_coder_config) {
        const chunk_config = chunk_config_utils.resolve_chunk_config(
            req.rpc_params.chunk_coder_config, req.account, req.system);
        if (!chunk_config._id) {
            chunk_config._id = system_store.new_system_store_id();
            changes.insert.chunk_configs = [chunk_config];
        }
        if (chunk_config !== tier.chunk_config) {
            updates.chunk_config = chunk_config._id;
            chunk_config_changed = true;
        }
    }

    let old_pool_names = [];
    if (req.rpc_params.data_placement) {
        updates.data_placement = req.rpc_params.data_placement;
        // if node_pools are defined use it for the update otherwise use the existing
        if (req.rpc_params.attached_pools) {
            updates.mirrors = _convert_pools_to_data_placement_structure(
                _.map(req.rpc_params.attached_pools, pool_name => req.system.pools_by_name[pool_name]._id),
                req.rpc_params.data_placement || tier.data_placement);

            _.forEach(tier.mirrors, mirror_object => {
                old_pool_names = _.concat(old_pool_names, _.map((mirror_object && mirror_object.spread_pools) || [], pool => pool.name));
            });
            old_pool_names = _.compact(old_pool_names);
        } else if (tier.data_placement !== req.rpc_params.data_placement) {
            let pool_ids = [];
            _.forEach(tier.mirrors, mirror_object => {
                pool_ids = _.concat(pool_ids, _.map((mirror_object && mirror_object.spread_pools) || [], pool => pool._id));
            });
            pool_ids = _.compact(pool_ids);

            updates.mirrors = _convert_pools_to_data_placement_structure(pool_ids, req.rpc_params.data_placement);
        }
    }

    return system_store.make_changes(changes)
        .then(res => {
            const bucket = find_bucket_by_tier(req);
            if (bucket) {
                if (req.rpc_params.data_placement) { //Placement policy changes
                    const desc_string = [];
                    let policy_type_change = String(tier.data_placement) === String(req.rpc_params.data_placement) ? 'No changes' :
                        `Changed to ${req.rpc_params.data_placement} from ${tier.data_placement}`;
                    let removed_pools = _.difference(old_pool_names, req.rpc_params.attached_pools || []);
                    let added_pools = _.difference(req.rpc_params.attached_pools || [], old_pool_names);
                    desc_string.push(`Bucket policy was changed by: ${req.account && req.account.email.unwrap()}`);
                    desc_string.push(`Policy type: ${policy_type_change}`);
                    if (removed_pools.length) {
                        desc_string.push(`Removed resources: ${removed_pools.join(', ')}`);
                    }
                    if (added_pools.length) {
                        desc_string.push(`Added resources: ${added_pools.join(', ')}`);
                    }
                    Dispatcher.instance().activity({
                        event: 'bucket.edit_policy',
                        level: 'info',
                        system: req.system._id,
                        actor: req.account && req.account._id,
                        bucket: bucket._id,
                        desc: desc_string.join('\n'),
                    });
                }

                if (req.rpc_params.chunk_coder_config && chunk_config_changed) {
                    const new_cc_type = req.rpc_params.chunk_coder_config.parity_frags ? 'Erasure Coding' : 'Replication';
                    const old_cc_type = tier.chunk_config.chunk_coder_config.parity_frags ? 'Erasure Coding' : 'Replication';
                    let desc_string;
                    if (new_cc_type === old_cc_type) {
                        desc_string = `Data resiliency configuration was updated to ${new_cc_type}. `;
                    } else {
                        desc_string = `Data resiliency type was changed from ${old_cc_type} to ${new_cc_type}`;
                    }
                    if (new_cc_type === 'Replication') {
                        desc_string += `\nNumber of replicas: ${req.rpc_params.chunk_coder_config.replicas}`;
                    } else {
                        desc_string += `\nNumber of data fragments: ${req.rpc_params.chunk_coder_config.data_frags}`;
                        desc_string += `\nNumber of parity fragments: ${req.rpc_params.chunk_coder_config.parity_frags}`;
                    }
                    Dispatcher.instance().activity({
                        event: 'bucket.edit_resiliency',
                        level: 'info',
                        system: req.system._id,
                        actor: req.account && req.account._id,
                        bucket: bucket._id,
                        desc: desc_string,
                    });
                }

            }

            return res;
        });
}



/**
 *
 * DELETE_TIER
 *
 */
function delete_tier(req) {
    dbg.log0('Deleting tier', req.rpc_params.name, 'on', req.system._id);
    const tier = find_tier_by_name(req);
    return system_store.make_changes({ remove: { tiers: [tier._id] } });
}




// TIERING POLICY /////////////////////////////////////////////////

function create_policy(req) {
    const policy = policy_defaults_from_req(req);
    check_tiering_policy_exists(req, policy.name);
    dbg.log0('Creating tiering policy', policy);
    return system_store.make_changes({ insert: { tieringpolicies: [policy] } })
        .then(() => {
            req.load_auth();
            const created_policy = find_policy_by_name(req);
            return get_tiering_policy_info(created_policy);
        });
}

function update_policy(req) {
    const policy = find_policy_by_name(req);
    const policy_defaults = policy_defaults_from_req(req);
    const updates = _.pick(policy_defaults, 'chunk_split_config', 'tiers');
    if (_.isEmpty(updates)) return;
    updates._id = policy._id;
    return system_store.make_changes({ update: { tieringpolicies: [updates] } })
        .then(() => {
            req.load_auth();
            const created_policy = find_policy_by_name(req);
            return get_tiering_policy_info(created_policy);
        });

}

function _check_validity_of_configuration({ req, bucket_class_buckets }) {
    const { tiers } = req.rpc_params;
    if (!_.every(bucket_class_buckets, bucket => tiers.length >= bucket.tiering.tiers.length)) throw new RpcError('BAD_REQUEST', 'cannot remove tiers');
    try {
        const pools = [];
        tiers.forEach(tier => {
            if (tier.data_placement === 'MIRROR' && tier.attached_pools.length < 2) throw new RpcError('BAD_REQUEST', 'cannot mirror on less than 2 pools');
            throw_on_invalid_pools_for_tier((tier.attached_pools || []).map(pool_name =>
                req.system.pools_by_name[pool_name]
            ));
            if (_.some(tier.attached_pools, pool => _.includes(pools, pool))) {
                throw new RpcError('BAD_REQUEST', 'can\'t use pool for more than one tier');
            } else {
                pools.push(...tier.attached_pools);
            }
        });
    } catch (error) {
        dbg.error('_check_validity_of_configuration failed with', error);
        if (error instanceof RpcError) throw error;
        throw new RpcError('BAD_REQUEST', 'invalid attached_pools in tiers');
    }
}

async function update_bucket_class(req) {
    const { name, policy, tiers } = req.rpc_params;
    const result = {
        error_message: "",
        should_revert: false,
        revert_to_policy: _.cloneDeep(req.rpc_params)
    };
    const changes = {
        insert: {
            tiers: [],
            chunk_configs: [],
        },
        update: {
            tieringpolicies: [],
            tiers: []
        },
    };
    let revert_to_policy;
    let update_db = false;
    try {
        // Filter and pick buckets that were created via OBC
        const bucket_class_buckets = _.filter(system_store.data.buckets, bucket =>
            bucket.bucket_claim && (_.isEqual(bucket.bucket_claim.bucket_class, name)));
        if (_.isEmpty(bucket_class_buckets)) return result; // TODO what should we do if pools is bad but nothing to revert to?
        revert_to_policy = _calc_revert_policy(name, bucket_class_buckets[0].tiering);
        _check_validity_of_configuration({ req, bucket_class_buckets });
        if (policy.tiers.length !== tiers.length) throw new RpcError('BAD_REQUEST', 'policy tiers and tiers length mismatch');
        for (const bucket of bucket_class_buckets) {
            let added_tier = false;
            const old_policy = bucket.tiering;
            const old_tiers = old_policy.tiers
                .map(obj => obj.tier.name);
            for (const [index, tier] of tiers.entries()) {
                const old_tier = old_tiers[index];
                const new_name = `${bucket.name.unwrap()}.${Math.round(Date.now() / 1000)}.${index}`;
                policy.tiers[index].tier = new_name;
                const pool_ids = _.map(tier.attached_pools,
                    pool_name => req.system.pools_by_name[pool_name]._id
                );
                const mirrors = _convert_pools_to_data_placement_structure(pool_ids, tier.data_placement);
                const chunk_config = chunk_config_utils.resolve_chunk_config(
                    tier.chunk_coder_config, req.account, req.system);
                if (!chunk_config._id) {
                    chunk_config._id = system_store.new_system_store_id();
                    changes.insert.chunk_configs.push(chunk_config);
                    update_db = true;
                }
                check_tier_exists(req, new_name);
                const new_tier = new_tier_defaults(
                    new_name,
                    req.system._id,
                    chunk_config._id,
                    mirrors
                );
                if (tier.data_placement) new_tier.data_placement = tier.data_placement;
                if (old_tier) {
                    const old_tier_full = req.system.tiers_by_name[old_tier.unwrap()];
                    if (_is_change_in_tier(old_tier_full, new_tier)) {
                        new_tier._id = old_tier_full._id;
                        changes.update.tiers.push(new_tier);
                        update_db = true;
                    }
                } else {
                    changes.insert.tiers.push(new_tier);
                    added_tier = true;
                    update_db = true;
                }
                policy.tiers[index].tier_id = new_tier._id;
            }
            const new_policy = new_policy_defaults(
                old_policy.name,
                req.system._id,
                policy.chunk_split_config,
                _.map(policy.tiers, t => ({
                    order: t.order,
                    tier: t.tier_id,
                    spillover: t.spillover || false,
                    disabled: t.disabled || false
                }))
            );
            new_policy._id = req.system.tiering_policies_by_name[old_policy.name.unwrap()]._id;
            if (added_tier) {
                changes.update.tieringpolicies.push(new_policy);
                update_db = true;
            }
        }
        if (update_db) await system_store.make_changes(changes);
    } catch (err) {
        if (err instanceof RpcError) {
            if (err.rpc_code !== 'BAD_REQUEST') throw err;
            result.error_message = err.message;
            result.should_revert = true;
            result.revert_to_policy = revert_to_policy;
        } else {
            throw err;
        }
    }
    return result;
}

function _calc_revert_policy(policy_name, tiering) {
    const revert_to_tiers = tiering.tiers.map(t => ({
        order: t.order,
        tier: t.tier.name,
    }));
    return {
        name: policy_name,
        policy: {
            name: tiering.name,
            tiers: revert_to_tiers,
        },
        tiers: tiering.tiers.map(t => ({
            name: t.tier.name,
            data_placement: t.tier.data_placement,
            attached_pools: _.map(_.flatMap(t.tier.mirrors, 'spread_pools'), pool => pool.name)
        }))
    };
}

function _is_change_in_tier(old_tier, new_tier) {
    if (new_tier.mirrors.length !== old_tier.mirrors.length) return true;
    for (const [mirror_index, mirror] of old_tier.mirrors.entries()) {
        for (const [pool_index, pool] of mirror.spread_pools.entries()) {
            if (String(pool) !== String(new_tier.mirrors[mirror_index].spread_pools[pool_index])) {
                return true;
            }

        }
    }
    return false;
}

function update_chunk_config_for_bucket(req) { // please remove when CCC is per tier and not per policy
    var bucket = req.system.buckets_by_name && req.system.buckets_by_name[req.rpc_params.bucket_name];
    if (!bucket || bucket.deleting) {
        dbg.error('BUCKET NOT FOUND', req.rpc_params.bucket_name);
        throw new RpcError('NO_SUCH_BUCKET', 'No such bucket: ' + req.rpc_params.bucket_name);
    }
    const policy = bucket.tiering;
    const changes = {
        insert: {},
        update: {},
    };
    const chunk_config = chunk_config_utils.resolve_chunk_config(
        req.rpc_params.chunk_coder_config, req.account, req.system);
    if (!chunk_config._id) {
        chunk_config._id = system_store.new_system_store_id();
        changes.insert.chunk_configs = [chunk_config];
    }
    changes.update.tiers = policy.tiers.map(t => ({
        _id: t.tier._id,
        chunk_config: chunk_config._id
    }));
    return system_store.make_changes(changes);
}

function add_tier_to_bucket(req) {
    var bucket = req.system.buckets_by_name && req.system.buckets_by_name[req.rpc_params.bucket_name];
    if (!bucket || bucket.deleting) {
        dbg.error('BUCKET NOT FOUND', req.rpc_params.bucket_name);
        throw new RpcError('NO_SUCH_BUCKET', 'No such bucket: ' + req.rpc_params.bucket_name);
    }
    const policy = bucket.tiering;
    const tier_params = req.rpc_params.tier;
    const changes = { insert: {}, update: {} };
    const policy_pool_ids = _.map(tier_params.attached_pools,
        pool_name => req.system.pools_by_name[pool_name]._id
    );
    const mirrors = _convert_pools_to_data_placement_structure(policy_pool_ids, req.rpc_params.data_placement);
    const info = req.system.tiering_policies_by_name[policy.name.unwrap()];
    const tier0_ccc = info.tiers[0].tier.chunk_config.chunk_coder_config;
    let chunk_config = chunk_config_utils.resolve_chunk_config(
        req.rpc_params.chunk_coder_config || tier0_ccc, req.account, req.system);

    const new_tier_name = bucket.name + '#' + Date.now().toString(36);
    check_tier_exists(req, new_tier_name);
    const tier = new_tier_defaults(
        new_tier_name,
        req.system._id,
        chunk_config._id,
        mirrors
    );
    changes.insert.tiers = [tier];
    const order = req.rpc_params.tier.order || policy.tiers.length;
    if (order < 0 || order > policy.tiers.length) {
        throw new RpcError('ILLEGAL_ORDER', `tier order ${order} is not allowed`);
    }
    const new_tiers = Array.from(policy.tiers);
    new_tiers.splice(order, 0, {
        order: order,
        tier: tier,
        spillover: tier_params.spillover || false,
        disabled: tier_params.disabled || false
    });
    changes.update.tieringpolicies = [{
        _id: policy._id,
        tiers: new_tiers.map((t, index) => ({
            order: index,
            tier: t.tier._id,
            spillover: t.spillover,
            disabled: t.disabled,
        })),
        chunk_split_config: policy.chunk_split_config
    }];
    return system_store.make_changes(changes);
}

function get_policy_pools(req) {
    const policy = find_policy_by_name(req);
    return get_tiering_policy_info(policy);
}

function read_policy(req) {
    const policy = find_policy_by_name(req);
    let pools = [];

    _.forEach(policy.tiers, t => {
        _.forEach(t.tier.mirrors, mirror_object => {
            pools = _.concat(pools, mirror_object.spread_pools);
        });
    });
    pools = _.compact(pools);
    const pool_names = pools.map(pool => pool.name);
    return Promise.all([
            nodes_client.instance().aggregate_nodes_by_pool(pool_names, req.system._id),
            nodes_client.instance().aggregate_hosts_by_pool(pool_names, req.system._id),
        ])
        .then(function(res) {
            let nodes_aggregate_pool = res[0];
            let hosts_aggregate_pool = res[1];
            return get_tiering_policy_info(policy,
                node_allocator.get_tiering_status(policy),
                nodes_aggregate_pool,
                hosts_aggregate_pool);
        });
}

function delete_policy(req) {
    dbg.log0('Deleting tiering policy', req.rpc_params.name);
    const policy = find_policy_by_name(req);
    return system_store.make_changes({
        remove: {
            tieringpolicies: [policy._id]
        }
    });
}


// UTILS //////////////////////////////////////////////////////////

function find_bucket_by_tier(req) {
    const tier = find_tier_by_name(req);
    const policy = _.find(system_store.data.tieringpolicies,
        p => _.find(p.tiers, t => String(t.tier._id) === String(tier._id))
    );

    if (!policy) {
        return null;
        //throw new RpcError('NOT_FOUND', 'POLICY OF TIER NOT FOUND ' + tier.name);
    }

    const bucket = _.find(system_store.data.buckets,
        bkt => String(bkt.tiering._id) === String(policy._id)
    );

    if (!bucket || bucket.deleting) {
        return null;
        //throw new RpcError('NOT_FOUND', 'BUCKET OF TIER POLICY NOT FOUND ' + policy.name);
    }

    return bucket;
}

function find_tier_by_name(req, tier_name) {
    const name = tier_name || req.rpc_params.name;
    const tier = req.system.tiers_by_name && req.system.tiers_by_name[name.unwrap()];
    if (!tier) {
        throw new RpcError('NO_SUCH_TIER', 'No such tier: ' + name);
    }
    return tier;
}

function find_policy_by_name(req) {
    const name = req.rpc_params.name;
    const policy = req.system.tiering_policies_by_name && req.system.tiering_policies_by_name[name.unwrap()];
    if (!policy) {
        throw new RpcError('NO_SUCH_TIERING_POLICY', 'No such tiering policy: ' + name);
    }
    return policy;
}

function policy_defaults_from_req(req) {
    return new_policy_defaults(
        req.rpc_params.name,
        req.system._id,
        req.rpc_params.chunk_split_config,
        _.map(req.rpc_params.tiers, t => ({
            order: t.order,
            tier: req.system.tiers_by_name[t.tier.unwrap()]._id,
            spillover: t.spillover || false,
            disabled: t.disabled || false
        }))
    );
}

function get_tier_extra_info(tier, tiering_pools_status, nodes_aggregate_pool, hosts_aggregate_pool) {
    const info = {
        has_any_pool_configured: false,
        num_nodes_in_mirror_group: 0,
        num_valid_nodes: 0,
        num_valid_hosts: 0,
        mirrors_with_valid_pool: 0,
        mirrors_with_enough_nodes: 0,
        has_internal_or_cloud_pool: false,
        use_internal: false,
        has_valid_pool: false,
        any_rebuilds: false,
    };
    const ccc = tier.chunk_config.chunk_coder_config;
    const required_valid_nodes = ccc ? (ccc.data_frags + ccc.parity_frags) * ccc.replicas : config.NODES_MIN_COUNT;
    const tiering_pools_used_agg = [0];
    tier.mirrors.forEach(mirror_object => {
        _.compact((mirror_object.spread_pools || []).map(pool => {
                info.has_any_pool_configured = true;
                const spread_pool = system_store.data.pools.find(pool_rec => String(pool_rec._id) === String(pool._id));
                tiering_pools_used_agg.push(_.get(spread_pool, 'storage_stats.blocks_size') || 0);
                return _.extend({ pool_id: pool._id }, tiering_pools_status[tier._id].pools[pool._id]);
            }))
            .forEach(pool_status => {
                info.any_rebuilds = info.any_rebuilds || _.get(nodes_aggregate_pool, `groups.${pool_status.pool_id}.data_activities.length`);
                const pool_num_nodes = _.get(nodes_aggregate_pool, `groups.${pool_status.pool_id}.storage_nodes.count`) || 0;
                const pool_num_hosts = _.get(hosts_aggregate_pool, `groups.${pool_status.pool_id}.nodes.online`) || 0;
                info.num_nodes_in_mirror_group += pool_num_nodes;
                info.num_valid_nodes += (pool_status.num_nodes || 0);
                info.num_valid_hosts += (pool_num_hosts || 0);
                info.has_valid_pool = info.has_valid_pool || pool_status.valid_for_allocation;
                info.has_internal_or_cloud_pool = info.has_internal_or_cloud_pool || (pool_status.resource_type !== 'HOSTS');
                info.use_internal = info.use_internal || (pool_status.resource_type === 'INTERNAL');
            });
        if (info.has_valid_pool || ((info.num_valid_nodes || 0) >= required_valid_nodes)) info.mirrors_with_valid_pool += 1;
        if (info.num_nodes_in_mirror_group >= required_valid_nodes || info.has_internal_or_cloud_pool) info.mirrors_with_enough_nodes += 1;

        const existing_minimum = _.isUndefined(info.num_of_nodes) ? Number.MAX_SAFE_INTEGER : info.num_of_nodes;
        const existing_hosts_minimum = _.isUndefined(info.num_of_hosts) ? Number.MAX_SAFE_INTEGER : info.num_of_hosts;
        const num_valid_nodes_for_minimum = _.isUndefined(info.num_valid_nodes) ? Number.MAX_SAFE_INTEGER : info.num_valid_nodes;
        const potential_new_minimum = info.has_internal_or_cloud_pool ?
            Number.MAX_SAFE_INTEGER : num_valid_nodes_for_minimum;
        info.num_of_nodes = info.use_internal ? 0 : Math.min(existing_minimum, potential_new_minimum);
        const potential_new_hosts_minimum = info.has_internal_or_cloud_pool ?
            Number.MAX_SAFE_INTEGER : info.num_valid_hosts;
        info.num_of_hosts = info.use_internal ? 0 : Math.min(existing_hosts_minimum, potential_new_hosts_minimum);
    });
    info.num_of_nodes = info.num_of_nodes || 0;
    info.num_of_hosts = info.num_of_hosts || 0;
    info.node_tolerance = ccc.parity_frags ?
        Math.max(info.num_of_nodes - ccc.parity_frags, 0) :
        Math.max(info.num_of_nodes - 1, 0);
    info.host_tolerance = ccc.parity_frags ?
        Math.max(info.num_of_hosts - ccc.parity_frags, 0) :
        Math.max(info.num_of_hosts - 1, 0);
    info.used_of_pools_in_tier = size_utils.json_to_bigint(size_utils.reduce_sum('blocks_size', tiering_pools_used_agg));
    return info;
}

function get_tier_info(tier, nodes_aggregate_pool, tiering_tier_status) {
    const mirrors_storage = [];
    let attached_pools = [];
    const mirror_groups = [];

    _.forEach(tier.mirrors, mirror => {
        let spread_storage;
        const pools_storage = _.map(mirror.spread_pools, pool =>
            _.defaults(_.get(nodes_aggregate_pool, ['groups', String(pool._id), 'storage']), {
                used: 0,
                total: 0,
                free: 0,
                unavailable_free: 0,
                unavailable_used: 0,
                used_other: 0,
                reserved: 0
            })
        );
        spread_storage = size_utils.reduce_storage(size_utils.reduce_sum, pools_storage);
        _.defaults(spread_storage, {
            used: 0,
            total: 0,
            free: 0,
            unavailable_free: 0,
            unavailable_used: 0,
            used_other: 0,
            reserved: 0
        });

        mirrors_storage.push(spread_storage);
        attached_pools = _.concat(attached_pools, mirror.spread_pools);
        mirror_groups.push({
            name: String(mirror._id),
            pools: _.map(mirror.spread_pools, 'name')
        });
    });

    attached_pools = _.compact(attached_pools).map(pool => pool.name);

    const storage = _.defaults(
        size_utils.reduce_storage(size_utils.reduce_sum, mirrors_storage), {
            used: 0,
            total: 0,
            free: 0,
            unavailable_free: 0,
            unavailable_used: 0,
            used_other: 0,
            reserved: 0
        });

    const data = _.pick(_.defaults(
        size_utils.reduce_storage(size_utils.reduce_minimum, tiering_tier_status && tiering_tier_status.mirrors_storage), {
            free: 0,
        }), 'free');

    return {
        name: tier.name,
        chunk_coder_config: tier.chunk_config.chunk_coder_config,
        data_placement: tier.data_placement,
        attached_pools,
        mirror_groups,
        storage,
        data,
    };
}

function get_tiering_policy_info(tiering_policy, tiering_pools_status,
    nodes_aggregate_pool, hosts_aggregate_pool) {
    const info = _.pick(tiering_policy, 'name');
    const tiers_storage = nodes_aggregate_pool ? [] : null;
    const tiers_data = tiering_pools_status ? [] : null;
    info.tiers = _.map(tiering_policy.tiers, t => {
        let mode;
        if (tiers_storage) {
            const extra_info = get_tier_extra_info(t.tier, tiering_pools_status, nodes_aggregate_pool, hosts_aggregate_pool);
            const tier_info = get_tier_info(t.tier,
                nodes_aggregate_pool,
                tiering_pools_status[String(t.tier._id)]);
            tiers_storage.push(tier_info.storage);
            tiers_data.push(tier_info.data);
            mode = calc_tier_policy_status(t.tier, tier_info, extra_info);
        }
        return {
            order: t.order,
            tier: t.tier.name,
            spillover: t.spillover || false,
            disabled: t.disabled || false,
            mode: mode || 'OPTIMAL',
        };
    });
    if (tiers_storage) {
        info.storage = size_utils.reduce_storage(size_utils.reduce_sum, tiers_storage);
    }
    if (tiers_data) {
        info.data = size_utils.reduce_storage(size_utils.reduce_sum, tiers_data);
    }
    return info;
}

function calc_tier_policy_status(tier, tier_info, extra_info) {
    let has_enough_healthy_nodes_for_tier = false;
    let has_enough_total_nodes_for_tier = false;
    let is_no_storage = false;
    let is_storage_low = false;

    const tier_free = size_utils.json_to_bigint(_.get(tier_info, 'storage.free') || 0);
    const tier_used = size_utils.json_to_bigint(_.get(tier_info, 'storage.used') || 0);
    const tier_used_other = size_utils.json_to_bigint(_.get(tier_info, 'storage.used_other') || 0);
    const tier_total = tier_free.plus(tier_used).plus(tier_used_other);
    // const low_tolerance = (extra_info.node_tolerance === 0 || extra_info.host_tolerance === 0);
    if (tier_free.isZero()) {
        is_no_storage = true;
    } else {
        let free_percent = tier_free.multiply(100).divide(tier_total);
        if (free_percent < 30) {
            is_storage_low = true;
        }
    }
    if (tier.mirrors.length > 0) {
        if (extra_info.mirrors_with_valid_pool === tier.mirrors.length) has_enough_healthy_nodes_for_tier = true;
        if (extra_info.mirrors_with_enough_nodes === tier.mirrors.length) has_enough_total_nodes_for_tier = true;
    }
    if (extra_info.use_internal && extra_info.mirrors_with_valid_pool === 0) {
        return 'INTERNAL_STORAGE_ISSUES';
    }
    if (!extra_info.has_any_pool_configured) {
        return 'NO_RESOURCES';
    }
    if (!has_enough_total_nodes_for_tier) {
        return 'NOT_ENOUGH_RESOURCES';
    }
    if (!has_enough_healthy_nodes_for_tier) {
        return 'NOT_ENOUGH_HEALTHY_RESOURCES';
    }
    if (is_no_storage) {
        return 'NO_CAPACITY';
    }
    if (is_storage_low) {
        return 'LOW_CAPACITY';
    }
    return 'OPTIMAL';
}

function get_associated_tiering_policies(tier) {
    const associated_tiering_policies = _.filter(tier.system.tiering_policies_by_name,
        policy => _.find(policy.tiers, t => String(t.tier._id) === String(tier._id))
    );

    return _.map(associated_tiering_policies, tiering_policies => tiering_policies._id);
}

function get_associated_buckets(policy) {
    const associated_buckets = _.filter(policy.system.buckets_by_name,
        bucket => String(bucket.tiering._id) === String(policy._id));

    return _.map(associated_buckets, bucket => bucket._id);
}

function throw_on_invalid_pools_for_tier(pools) {
    const deleting_pools = pools.filter(pool =>
        pool.hosts_pool_info &&
        pool.hosts_pool_info.host_count === 0
    );
    if (deleting_pools.length > 0) {
        throw new Error(`Invalid attached pools, ${deleting_pools.join(', ')} are in deleting state`);
    }
}

function check_tiering_policy_exists(req, policy_name) {
    const name = policy_name instanceof SensitiveString ? policy_name.unwrap() : policy_name;
    if (req.system.tiering_policies_by_name && req.system.tiering_policies_by_name[name]) throw new RpcError(`Policy ${policy_name} already exists`);
}

function check_tier_exists(req, tier_name) {
    const name = tier_name instanceof SensitiveString ? tier_name.unwrap() : tier_name;
    if (req.system.tiers_by_name && req.system.tiers_by_name[name]) throw new RpcError(`Tier ${tier_name} already exists`);
}


// EXPORTS
exports.get_associated_tiering_policies = get_associated_tiering_policies;
exports.get_associated_buckets = get_associated_buckets;
exports.new_tier_defaults = new_tier_defaults;
exports.new_policy_defaults = new_policy_defaults;
exports.get_tier_info = get_tier_info;
exports.get_tier_extra_info = get_tier_extra_info;
exports.get_tiering_policy_info = get_tiering_policy_info;
exports.update_chunk_config_for_bucket = update_chunk_config_for_bucket;
//Tiers
exports.create_tier = create_tier;
exports.read_tier = read_tier;
exports.update_tier = update_tier;
exports.delete_tier = delete_tier;
//Tiering Policy
exports.create_policy = create_policy;
exports.update_policy = update_policy;
exports.add_tier_to_bucket = add_tier_to_bucket;
exports.get_policy_pools = get_policy_pools;
exports.read_policy = read_policy;
exports.delete_policy = delete_policy;
exports.update_bucket_class = update_bucket_class;

// Utils
exports.check_tier_exists = check_tier_exists;
exports.check_tiering_policy_exists = check_tiering_policy_exists;