# Copyright (c) 2014-2016 Hewlett Packard Enterprise Development LP
#
# Redistribution and use of this software in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
#   Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
#
#   Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import yaml

from euca2ools.commands.euimage.pack.profiles import build_image_profile
from euca2ools.util import check_dict_whitelist


class ImagePackMetadata(object):
    def __init__(self):
        self.image_sha256sum = None
        self.image_size = None
        self.image_md_sha256sum = None
        self.version = 1  # Bump this with each incompatible change

    @classmethod
    def from_fileobj(cls, fileobj):
        new_md = cls()
        metadata = yaml.safe_load(fileobj)
        check_dict_whitelist(metadata, 'pack', ['image', 'image_metadata',
                                                'version'])
        if metadata.get('version'):
            # This is the version of the pack metadata, not the image.
            # If we make backwards-incompatible changes this allows us
            # to tell what to expect so we can continue to handle packs
            # that precede those changes.
            #
            # Because there is only one metadata version right now this
            # method is rather dumb -- it accepts only version 1.
            if int(metadata['version']) != 1:
                raise ValueError('pack has metadata version {0}; expected 1'
                                 .format(metadata['version']))
        image_info = metadata.get('image') or {}
        if not image_info.get('sha256sum'):
            raise ValueError('pack: image.sha256sum is missing or empty')
        new_md.image_sha256sum = image_info['sha256sum']
        if not image_info.get('size'):
            raise ValueError('pack: image.size is missing or zero')
        new_md.image_size = int(image_info['size'])
        image_md_info = metadata.get('image_metadata') or {}
        if not image_md_info.get('sha256sum'):
            raise ValueError(
                'pack: image_metadata.sha256sum is missing or empty')
        new_md.image_md_sha256sum = image_md_info['sha256sum']
        return new_md

    @classmethod
    def from_file(cls, filename):
        with open(filename) as fileobj:
            return cls.from_fileobj(fileobj)

    def dump_to_fileobj(self, fileobj):
        yaml.safe_dump(self.__serialize_as_dict(), fileobj,
                       default_flow_style=False)

    def dump_to_file(self, filename):
        with open(filename, 'w') as fileobj:
            self.dump_to_fileobj(fileobj)

    def __serialize_as_dict(self):
        return {'image': {'sha256sum': self.image_sha256sum,
                          'size': self.image_size},
                'image_metadata': {'sha256sum': self.image_md_sha256sum},
                'version': self.version}


class ImageMetadata(object):
    def __init__(self):
        self.name = None
        self.version = None
        self.release = None
        self.epoch = 0
        self.arch = None
        self.description = None
        self.profiles = {}

    @classmethod
    def from_fileobj(cls, fileobj):
        new_md = cls()
        metadata = yaml.safe_load(fileobj)
        check_dict_whitelist(metadata, 'image',
                             ['name', 'version', 'release', 'arch',
                              'description', 'profiles'])
        if not metadata.get('name'):
            raise ValueError('name is missing or empty')
        new_md.name = metadata['name']
        if not metadata.get('version'):
            raise ValueError('image "{0}": version is missing or empty'
                             .format(new_md.name))
        new_md.version = metadata['version']
        if not metadata.get('release'):
            raise ValueError('image "{0}": release is missing or empty'
                             .format(new_md.name))
        new_md.release = metadata['release']
        if metadata.get('epoch'):
            try:
                new_md.epoch = int(metadata['epoch'])
            except ValueError:
                raise ValueError('image "{0}": epoch must be an integer'
                                 .format(new_md.name))
            if new_md.epoch < 0:
                raise ValueError('image "{0}": epoch must not be negative'
                                 .format(new_md.name))
        if not metadata.get('arch'):
            raise ValueError('image "{0}": arch is missing or empty'
                             .format(new_md.name))
        new_md.arch = metadata['arch']
        if not metadata.get('description'):
            raise ValueError('image "{0}": description is missing or empty'
                             .format(new_md.name))
        new_md.description = metadata['description'].rstrip()
        profiles = metadata.get('profiles')
        if not profiles:
            raise ValueError('image "{0}" must have at least one profile '
                             '(use "default" for a single-profile image)'
                             .format(new_md.name))
        if not isinstance(profiles, dict):
            raise ValueError('image "{0}": profiles must be an associative '
                             'array'.format(new_md.name))
        for profile_name, profile_info in profiles.items():
            new_md.profiles[profile_name] = build_image_profile(profile_info,
                                                                new_md.arch)
        return new_md

    @classmethod
    def from_file(cls, filename):
        with open(filename) as fileobj:
            return cls.from_fileobj(fileobj)

    def install_profile(self, profile_name, services, image_fileobj,
                        image_size, args):
        # Since different profiles can require different args to install
        # correctly, we can't easily pick out the correct ones ahead
        # of time.  For simplicity's sake, we just pass everything and
        # let the profile grab what it needs.  Validation is the job of
        # the profile, which will probably simply delegate that work to
        # the commands it runs.
        euimage_tags = {'euimage:name': self.name,
                        'euimage:version': self.version,
                        'euimage:release': self.release,
                        'euimage:profile': profile_name}
        if self.epoch:
            euimage_tags['euimage:epoch'] = self.epoch
        if profile_name not in self.profiles:
            raise ValueError('no such profile: "{0}"'.format(profile_name))
        return self.profiles[profile_name].install(
            self, services, image_fileobj, image_size, args, tags=euimage_tags)

    def get_nvra(self):
        return '{0}-{1}-{2}.{3}'.format(self.name, self.version, self.release,
                                        self.arch)
