# Copyright 2013 OpenStack Foundation
# Copyright 2013 Rackspace Hosting
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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.
#

from trove.common import cfg
from trove.common import exception
from trove.common import utils
from trove.db import models as dbmodels
from trove.db import get_db_api
from trove.openstack.common import log as logging


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


def persisted_models():
    return {
        'datastore': DBDatastore,
        'capabilities': DBCapabilities,
        'datastore_version': DBDatastoreVersion,
        'capability_overrides': DBCapabilityOverrides,
    }


class DBDatastore(dbmodels.DatabaseModelBase):

    _data_fields = ['id', 'name', 'default_version_id']


class DBCapabilities(dbmodels.DatabaseModelBase):

    _data_fields = ['id', 'name', 'description', 'enabled']


class DBCapabilityOverrides(dbmodels.DatabaseModelBase):

    _data_fields = ['id', 'capability_id', 'datastore_version_id', 'enabled']


class DBDatastoreVersion(dbmodels.DatabaseModelBase):

    _data_fields = ['id', 'datastore_id', 'name', 'manager', 'image_id',
                    'packages', 'active']


class Capabilities(object):

    def __init__(self, datastore_version_id=None):
        self.capabilities = []
        self.datastore_version_id = datastore_version_id

    def __contains__(self, item):
        return item in [capability.name for capability in self.capabilities]

    def __len__(self):
        return len(self.capabilities)

    def __iter__(self):
        for item in self.capabilities:
            yield item

    def __repr__(self):
        return '<%s: %s>' % (type(self), self.capabilities)

    def add(self, capability, enabled):
        """
        Add a capability override to a datastore version.
        """
        if self.datastore_version_id is not None:
            DBCapabilityOverrides.create(
                capability_id=capability.id,
                datastore_version_id=self.datastore_version_id,
                enabled=enabled)
        self._load()

    def _load(self):
        """
        Bulk load and override default capabilities with configured
        datastore version specific settings.
        """
        capability_defaults = [Capability(c)
                               for c in DBCapabilities.find_all()]

        capability_overrides = []
        if self.datastore_version_id is not None:
            # This should always happen but if there is any future case where
            # we don't have a datastore version id number it won't stop
            # defaults from rendering.
            capability_overrides = [
                CapabilityOverride(ce)
                for ce in DBCapabilityOverrides.find_all(
                    datastore_version_id=self.datastore_version_id)
            ]

        def override(cap):
            # This logic is necessary to apply datastore version specific
            # capability overrides when they are present in the database.
            for capability_override in capability_overrides:
                if cap.id == capability_override.capability_id:
                    # we have a mapped entity that indicates this datastore
                    # version has an override so we honor that.
                    return capability_override

            # There were no overrides for this capability so we just hand it
            # right back.
            return cap

        self.capabilities = map(override, capability_defaults)

        LOG.debug('Capabilities for datastore %(ds_id)s: %(capabilities)s' %
                  {'ds_id': self.datastore_version_id,
                   'capabilities': self.capabilities})

    @classmethod
    def load(cls, datastore_version_id=None):
        """
        Generates a Capabilities object by looking up all capabilities from
        defaults and overrides and provides the one structure that should be
        used as the interface to controlling capabilities per datastore.

        :returns Capabilities:
        """
        self = cls(datastore_version_id)
        self._load()
        return self


class BaseCapability(object):
    def __init__(self, db_info):
        self.db_info = db_info

    def __repr__(self):
        return ('<%(my_class)s: name: %(name)s, enabled: %(enabled)s>' %
                {'my_class': type(self), 'name': self.name,
                 'enabled': self.enabled})

    @property
    def id(self):
        """
        The capability's id

        :returns str:
        """
        return self.db_info.id

    @property
    def enabled(self):
        """
        Is the capability/feature enabled?

        :returns bool:
        """
        return self.db_info.enabled

    def enable(self):
        """
        Enable the capability.
        """
        self.db_info.enabled = True
        self.db_info.save()

    def disable(self):
        """
        Disable the capability
        """
        self.db_info.enabled = False
        self.db_info.save()

    def delete(self):
        """
        Delete the capability from the database.
        """

        self.db_info.delete()


class CapabilityOverride(BaseCapability):
    """
    A capability override is simply an setting that applies to a
    specific datastore version that overrides the default setting in the
    base capability's entry for Trove.
    """
    def __init__(self, db_info):
        super(CapabilityOverride, self).__init__(db_info)
        # This *may* be better solved with a join in the SQLAlchemy model but
        # I was unable to get our query object to work properly for this.
        parent_capability = Capability.load(db_info.capability_id)
        if parent_capability:
            self.parent_name = parent_capability.name
            self.parent_description = parent_capability.description
        else:
            raise exception.CapabilityNotFound(
                _("Somehow we got a datastore version capability without a "
                  "parent, that shouldn't happen. %s") % db_info.capability_id)

    @property
    def name(self):
        """
        The name of the capability.

        :returns str:
        """
        return self.parent_name

    @property
    def description(self):
        """
        The description of the capability.

        :returns str:
        """
        return self.parent_description

    @property
    def capability_id(self):
        """
        Because capability overrides is an association table there are times
        where having the capability id is necessary.

        :returns str:
        """
        return self.db_info.capability_id

    @classmethod
    def load(cls, capability_id):
        """
        Generates a CapabilityOverride object from the capability_override id.

        :returns CapabilityOverride:
        """
        try:
            return cls(DBCapabilityOverrides.find_by(
                capability_id=capability_id))
        except exception.ModelNotFoundError:
            raise exception.CapabilityNotFound(
                _("Capability Override not found for "
                  "capability %s") % capability_id)

    @classmethod
    def create(cls, capability, datastore_version_id, enabled):
        """
        Create a new CapabilityOverride.

        :param capability:              The capability to be overridden for
                                        this DS Version
        :param datastore_version_id:    The datastore version to apply the
                                        override to.
        :param enabled:                 Set enabled to True or False

        :returns CapabilityOverride:
        """

        return CapabilityOverride(
            DBCapabilityOverrides.create(
                capability_id=capability.id,
                datastore_version_id=datastore_version_id,
                enabled=enabled)
        )


class Capability(BaseCapability):
    @property
    def name(self):
        """
        The Capability name

        :returns str:
        """
        return self.db_info.name

    @property
    def description(self):
        """
        The Capability description

        :returns str:
        """
        return self.db_info.description

    @classmethod
    def load(cls, capability_id_or_name):
        """
        Generates a Capability object by looking up the capability first by
        ID then by name.

        :returns Capability:
        """
        try:
            return cls(DBCapabilities.find_by(id=capability_id_or_name))
        except exception.ModelNotFoundError:
            try:
                return cls(DBCapabilities.find_by(name=capability_id_or_name))
            except exception.ModelNotFoundError:
                raise exception.CapabilityNotFound(
                    capability=capability_id_or_name)

    @classmethod
    def create(cls, name, description, enabled=False):
        """
        Creates a new capability.

        :returns Capability:
        """
        return Capability(DBCapabilities.create(
            name=name, description=description, enabled=enabled))


class Datastore(object):

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

    @classmethod
    def load(cls, id_or_name):
        try:
            return cls(DBDatastore.find_by(id=id_or_name))
        except exception.ModelNotFoundError:
            try:
                return cls(DBDatastore.find_by(name=id_or_name))
            except exception.ModelNotFoundError:
                raise exception.DatastoreNotFound(datastore=id_or_name)

    @property
    def id(self):
        return self.db_info.id

    @property
    def name(self):
        return self.db_info.name

    @property
    def default_version_id(self):
        return self.db_info.default_version_id

    def delete(self):
        self.db_info.delete()


class Datastores(object):

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

    @classmethod
    def load(cls, only_active=True):
        datastores = DBDatastore.find_all()
        if only_active:
            datastores = datastores.join(DBDatastoreVersion).filter(
                DBDatastoreVersion.active == 1)
        return cls(datastores)

    def __iter__(self):
        for item in self.db_info:
            yield item


class DatastoreVersion(object):

    def __init__(self, db_info):
        self._capabilities = None
        self.db_info = db_info
        self._datastore_name = None

    @classmethod
    def load(cls, datastore, id_or_name):
        try:
            return cls(DBDatastoreVersion.find_by(datastore_id=datastore.id,
                                                  id=id_or_name))
        except exception.ModelNotFoundError:
            versions = DBDatastoreVersion.find_all(datastore_id=datastore.id,
                                                   name=id_or_name)
            if versions.count() == 0:
                raise exception.DatastoreVersionNotFound(version=id_or_name)
            if versions.count() > 1:
                raise exception.NoUniqueMatch(name=id_or_name)
            return cls(versions.first())

    @classmethod
    def load_by_uuid(cls, uuid):
        try:
            return cls(DBDatastoreVersion.find_by(id=uuid))
        except exception.ModelNotFoundError:
            raise exception.DatastoreVersionNotFound(version=uuid)

    @property
    def id(self):
        return self.db_info.id

    @property
    def datastore_id(self):
        return self.db_info.datastore_id

    @property
    def datastore_name(self):
        if self._datastore_name is None:
            self._datastore_name = Datastore.load(self.datastore_id).name
        return self._datastore_name

    #TODO(tim.simpson): This would be less confusing if it was called "version"
    #                   and datastore_name was called "name".
    @property
    def name(self):
        return self.db_info.name

    @property
    def image_id(self):
        return self.db_info.image_id

    @property
    def packages(self):
        return self.db_info.packages

    @property
    def active(self):
        return (True if self.db_info.active else False)

    @property
    def manager(self):
        return self.db_info.manager

    @property
    def capabilities(self):
        if self._capabilities is None:
            self._capabilities = Capabilities.load(self.db_info.id)

        return self._capabilities


class DatastoreVersions(object):

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

    @classmethod
    def load(cls, id_or_name, only_active=True):
        datastore = Datastore.load(id_or_name)
        if only_active:
            versions = DBDatastoreVersion.find_all(datastore_id=datastore.id,
                                                   active=True)
        else:
            versions = DBDatastoreVersion.find_all(datastore_id=datastore.id)
        return cls(versions)

    @classmethod
    def load_all(cls, only_active=True):
        if only_active:
            return cls(DBDatastoreVersion.find_all(active=True))
        return cls(DBDatastoreVersion.find_all())

    def __iter__(self):
        for item in self.db_info:
            yield item


def get_datastore_version(type=None, version=None):
    datastore = type or CONF.default_datastore
    if not datastore:
        raise exception.DatastoreDefaultDatastoreNotFound()
    datastore = Datastore.load(datastore)
    version = version or datastore.default_version_id
    if not version:
        raise exception.DatastoreDefaultVersionNotFound(datastore=
                                                        datastore.name)
    datastore_version = DatastoreVersion.load(datastore, version)
    if datastore_version.datastore_id != datastore.id:
        raise exception.DatastoreNoVersion(datastore=datastore.name,
                                           version=datastore_version.name)
    if not datastore_version.active:
        raise exception.DatastoreVersionInactive(version=
                                                 datastore_version.name)
    return (datastore, datastore_version)


def update_datastore(name, default_version):
    db_api.configure_db(CONF)
    try:
        datastore = DBDatastore.find_by(name=name)
    except exception.ModelNotFoundError:
        # Create a new one
        datastore = DBDatastore()
        datastore.id = utils.generate_uuid()
        datastore.name = name

    if default_version:
        version = DatastoreVersion.load(datastore, default_version)
        if not version.active:
            raise exception.DatastoreVersionInactive(version=version.name)
        datastore.default_version_id = version.id

    db_api.save(datastore)


def update_datastore_version(datastore, name, manager, image_id, packages,
                             active):
    db_api.configure_db(CONF)
    datastore = Datastore.load(datastore)
    try:
        version = DBDatastoreVersion.find_by(datastore_id=datastore.id,
                                             name=name)
    except exception.ModelNotFoundError:
        # Create a new one
        version = DBDatastoreVersion()
        version.id = utils.generate_uuid()
        version.name = name
        version.datastore_id = datastore.id
    version.manager = manager
    version.image_id = image_id
    version.packages = packages
    version.active = active
    db_api.save(version)
