#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Alexander Bakanovskii <skottttt228@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = r"""
module: one_vnet
short_description: Manages OpenNebula virtual networks
version_added: 9.4.0
author: "Alexander Bakanovskii (@abakanovskii)"
requirements:
  - pyone
description:
  - Manages virtual networks in OpenNebula.
attributes:
  check_mode:
    support: partial
    details:
      - Note that check mode always returns C(changed=true) for existing networks, even if the network would not actually
        change.
  diff_mode:
    support: none
options:
  id:
    description:
      - A O(id) of the network you would like to manage.
      - If not set then a new network will be created with the given O(name).
    type: int
  name:
    description:
      - A O(name) of the network you would like to manage. If a network with the given name does not exist it will be created,
        otherwise it will be managed by this module.
    type: str
  template:
    description:
      - A string containing the network template contents.
    type: str
  state:
    description:
      - V(present) - state that is used to manage the network.
      - V(absent) - delete the network.
    choices: ["present", "absent"]
    default: present
    type: str

extends_documentation_fragment:
  - community.general.opennebula
  - community.general.attributes
"""

EXAMPLES = r"""
- name: Make sure the network is present by ID
  community.general.one_vnet:
    id: 0
    state: present
  register: result

- name: Make sure the network is present by name
  community.general.one_vnet:
    name: opennebula-bridge
    state: present
  register: result

- name: Create a new or update an existing network
  community.general.one_vnet:
    name: bridge-network
    template: |
      VN_MAD  = "bridge"
      BRIDGE  = "br0"
      BRIDGE_TYPE  = "linux"
      AR=[
        TYPE  = "IP4",
        IP    = 192.0.2.50,
        SIZE  = "20"
      ]
      DNS     = 192.0.2.1
      GATEWAY = 192.0.2.1

- name: Delete the network by ID
  community.general.one_vnet:
    id: 0
    state: absent
"""

RETURN = r"""
id:
  description: The network ID.
  type: int
  returned: when O(state=present)
  sample: 153
name:
  description: The network name.
  type: str
  returned: when O(state=present)
  sample: app1
template:
  description: The parsed network template.
  type: dict
  returned: when O(state=present)
  sample:
    BRIDGE: onebr.1000
    BRIDGE_TYPE: linux
    DESCRIPTION: sampletext
    PHYDEV: eth0
    SECURITY_GROUPS: 0
    VLAN_ID: 1000
    VN_MAD: 802.1Q
user_id:
  description: The network's user name.
  type: int
  returned: when O(state=present)
  sample: 1
user_name:
  description: The network's user ID.
  type: str
  returned: when O(state=present)
  sample: oneadmin
group_id:
  description: The network's group ID.
  type: int
  returned: when O(state=present)
  sample: 1
group_name:
  description: The network's group name.
  type: str
  returned: when O(state=present)
  sample: one-users
owner_id:
  description: The network's owner ID.
  type: int
  returned: when O(state=present)
  sample: 143
owner_name:
  description: The network's owner name.
  type: str
  returned: when O(state=present)
  sample: ansible-test
permissions:
  description: The network's permissions.
  type: dict
  returned: when O(state=present)
  contains:
    owner_u:
      description: The network's owner USAGE permissions.
      type: str
      sample: 1
    owner_m:
      description: The network's owner MANAGE permissions.
      type: str
      sample: 0
    owner_a:
      description: The network's owner ADMIN permissions.
      type: str
      sample: 0
    group_u:
      description: The network's group USAGE permissions.
      type: str
      sample: 0
    group_m:
      description: The network's group MANAGE permissions.
      type: str
      sample: 0
    group_a:
      description: The network's group ADMIN permissions.
      type: str
      sample: 0
    other_u:
      description: The network's other users USAGE permissions.
      type: str
      sample: 0
    other_m:
      description: The network's other users MANAGE permissions.
      type: str
      sample: 0
    other_a:
      description: The network's other users ADMIN permissions.
      type: str
      sample: 0
  sample:
    owner_u: 1
    owner_m: 0
    owner_a: 0
    group_u: 0
    group_m: 0
    group_a: 0
    other_u: 0
    other_m: 0
    other_a: 0
clusters:
  description: The network's clusters.
  type: list
  returned: when O(state=present)
  sample: [0, 100]
bridge:
  description: The network's bridge interface.
  type: str
  returned: when O(state=present)
  sample: br0
bridge_type:
  description: The network's bridge type.
  type: str
  returned: when O(state=present)
  sample: linux
parent_network_id:
  description: The network's parent network ID.
  type: int
  returned: when O(state=present)
  sample: 1
vn_mad:
  description: The network's VN_MAD.
  type: str
  returned: when O(state=present)
  sample: bridge
phydev:
  description: The network's physical device (NIC).
  type: str
  returned: when O(state=present)
  sample: eth0
vlan_id:
  description: The network's VLAN tag.
  type: int
  returned: when O(state=present)
  sample: 1000
outer_vlan_id:
  description: The network's outer VLAN tag.
  type: int
  returned: when O(state=present)
  sample: 1000
vrouters:
  description: The network's list of virtual routers IDs.
  type: list
  returned: when O(state=present)
  sample: [0, 1]
ar_pool:
  description: The network's list of ar_pool.
  type: list
  returned: when O(state=present)
  sample:
    - ar_id: 0
      ip: 192.0.2.1
      mac: 6c:1e:46:01:cd:d1
      size: 20
      type: IP4
    - ar_id: 1
      allocated: 0
      ip: 198.51.100.1
      mac: 5d:9b:c0:9e:f6:e5
      size: 20
      type: IP4
"""


from ansible_collections.community.general.plugins.module_utils.opennebula import OpenNebulaModule


class NetworksModule(OpenNebulaModule):

    def __init__(self):
        argument_spec = dict(
            id=dict(type='int', required=False),
            name=dict(type='str', required=False),
            state=dict(type='str', choices=['present', 'absent'], default='present'),
            template=dict(type='str', required=False),
        )

        mutually_exclusive = [
            ['id', 'name']
        ]

        required_one_of = [('id', 'name')]

        required_if = [
            ['state', 'present', ['template']]
        ]

        OpenNebulaModule.__init__(self,
                                  argument_spec,
                                  supports_check_mode=True,
                                  mutually_exclusive=mutually_exclusive,
                                  required_one_of=required_one_of,
                                  required_if=required_if)

    def run(self, one, module, result):
        params = module.params
        id = params.get('id')
        name = params.get('name')
        desired_state = params.get('state')
        template_data = params.get('template')

        self.result = {}

        template = self.get_template_instance(id, name)
        needs_creation = False
        if not template and desired_state != 'absent':
            if id:
                module.fail_json(msg="There is no template with id=" + str(id))
            else:
                needs_creation = True

        if desired_state == 'absent':
            self.result = self.delete_template(template)
        else:
            if needs_creation:
                self.result = self.create_template(name, template_data)
            else:
                self.result = self.update_template(template, template_data)

        self.exit()

    def get_template(self, predicate):
        # -2 means "Resources belonging to all users"
        # the other two parameters are used for pagination, -1 for both essentially means "return all"
        pool = self.one.vnpool.info(-2, -1, -1)

        for template in pool.VNET:
            if predicate(template):
                return template

        return None

    def get_template_by_id(self, template_id):
        return self.get_template(lambda template: (template.ID == template_id))

    def get_template_by_name(self, name):
        return self.get_template(lambda template: (template.NAME == name))

    def get_template_instance(self, requested_id, requested_name):
        if requested_id:
            return self.get_template_by_id(requested_id)
        else:
            return self.get_template_by_name(requested_name)

    def get_networks_ar_pool(self, template):
        ar_pool = []
        template_pool = template.AR_POOL.AR
        for ar in range(len(template_pool)):
            template_param = template_pool[ar]
            ar_pool.append({
                # These params will always be present
                'ar_id': template_param.AR_ID,
                'mac': template_param.MAC,
                'size': template_param.SIZE,
                'type': template_param.TYPE,
                # These are optional so firstly check for presence
                # and if not present set value to Null
                'allocated': getattr(template_param, 'ALLOCATED', 'Null'),
                'ip': getattr(template_param, 'IP', 'Null'),
                'global_prefix': getattr(template_param, 'GLOBAL_PREFIX', 'Null'),
                'parent_network_ar_id': getattr(template_param, 'PARENT_NETWORK_AR_ID', 'Null'),
                'ula_prefix': getattr(template_param, 'ULA_PREFIX', 'Null'),
                'vn_mad': getattr(template_param, 'VN_MAD', 'Null'),
            })
        return ar_pool

    def get_template_info(self, template):
        info = {
            'id': template.ID,
            'name': template.NAME,
            'template': template.TEMPLATE,
            'user_name': template.UNAME,
            'user_id': template.UID,
            'group_name': template.GNAME,
            'group_id': template.GID,
            'permissions': {
                'owner_u': template.PERMISSIONS.OWNER_U,
                'owner_m': template.PERMISSIONS.OWNER_M,
                'owner_a': template.PERMISSIONS.OWNER_A,
                'group_u': template.PERMISSIONS.GROUP_U,
                'group_m': template.PERMISSIONS.GROUP_M,
                'group_a': template.PERMISSIONS.GROUP_A,
                'other_u': template.PERMISSIONS.OTHER_U,
                'other_m': template.PERMISSIONS.OTHER_M,
                'other_a': template.PERMISSIONS.OTHER_A
            },
            'clusters': template.CLUSTERS.ID,
            'bridge': template.BRIDGE,
            'bride_type': template.BRIDGE_TYPE,
            'parent_network_id': template.PARENT_NETWORK_ID,
            'vn_mad': template.VN_MAD,
            'phydev': template.PHYDEV,
            'vlan_id': template.VLAN_ID,
            'outer_vlan_id': template.OUTER_VLAN_ID,
            'used_leases': template.USED_LEASES,
            'vrouters': template.VROUTERS.ID,
            'ar_pool': self.get_networks_ar_pool(template)
        }

        return info

    def create_template(self, name, template_data):
        if not self.module.check_mode:
            # -1 means that network won't be added to any cluster which happens by default
            self.one.vn.allocate("NAME = \"" + name + "\"\n" + template_data, -1)

        result = self.get_template_info(self.get_template_by_name(name))
        result['changed'] = True

        return result

    def update_template(self, template, template_data):
        if not self.module.check_mode:
            # 0 = replace the whole template
            self.one.vn.update(template.ID, template_data, 0)

        result = self.get_template_info(self.get_template_by_id(template.ID))
        if self.module.check_mode:
            # Unfortunately it is not easy to detect if the template would have changed, therefore always report a change here.
            result['changed'] = True
        else:
            # if the previous parsed template data is not equal to the updated one, this has changed
            result['changed'] = template.TEMPLATE != result['template']

        return result

    def delete_template(self, template):
        if not template:
            return {'changed': False}

        if not self.module.check_mode:
            self.one.vn.delete(template.ID)

        return {'changed': True}


def main():
    NetworksModule().run_module()


if __name__ == '__main__':
    main()