# -*- coding: utf-8 -*-
#

# Copyright (c) 2018, KubeVirt Team <@kubevirt>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from collections import defaultdict
from distutils.version import Version

from ansible.module_utils.common import dict_transformations
from ansible.module_utils.common._collections_compat import Sequence
from ansible_collections.community.kubernetes.plugins.module_utils.common import list_dict_str
from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule

import copy
import re

MAX_SUPPORTED_API_VERSION = 'v1alpha3'
API_GROUP = 'kubevirt.io'


# Put all args that (can) modify 'spec:' here:
VM_SPEC_DEF_ARG_SPEC = {
    'resource_definition': {
        'type': 'dict',
        'aliases': ['definition', 'inline']
    },
    'memory': {'type': 'str'},
    'memory_limit': {'type': 'str'},
    'cpu_cores': {'type': 'int'},
    'disks': {'type': 'list'},
    'labels': {'type': 'dict'},
    'interfaces': {'type': 'list'},
    'machine_type': {'type': 'str'},
    'cloud_init_nocloud': {'type': 'dict'},
    'bootloader': {'type': 'str'},
    'smbios_uuid': {'type': 'str'},
    'cpu_model': {'type': 'str'},
    'headless': {'type': 'str'},
    'hugepage_size': {'type': 'str'},
    'tablets': {'type': 'list'},
    'cpu_limit': {'type': 'int'},
    'cpu_shares': {'type': 'int'},
    'cpu_features': {'type': 'list'},
    'affinity': {'type': 'dict'},
    'anti_affinity': {'type': 'dict'},
    'node_affinity': {'type': 'dict'},
}
# And other common args go here:
VM_COMMON_ARG_SPEC = {
    'name': {'required': True},
    'namespace': {'required': True},
    'hostname': {'type': 'str'},
    'subdomain': {'type': 'str'},
    'state': {
        'default': 'present',
        'choices': ['present', 'absent'],
    },
    'force': {
        'type': 'bool',
        'default': False,
    },
    'merge_type': {'type': 'list', 'choices': ['json', 'merge', 'strategic-merge']},
    'wait': {'type': 'bool', 'default': True},
    'wait_timeout': {'type': 'int', 'default': 120},
    'wait_sleep': {'type': 'int', 'default': 5},
}
VM_COMMON_ARG_SPEC.update(VM_SPEC_DEF_ARG_SPEC)


def virtdict():
    """
    This function create dictionary, with defaults to dictionary.
    """
    return defaultdict(virtdict)


class KubeAPIVersion(Version):
    component_re = re.compile(r'(\d+ | [a-z]+)', re.VERBOSE)

    def __init__(self, vstring=None):
        if vstring:
            self.parse(vstring)

    def parse(self, vstring):
        self.vstring = vstring
        components = [x for x in self.component_re.split(vstring) if x]
        for i, obj in enumerate(components):
            try:
                components[i] = int(obj)
            except ValueError:
                pass

        errmsg = "version '{0}' does not conform to kubernetes api versioning guidelines".format(vstring)
        c = components

        if len(c) not in (2, 4) or c[0] != 'v' or not isinstance(c[1], int):
            raise ValueError(errmsg)
        if len(c) == 4 and (c[2] not in ('alpha', 'beta') or not isinstance(c[3], int)):
            raise ValueError(errmsg)

        self.version = components

    def __str__(self):
        return self.vstring

    def __repr__(self):
        return "KubeAPIVersion ('{0}')".format(str(self))

    def _cmp(self, other):
        if isinstance(other, str):
            other = KubeAPIVersion(other)

        myver = self.version
        otherver = other.version

        for ver in myver, otherver:
            if len(ver) == 2:
                ver.extend(['zeta', 9999])

        if myver == otherver:
            return 0
        if myver < otherver:
            return -1
        if myver > otherver:
            return 1

    # python2 compatibility
    def __cmp__(self, other):
        return self._cmp(other)


class KubeVirtRawModule(KubernetesRawModule):
    def __init__(self, *args, **kwargs):
        super(KubeVirtRawModule, self).__init__(*args, **kwargs)

    @staticmethod
    def merge_dicts(base_dict, merging_dicts):
        """This function merges a base dictionary with one or more other dictionaries.
        The base dictionary takes precedence when there is a key collision.
        merging_dicts can be a dict or a list or tuple of dicts.  In the latter case, the
        dictionaries at the front of the list have higher precedence over the ones at the end.
        """
        if not merging_dicts:
            merging_dicts = ({},)

        if not isinstance(merging_dicts, Sequence):
            merging_dicts = (merging_dicts,)

        new_dict = {}
        for d in reversed(merging_dicts):
            new_dict = dict_transformations.dict_merge(new_dict, d)

        new_dict = dict_transformations.dict_merge(new_dict, base_dict)

        return new_dict

    def get_resource(self, resource):
        try:
            existing = resource.get(name=self.name, namespace=self.namespace)
        except Exception:
            existing = None

        return existing

    def _define_datavolumes(self, datavolumes, spec):
        """
        Takes datavoulmes parameter of Ansible and create kubevirt API datavolumesTemplateSpec
        structure from it
        """
        if not datavolumes:
            return

        spec['dataVolumeTemplates'] = []
        for dv in datavolumes:
            # Add datavolume to datavolumetemplates spec:
            dvt = virtdict()
            dvt['metadata']['name'] = dv.get('name')
            dvt['spec']['pvc'] = {
                'accessModes': dv.get('pvc').get('accessModes'),
                'resources': {
                    'requests': {
                        'storage': dv.get('pvc').get('storage'),
                    }
                }
            }
            dvt['spec']['source'] = dv.get('source')
            spec['dataVolumeTemplates'].append(dvt)

            # Add datavolume to disks spec:
            if not spec['template']['spec']['domain']['devices']['disks']:
                spec['template']['spec']['domain']['devices']['disks'] = []

            spec['template']['spec']['domain']['devices']['disks'].append(
                {
                    'name': dv.get('name'),
                    'disk': dv.get('disk', {'bus': 'virtio'}),
                }
            )

            # Add datavolume to volumes spec:
            if not spec['template']['spec']['volumes']:
                spec['template']['spec']['volumes'] = []

            spec['template']['spec']['volumes'].append(
                {
                    'dataVolume': {
                        'name': dv.get('name')
                    },
                    'name': dv.get('name'),
                }
            )

    def _define_cloud_init(self, cloud_init_nocloud, template_spec):
        """
        Takes the user's cloud_init_nocloud parameter and fill it in kubevirt
        API strucuture. The name for disk is hardcoded to ansiblecloudinitdisk.
        """
        if cloud_init_nocloud:
            if not template_spec['volumes']:
                template_spec['volumes'] = []
            if not template_spec['domain']['devices']['disks']:
                template_spec['domain']['devices']['disks'] = []

            template_spec['volumes'].append({'name': 'ansiblecloudinitdisk', 'cloudInitNoCloud': cloud_init_nocloud})
            template_spec['domain']['devices']['disks'].append({
                'name': 'ansiblecloudinitdisk',
                'disk': {'bus': 'virtio'},
            })

    def _define_interfaces(self, interfaces, template_spec, defaults):
        """
        Takes interfaces parameter of Ansible and create kubevirt API interfaces
        and networks strucutre out from it.
        """
        if not interfaces and defaults and 'interfaces' in defaults:
            interfaces = copy.deepcopy(defaults['interfaces'])
            for d in interfaces:
                d['network'] = defaults['networks'][0]

        if interfaces:
            # Extract interfaces k8s specification from interfaces list passed to Ansible:
            spec_interfaces = []
            for i in interfaces:
                spec_interfaces.append(
                    self.merge_dicts(dict((k, v) for k, v in i.items() if k != 'network'), defaults['interfaces'])
                )
            if 'interfaces' not in template_spec['domain']['devices']:
                template_spec['domain']['devices']['interfaces'] = []
            template_spec['domain']['devices']['interfaces'].extend(spec_interfaces)

            # Extract networks k8s specification from interfaces list passed to Ansible:
            spec_networks = []
            for i in interfaces:
                net = i['network']
                net['name'] = i['name']
                spec_networks.append(self.merge_dicts(net, defaults['networks']))
            if 'networks' not in template_spec:
                template_spec['networks'] = []
            template_spec['networks'].extend(spec_networks)

    def _define_disks(self, disks, template_spec, defaults):
        """
        Takes disks parameter of Ansible and create kubevirt API disks and
        volumes strucutre out from it.
        """
        if not disks and defaults and 'disks' in defaults:
            disks = copy.deepcopy(defaults['disks'])
            for d in disks:
                d['volume'] = defaults['volumes'][0]

        if disks:
            # Extract k8s specification from disks list passed to Ansible:
            spec_disks = []
            for d in disks:
                spec_disks.append(
                    self.merge_dicts(dict((k, v) for k, v in d.items() if k != 'volume'), defaults['disks'])
                )
            if 'disks' not in template_spec['domain']['devices']:
                template_spec['domain']['devices']['disks'] = []
            template_spec['domain']['devices']['disks'].extend(spec_disks)

            # Extract volumes k8s specification from disks list passed to Ansible:
            spec_volumes = []
            for d in disks:
                volume = d['volume']
                volume['name'] = d['name']
                spec_volumes.append(self.merge_dicts(volume, defaults['volumes']))
            if 'volumes' not in template_spec:
                template_spec['volumes'] = []
            template_spec['volumes'].extend(spec_volumes)

    def find_supported_resource(self, kind):
        results = self.client.resources.search(kind=kind, group=API_GROUP)
        if not results:
            self.fail('Failed to find resource {0} in {1}'.format(kind, API_GROUP))
        sr = sorted(results, key=lambda r: KubeAPIVersion(r.api_version), reverse=True)
        for r in sr:
            if KubeAPIVersion(r.api_version) <= KubeAPIVersion(MAX_SUPPORTED_API_VERSION):
                return r
        self.fail("API versions {0} are too recent. Max supported is {1}/{2}.".format(
            str([r.api_version for r in sr]), API_GROUP, MAX_SUPPORTED_API_VERSION))

    def _construct_vm_definition(self, kind, definition, template, params, defaults=None):
        self.client = self.get_api_client()

        disks = params.get('disks', [])
        memory = params.get('memory')
        memory_limit = params.get('memory_limit')
        cpu_cores = params.get('cpu_cores')
        cpu_model = params.get('cpu_model')
        cpu_features = params.get('cpu_features')
        labels = params.get('labels')
        datavolumes = params.get('datavolumes')
        interfaces = params.get('interfaces')
        bootloader = params.get('bootloader')
        cloud_init_nocloud = params.get('cloud_init_nocloud')
        machine_type = params.get('machine_type')
        headless = params.get('headless')
        smbios_uuid = params.get('smbios_uuid')
        hugepage_size = params.get('hugepage_size')
        tablets = params.get('tablets')
        cpu_shares = params.get('cpu_shares')
        cpu_limit = params.get('cpu_limit')
        node_affinity = params.get('node_affinity')
        vm_affinity = params.get('affinity')
        vm_anti_affinity = params.get('anti_affinity')
        hostname = params.get('hostname')
        subdomain = params.get('subdomain')
        template_spec = template['spec']

        # Merge additional flat parameters:
        if memory:
            template_spec['domain']['resources']['requests']['memory'] = memory

        if cpu_shares:
            template_spec['domain']['resources']['requests']['cpu'] = cpu_shares

        if cpu_limit:
            template_spec['domain']['resources']['limits']['cpu'] = cpu_limit

        if tablets:
            for tablet in tablets:
                tablet['type'] = 'tablet'
            template_spec['domain']['devices']['inputs'] = tablets

        if memory_limit:
            template_spec['domain']['resources']['limits']['memory'] = memory_limit

        if hugepage_size is not None:
            template_spec['domain']['memory']['hugepages']['pageSize'] = hugepage_size

        if cpu_features is not None:
            template_spec['domain']['cpu']['features'] = cpu_features

        if cpu_cores is not None:
            template_spec['domain']['cpu']['cores'] = cpu_cores

        if cpu_model:
            template_spec['domain']['cpu']['model'] = cpu_model

        if labels:
            template['metadata']['labels'] = self.merge_dicts(labels, template['metadata']['labels'])

        if machine_type:
            template_spec['domain']['machine']['type'] = machine_type

        if bootloader:
            template_spec['domain']['firmware']['bootloader'] = {bootloader: {}}

        if smbios_uuid:
            template_spec['domain']['firmware']['uuid'] = smbios_uuid

        if headless is not None:
            template_spec['domain']['devices']['autoattachGraphicsDevice'] = not headless

        if vm_affinity or vm_anti_affinity:
            vms_affinity = vm_affinity or vm_anti_affinity
            affinity_name = 'podAffinity' if vm_affinity else 'podAntiAffinity'
            for affinity in vms_affinity.get('soft', []):
                if not template_spec['affinity'][affinity_name]['preferredDuringSchedulingIgnoredDuringExecution']:
                    template_spec['affinity'][affinity_name]['preferredDuringSchedulingIgnoredDuringExecution'] = []
                template_spec['affinity'][affinity_name]['preferredDuringSchedulingIgnoredDuringExecution'].append({
                    'weight': affinity.get('weight'),
                    'podAffinityTerm': {
                        'labelSelector': {
                            'matchExpressions': affinity.get('term').get('match_expressions'),
                        },
                        'topologyKey': affinity.get('topology_key'),
                    },
                })
            for affinity in vms_affinity.get('hard', []):
                if not template_spec['affinity'][affinity_name]['requiredDuringSchedulingIgnoredDuringExecution']:
                    template_spec['affinity'][affinity_name]['requiredDuringSchedulingIgnoredDuringExecution'] = []
                template_spec['affinity'][affinity_name]['requiredDuringSchedulingIgnoredDuringExecution'].append({
                    'labelSelector': {
                        'matchExpressions': affinity.get('term').get('match_expressions'),
                    },
                    'topologyKey': affinity.get('topology_key'),
                })

        if node_affinity:
            for affinity in node_affinity.get('soft', []):
                if not template_spec['affinity']['nodeAffinity']['preferredDuringSchedulingIgnoredDuringExecution']:
                    template_spec['affinity']['nodeAffinity']['preferredDuringSchedulingIgnoredDuringExecution'] = []
                template_spec['affinity']['nodeAffinity']['preferredDuringSchedulingIgnoredDuringExecution'].append({
                    'weight': affinity.get('weight'),
                    'preference': {
                        'matchExpressions': affinity.get('term').get('match_expressions'),
                    }
                })
            for affinity in node_affinity.get('hard', []):
                if not template_spec['affinity']['nodeAffinity']['requiredDuringSchedulingIgnoredDuringExecution']['nodeSelectorTerms']:
                    template_spec['affinity']['nodeAffinity']['requiredDuringSchedulingIgnoredDuringExecution']['nodeSelectorTerms'] = []
                template_spec['affinity']['nodeAffinity']['requiredDuringSchedulingIgnoredDuringExecution']['nodeSelectorTerms'].append({
                    'matchExpressions': affinity.get('term').get('match_expressions'),
                })

        if hostname:
            template_spec['hostname'] = hostname

        if subdomain:
            template_spec['subdomain'] = subdomain

        # Define disks
        self._define_disks(disks, template_spec, defaults)

        # Define cloud init disk if defined:
        # Note, that this must be called after _define_disks, so the cloud_init
        # is not first in order and it's not used as boot disk:
        self._define_cloud_init(cloud_init_nocloud, template_spec)

        # Define interfaces:
        self._define_interfaces(interfaces, template_spec, defaults)

        # Define datavolumes:
        self._define_datavolumes(datavolumes, definition['spec'])

        return self.merge_dicts(definition, self.resource_definitions[0])

    def construct_vm_definition(self, kind, definition, template, defaults=None):
        definition = self._construct_vm_definition(kind, definition, template, self.params, defaults)
        resource = self.find_supported_resource(kind)
        definition = self.set_defaults(resource, definition)
        return resource, definition

    def construct_vm_template_definition(self, kind, definition, template, params):
        definition = self._construct_vm_definition(kind, definition, template, params)
        resource = self.find_resource(kind, definition['apiVersion'], fail=True)

        # Set defaults:
        definition['kind'] = kind
        definition['metadata']['name'] = params.get('name')
        definition['metadata']['namespace'] = params.get('namespace')

        return resource, definition

    def execute_crud(self, kind, definition):
        """ Module execution """
        resource = self.find_supported_resource(kind)
        definition = self.set_defaults(resource, definition)
        return self.perform_action(resource, definition)