# Copyright 2014 Huawei Technologies Co. Ltd
#
# 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.

"""Metadata related database operations."""
import copy
import logging
import string

from compass.db.api import database
from compass.db.api import utils
from compass.db import callback as metadata_callback
from compass.db import exception
from compass.db import models
from compass.db import validator as metadata_validator


from compass.utils import setting_wrapper as setting
from compass.utils import util


def _add_field_internal(session, model, configs):
    fields = []
    for config in configs:
        if not isinstance(config, dict):
            raise exception.InvalidParameter(
                'config %s is not dict' % config
            )
        fields.append(utils.add_db_object(
            session, model, False,
            config['NAME'],
            field_type=config.get('FIELD_TYPE', basestring),
            display_type=config.get('DISPLAY_TYPE', 'text'),
            validator=config.get('VALIDATOR', None),
            js_validator=config.get('JS_VALIDATOR', None),
            description=config.get('DESCRIPTION', None)
        ))
    return fields


def add_os_field_internal(session):
    env_locals = {}
    env_locals.update(metadata_validator.VALIDATOR_LOCALS)
    env_locals.update(metadata_callback.CALLBACK_LOCALS)
    configs = util.load_configs(
        setting.OS_FIELD_DIR,
        env_locals=env_locals
    )
    return _add_field_internal(
        session, models.OSConfigField, configs
    )


def add_package_field_internal(session):
    env_locals = {}
    env_locals.update(metadata_validator.VALIDATOR_LOCALS)
    env_locals.update(metadata_callback.CALLBACK_LOCALS)
    configs = util.load_configs(
        setting.PACKAGE_FIELD_DIR,
        env_locals=env_locals
    )
    return _add_field_internal(
        session, models.PackageConfigField, configs
    )


def _add_metadata(
    session, field_model, metadata_model, id, path, name, config,
    exception_when_existing=True, parent=None, **kwargs
):
    if not isinstance(config, dict):
        raise exception.InvalidParameter(
            '%s config %s is not dict' % (path, config)
        )
    metadata_self = config.get('_self', {})
    if 'field' in metadata_self:
        field = utils.get_db_object(
            session, field_model, field=metadata_self['field']
        )
    else:
        field = None
    mapping_to_template = metadata_self.get('mapping_to', None)
    if mapping_to_template:
        mapping_to = string.Template(
            mapping_to_template
        ).safe_substitute(
            **kwargs
        )
    else:
        mapping_to = None
    metadata = utils.add_db_object(
        session, metadata_model, exception_when_existing,
        id, path, name=name, parent=parent, field=field,
        display_name=metadata_self.get('display_name', name),
        description=metadata_self.get('description', None),
        is_required=metadata_self.get('is_required', False),
        required_in_whole_config=metadata_self.get(
            'required_in_whole_config', False),
        mapping_to=mapping_to,
        validator=metadata_self.get('validator', None),
        js_validator=metadata_self.get('js_validator', None),
        default_value=metadata_self.get('default_value', None),
        default_callback=metadata_self.get('default_callback', None),
        default_callback_params=metadata_self.get(
            'default_callback_params', {}),
        options=metadata_self.get('options', None),
        options_callback=metadata_self.get('options_callback', None),
        options_callback_params=metadata_self.get(
            'options_callback_params', {}),
        autofill_callback=metadata_self.get(
            'autofill_callback', None),
        autofill_callback_params=metadata_self.get(
            'autofill_callback_params', {}),
        required_in_options=metadata_self.get(
            'required_in_options', False),
        **kwargs
    )
    key_extensions = metadata_self.get('key_extensions', {})
    general_keys = []
    for key, value in config.items():
        if key.startswith('_'):
            continue
        if key in key_extensions:
            if not key.startswith('$'):
                raise exception.InvalidParameter(
                    '%s subkey %s should start with $' % (
                        path, key
                    )
                )
            extended_keys = key_extensions[key]
            for extended_key in extended_keys:
                if extended_key.startswith('$'):
                    raise exception.InvalidParameter(
                        '%s extended key %s should not start with $' % (
                            path, extended_key
                        )
                    )
                sub_kwargs = dict(kwargs)
                sub_kwargs[key[1:]] = extended_key
                _add_metadata(
                    session, field_model, metadata_model,
                    id, '%s/%s' % (path, extended_key), extended_key, value,
                    exception_when_existing=exception_when_existing,
                    parent=metadata, **sub_kwargs
                )
        else:
            if key.startswith('$'):
                general_keys.append(key)
            _add_metadata(
                session, field_model, metadata_model,
                id, '%s/%s' % (path, key), key, value,
                exception_when_existing=exception_when_existing,
                parent=metadata, **kwargs
            )
        if len(general_keys) > 1:
            raise exception.InvalidParameter(
                'foud multi general keys in %s: %s' % (
                    path, general_keys
                )
            )
    return metadata


def add_os_metadata_internal(session, exception_when_existing=True):
    os_metadatas = []
    env_locals = {}
    env_locals.update(metadata_validator.VALIDATOR_LOCALS)
    env_locals.update(metadata_callback.CALLBACK_LOCALS)
    configs = util.load_configs(
        setting.OS_METADATA_DIR,
        env_locals=env_locals
    )
    for config in configs:
        os = utils.get_db_object(
            session, models.OperatingSystem, name=config['OS']
        )
        for key, value in config['METADATA'].items():
            os_metadatas.append(_add_metadata(
                session, models.OSConfigField,
                models.OSConfigMetadata,
                os.id, key, key, value,
                exception_when_existing=exception_when_existing,
                parent=None
            ))
    return os_metadatas


def add_package_metadata_internal(session, exception_when_existing=True):
    package_metadatas = []
    env_locals = {}
    env_locals.update(metadata_validator.VALIDATOR_LOCALS)
    env_locals.update(metadata_callback.CALLBACK_LOCALS)
    configs = util.load_configs(
        setting.PACKAGE_METADATA_DIR,
        env_locals=env_locals
    )
    for config in configs:
        adapter = utils.get_db_object(
            session, models.Adapter, name=config['ADAPTER']
        )
        for key, value in config['METADATA'].items():
            package_metadatas.append(_add_metadata(
                session, models.PackageConfigField,
                models.PackageConfigMetadata,
                adapter.id, key, key, value,
                exception_when_existing=exception_when_existing,
                parent=None
            ))
    return package_metadatas


def _filter_metadata(metadata, **kwargs):
    if not isinstance(metadata, dict):
        return metadata
    filtered_metadata = {}
    for key, value in metadata.items():
        if key == '_self':
            default_value = value.get('default_value', None)
            if default_value is None:
                default_callback_params = value.get(
                    'default_callback_params', {}
                )
                callback_params = dict(kwargs)
                if default_callback_params:
                    callback_params.update(default_callback_params)
                default_callback = value.get('default_callback', None)
                if default_callback:
                    default_value = default_callback(key, **callback_params)
            options = value.get('options', None)
            if options is None:
                options_callback_params = value.get(
                    'options_callback_params', {}
                )
                callback_params = dict(kwargs)
                if options_callback_params:
                    callback_params.update(options_callback_params)

                options_callback = value.get('options_callback', None)
                if options_callback:
                    options = options_callback(key, **callback_params)
            filtered_metadata[key] = value
            if default_value is not None:
                filtered_metadata[key]['default_value'] = default_value
            if options is not None:
                filtered_metadata[key]['options'] = options
        else:
            filtered_metadata[key] = _filter_metadata(value, **kwargs)
    return filtered_metadata


def get_package_metadatas_internal(session):
    metadata_mapping = {}
    adapters = utils.list_db_objects(
        session, models.Adapter
    )
    for adapter in adapters:
        if adapter.deployable:
            metadata_dict = adapter.metadata_dict()
            metadata_mapping[adapter.id] = _filter_metadata(
                metadata_dict, session=session
            )
        else:
            logging.info(
                'ignore metadata since its adapter %s is not deployable',
                adapter.id
            )
    return metadata_mapping


def get_os_metadatas_internal(session):
    metadata_mapping = {}
    oses = utils.list_db_objects(
        session, models.OperatingSystem
    )
    for os in oses:
        if os.deployable:
            metadata_dict = os.metadata_dict()
            metadata_mapping[os.id] = _filter_metadata(
                metadata_dict, session=session
            )
        else:
            logging.info(
                'ignore metadata since its os %s is not deployable',
                os.id
            )
    return metadata_mapping


def _validate_self(
    config_path, config_key, config,
    metadata, whole_check,
    **kwargs
):
    logging.debug('validate config self %s', config_path)
    if '_self' not in metadata:
        if isinstance(config, dict):
            _validate_config(
                config_path, config, metadata, whole_check, **kwargs
            )
        return
    field_type = metadata['_self'].get('field_type', basestring)
    if not isinstance(config, field_type):
        raise exception.InvalidParameter(
            '%s config type is not %s' % (config_path, field_type)
        )
    is_required = metadata['_self'].get(
        'is_required', False
    )
    required_in_whole_config = metadata['_self'].get(
        'required_in_whole_config', False
    )
    if isinstance(config, basestring):
        if config == '' and not is_required and not required_in_whole_config:
            # ignore empty config when it is optional
            return
    required_in_options = metadata['_self'].get(
        'required_in_options', False
    )
    options = metadata['_self'].get('options', None)
    if required_in_options:
        if field_type in [int, basestring, float, bool]:
            if options and config not in options:
                raise exception.InvalidParameter(
                    '%s config is not in %s' % (config_path, options)
                )
        elif field_type in [list, tuple]:
            if options and not set(config).issubset(set(options)):
                raise exception.InvalidParameter(
                    '%s config is not in %s' % (config_path, options)
                )
        elif field_type == dict:
            if options and not set(config.keys()).issubset(set(options)):
                raise exception.InvalidParameter(
                    '%s config is not in %s' % (config_path, options)
                )
    validator = metadata['_self'].get('validator', None)
    logging.debug('validate by validator %s', validator)
    if validator:
        if not validator(config_key, config, **kwargs):
            raise exception.InvalidParameter(
                '%s config is invalid' % config_path
            )
    if isinstance(config, dict):
        _validate_config(
            config_path, config, metadata, whole_check, **kwargs
        )


def _validate_config(
    config_path, config, metadata, whole_check,
    **kwargs
):
    logging.debug('validate config %s', config_path)
    generals = {}
    specified = {}
    for key, value in metadata.items():
        if key.startswith('$'):
            generals[key] = value
        elif key.startswith('_'):
            pass
        else:
            specified[key] = value
    config_keys = set(config.keys())
    specified_keys = set(specified.keys())
    intersect_keys = config_keys & specified_keys
    not_found_keys = config_keys - specified_keys
    redundant_keys = specified_keys - config_keys
    for key in redundant_keys:
        if '_self' not in specified[key]:
            continue
        if specified[key]['_self'].get('is_required', False):
            raise exception.InvalidParameter(
                '%s/%s does not find but it is required' % (
                    config_path, key
                )
            )
        if (
            whole_check and
            specified[key]['_self'].get(
                'required_in_whole_config', False
            )
        ):
            raise exception.InvalidParameter(
                '%s/%s does not find but it is required in whole config' % (
                    config_path, key
                )
            )
    for key in intersect_keys:
        _validate_self(
            '%s/%s' % (config_path, key),
            key, config[key], specified[key], whole_check,
            **kwargs
        )
    for key in not_found_keys:
        if not generals:
            raise exception.InvalidParameter(
                'key %s missing in metadata %s' % (
                    key, config_path
                )
            )
        for general_key, general_value in generals.items():
            _validate_self(
                '%s/%s' % (config_path, key),
                key, config[key], general_value, whole_check,
                **kwargs
            )


def _autofill_self_config(
    config_path, config_key, config,
    metadata,
    **kwargs
):
    if '_self' not in metadata:
        if isinstance(config, dict):
            _autofill_config(
                config_path, config, metadata, **kwargs
            )
        return config
    logging.debug(
        'autofill %s by metadata %s', config_path, metadata['_self']
    )
    autofill_callback = metadata['_self'].get(
        'autofill_callback', None
    )
    autofill_callback_params = metadata['_self'].get(
        'autofill_callback_params', {}
    )
    callback_params = dict(kwargs)
    if autofill_callback_params:
        callback_params.update(autofill_callback_params)
    default_value = metadata['_self'].get(
        'default_value', None
    )
    if default_value is not None:
        callback_params['default_value'] = default_value
    options = metadata['_self'].get(
        'options', None
    )
    if options is not None:
        callback_params['options'] = options
    if autofill_callback:
        config = autofill_callback(
            config_key, config, **callback_params
        )
    if config is None:
        new_config = {}
    else:
        new_config = config
    if isinstance(new_config, dict):
        _autofill_config(
            config_path, new_config, metadata, **kwargs
        )
        if new_config:
            config = new_config
    return config


def _autofill_config(
    config_path, config, metadata, **kwargs
):
    generals = {}
    specified = {}
    for key, value in metadata.items():
        if key.startswith('$'):
            generals[key] = value
        elif key.startswith('_'):
            pass
        else:
            specified[key] = value
    config_keys = set(config.keys())
    specified_keys = set(specified.keys())
    intersect_keys = config_keys & specified_keys
    not_found_keys = config_keys - specified_keys
    redundant_keys = specified_keys - config_keys
    for key in redundant_keys:
        self_config = _autofill_self_config(
            '%s/%s' % (config_path, key),
            key, None, specified[key], **kwargs
        )
        if self_config is not None:
            config[key] = self_config
    for key in intersect_keys:
        config[key] = _autofill_self_config(
            '%s/%s' % (config_path, key),
            key, config[key], specified[key],
            **kwargs
        )
    for key in not_found_keys:
        for general_key, general_value in generals.items():
            config[key] = _autofill_self_config(
                '%s/%s' % (config_path, key),
                key, config[key], general_value,
                **kwargs
            )
    return config


def validate_config_internal(
    config, metadata, whole_check, **kwargs
):
    _validate_config('', config, metadata, whole_check, **kwargs)


def autofill_config_internal(
    config, metadata, **kwargs
):
    return _autofill_config('', config, metadata, **kwargs)