#!/usr/bin/python
#
# Copyright (c) 2018 Zim Kalinowski, <zikalino@microsoft.com>
#
# 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 = '''
---
module: azure_rm_devtestlabvirtualmachine
version_added: "0.1.2"
short_description: Manage Azure DevTest Lab Virtual Machine instance
description:
    - Create, update and delete instance of Azure DevTest Lab Virtual Machine.

options:
    resource_group:
        description:
            - The name of the resource group.
        required: True
        type: str
    lab_name:
        description:
            - The name of the lab.
        required: True
        type: str
    name:
        description:
            - The name of the virtual machine.
        required: True
        type: str
    notes:
        description:
            - The notes of the virtual machine.
        type: str
    os_type:
        description:
            - Base type of operating system.
        type: str
        choices:
            - windows
            - linux
    vm_size:
        description:
            - A valid Azure VM size value. For example, C(Standard_D4).
            - The list of choices varies depending on the subscription and location. Check your subscription for available choices.
            - Available values can be found on this website, link U(https://docs.microsoft.com/en-us/azure/virtual-machines/windows/sizes-general).
            - Required when I(state=present).
        type: str
    user_name:
        description:
            - The user name of the virtual machine.
        type: str
    password:
        description:
            - The password of the virtual machine administrator.
        type: str
    ssh_key:
        description:
            - The SSH key of the virtual machine administrator.
        type: str
    lab_subnet:
        description:
            - An existing subnet within lab's virtual network.
            - It can be the subnet's resource id.
            - It can be a dict which contains C(virtual_network_name) and C(name).
        type: raw
    disallow_public_ip_address:
        description:
            - Indicates whether the virtual machine is to be created without a public IP address.
        type: str
    artifacts:
        description:
            - The artifacts to be installed on the virtual machine.
        type: list
        elements: dict
        suboptions:
            source_name:
                description:
                    - The artifact's source name.
                type: str
            source_path:
                description:
                    - The artifact's path in the source repository.
                type: str
            parameters:
                description:
                    - The parameters of the artifact.
                type: list
                elements: dict
                suboptions:
                    name:
                        description:
                            - The name of the artifact parameter.
                        type: str
                    value:
                        description:
                            - The value of the artifact parameter.
                        type: str
    image:
        description:
            - The Microsoft Azure Marketplace image reference of the virtual machine.
        type: dict
        suboptions:
            offer:
                description:
                    - The offer of the gallery image.
                type: str
            publisher:
                description:
                    - The publisher of the gallery image.
                type: str
            sku:
                description:
                    - The SKU of the gallery image.
                type: str
            os_type:
                description:
                    - The OS type of the gallery image.
                type: str
            version:
                description:
                    - The version of the gallery image.
                type: str
    expiration_date:
        description:
            - The expiration date for VM.
        type: str
    allow_claim:
        description:
            - Indicates whether another user can take ownership of the virtual machine.
        type: str
    storage_type:
        description:
            - Storage type to use for virtual machine.
        type: str
        choices:
            - standard
            - premium
    state:
        description:
            - Assert the state of the Virtual Machine.
            - Use C(present) to create or update an Virtual Machine and C(absent) to delete it.
        type: str
        default: present
        choices:
            - absent
            - present

extends_documentation_fragment:
    - azure.azcollection.azure
    - azure.azcollection.azure_tags

author:
    - Zim Kalinowski (@zikalino)

'''

EXAMPLES = '''
- name: Create (or update) Virtual Machine
  azure_rm_devtestlabvirtualmachine:
    resource_group: myrg
    lab_name: mylab
    name: myvm
    notes: Virtual machine notes....
    os_type: linux
    vm_size: Standard_A2_v2
    user_name: vmadmin
    password: ZSuppas$$21!
    lab_subnet:
      name: myvnSubnet
      virtual_network_name: myvn
    disallow_public_ip_address: false
    image:
      offer: 0001-com-ubuntu-server-focal
      publisher: Canonical
      sku: 20_04-lts
      os_type: Linux
      version: latest
    artifacts:
      - source_name: myartifact
        source_path: "/Artifacts/linux-install-mongodb"
    allow_claim: false
    expiration_date: "2019-02-22T01:49:12.117974Z"
'''

RETURN = '''
id:
    description:
        - The identifier of the DTL Virtual Machine resource.
    returned: always
    type: str
    sample: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myrg/providers/microsoft.devtestlab/labs/mylab/virtualmachines/myvm
compute_id:
    description:
        - The identifier of the underlying Compute Virtual Machine resource.
    returned: always
    type: str
    sample: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myrg/providers/microsoft.devtestlab/labs/mylab/virtualmachines/myvm
fqdn:
    description:
        - Fully qualified domain name or IP Address of the virtual machine.
    returned: always
    type: str
    sample: myvm.eastus.cloudapp.azure.com
'''

from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase
from ansible.module_utils.common.dict_transformations import _snake_to_camel

try:
    from azure.core.polling import LROPoller
    from azure.core.exceptions import ResourceNotFoundError
    from azure.mgmt.devtestlabs import DevTestLabsClient
except ImportError:
    # This is handled in azure_rm_common
    pass


class Actions:
    NoAction, Create, Update, Delete = range(4)


class AzureRMVirtualMachine(AzureRMModuleBase):
    """Configuration class for an Azure RM Virtual Machine resource"""

    def __init__(self):
        self.module_arg_spec = dict(
            resource_group=dict(
                type='str',
                required=True
            ),
            lab_name=dict(
                type='str',
                required=True
            ),
            name=dict(
                type='str',
                required=True
            ),
            notes=dict(
                type='str'
            ),
            os_type=dict(
                type='str',
                choices=['linux', 'windows']
            ),
            vm_size=dict(
                type='str'
            ),
            user_name=dict(
                type='str'
            ),
            password=dict(
                type='str',
                no_log=True
            ),
            ssh_key=dict(
                type='str',
                no_log=True
            ),
            lab_subnet=dict(
                type='raw'
            ),
            disallow_public_ip_address=dict(
                type='str'
            ),
            artifacts=dict(
                type='list',
                elements='dict',
                options=dict(
                    source_name=dict(
                        type='str',
                    ),
                    source_path=dict(
                        type='str'
                    ),
                    parameters=dict(
                        type='list',
                        elements='dict',
                        options=dict(
                            name=dict(
                                type='str'
                            ),
                            value=dict(
                                type='str'
                            )
                        )
                    )
                )
            ),
            image=dict(
                type='dict',
                options=dict(
                    offer=dict(
                        type='str'
                    ),
                    publisher=dict(
                        type='str'
                    ),
                    sku=dict(
                        type='str'
                    ),
                    os_type=dict(
                        type='str'
                    ),
                    version=dict(
                        type='str'
                    )
                )
            ),
            expiration_date=dict(
                type='str'
            ),
            allow_claim=dict(
                type='str'
            ),
            storage_type=dict(
                type='str',
                choices=['standard', 'premium']
            ),
            state=dict(
                type='str',
                default='present',
                choices=['present', 'absent']
            )
        )

        required_if = [
            ('state', 'present', [
             'image', 'lab_subnet', 'vm_size', 'os_type'])
        ]

        self.resource_group = None
        self.lab_name = None
        self.name = None
        self.lab_virtual_machine = dict()

        self.results = dict(changed=False)
        self.mgmt_client = None
        self.state = None
        self.to_do = Actions.NoAction

        super(AzureRMVirtualMachine, self).__init__(derived_arg_spec=self.module_arg_spec,
                                                    supports_check_mode=True,
                                                    supports_tags=True,
                                                    required_if=required_if)

    def exec_module(self, **kwargs):
        """Main module execution method"""

        for key in list(self.module_arg_spec.keys()) + ['tags']:
            if hasattr(self, key):
                setattr(self, key, kwargs[key])
            elif kwargs[key] is not None:
                self.lab_virtual_machine[key] = kwargs[key]

        self.lab_virtual_machine['gallery_image_reference'] = self.lab_virtual_machine.pop('image', None)

        if self.lab_virtual_machine.get('artifacts') is not None:
            for artifact in self.lab_virtual_machine.get('artifacts'):
                source_name = artifact.pop('source_name')
                source_path = artifact.pop('source_path')
                template = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DevTestLab/labs/{2}/artifactsources/{3}{4}"
                artifact['artifact_id'] = template.format(self.subscription_id, self.resource_group, self.lab_name, source_name, source_path)

        self.lab_virtual_machine['size'] = self.lab_virtual_machine.pop('vm_size')
        self.lab_virtual_machine['os_type'] = _snake_to_camel(self.lab_virtual_machine['os_type'], True)

        if self.lab_virtual_machine.get('storage_type'):
            self.lab_virtual_machine['storage_type'] = _snake_to_camel(self.lab_virtual_machine['storage_type'], True)

        lab_subnet = self.lab_virtual_machine.pop('lab_subnet')

        if isinstance(lab_subnet, str):
            vn_and_subnet = lab_subnet.split('/subnets/')
            if (len(vn_and_subnet) == 2):
                self.lab_virtual_machine['lab_virtual_network_id'] = vn_and_subnet[0]
                self.lab_virtual_machine['lab_subnet_name'] = vn_and_subnet[1]
            else:
                self.fail("Invalid 'lab_subnet' resource id format")
        else:
            template = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.DevTestLab/labs/{2}/virtualnetworks/{3}"
            self.lab_virtual_machine['lab_virtual_network_id'] = template.format(self.subscription_id,
                                                                                 self.resource_group,
                                                                                 self.lab_name,
                                                                                 lab_subnet.get('virtual_network_name'))
            self.lab_virtual_machine['lab_subnet_name'] = lab_subnet.get('name')

        response = None

        self.mgmt_client = self.get_mgmt_svc_client(DevTestLabsClient,
                                                    base_url=self._cloud_environment.endpoints.resource_manager)

        old_response = self.get_virtualmachine()

        if not old_response:
            self.log("Virtual Machine instance doesn't exist")
            if self.state == 'absent':
                self.log("Old instance didn't exist")
            else:
                self.to_do = Actions.Create
            # get location from the lab as it has to be the same and has to be specified (why??)
            lab = self.get_devtestlab()
            self.lab_virtual_machine['location'] = lab['location']
        else:
            self.log("Virtual Machine instance already exists")
            if self.state == 'absent':
                self.to_do = Actions.Delete
            elif self.state == 'present':
                self.lab_virtual_machine['location'] = old_response['location']

                if old_response['size'].lower() != self.lab_virtual_machine.get('size').lower():
                    self.lab_virtual_machine['size'] = old_response['size']
                    self.module.warn("Property 'size' cannot be changed")

                if self.lab_virtual_machine.get('storage_type') is not None and \
                   old_response['storage_type'].lower() != self.lab_virtual_machine.get('storage_type').lower():
                    self.lab_virtual_machine['storage_type'] = old_response['storage_type']
                    self.module.warn("Property 'storage_type' cannot be changed")

                if old_response.get('gallery_image_reference', {}) != self.lab_virtual_machine.get('gallery_image_reference', {}):
                    self.lab_virtual_machine['gallery_image_reference'] = old_response['gallery_image_reference']
                    self.module.warn("Property 'image' cannot be changed")

                # currently artifacts can be only specified when vm is created
                # and in addition we don't have detailed information, just a number of "total artifacts"
                if len(self.lab_virtual_machine.get('artifacts', [])) != old_response['artifact_deployment_status']['total_artifacts']:
                    self.module.warn("Property 'artifacts' cannot be changed")

                if self.lab_virtual_machine.get('disallow_public_ip_address') is not None:
                    if old_response['disallow_public_ip_address'] != self.lab_virtual_machine.get('disallow_public_ip_address'):
                        self.module.warn("Property 'disallow_public_ip_address' cannot be changed")
                self.lab_virtual_machine['disallow_public_ip_address'] = old_response['disallow_public_ip_address']

                if self.lab_virtual_machine.get('allow_claim') is not None:
                    if old_response['allow_claim'] != self.lab_virtual_machine.get('allow_claim'):
                        self.module.warn("Property 'allow_claim' cannot be changed")
                self.lab_virtual_machine['allow_claim'] = old_response['allow_claim']

                if self.lab_virtual_machine.get('notes') is not None:
                    if old_response['notes'] != self.lab_virtual_machine.get('notes'):
                        self.to_do = Actions.Update
                else:
                    self.lab_virtual_machine['notes'] = old_response['notes']

        if (self.to_do == Actions.Create) or (self.to_do == Actions.Update):
            self.log("Need to Create / Update the Virtual Machine instance")

            self.results['changed'] = True
            if self.check_mode:
                return self.results

            response = self.create_update_virtualmachine()

            self.log("Creation / Update done")
        elif self.to_do == Actions.Delete:
            self.log("Virtual Machine instance deleted")
            self.results['changed'] = True

            if self.check_mode:
                return self.results

            self.delete_virtualmachine()
        else:
            self.log("Virtual Machine instance unchanged")
            self.results['changed'] = False
            response = old_response

        if self.state == 'present':
            self.results.update({
                'id': response.get('id', None),
                'compute_id': response.get('compute_id', None),
                'fqdn': response.get('fqdn', None)
            })
        return self.results

    def create_update_virtualmachine(self):
        '''
        Creates or updates Virtual Machine with the specified configuration.

        :return: deserialized Virtual Machine instance state dictionary
        '''
        self.log("Creating / Updating the Virtual Machine instance {0}".format(self.name))

        try:
            response = self.mgmt_client.virtual_machines.begin_create_or_update(resource_group_name=self.resource_group,
                                                                                lab_name=self.lab_name,
                                                                                name=self.name,
                                                                                lab_virtual_machine=self.lab_virtual_machine)
            if isinstance(response, LROPoller):
                response = self.get_poller_result(response)

        except Exception as exc:
            self.log('Error attempting to create the Virtual Machine instance.')
            self.fail("Error creating the Virtual Machine instance: {0}".format(str(exc)))
        return response.as_dict()

    def delete_virtualmachine(self):
        '''
        Deletes specified Virtual Machine instance in the specified subscription and resource group.

        :return: True
        '''
        self.log("Deleting the Virtual Machine instance {0}".format(self.name))
        try:
            response = self.mgmt_client.virtual_machines.begin_delete(resource_group_name=self.resource_group,
                                                                      lab_name=self.lab_name,
                                                                      name=self.name)
        except Exception as e:
            self.log('Error attempting to delete the Virtual Machine instance.')
            self.fail("Error deleting the Virtual Machine instance: {0}".format(str(e)))

        if isinstance(response, LROPoller):
            response = self.get_poller_result(response)

        return True

    def get_virtualmachine(self):
        '''
        Gets the properties of the specified Virtual Machine.

        :return: deserialized Virtual Machine instance state dictionary
        '''
        self.log("Checking if the Virtual Machine instance {0} is present".format(self.name))
        found = False
        try:
            response = self.mgmt_client.virtual_machines.get(resource_group_name=self.resource_group,
                                                             lab_name=self.lab_name,
                                                             name=self.name)
            found = True
            self.log("Response : {0}".format(response))
            self.log("Virtual Machine instance : {0} found".format(response.name))
        except ResourceNotFoundError as e:
            self.log('Did not find the Virtual Machine instance.')
        if found is True:
            return response.as_dict()

        return False

    def get_devtestlab(self):
        '''
        Gets the properties of the specified DevTest Lab.

        :return: deserialized DevTest Lab instance state dictionary
        '''
        self.log("Checking if the DevTest Lab instance {0} is present".format(self.lab_name))
        try:
            response = self.mgmt_client.labs.get(resource_group_name=self.resource_group,
                                                 name=self.lab_name)
            self.log("Response : {0}".format(response))
            self.log("DevTest Lab instance : {0} found".format(response.name))
            return response.as_dict()
        except ResourceNotFoundError as e:
            self.fail('Did not find the DevTest Lab instance.')
            return False


def main():
    """Main execution"""
    AzureRMVirtualMachine()


if __name__ == '__main__':
    main()
