# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""
Policy for batching operations on a cluster.

NOTE: How update policy works

Input:
   cluster: the cluster whose nodes are to be updated.
Output:
   stored in action.data: A dictionary containing a detailed update schedule.
   {
     'status': 'OK',
     'update': {
       'pause_time': 2,
       'plan': [{
           'node-id-1',
           'node-id-2',
         }, {
           'node-id-3',
           'node-id-4',
         }, {
           'node-id-5',
         }
       ]
     }
   }
"""
import math

from senlin.common import consts
from senlin.common.i18n import _
from senlin.common import scaleutils as su
from senlin.common import schema
from senlin.policies import base


class BatchPolicy(base.Policy):
    """Policy for batching the operations on a cluster's nodes."""

    VERSION = '1.0'
    VERSIONS = {
        '1.0': [
            {'status': consts.EXPERIMENTAL, 'since': '2017.02'}
        ]
    }
    PRIORITY = 200

    TARGET = [
        ('BEFORE', consts.CLUSTER_UPDATE),
    ]

    PROFILE_TYPE = [
        'ANY'
    ]

    KEYS = (
        MIN_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME,
    ) = (
        'min_in_service', 'max_batch_size', 'pause_time',
    )

    properties_schema = {
        MIN_IN_SERVICE: schema.Integer(
            _('Minimum number of nodes in service when performing updates.'),
            default=1,
        ),
        MAX_BATCH_SIZE: schema.Integer(
            _('Maximum number of nodes that will be updated in parallel.'),
            default=-1,
        ),
        PAUSE_TIME: schema.Integer(
            _('Interval in seconds between update batches if any.'),
            default=60,
        )
    }

    def __init__(self, name, spec, **kwargs):
        super(BatchPolicy, self).__init__(name, spec, **kwargs)

        self.min_in_service = self.properties[self.MIN_IN_SERVICE]
        self.max_batch_size = self.properties[self.MAX_BATCH_SIZE]
        self.pause_time = self.properties[self.PAUSE_TIME]

    def _get_batch_size(self, total):
        """Get batch size for update operation.

        :param total: Total number of nodes.
        :returns: Size of each batch and number of batches.
        """
        batch_num = 0
        batch_size = 0
        diff = 0

        # if the number of nodes less than min_in_service,
        # we divided it to 2 batches
        diff = int(math.ceil(float(total) / 2))
        if total > self.min_in_service:
            diff = total - self.min_in_service

        # max_batch_size is -1 if not specified
        if self.max_batch_size == -1 or diff < self.max_batch_size:
            batch_size = diff
        else:
            batch_size = self.max_batch_size

        batch_num = int(math.ceil(float(total) / float(batch_size)))

        return batch_size, batch_num

    def _pick_nodes(self, nodes, batch_size, batch_num):
        """Select nodes based on size and number of batches.

        :param nodes: list of node objects.
        :param batch_size: the number of nodes of each batch.
        :param batch_num: the number of batches.
        :returns: a list of sets containing the nodes' IDs we
                  selected based on the input params.
        """
        candidates, good = su.filter_error_nodes(nodes)
        result = []
        # NOTE: we leave the nodes known to be good (ACTIVE) at the end of the
        # list so that we have a better chance to ensure 'min_in_service'
        # constraint
        for n in good:
            candidates.append(n.id)

        for start in range(0, len(candidates), batch_size):
            end = start + batch_size
            result.append(set(candidates[start:end]))

        return result

    def _create_plan(self, action):
        nodes = action.entity.nodes

        plan = {'pause_time': self.pause_time}
        if len(nodes) == 0:
            plan['plan'] = []
            return True, plan

        batch_size, batch_num = self._get_batch_size(len(nodes))
        plan['plan'] = self._pick_nodes(nodes, batch_size, batch_num)

        return True, plan

    def pre_op(self, cluster_id, action):

        pd = {
            'status': base.CHECK_OK,
            'reason': _('Batching request validated.'),
        }
        # for updating
        result, value = self._create_plan(action)

        if result is False:
            pd = {
                'status': base.CHECK_ERROR,
                'reason': value,
            }
        else:
            pd['update'] = value

        action.data.update(pd)
        action.store(action.context)

        return
