#!/usr/bin/python
# -*- 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


DOCUMENTATION = r'''

module: k8s_service

short_description: Manage Services on Kubernetes

author: KubeVirt Team (@kubevirt)

description:
  - Use Openshift Python SDK to manage Services on Kubernetes

extends_documentation_fragment:
  - community.kubernetes.k8s_auth_options

options:
  resource_definition:
    description:
    - A partial YAML definition of the Service object being created/updated. Here you can define Kubernetes
      Service Resource parameters not covered by this module's parameters.
    - "NOTE: I(resource_definition) has lower priority than module parameters. If you try to define e.g.
      I(metadata.namespace) here, that value will be ignored and I(metadata) used instead."
    aliases:
    - definition
    - inline
    type: dict
  src:
    description:
    - "Provide a path to a file containing a valid YAML definition of an object dated. Mutually
      exclusive with I(resource_definition). NOTE: I(kind), I(api_version), I(resource_name), and I(namespace)
      will be overwritten by corresponding values found in the configuration read in from the I(src) file."
    - Reads from the local file system. To read from the Ansible controller's file system, use the file lookup
      plugin or template lookup plugin, combined with the from_yaml filter, and pass the result to
      I(resource_definition). See Examples below.
    type: path
  state:
    description:
    - Determines if an object should be created, patched, or deleted. When set to C(present), an object will be
      created, if it does not already exist. If set to C(absent), an existing object will be deleted. If set to
      C(present), an existing object will be patched, if its attributes differ from those specified using
      module options and I(resource_definition).
    default: present
    choices:
    - present
    - absent
    type: str
  force:
    description:
    - If set to C(True), and I(state) is C(present), an existing object will be replaced.
    default: false
    type: bool
  merge_type:
    description:
    - Whether to override the default patch merge approach with a specific type. By default, the strategic
      merge will typically be used.
    - For example, Custom Resource Definitions typically aren't updatable by the usual strategic merge. You may
      want to use C(merge) if you see "strategic merge patch format is not supported"
    - See U(https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment)
    - Requires openshift >= 0.6.2
    - If more than one merge_type is given, the merge_types will be tried in order
    - If openshift >= 0.6.2, this defaults to C(['strategic-merge', 'merge']), which is ideal for using the same parameters
      on resource kinds that combine Custom Resources and built-in resources. For openshift < 0.6.2, the default
      is simply C(strategic-merge).
    choices:
    - json
    - merge
    - strategic-merge
    type: list
    elements: str
  name:
    description:
      - Use to specify a Service object name.
    required: true
    type: str
  namespace:
    description:
      - Use to specify a Service object namespace.
    required: true
    type: str
  type:
    description:
      - Specifies the type of Service to create.
      - See U(https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types)
    choices:
      - NodePort
      - ClusterIP
      - LoadBalancer
      - ExternalName
    type: str
  ports:
    description:
      - A list of ports to expose.
      - U(https://kubernetes.io/docs/concepts/services-networking/service/#multi-port-services)
    type: list
    elements: dict
  selector:
    description:
      - Label selectors identify objects this Service should apply to.
      - U(https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
    type: dict
  apply:
    description:
    - C(apply) compares the desired resource definition with the previously supplied resource definition,
      ignoring properties that are automatically generated
    - C(apply) works better with Services than 'force=yes'
    - mutually exclusive with C(merge_type)
    type: bool

requirements:
  - python >= 2.7
  - openshift >= 0.6.2
'''

EXAMPLES = r'''
- name: Expose https port with ClusterIP
  community.kubernetes.k8s_service:
    state: present
    name: test-https
    namespace: default
    ports:
    - port: 443
      protocol: TCP
    selector:
      key: special

- name: Expose https port with ClusterIP using spec
  community.kubernetes.k8s_service:
    state: present
    name: test-https
    namespace: default
    inline:
      spec:
        ports:
        - port: 443
          protocol: TCP
        selector:
          key: special
'''

RETURN = r'''
result:
  description:
  - The created, patched, or otherwise present Service object. Will be empty in the case of a deletion.
  returned: success
  type: complex
  contains:
     api_version:
       description: The versioned schema of this representation of an object.
       returned: success
       type: str
     kind:
       description: Always 'Service'.
       returned: success
       type: str
     metadata:
       description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
       returned: success
       type: complex
     spec:
       description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
       returned: success
       type: complex
     status:
       description: Current status details for the object.
       returned: success
       type: complex
'''

import copy
import traceback

from collections import defaultdict

from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC
from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule


SERVICE_ARG_SPEC = {
    'apply': {
        'type': 'bool',
        'default': False,
    },
    'state': {
        'default': 'present',
        'choices': ['present', 'absent'],
    },
    'force': {
        'type': 'bool',
        'default': False,
    },
    'resource_definition': {
        'type': 'dict',
        'aliases': ['definition', 'inline']
    },
    'name': {'required': True},
    'namespace': {'required': True},
    'merge_type': {'type': 'list', 'elements': 'str', 'choices': ['json', 'merge', 'strategic-merge']},
    'selector': {'type': 'dict'},
    'src': {
        'type': 'path',
    },
    'type': {
        'type': 'str',
        'choices': [
            'NodePort', 'ClusterIP', 'LoadBalancer', 'ExternalName'
        ],
    },
    'ports': {'type': 'list', 'elements': 'dict'},
}


class KubernetesService(KubernetesRawModule):
    def __init__(self, *args, **kwargs):
        super(KubernetesService, self).__init__(*args, k8s_kind='Service', **kwargs)

    @staticmethod
    def merge_dicts(x, y):
        for k in set(x.keys()).union(y.keys()):
            if k in x and k in y:
                if isinstance(x[k], dict) and isinstance(y[k], dict):
                    yield (k, dict(KubernetesService.merge_dicts(x[k], y[k])))
                else:
                    yield (k, y[k])
            elif k in x:
                yield (k, x[k])
            else:
                yield (k, y[k])

    @property
    def argspec(self):
        """ argspec property builder """
        argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
        argument_spec.update(SERVICE_ARG_SPEC)
        return argument_spec

    def execute_module(self):
        """ Module execution """
        self.client = self.get_api_client()

        api_version = 'v1'
        selector = self.params.get('selector')
        service_type = self.params.get('type')
        ports = self.params.get('ports')

        definition = defaultdict(defaultdict)

        definition['kind'] = 'Service'
        definition['apiVersion'] = api_version

        def_spec = definition['spec']
        def_spec['type'] = service_type
        def_spec['ports'] = ports
        def_spec['selector'] = selector

        def_meta = definition['metadata']
        def_meta['name'] = self.params.get('name')
        def_meta['namespace'] = self.params.get('namespace')

        # 'resource_definition:' has lower priority than module parameters
        definition = dict(self.merge_dicts(self.resource_definitions[0], definition))

        resource = self.find_resource('Service', api_version, fail=True)
        definition = self.set_defaults(resource, definition)
        result = self.perform_action(resource, definition)

        self.exit_json(**result)


def main():
    module = KubernetesService()
    try:
        module.execute_module()
    except Exception as e:
        module.fail_json(msg=str(e), exception=traceback.format_exc())


if __name__ == '__main__':
    main()
