#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2016, Thomas Stringer <tomstr@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_functionapp
version_added: "0.1.2"
short_description: Manage Azure Function Apps
description:
    - Create, update or delete an Azure Function App.
options:
    resource_group:
        description:
            - Name of resource group.
        required: true
        aliases:
            - resource_group_name
        type: str
    name:
        description:
            - Name of the Azure Function App.
        required: true
        type: str
    location:
        description:
            - Valid Azure location. Defaults to location of the resource group.
        type: str
    plan:
        description:
            - App service plan.
            - It can be name of existing app service plan in same resource group as function app.
            - It can be resource id of existing app service plan.
            - Resource id. For example /subscriptions/<subs_id>/resourceGroups/<resource_group>/providers/Microsoft.Web/serverFarms/<plan_name>.
            - It can be a dict which contains C(name), C(resource_group).
            - C(name). Name of app service plan.
            - C(resource_group). Resource group name of app service plan.
        type: raw
    container_settings:
        description:
            - Web app container settings.
        type: dict
        suboptions:
            name:
                description:
                    - Name of container. For example "imagename:tag".
                required: True
                type: str
            registry_server_url:
                description:
                    - Container registry server url. For example C(mydockerregistry.io).
                type: str
            registry_server_user:
                description:
                    - The container registry server user name.
                type: str
            registry_server_password:
                description:
                    - The container registry server password.
                type: str
    storage_account:
        description:
            - Name of the storage account to use.
            - Required when creating.
        type: str
        aliases:
            - storage
            - storage_account_name
    app_settings:
        description:
            - Dictionary containing application settings.
        type: dict
    state:
        description:
            - Assert the state of the Function App. Use C(present) to create or update a Function App and C(absent) to delete.
        default: present
        type: str
        choices:
            - absent
            - present

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

author:
    - Thomas Stringer (@trstringer)
'''

EXAMPLES = '''
- name: Create a function app
  azure_rm_functionapp:
    resource_group: myResourceGroup
    name: myFunctionApp
    storage_account: myStorageAccount

- name: Create a function app with app settings
  azure_rm_functionapp:
    resource_group: myResourceGroup
    name: myFunctionApp
    storage_account: myStorageAccount
    app_settings:
      setting1: value1
      setting2: value2

- name: Create container based function app
  azure_rm_functionapp:
    resource_group: myResourceGroup
    name: myFunctionApp
    storage_account: myStorageAccount
    plan:
      resource_group: myResourceGroup
      name: myAppPlan
    container_settings:
      name: httpd
      registry_server_url: index.docker.io

- name: Delete a function app
  azure_rm_functionapp:
    resource_group: myResourceGroup
    name: myFunctionApp
    state: absent
'''

RETURN = '''
state:
    description:
        - Current state of the Azure Function App.
    returned: success
    type: dict
    example:
        id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/myFunctionApp
        name: myfunctionapp
        kind: functionapp
        location: East US
        type: Microsoft.Web/sites
        state: Running
        host_names:
          - myfunctionapp.azurewebsites.net
        repository_site_name: myfunctionapp
        usage_state: Normal
        enabled: true
        enabled_host_names:
          - myfunctionapp.azurewebsites.net
          - myfunctionapp.scm.azurewebsites.net
        availability_state: Normal
        host_name_ssl_states:
          - name: myfunctionapp.azurewebsites.net
            ssl_state: Disabled
            host_type: Standard
          - name: myfunctionapp.scm.azurewebsites.net
            ssl_state: Disabled
            host_type: Repository
        server_farm_id: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/serverfarms/EastUSPlan
        reserved: false
        last_modified_time_utc: '2017-08-22T18:54:01.190Z'
        scm_site_also_stopped: false
        client_affinity_enabled: true
        client_cert_enabled: false
        host_names_disabled: false
        outbound_ip_addresses: ............
        container_size: 1536
        daily_memory_time_quota: 0
        resource_group: myResourceGroup
        default_host_name: myfunctionapp.azurewebsites.net
'''  # NOQA

from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt

try:
    from azure.core.exceptions import ResourceNotFoundError
    from azure.mgmt.web.models import Site, SiteConfig, NameValuePair, ManagedServiceIdentity, UserAssignedIdentity

except ImportError:
    # This is handled in azure_rm_common
    pass

container_settings_spec = dict(
    name=dict(type='str', required=True),
    registry_server_url=dict(type='str'),
    registry_server_user=dict(type='str'),
    registry_server_password=dict(type='str', no_log=True)
)


class AzureRMFunctionApp(AzureRMModuleBaseExt):

    def __init__(self):

        self.module_arg_spec = dict(
            resource_group=dict(type='str', required=True, aliases=['resource_group_name']),
            name=dict(type='str', required=True),
            state=dict(type='str', default='present', choices=['present', 'absent']),
            location=dict(type='str'),
            storage_account=dict(
                type='str',
                aliases=['storage', 'storage_account_name']
            ),
            app_settings=dict(type='dict'),
            plan=dict(
                type='raw'
            ),
            identity=dict(
                type='dict',
                options=self.managed_identity_multiple_spec
            ),
            container_settings=dict(
                type='dict',
                options=container_settings_spec
            )
        )

        self.results = dict(
            changed=False,
            state=dict()
        )

        self.resource_group = None
        self.name = None
        self.state = None
        self.location = None
        self.storage_account = None
        self.app_settings = None
        self.plan = None
        self.container_settings = None
        self._managed_identity = None

        # Managed Identity
        self.identity = None

        required_if = [('state', 'present', ['storage_account'])]

        super(AzureRMFunctionApp, self).__init__(
            self.module_arg_spec,
            supports_check_mode=True,
            required_if=required_if
        )

    @property
    def managed_identity(self):
        if not self._managed_identity:
            self._managed_identity = {"identity": ManagedServiceIdentity,
                                      "user_assigned": UserAssignedIdentity
                                      }
        return self._managed_identity

    def exec_module(self, **kwargs):

        for key in self.module_arg_spec:
            setattr(self, key, kwargs[key])
        if self.app_settings is None:
            self.app_settings = dict()

        try:
            resource_group = self.rm_client.resource_groups.get(self.resource_group)
        except Exception:
            self.fail('Unable to retrieve resource group')

        self.location = self.location or resource_group.location

        try:
            function_app = self.web_client.web_apps.get(resource_group_name=self.resource_group, name=self.name)
            # Newer SDK versions (0.40.0+) seem to return None if it doesn't exist instead of raising error
            exists = function_app is not None
        except ResourceNotFoundError as exc:
            exists = False

        if self.state == 'absent':
            if exists:
                if self.check_mode:
                    self.results['changed'] = True
                    return self.results
                try:
                    self.web_client.web_apps.delete(resource_group_name=self.resource_group, name=self.name)
                    self.results['changed'] = True
                except Exception as exc:
                    self.fail('Failure while deleting web app: {0}'.format(exc))
            else:
                self.results['changed'] = False
        else:
            kind = 'functionapp'
            linux_fx_version = None
            if self.container_settings and self.container_settings.get('name'):
                kind = 'functionapp,linux,container'
                linux_fx_version = 'DOCKER|'
                if self.container_settings.get('registry_server_url'):
                    self.app_settings['DOCKER_REGISTRY_SERVER_URL'] = 'https://' + self.container_settings['registry_server_url']
                    linux_fx_version += self.container_settings['registry_server_url'] + '/'
                linux_fx_version += self.container_settings['name']
                if self.container_settings.get('registry_server_user'):
                    self.app_settings['DOCKER_REGISTRY_SERVER_USERNAME'] = self.container_settings.get('registry_server_user')

                if self.container_settings.get('registry_server_password'):
                    self.app_settings['DOCKER_REGISTRY_SERVER_PASSWORD'] = self.container_settings.get('registry_server_password')

            if not self.plan and exists:
                self.plan = function_app.server_farm_id

            curr_identity = function_app.identity.as_dict() if exists and function_app.identity is not None else None
            update_identity = False

            if self.identity:
                update_identity, self.identity = self.update_managed_identity(new_identity=self.identity, curr_identity=curr_identity)

            if not exists:
                function_app = Site(
                    location=self.location,
                    kind=kind,
                    site_config=SiteConfig(
                        app_settings=self.aggregated_app_settings(),
                        scm_type='LocalGit'
                    ),
                    identity=self.identity
                )
                self.results['changed'] = True
            else:
                new_identity = None
                if update_identity:
                    new_identity = self.identity
                self.results['changed'], function_app = self.update(function_app, new_identity)

            # get app service plan
            if self.plan:
                if isinstance(self.plan, dict):
                    self.plan = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Web/serverfarms/{2}".format(
                        self.subscription_id,
                        self.plan.get('resource_group', self.resource_group),
                        self.plan.get('name')
                    )
                function_app.server_farm_id = self.plan

            # set linux fx version
            if linux_fx_version:
                function_app.site_config.linux_fx_version = linux_fx_version

            if self.check_mode:
                self.results['state'] = function_app.as_dict()
            elif self.results['changed']:
                try:
                    response = self.web_client.web_apps.begin_create_or_update(resource_group_name=self.resource_group,
                                                                               name=self.name,
                                                                               site_envelope=function_app)
                    new_function_app = self.get_poller_result(response)
                    self.results['state'] = new_function_app.as_dict()
                except Exception as exc:
                    self.fail('Error creating or updating web app: {0}'.format(exc))

        return self.results

    def update(self, source_function_app, identity=None):
        """Update the Site object if there are any changes"""

        source_app_settings = self.web_client.web_apps.list_application_settings(
            resource_group_name=self.resource_group,
            name=self.name
        )

        changed, target_app_settings = self.update_app_settings(source_app_settings.properties)

        source_function_app.site_config = SiteConfig(
            app_settings=target_app_settings,
            scm_type='LocalGit'
        )

        if identity is not None:
            source_function_app.identity = identity
            changed = True

        return changed, source_function_app

    def update_app_settings(self, source_app_settings):
        """Update app settings"""

        target_app_settings = self.aggregated_app_settings()
        target_app_settings_dict = dict([(i.name, i.value) for i in target_app_settings])
        return target_app_settings_dict != source_app_settings, target_app_settings

    def necessary_functionapp_settings(self):
        """Construct the necessary app settings required for an Azure Function App"""

        function_app_settings = []

        if self.container_settings is None:
            for key in ['AzureWebJobsStorage', 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', 'AzureWebJobsDashboard']:
                function_app_settings.append(NameValuePair(name=key, value=self.storage_connection_string))
            function_app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', value='~1'))
            function_app_settings.append(NameValuePair(name='WEBSITE_NODE_DEFAULT_VERSION', value='6.5.0'))
            function_app_settings.append(NameValuePair(name='WEBSITE_CONTENTSHARE', value=self.name))
        else:
            function_app_settings.append(NameValuePair(name='FUNCTIONS_EXTENSION_VERSION', value='~2'))
            function_app_settings.append(NameValuePair(name='WEBSITES_ENABLE_APP_SERVICE_STORAGE', value=False))
            function_app_settings.append(NameValuePair(name='AzureWebJobsStorage', value=self.storage_connection_string))

        return function_app_settings

    def aggregated_app_settings(self):
        """Combine both system and user app settings"""

        function_app_settings = self.necessary_functionapp_settings()
        for app_setting_key in self.app_settings:
            found_setting = None
            for s in function_app_settings:
                if s.name == app_setting_key:
                    found_setting = s
                    break
            if found_setting:
                found_setting.value = self.app_settings[app_setting_key]
            else:
                function_app_settings.append(NameValuePair(
                    name=app_setting_key,
                    value=self.app_settings[app_setting_key]
                ))
        return function_app_settings

    @property
    def storage_connection_string(self):
        """Construct the storage account connection string"""

        return 'DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}'.format(
            self.storage_account,
            self.storage_key
        )

    @property
    def storage_key(self):
        """Retrieve the storage account key"""

        return self.storage_client.storage_accounts.list_keys(
            resource_group_name=self.resource_group,
            account_name=self.storage_account
        ).keys[0].value


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

    AzureRMFunctionApp()


if __name__ == '__main__':
    main()
