# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
#    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 pecan
from pecan import expose
from pecan import rest
import six

import oslo_log.log as logging

import trio2o.common.client as t_client
from trio2o.common import constants
import trio2o.common.context as t_context
import trio2o.common.exceptions as t_exceptions
from trio2o.common.i18n import _
from trio2o.common.i18n import _LE
import trio2o.common.lock_handle as t_lock
from trio2o.common.quota import QUOTAS
from trio2o.common.scheduler import filter_scheduler
from trio2o.common import utils
from trio2o.common import xrpcapi
import trio2o.db.api as db_api
from trio2o.db import core
from trio2o.db import models


LOG = logging.getLogger(__name__)

MAX_METADATA_KEY_LENGTH = 255
MAX_METADATA_VALUE_LENGTH = 255


class ServerController(rest.RestController):

    def __init__(self, project_id):
        self.project_id = project_id
        self.clients = {constants.TOP: t_client.Client()}
        self.xjob_handler = xrpcapi.XJobAPI()
        self.filter_scheduler = filter_scheduler.FilterScheduler()

    def _get_client(self, pod_name=constants.TOP):
        if pod_name not in self.clients:
            self.clients[pod_name] = t_client.Client(pod_name)
        return self.clients[pod_name]

    def _get_all(self, context, params):
        filters = [{'key': key,
                    'comparator': 'eq',
                    'value': value} for key, value in params.iteritems()]
        ret = []
        pods = db_api.list_pods(context)
        for pod in pods:
            if not pod['az_name']:
                continue
            client = self._get_client(pod['pod_name'])
            servers = client.list_servers(context, filters=filters)
            ret.extend(servers)
        return ret

    @staticmethod
    def _construct_brief_server_entry(server):
        return {'id': server['id'],
                'name': server.get('name'),
                'status': server.get('status'),
                'tags': server.get('tags'),
                'metadata': server.get('metadata'),
                'links': server.get('links')}

    @expose(generic=True, template='json')
    def get_one(self, _id, **kwargs):
        context = t_context.extract_context_from_environ()

        if _id == 'detail':
            return {'servers': self._get_all(context, kwargs)}

        mappings = db_api.get_bottom_mappings_by_top_id(
            context, _id, constants.RT_SERVER)
        if not mappings:
            return utils.format_nova_error(
                404, _('Instance %s could not be found.') % _id)
        pod, bottom_id = mappings[0]
        client = self._get_client(pod['pod_name'])
        server = client.get_servers(context, bottom_id)
        if not server:
            return utils.format_nova_error(
                404, _('Instance %s could not be found.') % _id)
        else:
            return {'server': server}

    @expose(generic=True, template='json')
    def get_all(self, **kwargs):
        context = t_context.extract_context_from_environ()
        return {'servers': self._get_all(context, kwargs)}

    @expose(generic=True, template='json')
    def post(self, **kw):
        context = t_context.extract_context_from_environ()

        if 'server' not in kw:
            return utils.format_nova_error(
                400, _('server is not set'))

        az = kw['server'].get('availability_zone', '')
        pod, b_az = self.filter_scheduler.select_destination(
            context, az, self.project_id, pod_group='')

        if not pod:
            return utils.format_nova_error(
                500, _('Pod not configured or scheduling failure'))

        t_server_dict = kw['server']
        self._process_metadata_quota(context, t_server_dict)
        self._process_injected_file_quota(context, t_server_dict)

        server_body = self._get_create_server_body(kw['server'], b_az)

        security_groups = []
        if 'security_groups' not in kw['server']:
            security_groups = ['default']
        else:
            for sg in kw['server']['security_groups']:
                if 'name' not in sg:
                    return utils.format_nova_error(
                        400, _('Invalid input for field/attribute'))
                security_groups.append(sg['name'])

        server_body['networks'] = []
        if 'networks' in kw['server']:
            for net_info in kw['server']['networks']:
                if 'uuid' in net_info:
                    nic = {'net-id': net_info['uuid']}
                    server_body['networks'].append(nic)
                elif 'port' in net_info:
                    nic = {'port-id': net_info['port']}
                    server_body['networks'].append(nic)

        client = self._get_client(pod['pod_name'])
        server = client.create_servers(
            context,
            name=server_body['name'],
            image=server_body['imageRef'],
            flavor=server_body['flavorRef'],
            nics=server_body['networks'],
            security_groups=security_groups)

        with context.session.begin():
            core.create_resource(context, models.ResourceRouting,
                                 {'top_id': server['id'],
                                  'bottom_id': server['id'],
                                  'pod_id': pod['pod_id'],
                                  'project_id': self.project_id,
                                  'resource_type': constants.RT_SERVER})
        pecan.response.status = 202
        return {'server': server}

    @expose(generic=True, template='json')
    def delete(self, _id):
        context = t_context.extract_context_from_environ()

        mappings = db_api.get_bottom_mappings_by_top_id(context, _id,
                                                        constants.RT_SERVER)
        if not mappings:
            pecan.response.status = 404
            return {'Error': {'message': _('Server not found'), 'code': 404}}

        pod, bottom_id = mappings[0]
        client = self._get_client(pod['pod_name'])
        try:
            ret = client.delete_servers(context, bottom_id)
            # none return value indicates server not found
            if ret is None:
                self._remove_stale_mapping(context, _id)
                pecan.response.status = 404
                return {'Error': {'message': _('Server not found'),
                                  'code': 404}}
        except Exception as e:
            code = 500
            message = _('Delete server %(server_id)s fails') % {
                'server_id': _id}
            if hasattr(e, 'code'):
                code = e.code
            ex_message = str(e)
            if ex_message:
                message = ex_message
            LOG.error(message)

            pecan.response.status = code
            return {'Error': {'message': message, 'code': code}}

        pecan.response.status = 204
        return pecan.response

    def _get_or_create_route(self, context, pod_name, _id, _type):
        def list_resources(t_ctx, q_ctx, pod_, ele, _type_):
            client = self._get_client(pod_['pod_name'])
            return client.list_resources(_type_, t_ctx, [{'key': 'name',
                                                          'comparator': 'eq',
                                                          'value': ele['id']}])

        return t_lock.get_or_create_route(context, None,
                                          self.project_id,
                                          pod_name, {'id': _id},
                                          _type, list_resources)

    @staticmethod
    def _get_create_server_body(origin, bottom_az):
        body = {}
        copy_fields = ['name', 'imageRef', 'flavorRef',
                       'max_count', 'min_count']
        if bottom_az:
            body['availability_zone'] = bottom_az
        for field in copy_fields:
            if field in origin:
                body[field] = origin[field]
        return body

    @staticmethod
    def _remove_stale_mapping(context, server_id):
        filters = [{'key': 'top_id', 'comparator': 'eq', 'value': server_id},
                   {'key': 'resource_type',
                    'comparator': 'eq',
                    'value': constants.RT_SERVER}]
        with context.session.begin():
            core.delete_resources(context,
                                  models.ResourceRouting,
                                  filters)

    def _process_injected_file_quota(self, context, t_server_dict):
        try:
            ctx = context.elevated()
            injected_files = t_server_dict.get('injected_files', None)
            self._check_injected_file_quota(ctx, injected_files)
        except (t_exceptions.OnsetFileLimitExceeded,
                t_exceptions.OnsetFilePathLimitExceeded,
                t_exceptions.OnsetFileContentLimitExceeded) as e:
            msg = str(e)
            LOG.exception(_LE('Quota exceeded %(msg)s'),
                          {'msg': msg})
            return utils.format_nova_error(400, _('Quota exceeded %s') % msg)

    def _check_injected_file_quota(self, context, injected_files):
        """Enforce quota limits on injected files.

        Raises a QuotaError if any limit is exceeded.

        """

        if injected_files is None:
            return

        # Check number of files first
        try:
            QUOTAS.limit_check(context,
                               injected_files=len(injected_files))
        except t_exceptions.OverQuota:
            raise t_exceptions.OnsetFileLimitExceeded()

        # OK, now count path and content lengths; we're looking for
        # the max...
        max_path = 0
        max_content = 0
        for path, content in injected_files:
            max_path = max(max_path, len(path))
            max_content = max(max_content, len(content))

        try:
            QUOTAS.limit_check(context,
                               injected_file_path_bytes=max_path,
                               injected_file_content_bytes=max_content)
        except t_exceptions.OverQuota as exc:
            # Favor path limit over content limit for reporting
            # purposes
            if 'injected_file_path_bytes' in exc.kwargs['overs']:
                raise t_exceptions.OnsetFilePathLimitExceeded()
            else:
                raise t_exceptions.OnsetFileContentLimitExceeded()

    def _process_metadata_quota(self, context, t_server_dict):
        try:
            ctx = context.elevated()
            metadata = t_server_dict.get('metadata', None)
            self._check_metadata_properties_quota(ctx, metadata)
        except t_exceptions.InvalidMetadata as e1:
            LOG.exception(_LE('Invalid metadata %(exception)s'),
                          {'exception': str(e1)})
            return utils.format_nova_error(400, _('Invalid metadata'))
        except t_exceptions.InvalidMetadataSize as e2:
            LOG.exception(_LE('Invalid metadata size %(exception)s'),
                          {'exception': str(e2)})
            return utils.format_nova_error(400, _('Invalid metadata size'))
        except t_exceptions.MetadataLimitExceeded as e3:
            LOG.exception(_LE('Quota exceeded %(exception)s'),
                          {'exception': str(e3)})
            return utils.format_nova_error(400,
                                           _('Quota exceeded in metadata'))

    def _check_metadata_properties_quota(self, context, metadata=None):
        """Enforce quota limits on metadata properties."""
        if not metadata:
            metadata = {}
        if not isinstance(metadata, dict):
            msg = (_("Metadata type should be dict."))
            raise t_exceptions.InvalidMetadata(reason=msg)
        num_metadata = len(metadata)
        try:
            QUOTAS.limit_check(context, metadata_items=num_metadata)
        except t_exceptions.OverQuota as exc:
            quota_metadata = exc.kwargs['quotas']['metadata_items']
            raise t_exceptions.MetadataLimitExceeded(allowed=quota_metadata)

        # Because metadata is processed in the bottom pod, we just do
        # parameter validation here to ensure quota management
        for k, v in six.iteritems(metadata):
            try:
                utils.check_string_length(v)
                utils.check_string_length(k, min_len=1)
            except t_exceptions.InvalidInput as e:
                raise t_exceptions.InvalidMetadata(reason=str(e))

            if len(k) > MAX_METADATA_KEY_LENGTH:
                msg = _("Metadata property key greater than 255 characters")
                raise t_exceptions.InvalidMetadataSize(reason=msg)
            if len(v) > MAX_METADATA_VALUE_LENGTH:
                msg = _("Metadata property value greater than 255 characters")
                raise t_exceptions.InvalidMetadataSize(reason=msg)
