#    Copyright 2017 ARM Holdings.
#
#    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.

from oslo_log import log as logging
import pecan
import six

from zun.api.controllers import base
from zun.api.controllers import link
from zun.api.controllers.v1 import collection
from zun.api.controllers.v1.schemas import capsules as schema
from zun.api.controllers.v1.views import capsules_view as view
from zun.api import utils as api_utils
from zun.api import validation
from zun.common import consts
from zun.common import exception
from zun.common.i18n import _
from zun.common import name_generator
from zun.common import policy
from zun.common import utils
import zun.conf
from zun import objects
from zun.volume import cinder_api as cinder

CONF = zun.conf.CONF
LOG = logging.getLogger(__name__)


def _get_capsule(capsule_ident):
    """Get capsule by name or UUID"""
    capsule = api_utils.get_resource('Capsule', capsule_ident)
    if not capsule:
        pecan.abort(404, ('Not found; the capsule you requested '
                          'does not exist.'))
    return capsule


def check_policy_on_capsule(capsule, action):
    context = pecan.request.context
    policy.enforce(context, action, capsule, action=action)


class CapsuleCollection(collection.Collection):
    """API representation of a collection of Capsules."""

    fields = {
        'capsules',
        'next'
    }

    """A list containing capsules objects"""

    def __init__(self, **kwargs):
        self._type = 'capsules'

    @staticmethod
    def convert_with_links(rpc_capsules, limit, url=None,
                           expand=False, legacy_api_version=False, **kwargs):
        context = pecan.request.context
        collection = CapsuleCollection()
        collection.capsules = \
            [view.format_capsule(url, p, context,
                                 legacy_api_version=legacy_api_version)
             for p in rpc_capsules]
        collection.next = collection.get_next(limit, url=url, **kwargs)
        return collection


class CapsuleController(base.Controller):
    """Controller for Capsules"""

    _custom_actions = {

    }

    @base.Controller.api_version("1.1", "1.31")
    @pecan.expose('json')
    @exception.wrap_pecan_controller_exception
    def get_all(self, **kwargs):
        '''Retrieve a list of capsules.'''
        return self._do_get_all(legacy_api_version=True, **kwargs)

    @base.Controller.api_version("1.32")  # noqa
    @pecan.expose('json')
    @exception.wrap_pecan_controller_exception
    def get_all(self, **kwargs):
        '''Retrieve a list of capsules.'''
        return self._do_get_all(**kwargs)

    def _do_get_all(self, legacy_api_version=False, **kwargs):
        context = pecan.request.context
        policy.enforce(context, "capsule:get_all",
                       action="capsule:get_all")
        if utils.is_all_projects(kwargs):
            context.all_projects = True
        limit = api_utils.validate_limit(kwargs.get('limit'))
        sort_dir = api_utils.validate_sort_dir(kwargs.get('sort_dir', 'asc'))
        sort_key = kwargs.get('sort_key', 'id')
        resource_url = kwargs.get('resource_url')
        expand = kwargs.get('expand')
        filters = None
        marker_obj = None
        marker = kwargs.get('marker')
        if marker:
            marker_obj = objects.Capsule.get_by_uuid(context,
                                                     marker)
        capsules = objects.Capsule.list(context,
                                        limit,
                                        marker_obj,
                                        sort_key,
                                        sort_dir,
                                        filters=filters)

        return CapsuleCollection.convert_with_links(
            capsules, limit, url=resource_url, expand=expand,
            sort_key=sort_key, sort_dir=sort_dir,
            legacy_api_version=legacy_api_version)

    @base.Controller.api_version("1.1", "1.31")
    @pecan.expose('json')
    @api_utils.enforce_content_types(['application/json'])
    @exception.wrap_pecan_controller_exception
    @validation.validated(schema.capsule_create)
    def post(self, **capsule_dict):
        """Create a new capsule.

        :param capsule_dict: a capsule within the request body.
        """
        return self._do_post(legacy_api_version=True, **capsule_dict)

    @base.Controller.api_version("1.32")  # noqa
    @pecan.expose('json')
    @api_utils.enforce_content_types(['application/json'])
    @exception.wrap_pecan_controller_exception
    @validation.validated(schema.capsule_create)
    def post(self, **capsule_dict):
        """Create a new capsule.

        :param capsule_dict: a capsule within the request body.
        """
        return self._do_post(**capsule_dict)

    def _do_post(self, legacy_api_version=False, **capsule_dict):
        context = pecan.request.context
        compute_api = pecan.request.compute_api
        policy.enforce(context, "capsule:create",
                       action="capsule:create")

        # Abstract the capsule specification
        capsules_template = capsule_dict.get('template')

        spec_content, template_json = \
            utils.check_capsule_template(capsules_template)

        containers_spec, init_containers_spec = \
            utils.capsule_get_container_spec(spec_content)
        volumes_spec = utils.capsule_get_volume_spec(spec_content)

        # Create the capsule Object
        new_capsule = objects.Capsule(context, **capsule_dict)
        new_capsule.project_id = context.project_id
        new_capsule.user_id = context.user_id
        new_capsule.status = consts.CREATING
        new_capsule.create(context)
        new_capsule.volumes = []
        capsule_need_cpu = 0
        capsule_need_memory = 0
        container_volume_requests = []

        if spec_content.get('restart_policy'):
            capsule_restart_policy = spec_content.get('restart_policy')
        else:
            # NOTE(hongbin): this is deprecated but we need to maintain
            # backward-compatibility. Will remove this branch in the future.
            capsule_restart_policy = template_json.get('restart_policy',
                                                       'always')
        container_restart_policy = {"MaximumRetryCount": "0",
                                    "Name": capsule_restart_policy}
        new_capsule.restart_policy = container_restart_policy

        metadata_info = template_json.get('metadata', None)
        requested_networks_info = template_json.get('nets', [])
        requested_networks = \
            utils.build_requested_networks(context, requested_networks_info)

        if metadata_info:
            new_capsule.name = metadata_info.get('name', None)
            new_capsule.labels = metadata_info.get('labels', None)

        # create the capsule in DB so that it generates a 'id'
        new_capsule.save()

        extra_spec = {}
        az_info = template_json.get('availabilityZone')
        if az_info:
            extra_spec['availability_zone'] = az_info

        new_capsule.image = CONF.sandbox_image
        new_capsule.image_driver = CONF.sandbox_image_driver

        merged_containers_spec = init_containers_spec + containers_spec
        for container_spec in merged_containers_spec:
            container_dict = container_spec
            container_dict['project_id'] = context.project_id
            container_dict['user_id'] = context.user_id
            name = self._generate_name_for_capsule_container(new_capsule)
            container_dict['name'] = name

            if container_dict.get('args') and container_dict.get('command'):
                container_dict['command'] = \
                    container_dict['command'] + container_dict['args']
                container_dict.pop('args')
            elif container_dict.get('args'):
                container_dict['command'] = container_dict['args']
                container_dict.pop('args')

            # NOTE(kevinz): Don't support port remapping, will find a
            # easy way to implement it.
            # if container need to open some port, just open it in container,
            # user can change the security group and getting access to port.
            if container_dict.get('ports'):
                container_dict.pop('ports')

            if container_dict.get('resources'):
                resources_list = container_dict.get('resources')
                allocation = resources_list.get('requests')
                if allocation.get('cpu'):
                    capsule_need_cpu += allocation.get('cpu')
                    container_dict['cpu'] = allocation.get('cpu')
                if allocation.get('memory'):
                    capsule_need_memory += allocation.get('memory')
                    container_dict['memory'] = str(allocation['memory'])
                container_dict.pop('resources')

            container_dict['status'] = consts.CREATING
            container_dict['interactive'] = True
            container_dict['capsule_id'] = new_capsule.id
            container_dict['restart_policy'] = container_restart_policy
            if container_spec in init_containers_spec:
                if capsule_restart_policy == "always":
                    container_restart_policy = {"MaximumRetryCount": "10",
                                                "Name": "on-failure"}
                    container_dict['restart_policy'] = container_restart_policy
                utils.check_for_restart_policy(container_dict)
                new_container = objects.CapsuleInitContainer(context,
                                                             **container_dict)
            else:
                utils.check_for_restart_policy(container_dict)
                new_container = objects.CapsuleContainer(context,
                                                         **container_dict)
            new_container.create(context)

            if container_dict.get('volumeMounts'):
                for volume in container_dict['volumeMounts']:
                    volume['container_uuid'] = new_container.uuid
                    container_volume_requests.append(volume)

        # Deal with the volume support
        requested_volumes = \
            self._build_requested_volumes(context,
                                          volumes_spec,
                                          container_volume_requests,
                                          new_capsule)
        new_capsule.cpu = capsule_need_cpu
        new_capsule.memory = str(capsule_need_memory)
        new_capsule.save(context)

        kwargs = {}
        kwargs['extra_spec'] = extra_spec
        kwargs['requested_networks'] = requested_networks
        kwargs['requested_volumes'] = requested_volumes
        kwargs['run'] = True
        compute_api.container_create(context, new_capsule, **kwargs)
        # Set the HTTP Location Header
        pecan.response.location = link.build_url('capsules',
                                                 new_capsule.uuid)

        pecan.response.status = 202
        return view.format_capsule(pecan.request.host_url, new_capsule,
                                   context,
                                   legacy_api_version=legacy_api_version)

    @base.Controller.api_version("1.1", "1.31")
    @pecan.expose('json')
    @exception.wrap_pecan_controller_exception
    def get_one(self, capsule_ident):
        """Retrieve information about the given capsule.

        :param capsule_ident: UUID or name of a capsule.
        """
        return self._do_get_one(capsule_ident, legacy_api_version=True)

    @base.Controller.api_version("1.32")  # noqa
    @pecan.expose('json')
    @exception.wrap_pecan_controller_exception
    def get_one(self, capsule_ident):
        """Retrieve information about the given capsule.

        :param capsule_ident: UUID or name of a capsule.
        """
        return self._do_get_one(capsule_ident)

    def _do_get_one(self, capsule_ident, legacy_api_version=False):
        context = pecan.request.context
        capsule = _get_capsule(capsule_ident)
        check_policy_on_capsule(capsule.as_dict(), "capsule:get")
        return view.format_capsule(pecan.request.host_url, capsule, context,
                                   legacy_api_version=legacy_api_version)

    @pecan.expose('json')
    @exception.wrap_pecan_controller_exception
    def delete(self, capsule_ident, **kwargs):
        """Delete a capsule.

        :param capsule_ident: UUID or Name of a capsule.
        """
        context = pecan.request.context
        if utils.is_all_projects(kwargs):
            policy.enforce(context, "capsule:delete_all_projects",
                           action="capsule:delete_all_projects")
            context.all_projects = True
        capsule = _get_capsule(capsule_ident)
        check_policy_on_capsule(capsule.as_dict(), "capsule:delete")
        compute_api = pecan.request.compute_api
        capsule.task_state = consts.CONTAINER_DELETING
        capsule.save(context)
        compute_api.container_stop(context, capsule, 10)
        compute_api.container_delete(context, capsule)
        pecan.response.status = 204

    def _generate_name_for_capsule_container(self, new_capsule):
        """Generate a random name like: zeta-22-container."""
        name_gen = name_generator.NameGenerator()
        name = name_gen.generate()
        if new_capsule.name is None:
            return 'capsule-' + new_capsule.uuid + '-' + name
        else:
            return 'capsule-' + new_capsule.name + '-' + name

    def _build_requested_volumes(self, context, volume_spec,
                                 volume_mounts, capsule):
        # NOTE(hongbin): We assume cinder is the only volume provider here.
        # The logic needs to be re-visited if a second volume provider
        # (i.e. Manila) is introduced.
        # NOTE(kevinz): We assume the volume_mounts has been pretreated,
        # there won't occur that volume multiple attach and no untapped
        # volume.
        cinder_api = cinder.CinderAPI(context)
        volume_driver = "cinder"
        requested_volumes = {}
        volume_created = []
        try:
            for mount in volume_spec:
                mount_driver = mount[volume_driver]
                auto_remove = False
                if mount_driver.get("volumeID"):
                    uuid = mount_driver.get("volumeID")
                    volume = cinder_api.search_volume(uuid)
                    cinder_api.ensure_volume_usable(volume)
                else:
                    size = mount_driver.get("size")
                    volume = cinder_api.create_volume(size)
                    volume_created.append(volume)
                    if "autoRemove" in mount_driver.keys() \
                            and mount_driver.get("autoRemove", False):
                        auto_remove = True

                mount_destination = None
                container_uuid = None

                volume_object = objects.Volume(
                    context,
                    cinder_volume_id=volume.id,
                    volume_provider=volume_driver,
                    user_id=context.user_id,
                    project_id=context.project_id,
                    auto_remove=auto_remove)
                volume_object.create(context)

                for item in volume_mounts:
                    if item['name'] == mount['name']:
                        mount_destination = item['mountPath']
                        container_uuid = item['container_uuid']
                        volmapp = objects.VolumeMapping(
                            context,
                            container_path=mount_destination,
                            user_id=context.user_id,
                            project_id=context.project_id,
                            volume_id=volume_object.id)
                        requested_volumes.setdefault(container_uuid, [])
                        requested_volumes[container_uuid].append(volmapp)

                if not mount_destination or not container_uuid:
                    msg = _("volume mount parameters is invalid.")
                    raise exception.Invalid(msg)
        except Exception as e:
            # if volume search or created failed, will remove all
            # the created volume. The existed volume will remain.
            for volume in volume_created:
                try:
                    cinder_api.delete_volume(volume.id)
                except Exception as exc:
                    LOG.error('Error on deleting volume "%s": %s.',
                              volume.id, six.text_type(exc))

            # Since the container and capsule database model has been created,
            # we need to delete them here due to the volume create failed.
            for container in capsule.containers:
                try:
                    container.destroy(context)
                except Exception as exc:
                    LOG.warning('fail to delete the container %s: %s',
                                container.uuid, exc)

            capsule.destroy(context)

            raise e

        return requested_volumes
