# Copyright 2017 AT&T Intellectual Property.  All other 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 collections.abc import Iterable
import re

import hashlib
from oslo_serialization import jsonutils as json
from oslo_utils import uuidutils
import six
import yaml

_URL_RE = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|'
                     '(?:%[0-9a-fA-F][0-9a-fA-F]))+')


class DocumentDict(dict):
    """Wrapper for a document.

    Implements convenient properties for nested, commonly accessed document
    keys. Property setters are only implemented for mutable data.

    Useful for accessing nested dictionary keys without having to worry about
    exceptions getting thrown.

    .. note::

        As a rule of thumb, setters for any metadata properties should be
        avoided. Only implement or use for well-understood edge cases.

    """

    @property
    def meta(self):
        return (self.schema, self.layer, self.name)

    @property
    def metadata(self):
        return self.get('metadata') or DocumentDict({})

    @property
    def data(self):
        return self.get('data')

    @data.setter
    def data(self, value):
        self['data'] = value

    @property
    def name(self):
        return self.metadata.get('name') or ''

    @property
    def schema(self):
        return self.get('schema') or ''

    @property
    def layer(self):
        return self.layering_definition.get('layer') or ''

    @property
    def is_abstract(self):
        return self.layering_definition.get('abstract') is True

    @property
    def is_control(self):
        return self.metadata.get('schema', '').startswith('metadata/Control')

    @property
    def layering_definition(self):
        metadata = self.metadata or {}
        return metadata.get('layeringDefinition') or DocumentDict({})

    @property
    def layeringDefinition(self):
        metadata = self.metadata or {}
        return metadata.get('layeringDefinition') or DocumentDict({})

    @property
    def layer_order(self):
        if not self.schema.startswith('deckhand/LayeringPolicy'):
            raise TypeError(
                'layer_order only exists for LayeringPolicy documents')
        return self.data.get('layerOrder', [])

    @property
    def parent_selector(self):
        return self.layering_definition.get(
            'parentSelector') or DocumentDict({})

    @property
    def labels(self):
        return self.metadata.get('labels') or DocumentDict({})

    @property
    def substitutions(self):
        return self.metadata.get('substitutions', [])

    @substitutions.setter
    def substitutions(self, value):
        self.metadata.substitutions = value

    @property
    def actions(self):
        return self.layering_definition.get('actions', [])

    @property
    def storage_policy(self):
        return self.metadata.get('storagePolicy') or ''

    @storage_policy.setter
    def storage_policy(self, value):
        self.metadata['storagePolicy'] = value

    @property
    def is_encrypted(self):
        return self.storage_policy == 'encrypted'

    @property
    def has_barbican_ref(self):
        try:
            secret_ref = self.data
            secret_uuid = secret_ref.split('/')[-1]
        except Exception:
            secret_uuid = None
        return (
            isinstance(secret_ref, six.string_types) and
            _URL_RE.match(secret_ref) and
            'secrets' in secret_ref and
            uuidutils.is_uuid_like(secret_uuid)
        )

    @property
    def is_replacement(self):
        return self.metadata.get('replacement') is True

    @property
    def has_replacement(self):
        return isinstance(self.replaced_by, DocumentDict)

    @property
    def replaced_by(self):
        return getattr(self, '_replaced_by', None)

    @replaced_by.setter
    def replaced_by(self, other):
        setattr(self, '_replaced_by', other)

    def __hash__(self):
        return hash(json.dumps(self, sort_keys=True))

    @classmethod
    def from_list(cls, documents):
        """Convert an iterable of documents into instances of this class.

        :param documents: Documents to wrap in this class.
        :type documents: iterable
        """
        if not isinstance(documents, Iterable):
            documents = [documents]

        return [DocumentDict(d) for d in documents]

    @classmethod
    def redact(cls, field):
        return hashlib.sha256(json.dumps(field).encode('utf-8')).hexdigest()


def document_dict_representer(dumper, data):
    return dumper.represent_mapping('tag:yaml.org,2002:map', dict(data))


yaml.add_representer(DocumentDict, document_dict_representer)
# Required for py27 compatibility: yaml.safe_dump/safe_dump_all doesn't
# work unless SafeRepresenter add_representer method is called.
# Upd: somehow required also for py3*.
safe_representer = yaml.representer.SafeRepresenter
safe_representer.add_representer(DocumentDict, document_dict_representer)
