"""
Connection module for Amazon Lambda

.. versionadded:: 2016.3.0

:depends:

- boto
- boto3

The dependencies listed above can be installed via package or pip.

:configuration: This module accepts explicit Lambda credentials but can also
    utilize IAM roles assigned to the instance through Instance Profiles.
    Dynamic credentials are then automatically obtained from AWS API and no
    further configuration is necessary. More Information available here__.

.. __: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html

If IAM roles are not used you need to specify them either in a pillar or
in the minion's config file:

.. code-block:: yaml

    lambda.keyid: GKTADJGHEIQSXMKKRBJ08H
    lambda.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs

A region may also be specified in the configuration:

.. code-block:: yaml

    lambda.region: us-east-1

If a region is not specified, the default is us-east-1.

It's also possible to specify key, keyid and region via a profile, either
as a passed in dict, or as a string to pull from pillars or minion config:

.. code-block:: yaml

    myprofile:
        keyid: GKTADJGHEIQSXMKKRBJ08H
        key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
        region: us-east-1

.. versionchanged:: 2015.8.0
    All methods now return a dictionary. Create and delete methods return:

    .. code-block:: yaml

        created: true

    or

    .. code-block:: yaml

        created: false
        error:
          message: error message

    Request methods (e.g., `describe_function`) return:

    .. code-block:: yaml

        function:
          - {...}
          - {...}

    or

    .. code-block:: yaml

        error:
          message: error message

"""

# keep lint from choking on _get_conn and _cache_id
# pylint: disable=E0602


import logging
import random
import time

import salt.utils.compat
import salt.utils.files
import salt.utils.json
import salt.utils.versions
from salt.exceptions import SaltInvocationError

log = logging.getLogger(__name__)


# pylint: disable=import-error
try:
    # pylint: disable=unused-import
    import boto
    import boto3
    from botocore import __version__ as found_botocore_version

    # pylint: enable=unused-import
    from botocore.exceptions import ClientError

    logging.getLogger("boto").setLevel(logging.CRITICAL)
    logging.getLogger("boto3").setLevel(logging.CRITICAL)
    HAS_BOTO = True
except ImportError:
    HAS_BOTO = False
# pylint: enable=import-error


def __virtual__():
    """
    Only load if boto libraries exist and if boto libraries are greater than
    a given version.
    """
    # the boto_lambda execution module relies on the connect_to_region() method
    # which was added in boto 2.8.0
    # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
    # botocore version >= 1.5.2 is required due to lambda environment variables
    return salt.utils.versions.check_boto_reqs(
        boto_ver="2.8.0", boto3_ver="1.2.5", botocore_ver="1.5.2"
    )


def __init__(opts):
    if HAS_BOTO:
        __utils__["boto3.assign_funcs"](__name__, "lambda")


def _find_function(name, region=None, key=None, keyid=None, profile=None):
    """
    Given function name, find and return matching Lambda information.
    """
    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)

    for funcs in __utils__["boto3.paged_call"](conn.list_functions):
        for func in funcs["Functions"]:
            if func["FunctionName"] == name:
                return func
    return None


def function_exists(FunctionName, region=None, key=None, keyid=None, profile=None):
    """
    Given a function name, check to see if the given function name exists.

    Returns True if the given function exists and returns False if the given
    function does not exist.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.function_exists myfunction

    """

    try:
        func = _find_function(
            FunctionName, region=region, key=key, keyid=keyid, profile=profile
        )
        return {"exists": bool(func)}
    except ClientError as e:
        return {"error": __utils__["boto3.get_error"](e)}


def _get_role_arn(name, region=None, key=None, keyid=None, profile=None):
    if name.startswith("arn:aws:iam:"):
        return name

    account_id = __salt__["boto_iam.get_account_id"](
        region=region, key=key, keyid=keyid, profile=profile
    )
    if profile and "region" in profile:
        region = profile["region"]
    if region is None:
        region = "us-east-1"
    return f"arn:aws:iam::{account_id}:role/{name}"


def _filedata(infile):
    with salt.utils.files.fopen(infile, "rb") as f:
        return f.read()


def _resolve_vpcconfig(conf, region=None, key=None, keyid=None, profile=None):
    if isinstance(conf, str):
        conf = salt.utils.json.loads(conf)
    if not conf:
        return None
    if not isinstance(conf, dict):
        raise SaltInvocationError("VpcConfig must be a dict.")
    sns = [
        __salt__["boto_vpc.get_resource_id"](
            "subnet", s, region=region, key=key, keyid=keyid, profile=profile
        ).get("id")
        for s in conf.pop("SubnetNames", [])
    ]
    sgs = [
        __salt__["boto_secgroup.get_group_id"](
            s, region=region, key=key, keyid=keyid, profile=profile
        )
        for s in conf.pop("SecurityGroupNames", [])
    ]
    conf.setdefault("SubnetIds", []).extend(sns)
    conf.setdefault("SecurityGroupIds", []).extend(sgs)
    return conf


def create_function(
    FunctionName,
    Runtime,
    Role,
    Handler,
    ZipFile=None,
    S3Bucket=None,
    S3Key=None,
    S3ObjectVersion=None,
    Description="",
    Timeout=3,
    MemorySize=128,
    Publish=False,
    WaitForRole=False,
    RoleRetries=5,
    region=None,
    key=None,
    keyid=None,
    profile=None,
    VpcConfig=None,
    Environment=None,
):
    """
    .. versionadded:: 2017.7.0

    Given a valid config, create a function.

    Environment
        The parent object that contains your environment's configuration
        settings. This is a dictionary of the form:

        .. code-block:: python

            {
                'Variables': {
                    'VariableName': 'VariableValue'
                }
            }

    Returns ``{'created': True}`` if the function was created and ``{created:
    False}`` if the function was not created.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.create_function my_function python2.7 my_role my_file.my_function my_function.zip
        salt myminion boto_lamba.create_function my_function python2.7 my_role my_file.my_function salt://files/my_function.zip

    """

    role_arn = _get_role_arn(Role, region=region, key=key, keyid=keyid, profile=profile)
    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        if ZipFile:
            if S3Bucket or S3Key or S3ObjectVersion:
                raise SaltInvocationError(
                    "Either ZipFile must be specified, or "
                    "S3Bucket and S3Key must be provided."
                )
            if "://" in ZipFile:  # Looks like a remote URL to me...
                dlZipFile = __salt__["cp.cache_file"](path=ZipFile)
                if dlZipFile is False:
                    ret["result"] = False
                    ret["comment"] = f"Failed to cache ZipFile `{ZipFile}`."
                    return ret
                ZipFile = dlZipFile
            code = {
                "ZipFile": _filedata(ZipFile),
            }
        else:
            if not S3Bucket or not S3Key:
                raise SaltInvocationError(
                    "Either ZipFile must be specified, or "
                    "S3Bucket and S3Key must be provided."
                )
            code = {
                "S3Bucket": S3Bucket,
                "S3Key": S3Key,
            }
            if S3ObjectVersion:
                code["S3ObjectVersion"] = S3ObjectVersion
        kwargs = {}
        if VpcConfig is not None:
            kwargs["VpcConfig"] = _resolve_vpcconfig(
                VpcConfig, region=region, key=key, keyid=keyid, profile=profile
            )
        if Environment is not None:
            kwargs["Environment"] = Environment
        if WaitForRole:
            retrycount = RoleRetries
        else:
            retrycount = 1
        for retry in range(retrycount, 0, -1):
            try:
                func = conn.create_function(
                    FunctionName=FunctionName,
                    Runtime=Runtime,
                    Role=role_arn,
                    Handler=Handler,
                    Code=code,
                    Description=Description,
                    Timeout=Timeout,
                    MemorySize=MemorySize,
                    Publish=Publish,
                    **kwargs,
                )
            except ClientError as e:
                if (
                    retry > 1
                    and e.response.get("Error", {}).get("Code")
                    == "InvalidParameterValueException"
                ):
                    log.info(
                        "Function not created but IAM role may not have propagated,"
                        " will retry"
                    )
                    # exponential backoff
                    time.sleep(
                        (2 ** (RoleRetries - retry)) + (random.randint(0, 1000) / 1000)
                    )
                    continue
                else:
                    raise
            else:
                break
        if func:
            log.info("The newly created function name is %s", func["FunctionName"])

            return {"created": True, "name": func["FunctionName"]}
        else:
            log.warning("Function was not created")
            return {"created": False}
    except ClientError as e:
        return {"created": False, "error": __utils__["boto3.get_error"](e)}


def delete_function(
    FunctionName, Qualifier=None, region=None, key=None, keyid=None, profile=None
):
    """
    Given a function name and optional version qualifier, delete it.

    Returns {deleted: true} if the function was deleted and returns
    {deleted: false} if the function was not deleted.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.delete_function myfunction

    """

    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        if Qualifier:
            conn.delete_function(FunctionName=FunctionName, Qualifier=Qualifier)
        else:
            conn.delete_function(FunctionName=FunctionName)
        return {"deleted": True}
    except ClientError as e:
        return {"deleted": False, "error": __utils__["boto3.get_error"](e)}


def describe_function(FunctionName, region=None, key=None, keyid=None, profile=None):
    """
    Given a function name describe its properties.

    Returns a dictionary of interesting properties.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.describe_function myfunction

    """

    try:
        func = _find_function(
            FunctionName, region=region, key=key, keyid=keyid, profile=profile
        )
        if func:
            keys = (
                "FunctionName",
                "Runtime",
                "Role",
                "Handler",
                "CodeSha256",
                "CodeSize",
                "Description",
                "Timeout",
                "MemorySize",
                "FunctionArn",
                "LastModified",
                "VpcConfig",
                "Environment",
            )
            return {"function": {k: func.get(k) for k in keys}}
        else:
            return {"function": None}
    except ClientError as e:
        return {"error": __utils__["boto3.get_error"](e)}


def update_function_config(
    FunctionName,
    Role=None,
    Handler=None,
    Description=None,
    Timeout=None,
    MemorySize=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
    VpcConfig=None,
    WaitForRole=False,
    RoleRetries=5,
    Environment=None,
):
    """
    .. versionadded:: 2017.7.0

    Update the named lambda function to the configuration.

    Environment
        The parent object that contains your environment's configuration
        settings. This is a dictionary of the form:

        .. code-block:: python

            {
                'Variables': {
                    'VariableName': 'VariableValue'
                }
            }

    Returns ``{'updated': True}`` if the function was updated, and
    ``{'updated': False}`` if the function was not updated.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.update_function_config my_function my_role my_file.my_function "my lambda function"

    """

    args = dict(FunctionName=FunctionName)
    options = {
        "Handler": Handler,
        "Description": Description,
        "Timeout": Timeout,
        "MemorySize": MemorySize,
        "VpcConfig": VpcConfig,
        "Environment": Environment,
    }

    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
    for val, var in options.items():
        if var:
            args[val] = var
    if Role:
        args["Role"] = _get_role_arn(Role, region, key, keyid, profile)
    if VpcConfig:
        args["VpcConfig"] = _resolve_vpcconfig(
            VpcConfig, region=region, key=key, keyid=keyid, profile=profile
        )
    try:
        if WaitForRole:
            retrycount = RoleRetries
        else:
            retrycount = 1
        for retry in range(retrycount, 0, -1):
            try:
                r = conn.update_function_configuration(**args)
            except ClientError as e:
                if (
                    retry > 1
                    and e.response.get("Error", {}).get("Code")
                    == "InvalidParameterValueException"
                ):
                    log.info(
                        "Function not updated but IAM role may not have propagated,"
                        " will retry"
                    )
                    # exponential backoff
                    time.sleep(
                        (2 ** (RoleRetries - retry)) + (random.randint(0, 1000) / 1000)
                    )
                    continue
                else:
                    raise
            else:
                break
        if r:
            keys = (
                "FunctionName",
                "Runtime",
                "Role",
                "Handler",
                "CodeSha256",
                "CodeSize",
                "Description",
                "Timeout",
                "MemorySize",
                "FunctionArn",
                "LastModified",
                "VpcConfig",
                "Environment",
            )
            return {"updated": True, "function": {k: r.get(k) for k in keys}}
        else:
            log.warning("Function was not updated")
            return {"updated": False}
    except ClientError as e:
        return {"updated": False, "error": __utils__["boto3.get_error"](e)}


def update_function_code(
    FunctionName,
    ZipFile=None,
    S3Bucket=None,
    S3Key=None,
    S3ObjectVersion=None,
    Publish=False,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Upload the given code to the named lambda function.

    Returns {updated: true} if the function was updated and returns
    {updated: False} if the function was not updated.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.update_function_code my_function ZipFile=function.zip

    """

    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
    try:
        if ZipFile:
            if S3Bucket or S3Key or S3ObjectVersion:
                raise SaltInvocationError(
                    "Either ZipFile must be specified, or "
                    "S3Bucket and S3Key must be provided."
                )
            r = conn.update_function_code(
                FunctionName=FunctionName, ZipFile=_filedata(ZipFile), Publish=Publish
            )
        else:
            if not S3Bucket or not S3Key:
                raise SaltInvocationError(
                    "Either ZipFile must be specified, or "
                    "S3Bucket and S3Key must be provided."
                )
            args = {
                "S3Bucket": S3Bucket,
                "S3Key": S3Key,
            }
            if S3ObjectVersion:
                args["S3ObjectVersion"] = S3ObjectVersion
            r = conn.update_function_code(
                FunctionName=FunctionName, Publish=Publish, **args
            )
        if r:
            keys = (
                "FunctionName",
                "Runtime",
                "Role",
                "Handler",
                "CodeSha256",
                "CodeSize",
                "Description",
                "Timeout",
                "MemorySize",
                "FunctionArn",
                "LastModified",
                "VpcConfig",
                "Environment",
            )
            return {"updated": True, "function": {k: r.get(k) for k in keys}}
        else:
            log.warning("Function was not updated")
            return {"updated": False}
    except ClientError as e:
        return {"updated": False, "error": __utils__["boto3.get_error"](e)}


def add_permission(
    FunctionName,
    StatementId,
    Action,
    Principal,
    SourceArn=None,
    SourceAccount=None,
    Qualifier=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Add a permission to a lambda function.

    Returns {added: true} if the permission was added and returns
    {added: False} if the permission was not added.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.add_permission my_function my_id "lambda:*" \\
                           s3.amazonaws.com aws:arn::::bucket-name \\
                           aws-account-id

    """

    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        kwargs = {}
        for key in ("SourceArn", "SourceAccount", "Qualifier"):
            if locals()[key] is not None:
                kwargs[key] = str(locals()[key])
        conn.add_permission(
            FunctionName=FunctionName,
            StatementId=StatementId,
            Action=Action,
            Principal=str(Principal),
            **kwargs,
        )
        return {"updated": True}
    except ClientError as e:
        return {"updated": False, "error": __utils__["boto3.get_error"](e)}


def remove_permission(
    FunctionName,
    StatementId,
    Qualifier=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Remove a permission from a lambda function.

    Returns {removed: true} if the permission was removed and returns
    {removed: False} if the permission was not removed.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.remove_permission my_function my_id

    """

    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        kwargs = {}
        if Qualifier is not None:
            kwargs["Qualifier"] = Qualifier
        conn.remove_permission(
            FunctionName=FunctionName, StatementId=StatementId, **kwargs
        )
        return {"updated": True}
    except ClientError as e:
        return {"updated": False, "error": __utils__["boto3.get_error"](e)}


def get_permissions(
    FunctionName, Qualifier=None, region=None, key=None, keyid=None, profile=None
):
    """
    Get resource permissions for the given lambda function

    Returns dictionary of permissions, by statement ID

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.get_permissions my_function

        permissions: {...}
    """

    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        kwargs = {}
        if Qualifier is not None:
            kwargs["Qualifier"] = Qualifier
        # The get_policy call is not symmetric with add/remove_permissions. So
        # massage it until it is, for better ease of use.
        policy = conn.get_policy(FunctionName=FunctionName, **kwargs)
        policy = policy.get("Policy", {})
        if isinstance(policy, str):
            policy = salt.utils.json.loads(policy)
        if policy is None:
            policy = {}
        permissions = {}
        for statement in policy.get("Statement", []):
            condition = statement.get("Condition", {})
            principal = statement.get("Principal", {})
            if "AWS" in principal:
                principal = principal["AWS"].split(":")[4]
            else:
                principal = principal.get("Service")
            permission = {
                "Action": statement.get("Action"),
                "Principal": principal,
            }
            if "ArnLike" in condition:
                permission["SourceArn"] = condition["ArnLike"].get("AWS:SourceArn")
            if "StringEquals" in condition:
                permission["SourceAccount"] = condition["StringEquals"].get(
                    "AWS:SourceAccount"
                )
            permissions[statement.get("Sid")] = permission
        return {"permissions": permissions}
    except ClientError as e:
        err = __utils__["boto3.get_error"](e)
        if e.response.get("Error", {}).get("Code") == "ResourceNotFoundException":
            return {"permissions": None}
        return {"permissions": None, "error": err}


def list_functions(region=None, key=None, keyid=None, profile=None):
    """
    List all Lambda functions visible in the current scope.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.list_functions

    """
    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)

    ret = []
    for funcs in __utils__["boto3.paged_call"](conn.list_functions):
        ret += funcs["Functions"]
    return ret


def list_function_versions(
    FunctionName, region=None, key=None, keyid=None, profile=None
):
    """
    List the versions available for the given function.

    Returns list of function versions

    CLI Example:

    .. code-block:: yaml

        versions:
          - {...}
          - {...}

    """
    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        vers = []
        for ret in __utils__["boto3.paged_call"](
            conn.list_versions_by_function, FunctionName=FunctionName
        ):
            vers.extend(ret["Versions"])
        if not bool(vers):
            log.warning("No versions found")
        return {"Versions": vers}
    except ClientError as e:
        return {"error": __utils__["boto3.get_error"](e)}


def create_alias(
    FunctionName,
    Name,
    FunctionVersion,
    Description="",
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Given a valid config, create an alias to a function.

    Returns {created: true} if the alias was created and returns
    {created: False} if the alias was not created.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.create_alias my_function my_alias $LATEST "An alias"

    """
    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        alias = conn.create_alias(
            FunctionName=FunctionName,
            Name=Name,
            FunctionVersion=FunctionVersion,
            Description=Description,
        )
        if alias:
            log.info("The newly created alias name is %s", alias["Name"])

            return {"created": True, "name": alias["Name"]}
        else:
            log.warning("Alias was not created")
            return {"created": False}
    except ClientError as e:
        return {"created": False, "error": __utils__["boto3.get_error"](e)}


def delete_alias(FunctionName, Name, region=None, key=None, keyid=None, profile=None):
    """
    Given a function name and alias name, delete the alias.

    Returns {deleted: true} if the alias was deleted and returns
    {deleted: false} if the alias was not deleted.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.delete_alias myfunction myalias

    """

    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        conn.delete_alias(FunctionName=FunctionName, Name=Name)
        return {"deleted": True}
    except ClientError as e:
        return {"deleted": False, "error": __utils__["boto3.get_error"](e)}


def _find_alias(
    FunctionName,
    Name,
    FunctionVersion=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Given function name and alias name, find and return matching alias information.
    """
    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)

    args = {"FunctionName": FunctionName}
    if FunctionVersion:
        args["FunctionVersion"] = FunctionVersion

    for aliases in __utils__["boto3.paged_call"](conn.list_aliases, **args):
        for alias in aliases.get("Aliases"):
            if alias["Name"] == Name:
                return alias
    return None


def alias_exists(FunctionName, Name, region=None, key=None, keyid=None, profile=None):
    """
    Given a function name and alias name, check to see if the given alias exists.

    Returns True if the given alias exists and returns False if the given
    alias does not exist.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.alias_exists myfunction myalias

    """

    try:
        alias = _find_alias(
            FunctionName, Name, region=region, key=key, keyid=keyid, profile=profile
        )
        return {"exists": bool(alias)}
    except ClientError as e:
        return {"error": __utils__["boto3.get_error"](e)}


def describe_alias(FunctionName, Name, region=None, key=None, keyid=None, profile=None):
    """
    Given a function name and alias name describe the properties of the alias.

    Returns a dictionary of interesting properties.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.describe_alias myalias

    """

    try:
        alias = _find_alias(
            FunctionName, Name, region=region, key=key, keyid=keyid, profile=profile
        )
        if alias:
            keys = ("AliasArn", "Name", "FunctionVersion", "Description")
            return {"alias": {k: alias.get(k) for k in keys}}
        else:
            return {"alias": None}
    except ClientError as e:
        return {"error": __utils__["boto3.get_error"](e)}


def update_alias(
    FunctionName,
    Name,
    FunctionVersion=None,
    Description=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Update the named alias to the configuration.

    Returns {updated: true} if the alias was updated and returns
    {updated: False} if the alias was not updated.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.update_alias my_lambda my_alias $LATEST

    """

    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        args = {}
        if FunctionVersion:
            args["FunctionVersion"] = FunctionVersion
        if Description:
            args["Description"] = Description
        r = conn.update_alias(FunctionName=FunctionName, Name=Name, **args)
        if r:
            keys = ("Name", "FunctionVersion", "Description")
            return {"updated": True, "alias": {k: r.get(k) for k in keys}}
        else:
            log.warning("Alias was not updated")
            return {"updated": False}
    except ClientError as e:
        return {"created": False, "error": __utils__["boto3.get_error"](e)}


def create_event_source_mapping(
    EventSourceArn,
    FunctionName,
    StartingPosition,
    Enabled=True,
    BatchSize=100,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Identifies a stream as an event source for a Lambda function. It can be
    either an Amazon Kinesis stream or an Amazon DynamoDB stream. AWS Lambda
    invokes the specified function when records are posted to the stream.

    Returns {created: true} if the event source mapping was created and returns
    {created: False} if the event source mapping was not created.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.create_event_source_mapping arn::::eventsource myfunction LATEST

    """
    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        obj = conn.create_event_source_mapping(
            EventSourceArn=EventSourceArn,
            FunctionName=FunctionName,
            Enabled=Enabled,
            BatchSize=BatchSize,
            StartingPosition=StartingPosition,
        )
        if obj:
            log.info("The newly created event source mapping ID is %s", obj["UUID"])

            return {"created": True, "id": obj["UUID"]}
        else:
            log.warning("Event source mapping was not created")
            return {"created": False}
    except ClientError as e:
        return {"created": False, "error": __utils__["boto3.get_error"](e)}


def get_event_source_mapping_ids(
    EventSourceArn, FunctionName, region=None, key=None, keyid=None, profile=None
):
    """
    Given an event source and function name, return a list of mapping IDs

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.get_event_source_mapping_ids arn:::: myfunction

    """

    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
    try:
        mappings = []
        for maps in __utils__["boto3.paged_call"](
            conn.list_event_source_mappings,
            EventSourceArn=EventSourceArn,
            FunctionName=FunctionName,
        ):
            mappings.extend(
                [mapping["UUID"] for mapping in maps["EventSourceMappings"]]
            )
        return mappings
    except ClientError as e:
        return {"error": __utils__["boto3.get_error"](e)}


def _get_ids(
    UUID=None,
    EventSourceArn=None,
    FunctionName=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    if UUID:
        if EventSourceArn or FunctionName:
            raise SaltInvocationError(
                "Either UUID must be specified, or "
                "EventSourceArn and FunctionName must be provided."
            )
        return [UUID]
    else:
        if not EventSourceArn or not FunctionName:
            raise SaltInvocationError(
                "Either UUID must be specified, or "
                "EventSourceArn and FunctionName must be provided."
            )
        return get_event_source_mapping_ids(
            EventSourceArn=EventSourceArn,
            FunctionName=FunctionName,
            region=region,
            key=key,
            keyid=keyid,
            profile=profile,
        )


def delete_event_source_mapping(
    UUID=None,
    EventSourceArn=None,
    FunctionName=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Given an event source mapping ID or an event source ARN and FunctionName,
    delete the event source mapping

    Returns {deleted: true} if the mapping was deleted and returns
    {deleted: false} if the mapping was not deleted.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.delete_event_source_mapping 260c423d-e8b5-4443-8d6a-5e91b9ecd0fa

    """
    ids = _get_ids(UUID, EventSourceArn=EventSourceArn, FunctionName=FunctionName)
    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        for id in ids:
            conn.delete_event_source_mapping(UUID=id)
        return {"deleted": True}
    except ClientError as e:
        return {"deleted": False, "error": __utils__["boto3.get_error"](e)}


def event_source_mapping_exists(
    UUID=None,
    EventSourceArn=None,
    FunctionName=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Given an event source mapping ID or an event source ARN and FunctionName,
    check whether the mapping exists.

    Returns True if the given alias exists and returns False if the given
    alias does not exist.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.alias_exists myfunction myalias

    """

    desc = describe_event_source_mapping(
        UUID=UUID,
        EventSourceArn=EventSourceArn,
        FunctionName=FunctionName,
        region=region,
        key=key,
        keyid=keyid,
        profile=profile,
    )
    if "error" in desc:
        return desc
    return {"exists": bool(desc.get("event_source_mapping"))}


def describe_event_source_mapping(
    UUID=None,
    EventSourceArn=None,
    FunctionName=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Given an event source mapping ID or an event source ARN and FunctionName,
    obtain the current settings of that mapping.

    Returns a dictionary of interesting properties.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lambda.describe_event_source_mapping uuid

    """
    ids = _get_ids(UUID, EventSourceArn=EventSourceArn, FunctionName=FunctionName)
    if not ids:
        return {"event_source_mapping": None}

    UUID = ids[0]
    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        desc = conn.get_event_source_mapping(UUID=UUID)
        if desc:
            keys = (
                "UUID",
                "BatchSize",
                "EventSourceArn",
                "FunctionArn",
                "LastModified",
                "LastProcessingResult",
                "State",
                "StateTransitionReason",
            )
            return {"event_source_mapping": {k: desc.get(k) for k in keys}}
        else:
            return {"event_source_mapping": None}
    except ClientError as e:
        return {"error": __utils__["boto3.get_error"](e)}


def update_event_source_mapping(
    UUID,
    FunctionName=None,
    Enabled=None,
    BatchSize=None,
    region=None,
    key=None,
    keyid=None,
    profile=None,
):
    """
    Update the event source mapping identified by the UUID.

    Returns {updated: true} if the alias was updated and returns
    {updated: False} if the alias was not updated.

    CLI Example:

    .. code-block:: bash

        salt myminion boto_lamba.update_event_source_mapping uuid FunctionName=new_function

    """

    try:
        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
        args = {}
        if FunctionName is not None:
            args["FunctionName"] = FunctionName
        if Enabled is not None:
            args["Enabled"] = Enabled
        if BatchSize is not None:
            args["BatchSize"] = BatchSize
        r = conn.update_event_source_mapping(UUID=UUID, **args)
        if r:
            keys = (
                "UUID",
                "BatchSize",
                "EventSourceArn",
                "FunctionArn",
                "LastModified",
                "LastProcessingResult",
                "State",
                "StateTransitionReason",
            )
            return {
                "updated": True,
                "event_source_mapping": {k: r.get(k) for k in keys},
            }
        else:
            log.warning("Mapping was not updated")
            return {"updated": False}
    except ClientError as e:
        return {"created": False, "error": __utils__["boto3.get_error"](e)}
