# Copyright 2014 Rackspace
# 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 json

from oslo_log import log as logging

from trove.common import cfg
from trove.common import exception
from trove.common.exception import ModelNotFoundError
from trove.common.i18n import _
from trove.common import timeutils
from trove.common import utils
from trove.datastore import models as dstore_models
from trove.db import get_db_api
from trove.db import models as dbmodels


CONF = cfg.CONF
LOG = logging.getLogger(__name__)


class Configurations(object):

    DEFAULT_LIMIT = CONF.configurations_page_size

    @staticmethod
    def load(context):
        if context is None:
            raise TypeError(_("Argument context not defined."))
        elif id is None:
            raise TypeError(_("Argument is not defined."))

        if context.is_admin:
            db_info = DBConfiguration.find_all(deleted=False)
            if db_info.count() == 0:
                LOG.debug("No configurations found for admin user")
        else:
            db_info = DBConfiguration.find_all(tenant_id=context.project_id,
                                               deleted=False)
            if db_info.count() == 0:
                LOG.debug("No configurations found for tenant %s",
                          context.project_id)

        limit = utils.pagination_limit(context.limit,
                                       Configurations.DEFAULT_LIMIT)
        data_view = DBConfiguration.find_by_pagination('configurations',
                                                       db_info,
                                                       "foo",
                                                       limit=limit,
                                                       marker=context.marker)
        next_marker = data_view.next_page_marker
        return data_view.collection, next_marker


class Configuration(object):

    def __init__(self, context, configuration_id):
        self.context = context
        self.configuration_id = configuration_id

    @property
    def instances(self):
        return self.instances

    @property
    def items(self):
        return self.items

    @staticmethod
    def create(name, description, tenant_id, datastore, datastore_version):
        configurationGroup = DBConfiguration.create(
            name=name,
            description=description,
            tenant_id=tenant_id,
            datastore_version_id=datastore_version)
        return configurationGroup

    @staticmethod
    def create_items(cfg_id, values):
        LOG.debug("Saving configuration values for %(id)s - "
                  "values: %(values)s", {'id': cfg_id, 'values': values})
        config_items = []
        for key, val in values.items():
            config_item = DBConfigurationParameter.create(
                configuration_id=cfg_id,
                configuration_key=key,
                configuration_value=val)
            config_items.append(config_item)
        return config_items

    @staticmethod
    def delete(context, group):
        deleted_at = timeutils.utcnow()
        Configuration.remove_all_items(context, group.id, deleted_at)
        group.deleted = True
        group.deleted_at = deleted_at
        group.save()

    @staticmethod
    def remove_all_items(context, id, deleted_at):
        items = DBConfigurationParameter.find_all(configuration_id=id,
                                                  deleted=False).all()
        LOG.debug("Removing all configuration values for %s", id)
        for item in items:
            item.deleted = True
            item.deleted_at = deleted_at
            item.save()

    @staticmethod
    def load_configuration_datastore_version(context, id):
        config = Configuration.load(context, id)
        datastore_version = dstore_models.DatastoreVersion.load_by_uuid(
            config.datastore_version_id)
        return datastore_version

    @staticmethod
    def load(context, id):
        try:
            if context.is_admin:
                return DBConfiguration.find_by(id=id, deleted=False)
            else:
                return DBConfiguration.find_by(id=id,
                                               tenant_id=context.project_id,
                                               deleted=False)
        except ModelNotFoundError:
            msg = _("Configuration group with ID %s could not be found.") % id
            raise ModelNotFoundError(msg)

    @staticmethod
    def find_parameter_details(name, detail_list):
        for item in detail_list:
            if item.name == name:
                return item
        return None

    @staticmethod
    def load_items(context, id):
        datastore_v = Configuration.load_configuration_datastore_version(
            context,
            id)
        config_items = DBConfigurationParameter.find_all(
            configuration_id=id, deleted=False).all()

        detail_list = DatastoreConfigurationParameters.load_parameters(
            datastore_v.id)

        for item in config_items:
            rule = Configuration.find_parameter_details(
                str(item.configuration_key), detail_list)
            if not rule:
                continue
            if rule.data_type == 'boolean':
                item.configuration_value = bool(int(item.configuration_value))
            elif rule.data_type == 'integer':
                item.configuration_value = int(item.configuration_value)
            elif rule.data_type == 'float':
                item.configuration_value = float(item.configuration_value)
            else:
                item.configuration_value = str(item.configuration_value)
        return config_items

    def get_configuration_overrides(self):
        """Gets the overrides dictionary to apply to an instance."""
        overrides = {}
        if self.configuration_id:
            config_items = Configuration.load_items(self.context,
                                                    id=self.configuration_id)

            for i in config_items:
                overrides[i.configuration_key] = i.configuration_value
        return overrides

    def does_configuration_need_restart(self):
        datastore_v = Configuration.load_configuration_datastore_version(
            self.context,
            self.configuration_id)
        config_items = Configuration.load_items(self.context,
                                                id=self.configuration_id)
        LOG.debug("config_items: %s", config_items)
        detail_list = DatastoreConfigurationParameters.load_parameters(
            datastore_v.id)

        for i in config_items:
            LOG.debug("config item: %s", i)
            details = Configuration.find_parameter_details(
                i.configuration_key, detail_list)
            LOG.debug("parameter details: %s", details)
            if not details:
                raise exception.NotFound(uuid=i.configuration_key)
            if bool(details.restart_required):
                return True
        return False

    @staticmethod
    def save(configuration, configuration_items):
        DBConfiguration.save(configuration)
        for item in configuration_items:
            item["deleted_at"] = None
            DBConfigurationParameter.save(item)

    @staticmethod
    def find(context, configuration_id, datastore_version_id):
        try:
            info = Configuration.load(context, configuration_id)
            if (info.datastore_version_id == datastore_version_id):
                return Configuration(context, configuration_id)
        except exception.ModelNotFoundError:
            raise exception.NotFound(
                message='Configuration group id: %s could not be found.'
                % configuration_id)

        raise exception.ConfigurationDatastoreNotMatchInstance(
            config_datastore_version=info.datastore_version_id,
            instance_datastore_version=datastore_version_id)


class DBConfiguration(dbmodels.DatabaseModelBase):
    _data_fields = ['name', 'description', 'tenant_id', 'datastore_version_id',
                    'deleted', 'deleted_at', 'created', 'updated']
    _table_name = 'configurations'

    @property
    def datastore(self):
        datastore_version = dstore_models.DatastoreVersion.load_by_uuid(
            self.datastore_version_id)
        datastore = dstore_models.Datastore.load(
            datastore_version.datastore_id)
        return datastore

    @property
    def datastore_version(self):
        datastore_version = dstore_models.DatastoreVersion.load_by_uuid(
            self.datastore_version_id)
        return datastore_version


class DBConfigurationParameter(dbmodels.DatabaseModelBase):
    _auto_generated_attrs = []
    _data_fields = ['configuration_id', 'configuration_key',
                    'configuration_value', 'deleted',
                    'deleted_at']
    _table_name = 'configuration_parameters'

    def __hash__(self):
        return self.configuration_key.__hash__()


class DBDatastoreConfigurationParameters(dbmodels.DatabaseModelBase):
    """Model for storing the configuration parameters on a datastore."""
    _data_fields = [
        'name',
        'datastore_version_id',
        'restart_required',
        'max_size',
        'min_size',
        'data_type',
    ]
    _table_name = "datastore_configuration_parameters"


class DatastoreConfigurationParameters(object):

    def __init__(self, db_info):
        self.db_info = db_info

    @staticmethod
    def create(**kwargs):
        """Create a configuration parameter for a datastore version."""

        # Do we already have a parameter in the db?
        # yes: and its deleted then modify the param
        # yes: and its not deleted then error on create.
        # no: then just create the new param
        ds_v_id = kwargs.get('datastore_version_id')
        config_param_name = kwargs.get('name')
        try:
            DatastoreConfigurationParameters.load_parameter_by_name(
                ds_v_id,
                config_param_name)
            raise exception.ConfigurationParameterAlreadyExists(
                parameter_name=config_param_name,
                datastore_version=ds_v_id)
        except exception.NotFound:
            pass
        config_param = DBDatastoreConfigurationParameters.create(
            **kwargs)
        return config_param

    @staticmethod
    def delete(version_id, config_param_name):
        config_param = DatastoreConfigurationParameters.load_parameter_by_name(
            version_id, config_param_name)
        config_param.delete()

    @classmethod
    def load_parameters(cls, datastore_version_id):
        return DBDatastoreConfigurationParameters.find_all(
            datastore_version_id=datastore_version_id)

    @classmethod
    def load_parameter(cls, config_id):
        try:
            return DBDatastoreConfigurationParameters.find_by(
                id=config_id, deleted=False
            )
        except exception.NotFound:
            raise exception.NotFound(uuid=config_id)

    @classmethod
    def load_parameter_by_name(cls, datastore_version_id, config_param_name):
        try:
            return DBDatastoreConfigurationParameters.find_by(
                datastore_version_id=datastore_version_id,
                name=config_param_name
            )
        except exception.NotFound:
            raise exception.NotFound(uuid=config_param_name)


def create_or_update_datastore_configuration_parameter(name,
                                                       datastore_version_id,
                                                       restart_required,
                                                       data_type,
                                                       max_size,
                                                       min_size):
    get_db_api().configure_db(CONF)
    datastore_version = dstore_models.DatastoreVersion.load_by_uuid(
        datastore_version_id)
    try:
        config = DatastoreConfigurationParameters.load_parameter_by_name(
            datastore_version_id, name)
        config.restart_required = restart_required
        config.max_size = max_size
        config.min_size = min_size
        config.data_type = data_type
        get_db_api().save(config)
    except exception.NotFound:
        config = DBDatastoreConfigurationParameters(
            id=utils.generate_uuid(),
            name=name,
            datastore_version_id=datastore_version.id,
            restart_required=restart_required,
            data_type=data_type,
            max_size=max_size,
            min_size=min_size,
        )
        get_db_api().save(config)


def load_datastore_configuration_parameters(datastore, datastore_version,
                                            config_file, version_number=None):
    get_db_api().configure_db(CONF)
    (ds, ds_v) = dstore_models.get_datastore_version(
        type=datastore, version=datastore_version, return_inactive=True,
        version_number=version_number)
    with open(config_file) as f:
        config = json.load(f)
        for param in config['configuration-parameters']:
            create_or_update_datastore_configuration_parameter(
                param['name'],
                ds_v.id,
                param['restart_required'],
                param['type'],
                param.get('max'),
                param.get('min'),
            )


def remove_datastore_configuration_parameters(datastore, datastore_version,
                                              version_number=None):
    get_db_api().configure_db(CONF)
    (ds, ds_version) = dstore_models.get_datastore_version(
        type=datastore, version=datastore_version, return_inactive=True,
        version_number=version_number)
    db_params = DatastoreConfigurationParameters.load_parameters(ds_version.id)
    for db_param in db_params:
        db_param.delete()


def persisted_models():
    return {
        'configurations': DBConfiguration,
        'configuration_parameters': DBConfigurationParameter,
        'datastore_configuration_parameters':
            DBDatastoreConfigurationParameters,
    }
