# coding=utf-8
import collections.abc as collections
import re
import warnings
from typing import Any, Dict, List, Optional, Union

from urbanairship.enums import LiveActivityEvent, LiveUpdateEvent

# Valid autobadge values: auto, +N, -N
VALID_AUTOBADGE = re.compile(r"^(auto|[+-][\d]+)$")
VALID_ICON_COLOR = re.compile(r"^#[0-9a-f]{6}$")
VALID_ANDROID_CATEGORIES: List[str] = [
    "alarm",
    "call",
    "email",
    "err",
    "event",
    "msg",
    "promo",
    "recommendation",
    "service",
    "social",
    "status",
    "sys",
    "transport",
]


def notification(
    alert: Optional[str] = None,
    ios: Optional[Dict] = None,
    android: Optional[Dict] = None,
    amazon: Optional[Dict] = None,
    web: Optional[Dict] = None,
    wns: Optional[Dict] = None,
    actions: Optional[Dict] = None,
    interactive: Optional[Dict] = None,
    in_app: Optional[Dict] = None,
    open_platform: Optional[Dict] = None,
    sms: Optional[Dict] = None,
    email: Optional[Dict] = None,
) -> Dict[str, Any]:
    """Create a notification payload.

    :keyword alert: A simple text alert, applicable for all platforms.
    :keyword ios: An iOS platform override, as generated by :py:func:`ios`.
    :keyword android: An Android platform override, as generated by
        :py:func:`android`.
    :keyword amazon: An Amazon platform override, as generated by
        :py:func:`amazon`.
    :keyword web: A web platform override, as generated by :py:func:`web`.
    :keyword wns: A WNS platform override, as generated by :py:func:`wns`.
    :keyword sms: An Sms platform override as generated by :py:func:`sms`.
    :keyword actions: Used to perform a defined task.
    :keyword interactive: A dictionary with two attributes -- "type" and
        "button_actions", as generated by :py:func:`interactive`.
    :keyword open_platform: A dictionary with open platform name keys, and
        open platform overrides, as generated by :py:func:`open_platform`.

    """
    payload: Dict[str, Any] = {}
    if alert is not None:
        payload["alert"] = alert
    if actions is not None:
        payload["actions"] = actions
    if ios is not None:
        payload["ios"] = ios
    if android is not None:
        payload["android"] = android
    if amazon is not None:
        payload["amazon"] = amazon
    if web is not None:
        payload["web"] = web
    if wns is not None:
        payload["wns"] = wns
    if sms is not None:
        payload["sms"] = sms
    if email is not None:
        payload["email"] = email
    if interactive is not None:
        payload["interactive"] = interactive
    if in_app is not None:
        payload["in_app"] = in_app
    if open_platform is not None:
        for platform, overrides in open_platform.items():
            payload["open::" + platform] = overrides
    if not payload:
        raise ValueError("Notification body may not be empty")
    return payload


def ios(
    alert: Union[str, Dict, None] = None,
    badge: Optional[Union[int, str]] = None,
    sound: Optional[Union[str, Dict]] = None,
    content_available: bool = False,
    extra: Optional[Dict[str, Any]] = None,
    expiry: Optional[Union[int, str]] = None,
    interactive: Optional[Dict[str, Any]] = None,
    category: Optional[str] = None,
    title: Optional[str] = None,
    mutable_content: Optional[bool] = None,
    subtitle: Optional[str] = None,
    media_attachment: Optional[Dict] = None,
    priority: Optional[int] = None,
    collapse_id: Optional[str] = None,
    thread_id: Optional[str] = None,
    interruption_level: Optional[str] = None,
    relevance_score: Optional[float] = None,
    target_content_id: Optional[str] = None,
    live_activity: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """iOS specific platform override payload.

    :keyword alert: iOS format alert, as either a string or dictionary. If
        a dictionary, the accepted keys must match those in
        `Apple's Payload Key Reference
        <https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html>`_.
        Some options are only available with certain iOS versions on the device being
        sent to.
    :keyword badge: An integer badge value or an *autobadge* string.
    :keyword sound: If a string, the name of a sound file in the app bundle.
        If a dict, the 'name' key specifies the name of the sound file. Some
        options require iOS 12 or above on the device.
    :keyword content_available: If true, the payload is delivered to your app
        in the background. This flag is automatically set to true if there
        is an In-App Message in the payload.
    :keyword extra: A set of key/value pairs to include in the push payload
        sent to the device.
    :keyword expiry: An integer or time set in UTC as a string
    :keyword interactive: A dictionary with two attributes -- "type" and
        "button_actions", as generated by :py:func:`interactive`.
    :keyword category: A keyword used to categorize the notification. Must be
        a string
    :keyword title: Sets the title of the notification for Apple Watch. Must
        be a string
    :keyword mutable_content: Optional, a boolean. Defaults to false.
        When set to true, content may be modified by an extension.
        This flag will be automatically set to true if there is a
        media_attachment in the payload. iOS 10 or above.
    :keyword subtitle: Optional, a string that will display below the title of
        the notification.
        This is provided as a convenience for setting the subtitle in the alert
        JSON object.
        If a subtitle is also defined in the alert JSON object, this value is
        ignored. iOS 10 or above.
    :keyword media_attachment: Optional, a dictionary object `Media Attachment
        <https://docs.urbanairship.com/api/ua/#media-attachment>`_.
        Specifies a media attachment to be handled by the UA Media Attachment
        Extension.
    :keyword collapse_id: Optional, a string. When there is a newer message
        that renders an older, related message irrelevant to the client app,
        the new message replaces the older message with the same collapse_id.
        Similar to the GCM collapse_key. The value of this key must not exceed
        64 bytes. iOS 10 or above.
    :keyword thread_id: Optional, a string. A unique identifier used to group
        notifications into separate threads in the Notification Center and on
        the lock screen. Grouped notifications are available beginning
        with iOS 12.
    :keyword interruption_level: Optional, a string. Indicates the importance and
        delivery timing of a notification. Must be one of: passive, active,
        time-sensitive, critical. Note: Use of the 'critical' levels requires an
        entitlement grant from Apple. Once this grant has been enabled, contact Airship
        Support to enable support with our APIs.
    :keyword relevance_score: Optional, a number from 0.0 to 1.0. Used to sort
        notifications for an app. The notification with highest score is featured in
        the notification summary.
    :target_content_id: Optional, a string. The identifier of the window to bring
        forward when the notification is opened. Used for multi-window content, such as
        App Clips.
    :keyword live_activity: Optional, JSON object that represents the content sent
        to a Live Activity Notification.

    >>> ios(alert='Hello!', sound='cat.caf',
    ...     extra={'articleid': '12345'}) # doctest: +SKIP
    {'sound': 'cat.caf', 'extra': {'articleid': '12345'}, 'alert': 'Hello!'}

    """

    payload: Dict[str, Any] = {}
    if alert is not None:
        if not (isinstance(alert, (str, dict))):
            raise ValueError("iOS alert must be a string or dictionary")
        payload["alert"] = alert
    if badge is not None:
        if not (isinstance(badge, (str, int))):
            raise ValueError("iOS badge must be an integer or string")
        if isinstance(badge, str) and not VALID_AUTOBADGE.match(badge):
            raise ValueError("Invalid iOS autobadge value")
        payload["badge"] = badge
    if sound is not None:
        if not (isinstance(sound, (str, dict))):
            raise ValueError("iOS sound must a string or dictionary")
        if isinstance(sound, dict) and "name" not in sound:
            raise ValueError("if iOS sound is a dict, name key must be used")
        payload["sound"] = sound
    if content_available:
        payload["content-available"] = 1
    if extra is not None:
        payload["extra"] = extra
    if expiry is not None:
        if not (isinstance(expiry, (str, int))):
            raise ValueError("iOS expiry must be an integer or string")
        payload["expiry"] = expiry
    if interactive is not None:
        payload["interactive"] = interactive
    if category is not None:
        if not (isinstance(category, str)):
            raise ValueError("iOS category must be a string")
        payload["category"] = category
    if title is not None:
        if not (isinstance(title, str)):
            raise ValueError("iOS title must be a string")
        payload["title"] = title
    if mutable_content is not None:
        payload["mutable_content"] = mutable_content
    if subtitle is not None:
        payload["subtitle"] = subtitle
    if media_attachment is not None:
        payload["media_attachment"] = media_attachment
    if priority is not None:
        if priority not in {10, 5}:
            raise ValueError("iOS priority must be set to one of 5 or 10.")
        payload["priority"] = priority
    if collapse_id is not None:
        if not (isinstance(collapse_id, str)):
            raise ValueError("iOS collapse_id must be a string")
        payload["collapse_id"] = collapse_id
    if thread_id is not None:
        if not (isinstance(thread_id, str)):
            raise ValueError("iOS therad_id must be a string")
        payload["thread_id"] = thread_id
    if interruption_level is not None:
        if interruption_level not in [
            "passive",
            "active",
            "time-sensitive",
            "critical",
        ]:
            raise ValueError("Value for interruption level not valid")
        payload["interruption_level"] = interruption_level
    if relevance_score is not None:
        if 0.0 >= relevance_score >= 1.0:
            raise ValueError("relevance_score must be a float between 0.0 and 1.0")
        payload["relevance_score"] = relevance_score
    if target_content_id is not None:
        if not isinstance(target_content_id, str):
            raise ValueError("target_content_id must be a string")
        payload["target_content_id"] = target_content_id
    if live_activity is not None:
        payload["live_activity"] = live_activity

    return payload


def android(
    alert: Optional[str] = None,
    collapse_key: Optional[str] = None,
    time_to_live: Optional[int] = None,
    delay_while_idle: Optional[bool] = False,
    extra: Optional[Dict] = None,
    interactive: Optional[Dict] = None,
    local_only: Optional[bool] = None,
    wearable: Optional[Dict] = None,
    delivery_priority: Optional[str] = None,
    style: Optional[Dict] = None,
    title: Optional[str] = None,
    summary: Optional[str] = None,
    sound: Optional[str] = None,
    priority: Optional[int] = None,
    category: Optional[str] = None,
    visibility: Optional[int] = None,
    public_notification: Optional[Dict] = None,
    notification_tag: Optional[str] = None,
    notification_channel: Optional[str] = None,
    icon: Optional[str] = None,
    icon_color: Optional[str] = None,
    live_update: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Android specific platform override payload.

    All keyword arguments are optional.

    :keyword alert: String alert text.
    :keyword collapse_key: String
    :keyword time_to_live: Integer
    :keyword delay_while_idle: Boolean
    :keyword extra: A set of key/value pairs to include in the push payload
        sent to the device. All values must be strings.
    :keyword interactive: A dictionary with two attributes -- "type" and
        "button_actions", as generated by :py:func:`interactive`.
    :keyword local_only: Optional value for not showing the notification on
        wearable devices. Defaults to False.
    :keyword wearable: Optional object to define a wearable notification
        with the following optional fields: background_image, extra_pages, and
        interactive.
    :keyword delivery_priority: Optional string of either 'high' or 'normal'.
        Sets the GCM priority.
    :keyword style: Optional object. Defines an advanced style.
    :keyword title: Optional string. Represents the title of the notification.
    :keyword summary: Optional string. Represents a summary of the
        notification.
    :keyword sound: Optional string. Represents a sound file name included
        in the app resources.
    :keyword priority: Optional integer between -2 and 2. An Android L feature
        that determines location sort order.
    :keyword category: Optional string. An Android category.
    :keyword visibility: Optional integer between -1 and 1.
    :keyword public_notification: Optional object. A notification to show on
        the lock screen instead instead of the redacted one.
    :keyword notification_tag: Optional string. A string identifier used to
        replace an existing notification in the notification drawer.
    :keyword notification_channel: Optional string.  An identifier of a
        notification channel predefined by the application.
    :keyword icon: Optional string. The name of a drawable in the
        application's resource directory.
    :keyword icon_color: Optional. A string in the format of #rrggbb that
        defines the notification accent color.
    :keyword live_update: Optional, a JSON object that represents the content sent
        to a Live Update notification.

    See
    `GCM Advanced Topics <http://developer.android.com/google/gcm/adv.html>`_
    for details on ``collapse_key``, ``time_to_live``, and
    ``delay_while_idle``.

    >>> android(alert='Hello!', extra={'articleid': '12345'}) # doctest: +SKIP
    {'extra': {'articleid': '12345'}, 'alert': 'Hello!'}

    """
    payload: Dict[str, Any] = {}
    if alert is not None:
        payload["alert"] = alert
    if collapse_key is not None:
        payload["collapse_key"] = collapse_key
    if time_to_live is not None:
        payload["time_to_live"] = time_to_live
        if not (isinstance(time_to_live, (str, int))):
            raise ValueError(
                "Android time_to_live value must be an " "integer or time set in UTC as a string"
            )
    if delay_while_idle:
        payload["delay_while_idle"] = True
    if extra is not None:
        payload["extra"] = extra
    if interactive is not None:
        payload["interactive"] = interactive
    if local_only is not None:
        if not (isinstance(local_only, bool)):
            raise ValueError("Android local_only must be a boolean value")
        payload["local_only"] = local_only
    if wearable is not None:
        if not (isinstance(wearable, dict)):
            raise ValueError("Android wearable must be a dictionary")
        payload["wearable"] = wearable
    if delivery_priority is not None:
        if delivery_priority not in {"high", "normal"}:
            raise ValueError("delivery_priority must be set to one of 'high' or 'normal'.")
        payload["delivery_priority"] = delivery_priority
    if style is not None:
        payload["style"] = style
    if title is not None:
        payload["title"] = title
    if summary is not None:
        payload["summary"] = summary
    if sound is not None:
        payload["sound"] = sound
    if priority is not None:
        if priority not in range(-2, 3):
            raise ValueError(
                "priority must be set to one of {}.".format(
                    ", ".join([str(i) for i in range(-2, 3)])
                )
            )
        payload["priority"] = priority
    if category is not None:
        if category not in VALID_ANDROID_CATEGORIES:
            raise ValueError(
                "category must be set to one of {}.".format(", ".join(VALID_ANDROID_CATEGORIES))
            )
        payload["category"] = category
    if visibility is not None:
        if visibility not in range(-1, 2):
            raise ValueError(
                "visibility must be set to one of {}.".format(
                    ", ".join([str(i) for i in range(-1, 2)])
                )
            )
        payload["visibility"] = visibility
    if public_notification is not None:
        payload["public_notification"] = public_notification
    if notification_tag is not None:
        payload["notification_tag"] = notification_tag
    if notification_channel is not None:
        payload["notification_channel"] = notification_channel
    if icon is not None:
        payload["icon"] = icon
    if icon_color is not None:
        if isinstance(icon_color, str) and not VALID_ICON_COLOR.match(icon_color):
            raise ValueError("icon_color must be in format #rrggbb")
        payload["icon_color"] = icon_color
    if live_update is not None:
        payload["live_update"] = live_update

    return payload


def amazon(
    alert: Optional[str] = None,
    consolidation_key: Optional[str] = None,
    expires_after: Union[str, int, None] = None,
    extra: Optional[Dict] = None,
    title: Optional[str] = None,
    summary: Optional[str] = None,
    interactive: Optional[Dict] = None,
    style: Optional[str] = None,
    sound: Optional[str] = None,
    icon: Optional[str] = None,
    icon_color: Optional[str] = None,
    notification_channel: Optional[str] = None,
    notification_tag: Optional[str] = None,
) -> Dict[str, Any]:
    """Amazon specific platform override payload.

    All keyword arguments are optional.

    :keyword alert: String alert text.
    :keyword consolidation_key: String
    :keyword expires_after: Integer or UTC time (string)
    :keyword extra: A set of key/value pairs to include in the push payload
        sent to the device. All values must be strings.
    :keyword title: String
    :keyword summary: String
    :keyword interactive: A dictionary with two attributes -- "type" and
        "button_actions", as generated by :py:func:`interactive`.
    :keyword notification_tag: Optional string. A string identifier used to
        replace an existing notification in the notification drawer.
    :keyword notification_channel: Optional string. An identifier of a
        notification channel predefined by the application.
    :keyword icon: Optional string. The name of a drawable in the
        application's resource directory.
    :keyword icon_color: Optional. A string in the format of #rrggbb that
        defines the notification icon accent color.


    >>> amazon(alert='Hello!', title='My Title',
    ...     extra={'articleid': '12345'}) # doctest: +SKIP
    {'title': 'My Title', 'extra': {'articleid': '12345'}, 'alert': 'Hello!'}

    """
    payload: Dict[str, Any] = {}
    if alert is not None:
        payload["alert"] = alert
    if consolidation_key is not None:
        payload["consolidation_key"] = consolidation_key
    if expires_after is not None:
        payload["expires_after"] = expires_after
        if not (isinstance(expires_after, (str, int))):
            raise ValueError(
                "Amazon time_to_live value must be an " "integer or time set in UTC as a string"
            )
    if extra is not None:
        payload["extra"] = extra
    if title is not None:
        payload["title"] = title
    if summary is not None:
        payload["summary"] = summary
    if interactive is not None:
        payload["interactive"] = interactive
    if style is not None:
        payload["style"] = style
    if sound is not None:
        payload["sound"] = sound
    if notification_tag is not None:
        payload["notification_tag"] = notification_tag
    if notification_channel is not None:
        payload["notification_channel"] = notification_channel
    if icon is not None:
        payload["icon"] = icon
    if icon_color is not None:
        if isinstance(icon_color, str) and not VALID_ICON_COLOR.match(icon_color):
            raise ValueError("icon_color must be in format #rrggbb")
        payload["icon_color"] = icon_color

    return payload


def web(
    alert: Optional[str] = None,
    extra: Optional[Dict] = None,
    icon: Optional[Dict] = None,
    title: Optional[str] = None,
    interactive: Optional[Dict] = None,
    time_to_live: Union[str, int, None] = None,
    require_interaction: Optional[bool] = None,
) -> Dict[str, Any]:
    """Web specific platform override payload.

    All keyword arguments are optional

    :keyword alert: An optional string alert text.
    :keyword extra: An optional set of key/value pairs to include in the push
        payload sent to the device.
    :keyword icon: An optional JSON dictionary of string key and value pairs.
        Key must be 'url' and value, an HTTPS URL to the icon resource.
    :keyword title: Optional string. Represents the title of the notification.
    :keyword interactive: An optional dictionary with two attributes --
        ``type`` and ``button_actions``, as generated by :py:func:`interactive`
    :keyword time_to_live: An optional integer or time set in UTC as a string
    :keyword require_interaction: An optional boolean value that requires user
        interaction on the notification before it is dismissed.


    >>> web(extra={'articleid': '12345'},
    ...    icon={'url': 'https://example.com/icon.png'},
    ...    title='This is a title!') # doctest: +SKIP
    {'title': 'This is a title!', 'extra': {'articleid': '12345'},
     'icon': {'url': 'https://example.com/icon.png'}, 'alert': 'Hello!'}

    """
    payload: Dict[str, Any] = {}
    if alert is not None:
        payload["alert"] = alert
    if extra is not None:
        payload["extra"] = extra
    if icon is not None:  # todo: possibly set 'url' key
        payload["icon"] = icon
        if not isinstance(icon, dict):
            raise TypeError("icon must be a dictionary")
    if title is not None:
        payload["title"] = title
    if interactive is not None:
        payload["interactive"] = interactive
    if time_to_live is not None:
        if not (isinstance(time_to_live, (str, int))):
            raise ValueError("Web time_to_live must be an integer or string")
        payload["time_to_live"] = time_to_live
    if require_interaction is not None:
        if not (isinstance(require_interaction, (bool))):
            raise ValueError("require_interaction must be boolean type")
        payload["require_interaction"] = require_interaction
    return payload


def sms(
    alert: Optional[str] = None,
    expiry: Union[str, int, None] = None,
    template_alert: Optional[str] = None,
    shorten_links: Optional[bool] = None,
) -> Dict[str, Any]:
    """Sms platform specific override payload
    See: https://docs.airship.com/api/ua/#schemas-smsoverrideobject
    One of alert or template_alert must be used.

    :keyword alert: An optional string. Overrides the alert provided at the
        top level of the notification for Sms channels.
        The maximum length of an Sms alert is 1600 characters.
    :keyword template_alert: for use with CreateAndSendPush. Substitutions must be added
        to the Sms objects passed to CreateAndSendPush that match the bracketed
        fields in this alert string.
    :keyword expiry: Optional. Delivery expiration, as either absolute
        UTC timestamp (string), or number of seconds from now (integer).
    :keyword shorten_links: Optional. If true, Airship will shorten http/https links
        (space delimited) in the message text fields, producing unique, 25 character
        URLs for each member of your audience. If using this feature, please see the
        documentation linked above.


    >>> sms(alert='sms override alert!', expiry='2018-04-01T12:00:00') # doctest: +SKIP
    {'alert': 'sms override alert!', 'expiry': '2018-04-01T12:00:00'}
    """
    payload: Dict[str, Any] = {}
    if not alert and not template_alert:
        raise ValueError("One of alert and template_alert must be set.")
    elif alert and template_alert:
        raise ValueError("alert and template_alert cannot both be used.")

    if alert is not None:
        payload = {"alert": alert}
    elif template_alert is not None:
        payload = {"template": {"fields": {"alert": template_alert}}}

    if expiry is not None:
        if not (isinstance(expiry, (str, int))):
            raise ValueError("expiry value must be a string or integer")
        payload["expiry"] = expiry

    if shorten_links is not None:
        if not isinstance(shorten_links, bool):
            raise ValueError("shorten_links must be a boolean")
        payload["shorten_links"] = shorten_links

    return payload


def mms(
    fallback_text: str,
    content_type: str,
    url: str,
    shorten_links: Optional[bool] = None,
    content_length: Optional[int] = None,
    text: Optional[str] = None,
    subject: Optional[str] = None,
) -> Dict[str, Any]:
    """
    Provides the content for a push to MMS channels. If sms is in the `device_type`
    array, your request may include this object. It cannot be combined with an
    SMS Platform Override as a single push can only include either an SMS or MMS
    payload.
    Please see the documentation here: https://docs.airship.com/api/ua/#schemas-mmsoverrideobject

    :param fallback_text: Required. If a member of your audience cannot receive MMS
        messages, they will receive your fallback text with a link to the
        original content.
    :param content_type: Required. The MIME type of the image specified in the URL.
        The MIME type must match the extension in the url.
    :param url: Required. The http or https URL of the media attachment for the slide.
        The URL must end in one of .gif, .jpeg, .jpg, .png, or .vcf (case-insensitive).
        While the content_length field is optional, your image must still be under 2MB
        for JPEGs and PNGs, 1 MB for GIFs, or 2048000 bytes for text/vcard.
    :param shorten_links: Optional. If true, Airship will shorten http/https links
        (space delimited) in the message text fields, producing unique, 25 character
        URLs for each member of your audience. If using this feature, please see the
        documentation linked above.
    :param content_length: Optional. The length of the attachment in bytes. If using
        this feature, please see the documentation linked above.
    :param text: Optional. Text that you want to display along with the media
        attachment. The order of media and text in the message is not guaranteed.
    :param subject: Optional. The subject for the MMS message, normally displayed in
        bold. The subject might not appear for recipients if the Sender is a Toll-Free
        number.
    """
    slides: List = [{"media": {"content_type": content_type, "url": url}}]
    if text is not None:
        slides[0]["text"] = text
    if content_length is not None:
        slides[0]["media"]["content_length"] = content_length

    payload: Dict[str, Any] = {"fallback_text": fallback_text, "slides": slides}

    if shorten_links is not None:
        payload["shorten_links"] = shorten_links

    if subject is not None:
        payload["subject"] = subject

    return {"mms": payload}


def email(
    message_type: str,
    plaintext_body: str,
    reply_to: str,
    sender_address: str,
    sender_name: str,
    subject: str,
    attachments: Optional[List[str]] = None,
    html_body: Optional[str] = None,
    variable_defaults: Optional[Dict[str, str]] = None,
    bcc: Optional[List[str]] = None,
    bypass_opt_in_level: Optional[bool] = None,
    click_tracking: Optional[bool] = None,
    open_tracking: Optional[bool] = None,
):
    """
    Required platform override when email in ua.device_types.
    Specifies content of the email being sent. All other notification
    fields are not used in the email if included.

    See https://docs.airship.com/api/ua/#schemas-emailoverrideobject
    for additional caveats.

    :param message_type: Required. One of transactional or commercial.
    :param plaintext_body: Required. The plain text body of the email. May include
        variable substitution fields for use with create and send inline templates.
    :param reply_to: Required. The reply-to address.
    :param sender_address: Required. The email address of the sender.
        The domain of the email must be enabled in the email
        provider at the delivery tier.
    :param sender_name: Required. The common name of the email sender.
    :param subject: Required. The subject line of the email. May include variable
        substitution fields for use with create and send inline templates.
    :param html_body: Optional. The HTML content of the email.
    :param variable_details: Required for CreateAndSendPush with inline templates.
        a list of dicts of default values for inline template variables.
    :param attachments: Optional. A list of `attachment_id` values from the API.
    :param bcc: Optional. A list of email addresses that you want to blind copy on
        this email. Using addresses that Airship has not enabled for BCC will return
        a 400.
    :param bypass_opt_in_level: Optional. You can set this toggle when `message_type` is
        set to `transactional` to send a business critical email. If true, the message
        will be sent to your entire audience, ignoring `transactional_opted_out` status.
    :param click_tracking: Optional. `True` by default. Set to `False` to prevent click
        tracking for GDPR compliance.
    :param open_tracking: Optional. `True` by default. Set to `False` to prevent "open"
        tracking for GDPR compliance.
    """
    if message_type not in ("transactional", "commercial"):
        raise ValueError("message_type must be either transactional or commercial")

    payload: Dict[str, Any] = {}
    payload["message_type"] = message_type
    payload["reply_to"] = reply_to
    payload["sender_address"] = sender_address
    payload["sender_name"] = sender_name

    alert_payload: Dict[str, Any] = {
        "subject": subject,
        "plaintext_body": plaintext_body,
    }
    if html_body is not None:
        alert_payload["html_body"] = html_body

    if attachments is not None:
        if not isinstance(attachments, list):
            raise ValueError("attachments must be a list.")
        payload["attachments"] = [{"id": id} for id in attachments]

    if bcc is not None:
        alert_payload["bcc"] = bcc

    if bypass_opt_in_level is not None:
        alert_payload["bypass_opt_in_level"] = bypass_opt_in_level

    if click_tracking is not None:
        if not isinstance(click_tracking, bool):
            raise ValueError("click_tracking must be a boolean value.")
        alert_payload["click_tracking"] = click_tracking

    if open_tracking is not None:
        if not isinstance(open_tracking, bool):
            raise ValueError("open_tracking must be a boolean value.")
        alert_payload["open_tracking"] = open_tracking

    if variable_defaults is not None:
        payload["template"] = {
            "variable_defaults": variable_defaults,
            "fields": alert_payload,
        }
    else:
        payload.update(alert_payload)

    return payload


def open_platform(
    alert: Optional[str] = None,
    title: Optional[str] = None,
    extra: Optional[Dict[str, Any]] = None,
    summary: Optional[str] = None,
    media_attachment: Optional[str] = None,
    interactive: Optional[Dict[str, Any]] = None,
    template_alert: Optional[str] = None,
) -> Dict[str, Any]:
    """Open platform specific override payload.

    All keyword arguments are optional.

    :keyword alert: String alert text.
    :keyword title: String, represents the title of the notification.
    :keyword summary: A string value for providing a content summary.
    :keyword media_attachment: A URI for an image or video somewhere on the
        internet.
    :keyword extra: A set of key/value pairs to include in the push payload
        sent to the device.
    :keyword interactive: An interactive payload override, as generated by
        :py:func:`interactive`. Included button actions must be of type
        ``add_tag``, ``remove tag``, ``app_defined``, or ``open`` with subtype
        ``url``.
    :keyword template_alert: For use with CreateAndSendPush. A string with
        inline template substitution fields. The name of these must match
        subsititutions on the OpenChannel objects used with CreateAndSendPush.

    >>> sms_overrides = open_platform(
    ...    alert='Hello!',
    ...    title='See my new homepage!',
    ...    summary='A longer summary of some content',
    ...    media_attachment='https://example.com/cat_standing_up.jpeg',
    ...    extra={'some_info': 'for sms only'},
    ...    interactive=interactive(
    ...            type='ua_yes_no_foreground',
    ...            button_actions={
    ...                'yes': actions(open_={
    ...                    'type':'url',
    ...                    'content':'https://www.urbanairship.com'
    ...                    }),
    ...                'no': actions(app_defined={'foo': 'bar'})
    ...            }
    ...        ) # doctest: +SKIP
    {'alert': 'Hello!', 'title': 'See my new homepage!', 'summary':
    'A longer summary of some content', 'media_attachment':
    'https://example.com/cat_standing_up.jpeg', 'extra': {'some_info':
    'for sms only'}, 'interactive': {'type': 'ua_yes_no_foreground',
    'button_actions': { 'yes': {'open': {'type': 'url', 'content':
    'http://www.urbanairship.com'}}, 'no': {'app_defined': {'foo': 'bar'}}}}}

    >>> notification(open_platform={'sms': sms_overrides}) # doctest: +SKIP

    """
    payload: Dict[str, Any] = {}
    if extra is not None:
        payload["extra"] = extra
    if interactive is not None:
        payload["interactive"] = interactive

    alert_payload: Dict[str, Any] = {}
    if title is not None:
        alert_payload["title"] = title
    if summary is not None:
        alert_payload["summary"] = summary
    if media_attachment is not None:
        alert_payload["media_attachment"] = media_attachment

    if template_alert is not None:
        alert_payload["alert"] = template_alert
        payload = {"template": {"fields": alert_payload}}
    else:
        alert_payload["alert"] = alert
        payload.update(alert_payload)

    return payload


def wns_payload(
    alert: Optional[str] = None,
    toast: Optional[str] = None,
    tile: Optional[str] = None,
    badge: Optional[str] = None,
) -> Dict[str, Any]:
    """WNS specific platform override payload.

    Must include exactly one of ``alert``, ``toast``, ``tile``, or ``badge``.

    """
    if sum(1 for x in (alert, toast, tile, badge) if x) != 1:
        raise ValueError("WNS payload must have one notification type.")
    payload: Dict[str, Any] = {}
    if alert is not None:
        payload["alert"] = alert
    if toast is not None:
        payload["toast"] = toast
    if tile is not None:
        payload["tile"] = tile
    if badge is not None:
        payload["badge"] = badge
    return payload


def message(
    title: str,
    body: str,
    content_type: Optional[str] = None,
    content_encoding: Optional[str] = None,
    extra: Optional[Dict[str, Any]] = None,
    expiry: Union[str, int, None] = None,
    icons: Optional[Dict] = None,
    options: Optional[Dict[Any, Any]] = None,
    campaigns: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Rich push message payload creation.

    :param title: Required, string
    :param body: Required, string
    :keyword content_type: Optional, MIME type of the body
    :keyword content_encoding: Optional, encoding of the data in body,
        e.g. ``utf-8``.
    :keyword extra: Optional, dictionary of string values.
    :keyword expiry: time when message will delete from Inbox
        (UTC time or in seconds)
    :keyword icons: Optional JSON dictionary of string key and value
        pairs. Values must be URIs or URLs to the icon resources
    :keyword options: Optional JSON dictionary of key and value pairs
        specifying non-payload options
    :keyword campaigns: Optional dictionary containing list of 1 to
        10 'categories'

    """
    payload: Dict[str, Any] = {"title": title, "body": body}
    if content_type is not None:
        payload["content_type"] = content_type
    if content_encoding is not None:
        payload["content_encoding"] = content_encoding
    if extra is not None:
        payload["extra"] = extra
    if expiry is not None:
        payload["expiry"] = expiry
        if not (isinstance(expiry, (str, int))):
            raise ValueError("Expiry value must be an " "integer or time set in UTC as a string")
    if icons is not None:
        if not isinstance(icons, dict):
            raise TypeError("icons must be a dictionary")
        payload["icons"] = icons
    if options is not None:
        if not isinstance(options, dict):
            raise TypeError("options must be a dictionary")
        payload["options"] = options
    if campaigns is not None:
        if not isinstance(campaigns, dict):
            raise TypeError("campaigns must be a dictionary")
        payload["campaigns"] = campaigns

    return payload


def in_app(
    alert: str,
    display_type: str,
    expiry: Optional[str] = None,
    display: Optional[Dict[str, Any]] = None,
    actions: Optional[Dict[str, Any]] = None,
    interactive: Optional[Dict[str, Any]] = None,
    extra: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """In-App push message payload creation.

    :param alert: Required, string
    :param display_type: Required, String. List of valid display_type_
        attributes.
    :keyword expiry: Optional, String specifying an expiry value.
    :keyword display: Optional, A dictionary specifying the display_
        appearance of the in-app message.
    :keyword actions: Optional, Used to perform a defined task.
    :keyword interactive: Optional, A dictionary with two attributes –
        "type" and "button_actions", as generated by interactive().
    :keyword extra: Optional, Object.

    .. _display: https://docs.urbanairship.com/api/ua.html#display-object
    .. _display_type: https://docs.urbanairship.com/api/ua.html#in-app-message

    """

    payload: Dict[str, Any] = {}

    payload["alert"] = alert
    payload["display_type"] = display_type

    if expiry is not None:
        payload["expiry"] = expiry
    if display is not None:
        payload["display"] = display
    if actions is not None:
        payload["actions"] = actions
    if interactive is not None:
        payload["interactive"] = interactive
    if extra is not None:
        extra["extra"] = extra

    return payload


def device_types(*types: Any) -> List[str]:
    """Create a device type specifier.

    >>> device_types('ios', 'wns')
    ['ios', 'wns']
    >>> device_types('ios', 'symbian')
    Traceback (most recent call last):
        ...
    ValueError: Invalid device type 'symbian'

    """
    valid_device_types = ("ios", "android", "amazon", "wns", "web", "sms", "email")
    if len(types) == 1 and types[0] == "all":
        warnings.warn(
            "The device type 'all' has been deprecated.",
            DeprecationWarning,
            stacklevel=2,
        )
        return ["all"]

    for t in types:
        is_open = isinstance(t, str) and t.startswith("open::")
        if t not in valid_device_types and not is_open:
            raise ValueError("Invalid device type '%s'" % t)
    return [t for t in types]


def options(
    expiry: Optional[Union[str, int, None]] = None,
    bypass_frequency_limits: Optional[bool] = None,
    bypass_holdout_groups: Optional[bool] = None,
    no_throttle: Optional[bool] = None,
    omit_from_activity_log: Optional[bool] = None,
    personalization: Optional[bool] = None,
    redact_payload: Optional[bool] = None,
) -> Dict[str, Any]:
    """
    Options payload creation.

    :param expiry: If true, Delivery expiration, as either absolute ISO UTC timestamp,
                   or number of seconds from now.
    :param bypass_frequency_limits: If true, the push ignores any message
                                    limits that would otherwise apply to your message.
    :param bypass_holdout_groups: Indicates whether users that are part of a
                                  holdout group should be included in a push
                                  if a holdout experiment is active at send time.
    :param no_throttle: If true, the push will ignore global throttling rates
                        that have been configured for the application.
    :param omit_from_activity_log: If true, the push is omitted from the
                                   Activity Log.
    :param personalization: If true, enables Handlebars-style template syntax
                            for eligible fields in a notification or message center objects.
    :param redact_payload: If true, the push payload will not appear in
                           Airship's logs.

    :return: A dictionary representing the options payload.
    """

    payload: Dict[str, Any] = {}

    if expiry is not None:
        if not isinstance(expiry, (str, int)):
            raise ValueError("Expiry value must be an integer or time set in UTC as a string")
        payload["expiry"] = expiry

    if bypass_frequency_limits is not None:
        payload["bypass_frequency_limits"] = bypass_frequency_limits

    if bypass_holdout_groups is not None:
        payload["bypass_holdout_groups"] = bypass_holdout_groups

    if no_throttle is not None:
        payload["no_throttle"] = no_throttle

    if omit_from_activity_log is not None:
        payload["omit_from_activity_log"] = omit_from_activity_log

    if personalization is not None:
        payload["personalization"] = personalization

    if redact_payload is not None:
        payload["redact_payload"] = redact_payload

    return payload


def campaigns(categories: Union[str, List[str], None] = None) -> Dict[str, Any]:
    """Campaigns payload creation.

    :keyword categories: List of category strings or a single string.
    """
    payload: Dict[str, Any] = {}
    if categories is not None:
        if isinstance(categories, str):
            categories_list = [categories]
        elif isinstance(categories, list):
            categories_list = categories
        else:
            raise TypeError("categories must be a string or a list of strings")
        if not categories_list or len(categories_list) > 10:
            raise ValueError("Categories list must contain between 1 and 10 items")
        for c in categories_list:
            if not isinstance(c, str):
                raise ValueError(f"Invalid category type '{c}'")
            if not len(c) or len(c) > 64:
                raise ValueError(f"Invalid category name '{c}'")
        payload["categories"] = categories_list
    return payload


def actions(
    add_tag: Union[str, List[str], None] = None,
    remove_tag: Union[str, List[str], None] = None,
    open_: Optional[Dict[str, Any]] = None,
    share: Optional[str] = None,
    app_defined: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Actions payload creation.

    :keyword add_tag: Adds a tag to the device. Expects a
        string or a list of strings.
    :keyword remove_tag: Removes a tag from the device. Expects
        a string or a list of strings.
    :keyword open_: Opens type url, deep_link or landing_page. Expects a
        dictionary with "type" and "content".
        See API docs for more information.
    :keyword share: Sends a share notification. Expects a string.
    :keyword app_defined: Sends application defined actions. Expects
        a dictionary.

    >>> actions(add_tag='new_tag', remove_tag='old_tag',
    ...     open_={'type': 'url',
    ...         'content': 'http://www.urbanairship.com'}) # doctest: +SKIP
    {'open': {'type': 'url', 'content': 'http://www.urbanairship.com},
     'add_tag': 'new_tag', 'remove_tag': 'old_tag'}

    """
    payload: Dict[str, Any] = {}
    if add_tag is not None:
        if not (isinstance(add_tag, (collections.Sequence))):
            raise TypeError("add_tag must be a string or a list of strings")
        if isinstance(add_tag, list) and not add_tag:
            raise ValueError("add_tag list cannot be empty")
        payload["add_tag"] = add_tag
    if remove_tag is not None:
        if not (isinstance(remove_tag, (collections.Sequence))):
            raise TypeError("remove_tag must be a string or a list of strings")
        if isinstance(remove_tag, list) and not remove_tag:
            raise ValueError("remove_tag list cannot be empty")
        payload["remove_tag"] = remove_tag
    if open_ is not None:
        if not (isinstance(open_, dict)):
            raise TypeError("open_ must be a dictionary")
        payload["open"] = open_
    if share is not None:
        if not (isinstance(share, str)):
            raise TypeError("share must be a string")
        payload["share"] = share
    if app_defined is not None:
        if not (isinstance(app_defined, dict)):
            raise TypeError("app_defined must be a dictionary")
        payload["app_defined"] = app_defined
    return payload


def interactive(
    type: Optional[str] = None, button_actions: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """Interactive payload creation.

    :keyword type: The name of one of the predefined interactive notifications
        or a custom defined interactive notification. Expects a string.
    :keyword button_actions: A button_actions object that maps button IDs to
        valid action objects. Expects a dictionary.
    """

    payload: Dict[str, Any] = {}
    if type is not None:
        payload["type"] = type
        if button_actions is not None:
            if not isinstance(button_actions, dict):
                raise TypeError("'button_actions' must be a dictionary")
            payload["button_actions"] = button_actions
    else:
        raise AttributeError("'interactive' must have a type attribute")

    return payload


def wearable(
    background_image: Optional[str] = None,
    extra_pages: Union[List[str], None] = None,
    interactive: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Android wearable payload builder.

    :keyword background_image: Optional string. A URL that specifies the
        background image to display on a wearable device.
    :keyword extra_pages: Optional array of objects.
    :keyword interactive: Optional object. Override the interactive
        notification payload for the wearable device.
    """
    payload = {
        "background_image": background_image,
        "extra_pages": extra_pages,
        "interactive": interactive,
    }
    return {key: val for key, val in iter(payload.items()) if val is not None}


def public_notification(
    title: Optional[str] = None,
    alert: Optional[str] = None,
    summary: Optional[str] = None,
) -> Dict[str, Any]:
    """Android L public notification payload builder.

    :keyword title: Optional string. The notification title.
    :keyword alert: Optional string. The notification alert.
    :keyword summary: Optional string. The notification summary.
    """
    payload: Dict[str, Any] = {"title": title, "alert": alert, "summary": summary}
    return {key: val for key, val in iter(payload.items()) if val is not None}


def style(
    style_type: Optional[str],
    content: Union[str, List[str], None],
    title: Optional[str] = None,
    summary: Optional[str] = None,
) -> Dict[str, Any]:
    """Android/Amazon style builder.

    :keyword style_type: String. Must be one of "big_text", "big_picture",
        or "inbox".
    :keyword content: String or array of strings. Content of the style object.
        If style_type is set to "inbox", this will be an array of strings.
        Otherwise, it will be a single string.
    :keyword title: Optional string. Override the notification.
    :keyword summary: Optional string. Override the summary of the
        notification.

    """
    mapping: Dict[Any, Any] = {
        "big_text": "big_text",
        "big_picture": "big_picture",
        "inbox": "lines",
    }
    if style_type not in mapping.keys():
        raise ValueError("style_type must be one of {}.".format(", ".join(mapping.keys())))
    payload: Dict[str, Any] = {
        "type": style_type,
        mapping[style_type]: content,
        "title": title,
        "summary": summary,
    }
    return {key: val for key, val in iter(payload.items()) if val is not None}


def media_attachment(
    url: str,
    content: Optional[Dict[str, Any]] = None,
    options: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """iOS media_attachment builder.

    :keyword url: String. Specifies the URL to be downloaded by the UA
        Media Attachment extension.
    :keyword content: Optional dictionary. Describes portions of the
        notification that should be modified if the media attachment
        succeeds. See :func:`content`.
    :keyword options: Optional dictionary. Describes how to display the
        resource given by the URL. See :func:`options`.
    """
    payload = {"url": url, "content": content, "options": options}
    return {key: val for key, val in iter(payload.items()) if val is not None}


def content(
    title: Optional[str] = None,
    subtitle: Optional[str] = None,
    body: Optional[str] = None,
) -> Dict[str, Any]:
    """iOS content builder. Each argument describes the portions of the
    notification that should be modified if the media_attachment succeeds.

    :keyword title: String.
    :keyword subtitle: String.
    :keyword body: String.
    """
    payload = {"title": title, "subtitle": subtitle, "options": options}
    return {key: val for key, val in iter(payload.items()) if val is not None}


def crop(
    x: Optional[float] = None,
    y: Optional[float] = None,
    width: Optional[float] = None,
    height: Optional[float] = None,
) -> Dict[str, Any]:
    """iOS crop builder.

    :keyword x: Optional float. The X offset where the crop begins.
    :keyword y: Optional float. The Y offset where the crop begins.
    :keyword width: Optional float. The width of the final crop.
    :keyword height: Optional float. The height of the final crop.
    """
    payload: Dict[str, Any] = {
        "x": x,
        "y": y,
        "width": width,
        "height": height,
    }
    return {key: val for key, val in iter(payload.items()) if val is not None}


def localization(
    language: Optional[str] = None,
    country: Optional[str] = None,
    notification: Optional[Dict[str, Any]] = None,
    message: Optional[Dict[str, Any]] = None,
    in_app: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """
    Localized content for a push notification, message center message, or in app
    message. Localization must include at least one of language or country.
    Localization content must include at least one of notification, message, or
    in_app.

    :param language: Optional. The ISO 639-1 two-letter language code for this
        localization.
    :param country: Optional. The ISO 3166-2 two-letter country code for this
        localization.
    :param notification: Optional. An urbanairship.notification containing localized
        push notification content.
    :param message: Optional. An urbanairship.message containing localized message
        center content.
    :param in_app: Optional. An urbanarship.in_app containing localized in_app content.
    """
    if language is None and country is None:
        raise ValueError("One of language or country must be included.")
    if all([notification is None, message is None, in_app is None]):
        raise ValueError("At least one of notification, message, or in_app must be included.")

    data: Dict[str, Any] = {}
    if language:
        data["language"] = language
    if country:
        data["country"] = country
    if notification:
        data["notification"] = notification
    if message:
        data["message"] = message
    if in_app:
        data["in_app"] = in_app

    return data


def live_activity(
    name: str,
    event: LiveActivityEvent,
    alert: Optional[Dict[str, Any]] = None,
    priority: Optional[int] = None,
    content_state: Optional[Dict[str, Any]] = None,
    relevance_score: Optional[float] = None,
    stale_date: Optional[int] = None,
    dismissal_date: Optional[int] = None,
    timestamp: Optional[int] = None,
) -> Dict[str, Any]:
    """Generates the Live Activity payload.

    :param name: The name of the Live Activity.
    :param event: The event type. Expected values: "update" or "end".
    :param alert: The alert object. Expects a dictionary with keys "body", "sound",
        and "title".
    :param priority: The APNs priority. Valid values: 5 or 10.
    :param content_state: A dictionary of string keys to arbitrary JSON values that
        represents the content state to be updated by the Live Activity notification.
    :param dismissal_date: Optional epoch timestamp for ending the Live Activity.
    :param relevance_score: Optional relevance score.
    :param stale_date: Optional timestamp indicating when the Live Activity becomes
        outdated.
    :param timestamp: Optional timestamp for the Live Activity update.
    :return: A dictionary representing the Live Activity payload.
    """

    ALLOWED_ALERT_KEYS = {"body", "sound", "title"}
    payload: Dict[str, Any] = {}

    if not name:
        raise ValueError("'name' is required")
    payload["name"] = name

    payload["event"] = event.value

    if alert:
        if not isinstance(alert, dict):
            raise TypeError("'alert' must be a dictionary")
        if not set(alert.keys()).issubset(ALLOWED_ALERT_KEYS):
            raise ValueError(
                f"'alert' contains invalid keys. Allowed keys are: {ALLOWED_ALERT_KEYS}"
            )
        payload["alert"] = alert

    if priority:
        if priority not in [5, 10]:
            raise ValueError("'priority' must be 5 or 10")
        payload["priority"] = priority

    if content_state:
        if not isinstance(content_state, dict):
            raise TypeError("'content_state' must be a dictionary")
        if not all(isinstance(key, str) for key in content_state.keys()):
            raise TypeError("'All content_state keys must be string'")
        payload["content_state"] = content_state

    if relevance_score:
        payload["relevance_score"] = relevance_score

    if stale_date:
        payload["stale_date"] = stale_date

    if dismissal_date:
        payload["dismissal_date"] = dismissal_date

    if timestamp is not None:
        payload["timestamp"] = timestamp

    return payload


def live_update(
    name: str,
    event: LiveUpdateEvent,
    content_state: Optional[Dict[str, Any]] = None,
    type_: Optional[str] = None,
    dismissal_date: Optional[int] = None,
    timestamp: Optional[int] = None,
) -> Dict[str, Any]:
    """
    Generates the Live Update payload.

    :param name: The name of the Live Update to target. When event is "update" or
        "end", the audience is limited to devices that have a Live Update started for
        the specified name.
    :param event: The event type for the Live Update. Expected values are members of
        the LiveUpdateEvent enum: "start", "update", or "end".
    :param content_state: A dictionary of string keys to arbitrary JSON values that
        represents the content state to be updated by the Live Update notification.
    :param type_: Used to map Live Update events to the corresponding handler in your
        app. This is required to identify what kind of live update is being sent, so
        the application knows how to handle it.
    :param dismissal_date: Optional epoch timestamp for ending the Live Update.
    :param timestamp: Optional timestamp for the Live Update update.
    :return: A dictionary representing the Live Update payload.
    """

    payload: Dict[str, Any] = {}

    if not name:
        raise ValueError("'name' is required")
    payload["name"] = name

    payload["event"] = event.value

    if content_state:
        if not isinstance(content_state, dict):
            raise TypeError("'content_state' must be a dictionary")
        if not all(isinstance(key, str) for key in content_state.keys()):
            raise TypeError("'All content_state keys must be string'")
        payload["content_state"] = content_state

    if type_:
        payload["type"] = type_

    if dismissal_date:
        payload["dismissal_date"] = dismissal_date

    if timestamp:
        payload["timestamp"] = timestamp

    return payload
