import wsme

from orm.common.orm_common.utils.cross_api_utils import (set_utils_conf,
                                                         get_regions_of_group,
                                                         validate_description)
from orm.services.flavor_manager.fms_rest.data.sql_alchemy import db_models
from orm.services.flavor_manager.fms_rest.data.wsme.model import Model
from orm.services.flavor_manager.fms_rest.logic.error_base import ErrorStatus

from oslo_config import cfg
from pecan import conf, request


class TenantWrapper(Model):
    """user model the customer

    """
    tenants = wsme.wsattr([str], mandatory=False)

    def __init__(self, tenants=[]):  # pragma: no cover
        """region array

        :param tenants: array of tenants
        """

        self.tenants = tenants


class ExtraSpecsWrapper(Model):
    """extra spec model."""

    os_extra_specs = wsme.wsattr(wsme.types.DictType(str, str),
                                 mandatory=False)
    extra_specs = wsme.wsattr(wsme.types.DictType(str, str),
                              mandatory=False, name="extra-specs")

    def __init__(self, os_extra_specs={}, extra_specs={}):
        """init func."""

        # self.extra_specs = extra_specs
        if extra_specs:
            self.extra_specs = extra_specs
        if os_extra_specs:
            self.os_extra_specs = os_extra_specs

    def to_db_model(self):
        extra_spec = []
        autogen_es = cfg.CONF.flavor_series_metadata.autogen_extra_specs
        for key, value in self.os_extra_specs.iteritems():
            if key in autogen_es:
                continue
            extra_spec_rec = db_models.FlavorExtraSpec()
            extra_spec_rec.key_name = key
            extra_spec_rec.key_value = value
            extra_spec.append(extra_spec_rec)

        return extra_spec

    def _get_extra_specs_urlpath(self):
        # return the way to return extra specs depends on url used
        return '{}'.format(request.upath_info).split("/")[-1].strip()

    @staticmethod
    def from_db_model(os_extra_specs):
        extra_spec_method = ExtraSpecsWrapper()._get_extra_specs_urlpath()
        extra_specs = ExtraSpecsWrapper()
        setattr(extra_specs, extra_spec_method, {})
        for extra_spec in os_extra_specs:
            getattr(extra_specs,
                    extra_spec_method,
                    None)[extra_spec.key_name] = extra_spec.key_value
        return extra_specs


class TagsWrapper(Model):
    """Tags model."""

    tags = wsme.wsattr(wsme.types.DictType(str, str), mandatory=True)

    def __init__(self, tags={}):
        """init func."""

        self.tags = tags

    def to_db_model(self):
        tag = []

        for key, value in self.tags.iteritems():
            tags_rec = db_models.FlavorTag()
            tags_rec.key_name = key
            tags_rec.key_value = value
            tag.append(tags_rec)

        return tag

    @staticmethod
    def from_db_model(tags):
        my_tags = TagsWrapper()
        for tag in tags:
            my_tags.tags[tag.key_name] = tag.key_value

        return my_tags


class Region(Model):
    """network model the customer

    """
    name = wsme.wsattr(str, mandatory=True)
    type = wsme.wsattr(str, default="single", mandatory=False)
    status = wsme.wsattr(str, mandatory=False)
    error_message = wsme.wsattr(str, mandatory=False)

    def __init__(self, name="", type="single", status="", error_message=""):
        """region array

        :param name:  region names
        :param type:  region type
        :param status:  region creation status
        :param error_message:  region creation error status message
        """

        self.name = name
        self.type = type
        self.status = status
        if error_message:
            self.error_message = error_message

    def to_db_model(self):
        if self.name == '' or self.name.isspace():
            raise ErrorStatus(400, 'Cannot add region with empty name')
        region_rec = db_models.FlavorRegion()
        region_rec.region_name = self.name
        region_rec.region_type = self.type

        return region_rec


class RegionWrapper(Model):  # pragma: no cover
    """regions model

    """
    regions = wsme.wsattr([Region], mandatory=False)

    def __init__(self, regions=[]):
        """init

        :param regions: array of regions
        """

        self.regions = regions


class Flavor(Model):
    """flavor entity with all it's related data

    """
    id = wsme.wsattr(wsme.types.text, mandatory=False)
    name = wsme.wsattr(wsme.types.text, mandatory=False)
    alias = wsme.wsattr(wsme.types.text, mandatory=False)
    description = wsme.wsattr(wsme.types.text, mandatory=False)
    series = wsme.wsattr(wsme.types.text, mandatory=True)
    ram = wsme.wsattr(wsme.types.text, mandatory=True)
    vcpus = wsme.wsattr(wsme.types.text, mandatory=True)
    disk = wsme.wsattr(wsme.types.text, mandatory=True)
    swap = wsme.wsattr(wsme.types.text, mandatory=False)
    ephemeral = wsme.wsattr(wsme.types.text, mandatory=False)
    regions = wsme.wsattr(wsme.types.ArrayType(Region), mandatory=False)
    visibility = wsme.wsattr(wsme.types.text, mandatory=True)
    tenants = wsme.wsattr(wsme.types.ArrayType(str), mandatory=False)
    status = wsme.wsattr(wsme.types.text, mandatory=False)
    tag = wsme.wsattr(wsme.types.DictType(str, str), mandatory=False)
    options = wsme.wsattr(wsme.types.DictType(str, str), mandatory=False)
    extra_specs = wsme.wsattr(wsme.types.DictType(str, str), mandatory=False,
                              name="extra-specs")

    def __init__(self,
                 id="",
                 name="",
                 alias=None,
                 description="",
                 series="",
                 ram="0",
                 vcpus="0",
                 disk="0",
                 swap="0",
                 ephemeral="0",
                 extra_specs={},
                 tag={},
                 options={},
                 regions=[],
                 visibility="",
                 tenants=[],
                 status=""):
        """Create a new Flavor.

        :param id: flavor UUID
        :param name: name extracted from flavor parameters
        :param description: flavor description
        :param series:  series possible
        :param ram: RAM in MB
        :param vcpus:
        :param disk: Disk in GB
        :param swap: is optional and default is 0
        :param ephemeral: is optional and default is 0, size in GB
        :param extra_spec: key-value dictionary
        :param tags: key-value dictionary
        :param options: key-value dictionary
        :param regions:
        :param visibility:
        :param tenants:
        :param status: status of creation
        """
        self.id = id
        self.name = name
        if alias is not None:
            self.alias = alias
        self.description = description
        self.series = series
        self.ram = ram
        self.vcpus = vcpus
        self.disk = disk
        self.swap = swap
        self.ephemeral = ephemeral
        self.extra_specs = extra_specs
        self.tag = tag
        self.options = options
        self.regions = regions
        self.visibility = visibility
        self.tenants = tenants
        self.status = status

    def validate_model(self, context=None):
        series_metadata = {}
        valid_numa = []
        valid_vnf = []

        if self.series:
            valid_flavor_series = cfg.CONF.fms.flavor_series
            if self.series not in valid_flavor_series:
                raise ErrorStatus(400, "Series possible values are {}".format(
                                  valid_flavor_series))
        else:
            raise ErrorStatus(400, "Series not specified.")

        if self.series in cfg.CONF['flavor_series_metadata']:
            series_metadata = cfg.CONF['flavor_series_metadata'][self.series]
        else:
            raise ErrorStatus(400, "Cannot retrieve requested flavor"
                              " series metadata.")

        if 'valid_options_numa' in series_metadata:
            valid_numa = [x for x in
                          series_metadata['valid_options_numa'].split(',')]

        if 'valid_options_vnf' in series_metadata:
            valid_vnf = [x for x in
                         series_metadata['valid_options_vnf'].split(',')]

        # validate that input entries are valid
        try:
            # flavor option values must be either 'true' or 'false' (in quotes)

            if self.options:
                option_values = self.options.values()
                invalid_opt_vals = [x for x in option_values if (x.lower()
                                    not in ['true', 'false'])]
                if invalid_opt_vals:
                    raise ErrorStatus(400, "All flavor option values must have"
                                      " a value of 'true' or 'false'")

            # validate series and set flavor vcpu and vram limits
            requested_numa = [n for n in valid_numa if n in
                              self.options.keys() and
                              self.options[n].lower() == 'true']

            if requested_numa:
                vcpu_limit = int(series_metadata['vcpu_limit'])
                vram_limit = int(series_metadata['vram_limit'])
            else:
                vcpu_limit = int(series_metadata['alt_vcpu_limit'])
                vram_limit = int(series_metadata['alt_vram_limit'])

            # determine other flavor limits
            swap_file_limit = cfg.CONF.fms.flavor_swap_file_limit
            ephemeral_limit = cfg.CONF.fms.flavor_ephemeral_limit

            isValid = validate_description(self.description)
            if not isValid:
                raise ErrorStatus(400, "Flavor description does not allow"
                                  " special characters: only dashes,"
                                  " commas, and period allowed.")
            if not self.ram.isdigit():
                raise ErrorStatus(400, "ram must be a number")
            if not self.vcpus.isdigit():
                raise ErrorStatus(400, "vcpus must be a number")
            if not self.validInt(self.disk):
                raise ErrorStatus(400, "disk must be a number")
            if not self.swap.isdigit():
                raise ErrorStatus(400, "swap must be a number")
            if self.ephemeral and not self.ephemeral.isdigit():
                raise ErrorStatus(400, "ephemeral must be a number")
            if int(self.ram) not in range(1024, vram_limit + 1, 1024):
                raise ErrorStatus(400,
                                  "ram value is out of range. Expected range"
                                  " is 1024(1GB)-%6d(%3dGB) and must be a"
                                  " multiple of 1024" %
                                  (vram_limit, vram_limit / 1024))
            if int(self.vcpus) not in range(1, vcpu_limit + 1):
                raise ErrorStatus(400, "vcpus value is out of range. Expected"
                                  "range is 1-%2d" % (vcpu_limit))
            if int(self.disk) < 0:
                raise ErrorStatus(400, "disk cannot be less than zero")

            if not self.ephemeral:
                self.ephemeral = "0"
            elif (self.ephemeral and
                  int(self.ephemeral) not in range(0, ephemeral_limit + 1)):
                raise ErrorStatus(400,
                                  "ephemeral value is out of range. Expected"
                                  " range is 0-%5d(%2dTB)" %
                                  (ephemeral_limit, ephemeral_limit / 1000))

            if int(self.swap) not in range(0, swap_file_limit + 1, 1024):
                raise ErrorStatus(400,
                                  "swap value is out of range. Expected"
                                  " range is 0-%6d(%3dGB) and must be a"
                                  " multiple of 1024" %
                                  (swap_file_limit, swap_file_limit / 1024))
        except ValueError:
            raise ErrorStatus(400, "ram, vcpus, disk, ephemeral and swap must"
                              " be integers")

        for symbol, value in self.extra_specs.iteritems():
            if symbol == 'numa_override' and value not in valid_numa:
                raise ErrorStatus(400,
                                  "Invalid value. numa_override possible"
                                  " values: " + str(valid_numa))
            if symbol == 'vlan_category' and value not in valid_vnf:
                raise ErrorStatus(400,
                                  "Invalid value. vlan_category possible"
                                  " values: " + str(valid_vnf))

        # region  type can be group only in create flavor!!
        if not context == "create":
            for region in self.regions:
                if region.type == "group":
                    raise ErrorStatus(400,
                                      "region type \'group\' is invalid in"
                                      " this action, \'group\' can be only"
                                      " in create flavor action")

    def to_db_model(self):
        flavor = db_models.Flavor()
        extra_spec = []
        tags = []
        options = []
        regions = []
        tenants = []
        autogen_es = cfg.CONF.flavor_series_metadata.autogen_extra_specs

        for symbol, value in self.extra_specs.iteritems():
            if symbol in autogen_es:
                continue
            es = db_models.FlavorExtraSpec()
            es.key_name = symbol
            es.key_value = value
            extra_spec.append(es)

        if self.series:
            extra_spec.extend(self.get_extra_spec_needed())

        for symbol, value in self.tag.iteritems():
            tag = db_models.FlavorTag()
            tag.key_name = symbol
            tag.key_value = value
            tags.append(tag)

        for symbol, value in self.options.iteritems():
            option = db_models.FlavorOption()
            option.key_name = symbol
            option.key_value = value.lower()
            options.append(option)

        for region in self.regions:
            regions.append(region.to_db_model())

        for tenant in self.tenants:
            tenant_rec = db_models.FlavorTenant()
            tenant_rec.tenant_id = tenant
            tenants.append(tenant_rec)

        flavor.id = self.id
        flavor.name = self.name
        if self.alias is not wsme.Unset:
            flavor.alias = self.alias
        flavor.description = self.description
        flavor.series = self.series
        flavor.ram = int(self.ram)
        flavor.vcpus = int(self.vcpus)
        flavor.disk = int(self.disk)
        flavor.swap = int(self.swap)
        flavor.ephemeral = int(self.ephemeral)
        flavor.visibility = self.visibility

        flavor.flavor_extra_specs = extra_spec
        flavor.flavor_tags = tags
        flavor.flavor_options = options
        flavor.flavor_regions = regions
        flavor.flavor_tenants = tenants

        return flavor

    @staticmethod
    def from_db_model(sql_flavor):
        flavor = Flavor()
        flavor.id = sql_flavor.id
        flavor.series = sql_flavor.series
        flavor.description = sql_flavor.description
        flavor.ram = str(sql_flavor.ram)
        flavor.ephemeral = str(sql_flavor.ephemeral)
        flavor.visibility = sql_flavor.visibility
        flavor.vcpus = str(sql_flavor.vcpus)
        flavor.swap = str(sql_flavor.swap)
        flavor.disk = str(sql_flavor.disk)
        flavor.name = sql_flavor.name
        if sql_flavor.alias is not None:
            flavor.alias = sql_flavor.alias

        for tenant in sql_flavor.flavor_tenants:
            flavor.tenants.append(tenant.tenant_id)

        for sql_region in sql_flavor.flavor_regions:
            region = Region()
            region.name = sql_region.region_name
            region.type = sql_region.region_type
            flavor.regions.append(region)

        for extra_spec in sql_flavor.flavor_extra_specs:
            flavor.extra_specs[extra_spec.key_name] = extra_spec.key_value

        for tag in sql_flavor.flavor_tags:
            flavor.tag[tag.key_name] = tag.key_value

        for option in sql_flavor.flavor_options:
            flavor.options[option.key_name] = option.key_value

        return flavor

    def get_extra_spec_needed(self):
        extra_spec_needed = []
        requested_options = []
        mixed_options = {}
        series_metadata = cfg.CONF['flavor_series_metadata'][self.series]

        # Retreive default extra specs and mixed options for series
        for f_key, f_val in series_metadata.items():
            if f_key.startswith("es_default_"):
                es = db_models.FlavorExtraSpec(key_name_value=f_val)
                extra_spec_needed.append(es)
            if f_key.startswith("es_mixed_"):
                mixed_es_trimmed = f_key.replace('es_mixed_', '')
                mixed_options[mixed_es_trimmed] = f_val

        # Evaluate numa options
        if 'valid_options_numa' in series_metadata:
            valid_numa = [x for x in
                          series_metadata['valid_options_numa'].split(',')]

            option_numa = [n for n in valid_numa if n in
                           self.options.keys() and
                           self.options[n].lower() == 'true']

            if not option_numa:
                es = db_models.FlavorExtraSpec(
                    key_name_value=series_metadata['es_alt_numa_nodes'])
                extra_spec_needed.append(es)
            else:
                es = db_models.FlavorExtraSpec(
                    key_name_value=series_metadata['es_numa_nodes'])
                extra_spec_needed.append(es)
                requested_options.extend(option_numa)

        # Evaluate pci options
        if 'valid_options_pci' in series_metadata:
            valid_pci = [x for x in
                         series_metadata['valid_options_pci'].split(',')]

            option_pci = [n for n in valid_pci if n in
                          self.options.keys() and
                          self.options[n].lower() == 'true']

            if option_pci:
                requested_options.extend(option_pci)

        # Evaluate thread options
        if 'valid_options_thread' in series_metadata:
            valid_thread = [x for x in
                            series_metadata['valid_options_thread'].split(',')]

            option_thread = [n for n in valid_thread if n in
                             self.options.keys() and
                             self.options[n].lower() == 'true' and
                             self.visibility.lower() == 'private']

            if option_thread:
                es = db_models.FlavorExtraSpec(
                    key_name_value=series_metadata['es_thread_policy'])
                extra_spec_needed.append(es)
                requested_options.extend(option_thread)

        # Evalulate mixed options
        assorted_opts = []
        for mixed_key, mixed_value in mixed_options.items():
            assorted_opts = [z for z in mixed_key.split('_')]

            mixed_present = True
            for opt in assorted_opts:
                mixed_present &= True if opt in requested_options else False

            if mixed_present:
                es = db_models.FlavorExtraSpec(key_name_value=mixed_value)
                extra_spec_needed.append(es)

        # convert the key_value to a string to avoid/fix pecan json
        # rendering error in update extra_specs
        i = 0
        while i < len(extra_spec_needed):
            extra_spec_needed[i].key_value = str(
                extra_spec_needed[i].key_value)
            i += 1

        return extra_spec_needed

    def get_as_summary_dict(self):
        return dict(
            id=self.id,
            name=self.name,
            description=self.description
        )

    def handle_region_groups(self):
        regions_to_add = []
        # get copy of it to be able to delete from the origin
        for region in self.regions[:]:
            if region.type == "group":
                group_regions = self.get_regions_for_group(region.name)
                if group_regions is None:
                    raise ValueError("Group {} not found".format(region.name))
                for group_region in group_regions:
                    regions_to_add.append(Region(name=group_region,
                                                 type='single'))
                self.regions.remove(region)

        # remove duplicates if exist
        self.regions.extend(set(regions_to_add))

    def get_regions_for_group(self, group_name):
        set_utils_conf(conf)
        regions = get_regions_of_group(group_name)
        return regions

    def validInt(self, check_value):
        try:
            int(check_value)
        except ValueError:
            return False
        return True


class FlavorWrapper(Model):
    """flavor model

    """
    flavor = wsme.wsattr(Flavor, mandatory=True, name='flavor')

    def __init__(self, flavor=Flavor()):
        """init

        :param flavor: flavor dict
        """

        self.flavor = flavor

    def to_db_model(self):
        return self.flavor.to_db_model()

    def validate_model(self):
        return self.flavor.validate_model()

    def get_extra_spec_needed(self):
        return self.flavor.get_extra_spec_needed()

    @staticmethod
    def from_db_model(sql_flavor):
        flavors = FlavorWrapper()
        flavors.flavor = Flavor.from_db_model(sql_flavor)
        return flavors


'''
' FlavorSummary a DataObject contains all the fields defined in FlavorSummary.
'''


class FlavorSummary(Model):
    name = wsme.wsattr(wsme.types.text)
    id = wsme.wsattr(wsme.types.text)
    description = wsme.wsattr(wsme.types.text)
    visibility = wsme.wsattr(wsme.types.text)

    def __init__(self, name='', id='', description='', visibility=''):
        Model.__init__(self)

        self.name = name
        self.id = id
        self.description = description
        self.visibility = visibility

    @staticmethod
    def from_db_model(sql_flavor):
        flavor = FlavorSummary()
        flavor.id = sql_flavor.id
        flavor.name = sql_flavor.name
        flavor.description = sql_flavor.description
        flavor.visibility = sql_flavor.visibility

        return flavor


class FlavorSummaryResponse(Model):
    flavors = wsme.wsattr([FlavorSummary], mandatory=True)

    def __init__(self):  # pragma: no cover
        Model.__init__(self)
        self.flavors = []


class FlavorListFullResponse(Model):
    flavors = wsme.wsattr([Flavor], mandatory=True)

    def __init__(self):  # pragma: no cover
        Model.__init__(self)
        self.flavors = []
