#    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.

import jsonschema
import webob

from nova.api.openstack import common
from nova.api.openstack.compute.schemas import server_tags as schema
from nova.api.openstack.compute.views import server_tags
from nova.api.openstack import wsgi
from nova.api import validation
from nova.api.validation import parameter_types
from nova.compute import api as compute
from nova.compute import vm_states
from nova import context as nova_context
from nova import exception
from nova.i18n import _
from nova.notifications import base as notifications_base
from nova import objects
from nova.policies import server_tags as st_policies


def _get_tags_names(tags):
    return [t.tag for t in tags]


def _get_instance_mapping(context, server_id):
    try:
        return objects.InstanceMapping.get_by_instance_uuid(context,
                                                            server_id)
    except exception.InstanceMappingNotFound as e:
        raise webob.exc.HTTPNotFound(explanation=e.format_message())


@validation.validated
class ServerTagsController(wsgi.Controller):
    _view_builder_class = server_tags.ViewBuilder

    def __init__(self):
        super(ServerTagsController, self).__init__()
        self.compute_api = compute.API()

    def _check_instance_in_valid_state(self, context, server_id, action):
        instance = common.get_instance(self.compute_api, context, server_id)
        if instance.vm_state not in (vm_states.ACTIVE, vm_states.PAUSED,
                                     vm_states.SUSPENDED, vm_states.STOPPED):
            exc = exception.InstanceInvalidState(attr='vm_state',
                                                 instance_uuid=instance.uuid,
                                                 state=instance.vm_state,
                                                 method=action)
            common.raise_http_conflict_for_instance_invalid_state(exc, action,
                                                                  server_id)
        return instance

    @wsgi.api_version("2.26")
    @wsgi.response(204)
    @wsgi.expected_errors(404)
    @validation.query_schema(schema.show_query)
    @validation.response_body_schema(schema.show_response)
    def show(self, req, server_id, id):
        context = req.environ["nova.context"]
        im = _get_instance_mapping(context, server_id)
        context.can(st_policies.POLICY_ROOT % 'show',
                    target={'project_id': im.project_id})

        try:
            with nova_context.target_cell(context, im.cell_mapping) as cctxt:
                exists = objects.Tag.exists(cctxt, server_id, id)
        except (exception.InstanceNotFound) as e:
            raise webob.exc.HTTPNotFound(explanation=e.format_message())

        if not exists:
            msg = (_("Server %(server_id)s has no tag '%(tag)s'")
                   % {'server_id': server_id, 'tag': id})
            raise webob.exc.HTTPNotFound(explanation=msg)

    @wsgi.api_version("2.26")
    @wsgi.expected_errors(404)
    @validation.query_schema(schema.index_query)
    @validation.response_body_schema(schema.index_response)
    def index(self, req, server_id):
        context = req.environ["nova.context"]
        im = _get_instance_mapping(context, server_id)
        context.can(st_policies.POLICY_ROOT % 'index',
                    target={'project_id': im.project_id})

        try:
            with nova_context.target_cell(context, im.cell_mapping) as cctxt:
                tags = objects.TagList.get_by_resource_id(cctxt, server_id)
        except (exception.InstanceNotFound) as e:
            raise webob.exc.HTTPNotFound(explanation=e.format_message())

        return {'tags': _get_tags_names(tags)}

    @wsgi.api_version("2.26")
    @wsgi.expected_errors((400, 404, 409))
    @validation.schema(schema.update)
    @validation.response_body_schema(schema.update_response)
    def update(self, req, server_id, id, body):
        context = req.environ["nova.context"]
        im = _get_instance_mapping(context, server_id)
        context.can(st_policies.POLICY_ROOT % 'update',
                    target={'project_id': im.project_id})

        with nova_context.target_cell(context, im.cell_mapping) as cctxt:
            instance = self._check_instance_in_valid_state(
                cctxt, server_id, 'update tag')

        try:
            jsonschema.validate(id, parameter_types.tag)
        except jsonschema.ValidationError as e:
            msg = (_("Tag '%(tag)s' is invalid. It must be a non empty string "
                     "without characters '/' and ','. Validation error "
                     "message: %(err)s") % {'tag': id, 'err': e.message})
            raise webob.exc.HTTPBadRequest(explanation=msg)

        try:
            with nova_context.target_cell(context, im.cell_mapping) as cctxt:
                tags = objects.TagList.get_by_resource_id(cctxt, server_id)
        except exception.InstanceNotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.format_message())

        if len(tags) >= objects.instance.MAX_TAG_COUNT:
            msg = (_("The number of tags exceeded the per-server limit %d")
                   % objects.instance.MAX_TAG_COUNT)
            raise webob.exc.HTTPBadRequest(explanation=msg)

        if id in _get_tags_names(tags):
            # NOTE(snikitin): server already has specified tag
            return webob.Response(status_int=204)

        try:
            with nova_context.target_cell(context, im.cell_mapping) as cctxt:
                tag = objects.Tag(context=cctxt, resource_id=server_id, tag=id)
                tag.create()
                instance.tags = objects.TagList.get_by_resource_id(cctxt,
                                                                   server_id)
        except exception.InstanceNotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.format_message())

        notifications_base.send_instance_update_notification(
            context, instance, service="nova-api")

        response = webob.Response(status_int=201)
        response.headers['Location'] = self._view_builder.get_location(
            req, server_id, id)
        return response

    @wsgi.api_version("2.26")
    @wsgi.expected_errors((404, 409))
    @validation.schema(schema.update_all)
    @validation.response_body_schema(schema.update_all_response)
    def update_all(self, req, server_id, body):
        context = req.environ["nova.context"]
        im = _get_instance_mapping(context, server_id)
        context.can(st_policies.POLICY_ROOT % 'update_all',
                    target={'project_id': im.project_id})

        with nova_context.target_cell(context, im.cell_mapping) as cctxt:
            instance = self._check_instance_in_valid_state(
                cctxt, server_id, 'update tags')

        try:
            with nova_context.target_cell(context, im.cell_mapping) as cctxt:
                tags = objects.TagList.create(cctxt, server_id, body['tags'])
                instance.tags = tags
        except exception.InstanceNotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.format_message())

        notifications_base.send_instance_update_notification(
            context, instance, service="nova-api")

        return {'tags': _get_tags_names(tags)}

    @wsgi.api_version("2.26")
    @wsgi.response(204)
    @wsgi.expected_errors((404, 409))
    @validation.response_body_schema(schema.delete_response)
    def delete(self, req, server_id, id):
        context = req.environ["nova.context"]
        im = _get_instance_mapping(context, server_id)
        context.can(st_policies.POLICY_ROOT % 'delete',
                    target={'project_id': im.project_id})

        with nova_context.target_cell(context, im.cell_mapping) as cctxt:
            instance = self._check_instance_in_valid_state(
                cctxt, server_id, 'delete tag')

        try:
            with nova_context.target_cell(context, im.cell_mapping) as cctxt:
                objects.Tag.destroy(cctxt, server_id, id)
                instance.tags = objects.TagList.get_by_resource_id(cctxt,
                                                                   server_id)
        except (exception.InstanceTagNotFound,
                exception.InstanceNotFound) as e:
            raise webob.exc.HTTPNotFound(explanation=e.format_message())

        notifications_base.send_instance_update_notification(
            context, instance, service="nova-api")

    @wsgi.api_version("2.26")
    @wsgi.response(204)
    @wsgi.expected_errors((404, 409))
    @validation.response_body_schema(schema.delete_all_response)
    def delete_all(self, req, server_id):
        context = req.environ["nova.context"]
        im = _get_instance_mapping(context, server_id)
        context.can(st_policies.POLICY_ROOT % 'delete_all',
                    target={'project_id': im.project_id})

        with nova_context.target_cell(context, im.cell_mapping) as cctxt:
            instance = self._check_instance_in_valid_state(
                cctxt, server_id, 'delete tags')

        try:
            with nova_context.target_cell(context, im.cell_mapping) as cctxt:
                objects.TagList.destroy(cctxt, server_id)
                instance.tags = objects.TagList()
        except exception.InstanceNotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.format_message())

        notifications_base.send_instance_update_notification(
            context, instance, service="nova-api")
