# Copyright 2016 IBM, Corp.
#
# 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.

"""etcd storage backend."""

import json

from datetime import datetime
import etcd
from oslo_concurrency import lockutils
from oslo_log import log
from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import six

from zun.common import exception
from zun.common.i18n import _
from zun.common import singleton
import zun.conf
from zun.db.etcd import models


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


def get_connection():
    connection = EtcdAPI(host=CONF.etcd.etcd_host,
                         port=CONF.etcd.etcd_port)
    return connection


def clean_all_data():
    conn = get_connection()
    conn.clean_all_zun_data()


def add_identity_filter(query, value):
    """Adds an identity filter to a query.

    Filters results by ID, if supplied value is a valid integer.
    Otherwise attempts to filter results by UUID.

    :param query: Initial query to add filter to.
    :param value: Value for filtering results by.
    :return: Modified query.
    """
    if strutils.is_int_like(value):
        return query.filter_by(id=value)
    elif uuidutils.is_uuid_like(value):
        return query.filter_by(uuid=value)
    else:
        raise exception.InvalidIdentity(identity=value)


def translate_etcd_result(etcd_result, model_type):
    """Translate etcd unicode result to etcd models."""
    try:
        data = json.loads(etcd_result.value)
        ret = None
        if model_type == 'container':
            ret = models.Container(data)
        elif model_type == 'zun_service':
            ret = models.ZunService(data)
        elif model_type == 'image':
            ret = models.Image(data)
        elif model_type == 'resource_class':
            ret = models.ResourceClass(data)
        else:
            raise exception.InvalidParameterValue(
                _('The model_type value: %s is invalid.'), model_type)
        return ret
    except (ValueError, TypeError) as e:
        LOG.error("Error occurred while translating etcd result: %s",
                  six.text_type(e))
        raise


@six.add_metaclass(singleton.Singleton)
class EtcdAPI(object):
    """etcd API."""

    def __init__(self, host, port):
        self.client = etcd.Client(host=host, port=port)

    @lockutils.synchronized('etcd-client')
    def clean_all_zun_data(self):
        try:
            for d in self.client.read('/').children:
                if d.key in ('/containers',):
                    self.client.delete(d.key, recursive=True)
        except etcd.EtcdKeyNotFound as e:
            LOG.error('Error occurred while cleaning zun data: %s',
                      six.text_type(e))
            raise

    def _add_tenant_filters(self, context, filters):
        filters = filters or {}
        if context.is_admin and context.all_tenants:
            return filters

        if context.project_id:
            filters['project_id'] = context.project_id
        else:
            filters['user_id'] = context.user_id

        return filters

    def _filter_resources(self, resources, filters):
        for c in list(resources):
            for k, v in filters.items():
                if c.get(k) != v:
                    resources.remove(c)
                    break

        return resources

    def _process_list_result(self, res_list, limit=None, sort_key=None):
        if len(res_list) == 0:
            return []
        sorted_res_list = res_list
        if sort_key:
            if not hasattr(res_list[0], sort_key):
                raise exception.InvalidParameterValue(
                    err='Container has no attribute: %s' % sort_key)
            sorted_res_list = sorted(res_list, key=lambda k: k.get(sort_key))

        if limit:
            sorted_res_list = sorted_res_list[0:limit]

        return sorted_res_list

    def list_containers(self, context, filters=None, limit=None,
                        marker=None, sort_key=None, sort_dir=None):
        try:
            res = getattr(self.client.read('/containers'), 'children', None)
        except etcd.EtcdKeyNotFound:
            # Before the first container been created, path '/containers'
            # does not exist.
            return []
        except Exception as e:
            LOG.error(
                "Error occurred while reading from etcd server: %s",
                six.text_type(e))
            raise

        containers = []
        for c in res:
            if c.value is not None:
                containers.append(translate_etcd_result(c, 'container'))
        filters = self._add_tenant_filters(context, filters)
        filtered_containers = self._filter_resources(
            containers, filters)
        return self._process_list_result(filtered_containers,
                                         limit=limit, sort_key=sort_key)

    def _validate_unique_container_name(self, context, name):
        if not CONF.compute.unique_container_name_scope:
            return
        lowername = name.lower()
        filters = {'name': name}
        if CONF.compute.unique_container_name_scope == 'project':
            filters['project_id'] = context.project_id
        elif CONF.compute.unique_container_name_scope == 'global':
            pass
        else:
            return

        try:
            containers = self.list_containers(context, filters=filters)
        except etcd.EtcdKeyNotFound:
            return
        except Exception as e:
            LOG.error('Error occurred while retrieving container: %s',
                      six.text_type(e))
            raise
        if len(containers) > 0:
            raise exception.ContainerAlreadyExists(field='name',
                                                   value=lowername)

    @lockutils.synchronized('etcd_container')
    def create_container(self, context, container_data):
        # ensure defaults are present for new containers
        if not container_data.get('uuid'):
            container_data['uuid'] = uuidutils.generate_uuid()

        if container_data.get('name'):
            self._validate_unique_container_name(context,
                                                 container_data['name'])

        container = models.Container(container_data)
        try:
            container.save()
        except Exception:
            raise

        return container

    def get_container_by_uuid(self, context, container_uuid):
        try:
            res = self.client.read('/containers/' + container_uuid)
            container = translate_etcd_result(res, 'container')
            filtered_containers = self._filter_resources(
                [container], self._add_tenant_filters(context, {}))
            if len(filtered_containers) > 0:
                return filtered_containers[0]
            else:
                raise exception.ContainerNotFound(container=container_uuid)
        except etcd.EtcdKeyNotFound:
            raise exception.ContainerNotFound(container=container_uuid)
        except Exception as e:
            LOG.error('Error occurred while retrieving container: %s',
                      six.text_type(e))
            raise

    def get_container_by_name(self, context, container_name):
        try:
            filters = self._add_tenant_filters(
                context, {'name': container_name})
            containers = self.list_containers(context, filters=filters)
        except etcd.EtcdKeyNotFound:
            raise exception.ContainerNotFound(container=container_name)
        except Exception as e:
            LOG.error('Error occurred while retrieving container: %s',
                      six.text_type(e))
            raise

        if len(containers) > 1:
            raise exception.Conflict('Multiple containers exist with same '