"""
Management of Keystone users
============================

:depends:   - keystoneclient Python module
:configuration: See :py:mod:`salt.modules.keystone` for setup instructions.

.. code-block:: yaml

    Keystone tenants:
      keystone.tenant_present:
        - names:
          - admin
          - demo
          - service

    Keystone roles:
      keystone.role_present:
        - names:
          - admin
          - Member

    admin:
      keystone.user_present:
        - password: R00T_4CC3SS
        - email: admin@domain.com
        - roles:
            admin:   # tenants
              - admin  # roles
            service:
              - admin
              - Member
        - require:
          - keystone: Keystone tenants
          - keystone: Keystone roles

    nova:
      keystone.user_present:
        - password: '$up3rn0v4'
        - email: nova@domain.com
        - tenant: service
        - roles:
            service:
              - admin
        - require:
          - keystone: Keystone tenants
          - keystone: Keystone roles

    demo:
      keystone.user_present:
        - password: 'd3m0n$trati0n'
        - email: demo@domain.com
        - tenant: demo
        - roles:
            demo:
              - Member
        - require:
          - keystone: Keystone tenants
          - keystone: Keystone roles

    nova service:
      keystone.service_present:
        - name: nova
        - service_type: compute
        - description: OpenStack Compute Service

"""


def __virtual__():
    """
    Only load if the keystone module is in __salt__
    """
    if "keystone.auth" in __salt__:
        return "keystone"
    return (False, "keystone module could not be loaded")


_OS_IDENTITY_API_VERSION = 2
_TENANT_ID = "tenant_id"


def _api_version(profile=None, **connection_args):
    """
    Sets global variables _OS_IDENTITY_API_VERSION and _TENANT_ID
    depending on API version.
    """
    global _TENANT_ID
    global _OS_IDENTITY_API_VERSION
    try:
        if (
            float(
                __salt__["keystone.api_version"](
                    profile=profile, **connection_args
                ).strip("v")
            )
            >= 3
        ):
            _TENANT_ID = "project_id"
            _OS_IDENTITY_API_VERSION = 3
    except KeyError:
        pass


def user_present(
    name,
    password,
    email,
    tenant=None,
    enabled=True,
    roles=None,
    profile=None,
    password_reset=True,
    project=None,
    **connection_args,
):
    """
    Ensure that the keystone user is present with the specified properties.

    name
        The name of the user to manage

    password
        The password to use for this user.

        .. note::

            If the user already exists and a different password was set for
            the user than the one specified here, the password for the user
            will be updated. Please set the ``password_reset`` option to
            ``False`` if this is not the desired behavior.

    password_reset
        Whether or not to reset password after initial set. Defaults to
        ``True``.

    email
        The email address for this user

    tenant
        The tenant (name) for this user

    project
        The project (name) for this user (overrides tenant in api v3)

    enabled
        Availability state for this user

    roles
        The roles the user should have under given tenants.
        Passed as a dictionary mapping tenant names to a list
        of roles in this tenant, i.e.::

            roles:
                admin:   # tenant
                  - admin  # role
                service:
                  - admin
                  - Member
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'User "{name}" will be updated',
    }

    _api_version(profile=profile, **connection_args)

    if project and not tenant:
        tenant = project

    # Validate tenant if set
    if tenant is not None:
        tenantdata = __salt__["keystone.tenant_get"](
            name=tenant, profile=profile, **connection_args
        )
        if "Error" in tenantdata:
            ret["result"] = False
            ret["comment"] = f'Tenant / project "{tenant}" does not exist'
            return ret
        tenant_id = tenantdata[tenant]["id"]
    else:
        tenant_id = None

    # Check if user is already present
    user = __salt__["keystone.user_get"](name=name, profile=profile, **connection_args)
    if "Error" not in user:

        change_email = False
        change_enabled = False
        change_tenant = False
        change_password = False

        if user[name].get("email", None) != email:
            change_email = True

        if user[name].get("enabled", None) != enabled:
            change_enabled = True

        if tenant and (
            _TENANT_ID not in user[name]
            or user[name].get(_TENANT_ID, None) != tenant_id
        ):
            change_tenant = True

        if password_reset is True and not __salt__["keystone.user_verify_password"](
            name=name, password=password, profile=profile, **connection_args
        ):
            change_password = True

        if __opts__.get("test") and (
            change_email or change_enabled or change_tenant or change_password
        ):
            ret["result"] = None
            ret["comment"] = f'User "{name}" will be updated'
            if change_email is True:
                ret["changes"]["Email"] = "Will be updated"
            if change_enabled is True:
                ret["changes"]["Enabled"] = "Will be True"
            if change_tenant is True:
                ret["changes"]["Tenant"] = f'Will be added to "{tenant}" tenant'
            if change_password is True:
                ret["changes"]["Password"] = "Will be updated"
            return ret

        ret["comment"] = f'User "{name}" is already present'

        if change_email:
            __salt__["keystone.user_update"](
                name=name, email=email, profile=profile, **connection_args
            )
            ret["comment"] = f'User "{name}" has been updated'
            ret["changes"]["Email"] = "Updated"

        if change_enabled:
            __salt__["keystone.user_update"](
                name=name, enabled=enabled, profile=profile, **connection_args
            )
            ret["comment"] = f'User "{name}" has been updated'
            ret["changes"]["Enabled"] = f"Now {enabled}"

        if change_tenant:
            __salt__["keystone.user_update"](
                name=name, tenant=tenant, profile=profile, **connection_args
            )
            ret["comment"] = f'User "{name}" has been updated'
            ret["changes"]["Tenant"] = f'Added to "{tenant}" tenant'

        if change_password:
            __salt__["keystone.user_password_update"](
                name=name, password=password, profile=profile, **connection_args
            )
            ret["comment"] = f'User "{name}" has been updated'
            ret["changes"]["Password"] = "Updated"

        if roles:
            for tenant in roles:
                args = dict(
                    {"user_name": name, "tenant_name": tenant, "profile": profile},
                    **connection_args,
                )
                tenant_roles = __salt__["keystone.user_role_list"](**args)
                for role in roles[tenant]:
                    if role not in tenant_roles:
                        if __opts__.get("test"):
                            ret["result"] = None
                            ret["comment"] = 'User roles "{}" will been updated'.format(
                                name
                            )
                            return ret
                        addargs = dict(
                            {
                                "user": name,
                                "role": role,
                                "tenant": tenant,
                                "profile": profile,
                            },
                            **connection_args,
                        )
                        newrole = __salt__["keystone.user_role_add"](**addargs)
                        if "roles" in ret["changes"]:
                            ret["changes"]["roles"].append(newrole)
                        else:
                            ret["changes"]["roles"] = [newrole]
                roles_to_remove = list(set(tenant_roles) - set(roles[tenant]))
                for role in roles_to_remove:
                    if __opts__.get("test"):
                        ret["result"] = None
                        ret["comment"] = 'User roles "{}" will been updated'.format(
                            name
                        )
                        return ret
                    addargs = dict(
                        {
                            "user": name,
                            "role": role,
                            "tenant": tenant,
                            "profile": profile,
                        },
                        **connection_args,
                    )
                    oldrole = __salt__["keystone.user_role_remove"](**addargs)
                    if "roles" in ret["changes"]:
                        ret["changes"]["roles"].append(oldrole)
                    else:
                        ret["changes"]["roles"] = [oldrole]
    else:
        # Create that user!
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Keystone user "{name}" will be added'
            ret["changes"]["User"] = "Will be created"
            return ret
        __salt__["keystone.user_create"](
            name=name,
            password=password,
            email=email,
            tenant_id=tenant_id,
            enabled=enabled,
            profile=profile,
            **connection_args,
        )
        if roles:
            for tenant in roles:
                for role in roles[tenant]:
                    __salt__["keystone.user_role_add"](
                        user=name,
                        role=role,
                        tenant=tenant,
                        profile=profile,
                        **connection_args,
                    )
        ret["comment"] = f"Keystone user {name} has been added"
        ret["changes"]["User"] = "Created"

    return ret


def user_absent(name, profile=None, **connection_args):
    """
    Ensure that the keystone user is absent.

    name
        The name of the user that should not exist
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'User "{name}" is already absent',
    }

    # Check if user is present
    user = __salt__["keystone.user_get"](name=name, profile=profile, **connection_args)
    if "Error" not in user:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'User "{name}" will be deleted'
            return ret
        # Delete that user!
        __salt__["keystone.user_delete"](name=name, profile=profile, **connection_args)
        ret["comment"] = f'User "{name}" has been deleted'
        ret["changes"]["User"] = "Deleted"

    return ret


def tenant_present(
    name, description=None, enabled=True, profile=None, **connection_args
):
    """
    Ensures that the keystone tenant exists

    name
        The name of the tenant to manage

    description
        The description to use for this tenant

    enabled
        Availability state for this tenant
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'Tenant / project "{name}" already exists',
    }

    _api_version(profile=profile, **connection_args)

    # Check if tenant is already present
    tenant = __salt__["keystone.tenant_get"](
        name=name, profile=profile, **connection_args
    )

    if "Error" not in tenant:
        if tenant[name].get("description", None) != description:
            if __opts__.get("test"):
                ret["result"] = None
                ret["comment"] = f'Tenant / project "{name}" will be updated'
                ret["changes"]["Description"] = "Will be updated"
                return ret
            __salt__["keystone.tenant_update"](
                name=name,
                description=description,
                enabled=enabled,
                profile=profile,
                **connection_args,
            )
            ret["comment"] = f'Tenant / project "{name}" has been updated'
            ret["changes"]["Description"] = "Updated"
        if tenant[name].get("enabled", None) != enabled:
            if __opts__.get("test"):
                ret["result"] = None
                ret["comment"] = f'Tenant / project "{name}" will be updated'
                ret["changes"]["Enabled"] = f"Will be {enabled}"
                return ret
            __salt__["keystone.tenant_update"](
                name=name,
                description=description,
                enabled=enabled,
                profile=profile,
                **connection_args,
            )
            ret["comment"] = f'Tenant / project "{name}" has been updated'
            ret["changes"]["Enabled"] = f"Now {enabled}"
    else:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Tenant / project "{name}" will be added'
            ret["changes"]["Tenant"] = "Will be created"
            return ret
        # Create tenant
        if _OS_IDENTITY_API_VERSION > 2:
            created = __salt__["keystone.project_create"](
                name=name,
                domain="default",
                description=description,
                enabled=enabled,
                profile=profile,
                **connection_args,
            )
        else:
            created = __salt__["keystone.tenant_create"](
                name=name,
                description=description,
                enabled=enabled,
                profile=profile,
                **connection_args,
            )
        ret["changes"]["Tenant"] = "Created" if created is True else "Failed"
        ret["result"] = created
        ret["comment"] = f'Tenant / project "{name}" has been added'
    return ret


def tenant_absent(name, profile=None, **connection_args):
    """
    Ensure that the keystone tenant is absent.

    name
        The name of the tenant that should not exist
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'Tenant / project "{name}" is already absent',
    }

    # Check if tenant is present
    tenant = __salt__["keystone.tenant_get"](
        name=name, profile=profile, **connection_args
    )
    if "Error" not in tenant:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Tenant / project "{name}" will be deleted'
            return ret
        # Delete tenant
        __salt__["keystone.tenant_delete"](
            name=name, profile=profile, **connection_args
        )
        ret["comment"] = f'Tenant / project "{name}" has been deleted'
        ret["changes"]["Tenant/Project"] = "Deleted"

    return ret


def project_present(
    name, description=None, enabled=True, profile=None, **connection_args
):
    """
    Ensures that the keystone project exists
    Alias for tenant_present from V2 API to fulfill
    V3 API naming convention.

    .. versionadded:: 2016.11.0

    name
        The name of the project to manage

    description
        The description to use for this project

    enabled
        Availability state for this project

    .. code-block:: yaml

        nova:
            keystone.project_present:
                - enabled: True
                - description: 'Nova Compute Service'

    """

    return tenant_present(
        name,
        description=description,
        enabled=enabled,
        profile=profile,
        **connection_args,
    )


def project_absent(name, profile=None, **connection_args):
    """
    Ensure that the keystone project is absent.
    Alias for tenant_absent from V2 API to fulfill
    V3 API naming convention.

    .. versionadded:: 2016.11.0

    name
        The name of the project that should not exist

    .. code-block:: yaml

        delete_nova:
            keystone.project_absent:
                - name: nova
    """

    return tenant_absent(name, profile=profile, **connection_args)


def role_present(name, profile=None, **connection_args):
    """'
    Ensures that the keystone role exists

    name
        The name of the role that should be present
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'Role "{name}" already exists',
    }

    # Check if role is already present
    role = __salt__["keystone.role_get"](name=name, profile=profile, **connection_args)

    if "Error" not in role:
        return ret
    else:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Role "{name}" will be added'
            return ret
        # Create role
        __salt__["keystone.role_create"](name, profile=profile, **connection_args)
        ret["comment"] = f'Role "{name}" has been added'
        ret["changes"]["Role"] = "Created"
    return ret


def role_absent(name, profile=None, **connection_args):
    """
    Ensure that the keystone role is absent.

    name
        The name of the role that should not exist
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'Role "{name}" is already absent',
    }

    # Check if role is present
    role = __salt__["keystone.role_get"](name=name, profile=profile, **connection_args)
    if "Error" not in role:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Role "{name}" will be deleted'
            return ret
        # Delete role
        __salt__["keystone.role_delete"](name=name, profile=profile, **connection_args)
        ret["comment"] = f'Role "{name}" has been deleted'
        ret["changes"]["Role"] = "Deleted"

    return ret


def service_present(
    name, service_type, description=None, profile=None, **connection_args
):
    """
    Ensure service present in Keystone catalog

    name
        The name of the service

    service_type
        The type of Openstack Service

    description (optional)
        Description of the service
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'Service "{name}" already exists',
    }

    # Check if service is already present
    role = __salt__["keystone.service_get"](
        name=name, profile=profile, **connection_args
    )

    if "Error" not in role:
        return ret
    else:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Service "{name}" will be added'
            return ret
        # Create service
        __salt__["keystone.service_create"](
            name, service_type, description, profile=profile, **connection_args
        )
        ret["comment"] = f'Service "{name}" has been added'
        ret["changes"]["Service"] = "Created"

    return ret


def service_absent(name, profile=None, **connection_args):
    """
    Ensure that the service doesn't exist in Keystone catalog

    name
        The name of the service that should not exist
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": f'Service "{name}" is already absent',
    }

    # Check if service is present
    role = __salt__["keystone.service_get"](
        name=name, profile=profile, **connection_args
    )
    if "Error" not in role:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Service "{name}" will be deleted'
            return ret
        # Delete service
        __salt__["keystone.service_delete"](
            name=name, profile=profile, **connection_args
        )
        ret["comment"] = f'Service "{name}" has been deleted'
        ret["changes"]["Service"] = "Deleted"

    return ret


def endpoint_present(
    name,
    publicurl=None,
    internalurl=None,
    adminurl=None,
    region=None,
    profile=None,
    url=None,
    interface=None,
    **connection_args,
):
    """
    Ensure the specified endpoints exists for service

    name
        The Service name

    publicurl
        The public url of service endpoint (for V2 API)

    internalurl
        The internal url of service endpoint (for V2 API)

    adminurl
        The admin url of the service endpoint (for V2 API)

    region
        The region of the endpoint

    url
        The endpoint URL (for V3 API)

    interface
        The interface type, which describes the visibility
        of the endpoint. (for V3 API)

    """
    ret = {"name": name, "changes": {}, "result": True, "comment": ""}

    _api_version(profile=profile, **connection_args)

    endpoint = __salt__["keystone.endpoint_get"](
        name, region, profile=profile, interface=interface, **connection_args
    )

    def _changes(desc):
        return ret.get("comment", "") + desc + "\n"

    def _create_endpoint():
        if _OS_IDENTITY_API_VERSION > 2:
            ret["changes"] = __salt__["keystone.endpoint_create"](
                name,
                region=region,
                url=url,
                interface=interface,
                profile=profile,
                **connection_args,
            )
        else:
            ret["changes"] = __salt__["keystone.endpoint_create"](
                name,
                region=region,
                publicurl=publicurl,
                adminurl=adminurl,
                internalurl=internalurl,
                profile=profile,
                **connection_args,
            )

    if endpoint and "Error" not in endpoint and endpoint.get("region") == region:

        if _OS_IDENTITY_API_VERSION > 2:

            change_url = False
            change_interface = False

            if endpoint.get("url", None) != url:
                ret["comment"] = _changes(
                    'URL changes from "{}" to "{}"'.format(
                        endpoint.get("url", None), url
                    )
                )
                change_url = True

            if endpoint.get("interface", None) != interface:
                ret["comment"] = _changes(
                    'Interface changes from "{}" to "{}"'.format(
                        endpoint.get("interface", None), interface
                    )
                )
                change_interface = True

            if __opts__.get("test") and (change_url or change_interface):
                ret["result"] = None
                ret["changes"]["Endpoint"] = "Will be updated"
                ret["comment"] += 'Endpoint for service "{}" will be updated'.format(
                    name
                )
                return ret

            if change_url:
                ret["changes"]["url"] = url

            if change_interface:
                ret["changes"]["interface"] = interface

        else:
            change_publicurl = False
            change_adminurl = False
            change_internalurl = False

            if endpoint.get("publicurl", None) != publicurl:
                change_publicurl = True

                ret["comment"] = _changes(
                    'Public URL changes from "{}" to "{}"'.format(
                        endpoint.get("publicurl", None), publicurl
                    )
                )

            if endpoint.get("adminurl", None) != adminurl:
                change_adminurl = True
                ret["comment"] = _changes(
                    'Admin URL changes from "{}" to "{}"'.format(
                        endpoint.get("adminurl", None), adminurl
                    )
                )

            if endpoint.get("internalurl", None) != internalurl:
                change_internalurl = True
                ret["comment"] = _changes(
                    'Internal URL changes from "{}" to "{}"'.format(
                        endpoint.get("internalurl", None), internalurl
                    )
                )

            if __opts__.get("test") and (
                change_publicurl or change_adminurl or change_internalurl
            ):
                ret["result"] = None
                ret["comment"] += 'Endpoint for service "{}" will be updated'.format(
                    name
                )
                ret["changes"]["Endpoint"] = "Will be updated"
                return ret

            if change_publicurl:
                ret["changes"]["publicurl"] = publicurl

            if change_adminurl:
                ret["changes"]["adminurl"] = adminurl

            if change_internalurl:
                ret["changes"]["internalurl"] = internalurl

        if ret["comment"]:  # changed
            __salt__["keystone.endpoint_delete"](
                name, region, profile=profile, interface=interface, **connection_args
            )
            _create_endpoint()
            ret["comment"] += f'Endpoint for service "{name}" has been updated'

    else:
        # Add new endpoint
        if __opts__.get("test"):
            ret["result"] = None
            ret["changes"]["Endpoint"] = "Will be created"
            ret["comment"] = f'Endpoint for service "{name}" will be added'
            return ret
        _create_endpoint()
        ret["comment"] = f'Endpoint for service "{name}" has been added'

    if ret["comment"] == "":  # => no changes
        ret["comment"] = f'Endpoint for service "{name}" already exists'
    return ret


def endpoint_absent(name, region=None, profile=None, interface=None, **connection_args):
    """
    Ensure that the endpoint for a service doesn't exist in Keystone catalog

    name
        The name of the service whose endpoints should not exist

    region (optional)
        The region of the endpoint.  Defaults to ``RegionOne``.

    interface
        The interface type, which describes the visibility
        of the endpoint. (for V3 API)
    """
    ret = {
        "name": name,
        "changes": {},
        "result": True,
        "comment": 'Endpoint for service "{}"{} is already absent'.format(
            name,
            f', interface "{interface}",' if interface is not None else "",
        ),
    }

    # Check if service is present
    endpoint = __salt__["keystone.endpoint_get"](
        name, region, profile=profile, interface=interface, **connection_args
    )
    if not endpoint:
        return ret
    else:
        if __opts__.get("test"):
            ret["result"] = None
            ret["comment"] = f'Endpoint for service "{name}" will be deleted'
            return ret
        # Delete service
        __salt__["keystone.endpoint_delete"](
            name, region, profile=profile, interface=interface, **connection_args
        )
        ret["comment"] = 'Endpoint for service "{}"{} has been deleted'.format(
            name,
            f', interface "{interface}",' if interface is not None else "",
        )
        ret["changes"]["endpoint"] = "Deleted"
    return ret
