# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# For LaMetric to work, you need to first setup a custom application on their
# website. it can be done as follows:

# Cloud Mode:
# - Sign Up and login to the developer webpage https://developer.lametric.com
#
# - Create a **Indicator App** if you haven't already done so from here:
#     https://developer.lametric.com/applications/sources
#
#   There is a great official tutorial on how to do this here:
#     https://lametric-documentation.readthedocs.io/en/latest/\
#           guides/first-steps/first-lametric-indicator-app.html
#
# - Make sure to set the **Communication Type** to **PUSH**.
#
# - You will be able to **Publish** your app once you've finished setting it
#   up.  This will allow it to be accessible from the internet using the
#   `cloud` mode of this Apprise Plugin. The **Publish** button shows up
#   from within the settings of your Lametric App upon clicking on the
#   **Draft Vx** folder (where `x` is the version - usually a 1)
#
# When you've completed, the site would have provided you a **PUSH URL** that
# looks like this:
#    https://developer.lametric.com/api/v1/dev/widget/update/\
#             com.lametric.{app_id}/{app_ver}
#
# You will need to record the `{app_id}` and `{app_ver}` to use the `cloud`
# mode.
#
# The same page should also provide you with an **Access Token**.  It's
# approximately 86 characters with two equal (`=`) characters at the end of it.
# This becomes your `{app_token}`. Here is an example of what one might
# look like:
#    K2MxWI0NzU0ZmI2NjJlZYTgViMDgDRiN8YjlmZjRmNTc4NDVhJzk0RiNjNh0EyKWW==`
#
# The syntax for the cloud mode is:
# * `lametric://{app_token}@{app_id}/{app_ver}?mode=cloud`

# Device Mode:
# - Sign Up and login to the developer webpage https://developer.lametric.com
# - Locate your Device API Key; you can find it here:
#      https://developer.lametric.com/user/devices
# - From here you can get your your API Key for the device you plan to notify.
# - Your devices IP Address can be found in LaMetric Time app at:
#       Settings -> Wi-Fi -> IP Address
#
# The syntax for the device mode is:
#  * `lametric://{apikey}@{host}`

# A great source for API examples (Device Mode):
# - https://lametric-documentation.readthedocs.io/en/latest/reference-docs\
#       /device-notifications.html
#
# A great source for API examples (Cloud Mode):
# - https://lametric-documentation.readthedocs.io/en/latest/reference-docs\
#       /lametric-cloud-reference.html

# A great source for the icon reference:
# - https://developer.lametric.com/icons


import contextlib
from json import dumps
import re

import requests

from ..common import NotifyType
from ..locale import gettext_lazy as _
from ..utils.parse import is_hostname, is_ipaddr, validate_regex
from .base import NotifyBase

# A URL Parser to detect App ID
LAMETRIC_APP_ID_DETECTOR_RE = re.compile(
    r"(com\.lametric\.)?(?P<app_id>[0-9a-z.-]{1,64})"
    r"(/(?P<app_ver>[1-9][0-9]*))?",
    re.I,
)

# Tokens are huge
LAMETRIC_IS_APP_TOKEN = re.compile(r"^[a-z0-9]{80,}==$", re.I)


class LametricMode:
    """Define Lametric Notification Modes."""

    # App posts upstream to the developer API on Lametric's website
    CLOUD = "cloud"

    # Device mode posts directly to the device that you identify
    DEVICE = "device"


LAMETRIC_MODES = (
    LametricMode.CLOUD,
    LametricMode.DEVICE,
)


class LametricPriority:
    """Priority of the message."""

    # info: this priority means that notification will be displayed on the
    #        same “level” as all other notifications on the device that come
    #        from apps (for example facebook app). This notification will not
    #        be shown when screensaver is active. By default message is sent
    #        with "info" priority. This level of notification should be used
    #        for notifications like news, weather, temperature, etc.
    INFO = "info"

    # warning: notifications with this priority will interrupt ones sent with
    #           lower priority (“info”). Should be used to notify the user
    #           about something important but not critical. For example,
    #           events like “someone is coming home” should use this priority
    #           when sending notifications from smart home.
    WARNING = "warning"

    # critical: the most important notifications. Interrupts notification
    #            with priority info or warning and is displayed even if
    #            screensaver is active. Use with care as these notifications
    #            can pop in the middle of the night. Must be used only for
    #            really important notifications like notifications from smoke
    #            detectors, water leak sensors, etc. Use it for events that
    #            require human interaction immediately.
    CRITICAL = "critical"


LAMETRIC_PRIORITIES = (
    LametricPriority.INFO,
    LametricPriority.WARNING,
    LametricPriority.CRITICAL,
)


class LametricIconType:
    """Represents the nature of notification."""

    # info  - "i" icon will be displayed prior to the notification. Means that
    #         notification contains information, no need to take actions on it.
    INFO = "info"

    # alert: "!!!" icon will be displayed prior to the notification. Use it
    #         when you want the user to pay attention to that notification as
    #         it indicates that something bad happened and user must take
    #         immediate action.
    ALERT = "alert"

    # none: no notification icon will be shown.
    NONE = "none"


LAMETRIC_ICON_TYPES = (
    LametricIconType.INFO,
    LametricIconType.ALERT,
    LametricIconType.NONE,
)


class LametricSoundCategory:
    """Define Sound Categories."""

    NOTIFICATIONS = "notifications"
    ALARMS = "alarms"


class LametricSound:
    """There are 2 categories of sounds, to make things simple we just lump
    them all togther in one class object.

    Syntax is (Category, (AlarmID, Alias1, Alias2, ...))
    """

    # Alarm Category Sounds
    ALARM01 = (LametricSoundCategory.ALARMS, ("alarm1", "a1", "a01"))
    ALARM02 = (LametricSoundCategory.ALARMS, ("alarm2", "a2", "a02"))
    ALARM03 = (LametricSoundCategory.ALARMS, ("alarm3", "a3", "a03"))
    ALARM04 = (LametricSoundCategory.ALARMS, ("alarm4", "a4", "a04"))
    ALARM05 = (LametricSoundCategory.ALARMS, ("alarm5", "a5", "a05"))
    ALARM06 = (LametricSoundCategory.ALARMS, ("alarm6", "a6", "a06"))
    ALARM07 = (LametricSoundCategory.ALARMS, ("alarm7", "a7", "a07"))
    ALARM08 = (LametricSoundCategory.ALARMS, ("alarm8", "a8", "a08"))
    ALARM09 = (LametricSoundCategory.ALARMS, ("alarm9", "a9", "a09"))
    ALARM10 = (LametricSoundCategory.ALARMS, ("alarm10", "a10"))
    ALARM11 = (LametricSoundCategory.ALARMS, ("alarm11", "a11"))
    ALARM12 = (LametricSoundCategory.ALARMS, ("alarm12", "a12"))
    ALARM13 = (LametricSoundCategory.ALARMS, ("alarm13", "a13"))

    # Notification Category Sounds
    BICYCLE = (LametricSoundCategory.NOTIFICATIONS, ("bicycle", "bike"))
    CAR = (LametricSoundCategory.NOTIFICATIONS, ("car",))
    CASH = (LametricSoundCategory.NOTIFICATIONS, ("cash",))
    CAT = (LametricSoundCategory.NOTIFICATIONS, ("cat",))
    DOG01 = (LametricSoundCategory.NOTIFICATIONS, ("dog", "dog1", "dog01"))
    DOG02 = (LametricSoundCategory.NOTIFICATIONS, ("dog2", "dog02"))
    ENERGY = (LametricSoundCategory.NOTIFICATIONS, ("energy",))
    KNOCK = (LametricSoundCategory.NOTIFICATIONS, ("knock-knock", "knock"))
    EMAIL = (
        LametricSoundCategory.NOTIFICATIONS,
        ("letter_email", "letter", "email"),
    )
    LOSE01 = (LametricSoundCategory.NOTIFICATIONS, ("lose1", "lose01", "lose"))
    LOSE02 = (LametricSoundCategory.NOTIFICATIONS, ("lose2", "lose02"))
    NEGATIVE01 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("negative1", "negative01", "neg01", "neg1", "-"),
    )
    NEGATIVE02 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("negative2", "negative02", "neg02", "neg2", "--"),
    )
    NEGATIVE03 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("negative3", "negative03", "neg03", "neg3", "---"),
    )
    NEGATIVE04 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("negative4", "negative04", "neg04", "neg4", "----"),
    )
    NEGATIVE05 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("negative5", "negative05", "neg05", "neg5", "-----"),
    )
    NOTIFICATION01 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("notification", "notification1", "notification01", "not01", "not1"),
    )
    NOTIFICATION02 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("notification2", "notification02", "not02", "not2"),
    )
    NOTIFICATION03 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("notification3", "notification03", "not03", "not3"),
    )
    NOTIFICATION04 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("notification4", "notification04", "not04", "not4"),
    )
    OPEN_DOOR = (
        LametricSoundCategory.NOTIFICATIONS,
        ("open_door", "open", "door"),
    )
    POSITIVE01 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("positive1", "positive01", "pos01", "p1", "+"),
    )
    POSITIVE02 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("positive2", "positive02", "pos02", "p2", "++"),
    )
    POSITIVE03 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("positive3", "positive03", "pos03", "p3", "+++"),
    )
    POSITIVE04 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("positive4", "positive04", "pos04", "p4", "++++"),
    )
    POSITIVE05 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("positive5", "positive05", "pos05", "p5", "+++++"),
    )
    POSITIVE06 = (
        LametricSoundCategory.NOTIFICATIONS,
        ("positive6", "positive06", "pos06", "p6", "++++++"),
    )
    STATISTIC = (LametricSoundCategory.NOTIFICATIONS, ("statistic", "stat"))
    THUNDER = (LametricSoundCategory.NOTIFICATIONS, "thunder")
    WATER01 = (LametricSoundCategory.NOTIFICATIONS, ("water1", "water01"))
    WATER02 = (LametricSoundCategory.NOTIFICATIONS, ("water2", "water02"))
    WIN01 = (LametricSoundCategory.NOTIFICATIONS, ("win", "win01", "win1"))
    WIN02 = (LametricSoundCategory.NOTIFICATIONS, ("win2", "win02"))
    WIND = (LametricSoundCategory.NOTIFICATIONS, ("wind",))
    WIND_SHORT = (LametricSoundCategory.NOTIFICATIONS, ("wind_short",))


# A listing of all the sounds; the order DOES matter, content is read from
# top down and then right to left (over aliases). Longer similar sounding
# elements should be placed higher in the list over others. for example
# ALARM10 should come before ALARM01 (because ALARM01 can match on 'alarm1'
# which is very close to 'alarm10'
LAMETRIC_SOUNDS = (
    # Alarm Category Entries
    LametricSound.ALARM13,
    LametricSound.ALARM12,
    LametricSound.ALARM11,
    LametricSound.ALARM10,
    LametricSound.ALARM09,
    LametricSound.ALARM08,
    LametricSound.ALARM07,
    LametricSound.ALARM06,
    LametricSound.ALARM05,
    LametricSound.ALARM04,
    LametricSound.ALARM03,
    LametricSound.ALARM02,
    LametricSound.ALARM01,
    # Notification Category Entries
    LametricSound.BICYCLE,
    LametricSound.CAR,
    LametricSound.CASH,
    LametricSound.CAT,
    LametricSound.DOG02,
    LametricSound.DOG01,
    LametricSound.ENERGY,
    LametricSound.KNOCK,
    LametricSound.EMAIL,
    LametricSound.LOSE02,
    LametricSound.LOSE01,
    LametricSound.NEGATIVE01,
    LametricSound.NEGATIVE02,
    LametricSound.NEGATIVE03,
    LametricSound.NEGATIVE04,
    LametricSound.NEGATIVE05,
    LametricSound.NOTIFICATION04,
    LametricSound.NOTIFICATION03,
    LametricSound.NOTIFICATION02,
    LametricSound.NOTIFICATION01,
    LametricSound.OPEN_DOOR,
    LametricSound.POSITIVE01,
    LametricSound.POSITIVE02,
    LametricSound.POSITIVE03,
    LametricSound.POSITIVE04,
    LametricSound.POSITIVE05,
    LametricSound.POSITIVE01,
    LametricSound.STATISTIC,
    LametricSound.THUNDER,
    LametricSound.WATER02,
    LametricSound.WATER01,
    LametricSound.WIND,
    LametricSound.WIND_SHORT,
    LametricSound.WIN01,
    LametricSound.WIN02,
)


class NotifyLametric(NotifyBase):
    """A wrapper for LaMetric Notifications."""

    # The default descriptive name associated with the Notification
    service_name = "LaMetric"

    # The services URL
    service_url = "https://lametric.com"

    # The default protocol
    protocol = "lametric"

    # The default secure protocol
    secure_protocol = "lametrics"

    # Allow 300 requests per minute.
    # 60/300 = 0.2
    request_rate_per_sec = 0.20

    # A URL that takes you to the setup/help of the specific protocol
    setup_url = "https://github.com/caronc/apprise/wiki/Notify_lametric"

    # Lametric does have titles when creating a message
    title_maxlen = 0

    # URL used for notifying Lametric App's created in the Dev Portal
    cloud_notify_url = (
        "https://developer.lametric.com/api/v1"
        "/dev/widget/update/com.lametric.{app_id}/{app_ver}"
    )

    # URL used for local notifications directly to the device
    device_notify_url = "{schema}://{host}{port}/api/v2/device/notifications"

    # The Device User ID
    default_device_user = "dev"

    # Track all icon mappings back to Apprise Icon NotifyType's
    # See: https://developer.lametric.com/icons
    # Icon ID looks like <prefix>XXX, where <prefix> is:
    #   - "i" (for static icon)
    #   - "a" (for animation)
    #   - XXX - is the number of the icon and can be found at:
    #            https://developer.lametric.com/icons
    lametric_icon_id_mapping = {
        # 620/Info
        NotifyType.INFO: "i620",
        # 9182/info_good
        NotifyType.SUCCESS: "i9182",
        # 9183/info_caution
        NotifyType.WARNING: "i9183",
        # 9184/info_error
        NotifyType.FAILURE: "i9184",
    }

    # Define object templates
    templates = (
        # Cloud (App) Mode
        "{schema}://{app_token}@{app_id}",
        "{schema}://{app_token}@{app_id}/{app_ver}",
        # Device Mode
        "{schema}://{apikey}@{host}",
        "{schema}://{user}:{apikey}@{host}",
        "{schema}://{apikey}@{host}:{port}",
        "{schema}://{user}:{apikey}@{host}:{port}",
    )

    # Define our template tokens
    template_tokens = dict(
        NotifyBase.template_tokens,
        **{
            # Used for Local Device mode
            "apikey": {
                "name": _("Device API Key"),
                "type": "string",
                "private": True,
            },
            # Used for Cloud mode
            "app_id": {
                "name": _("App ID"),
                "type": "string",
                "private": True,
            },
            # Used for Cloud mode
            "app_ver": {
                "name": _("App Version"),
                "type": "string",
                "regex": (r"^[1-9][0-9]*$", ""),
                "default": "1",
            },
            # Used for Cloud mode
            "app_token": {
                "name": _("App Access Token"),
                "type": "string",
                "regex": (r"^[A-Z0-9]{80,}==$", "i"),
            },
            "host": {
                "name": _("Hostname"),
                "type": "string",
            },
            "port": {
                "name": _("Port"),
                "type": "int",
                "min": 1,
                "max": 65535,
                "default": 8080,
            },
            "user": {
                "name": _("Username"),
                "type": "string",
            },
        },
    )

    # Define our template arguments
    template_args = dict(
        NotifyBase.template_args,
        **{
            "apikey": {
                "alias_of": "apikey",
            },
            "app_id": {
                "alias_of": "app_id",
            },
            "app_ver": {
                "alias_of": "app_ver",
            },
            "app_token": {
                "alias_of": "app_token",
            },
            "priority": {
                "name": _("Priority"),
                "type": "choice:string",
                "values": LAMETRIC_PRIORITIES,
                "default": LametricPriority.INFO,
            },
            "icon": {
                "name": _("Custom Icon"),
                "type": "string",
            },
            "icon_type": {
                "name": _("Icon Type"),
                "type": "choice:string",
                "values": LAMETRIC_ICON_TYPES,
                "default": LametricIconType.NONE,
            },
            "mode": {
                "name": _("Mode"),
                "type": "choice:string",
                "values": LAMETRIC_MODES,
                "default": LametricMode.DEVICE,
            },
            "sound": {
                "name": _("Sound"),
                "type": "string",
            },
            # Lifetime is in seconds
            "cycles": {
                "name": _("Cycles"),
                "type": "int",
                "min": 0,
                "default": 1,
            },
        },
    )

    def __init__(
        self,
        apikey=None,
        app_token=None,
        app_id=None,
        app_ver=None,
        priority=None,
        icon=None,
        icon_type=None,
        sound=None,
        mode=None,
        cycles=None,
        **kwargs,
    ):
        """Initialize LaMetric Object."""
        super().__init__(**kwargs)

        self.mode = (
            mode.strip().lower()
            if isinstance(mode, str)
            else self.template_args["mode"]["default"]
        )

        # Default Cloud Argument
        self.lametric_app_id = None
        self.lametric_app_ver = None
        self.lametric_app_access_token = None

        # Default Device/Cloud Argument
        self.lametric_apikey = None

        if self.mode not in LAMETRIC_MODES:
            msg = f"An invalid LaMetric Mode ({mode}) was specified."
            self.logger.warning(msg)
            raise TypeError(msg)

        if self.mode == LametricMode.CLOUD:
            try:
                results = LAMETRIC_APP_ID_DETECTOR_RE.match(app_id)
            except TypeError:
                msg = (
                    "An invalid LaMetric Application ID "
                    f"({app_id}) was specified."
                )
                self.logger.warning(msg)
                raise TypeError(msg) from None

            # Detect our Access Token
            self.lametric_app_access_token = validate_regex(
                app_token, *self.template_tokens["app_token"]["regex"]
            )
            if not self.lametric_app_access_token:
                msg = (
                    "An invalid LaMetric Application Access Token "
                    f"({app_token}) was specified."
                )
                self.logger.warning(msg)
                raise TypeError(msg)

            # If app_ver is specified, it over-rides all
            if app_ver:
                self.lametric_app_ver = validate_regex(
                    app_ver, *self.template_tokens["app_ver"]["regex"]
                )
                if not self.lametric_app_ver:
                    msg = (
                        "An invalid LaMetric Application Version "
                        f"({app_ver}) was specified."
                    )
                    self.logger.warning(msg)
                    raise TypeError(msg)

            else:
                # If app_ver wasn't specified, we parse it from the
                # Application ID
                self.lametric_app_ver = (
                    results.group("app_ver")
                    if results.group("app_ver")
                    else self.template_tokens["app_ver"]["default"]
                )

            # Store our Application ID
            self.lametric_app_id = results.group("app_id")

        if self.mode == LametricMode.DEVICE:
            self.lametric_apikey = validate_regex(apikey)
            if not self.lametric_apikey:
                msg = (
                    "An invalid LaMetric Device API Key "
                    f"({apikey}) was specified."
                )
                self.logger.warning(msg)
                raise TypeError(msg)

        if priority not in LAMETRIC_PRIORITIES:
            self.priority = self.template_args["priority"]["default"]

        else:
            self.priority = priority

        # assign our icon (if it was defined); we also eliminate
        # any hashtag (#) entries that might be present
        self.icon = (
            re.search(r"[#\s]*(?P<value>.+?)\s*$", icon).group("value")
            if isinstance(icon, str)
            else None
        )

        if icon_type not in LAMETRIC_ICON_TYPES:
            self.icon_type = self.template_args["icon_type"]["default"]

        else:
            self.icon_type = icon_type

        # The number of times the message should be displayed
        self.cycles = (
            self.template_args["cycles"]["default"]
            if not (
                isinstance(cycles, int)
                and cycles > self.template_args["cycles"]["min"]
            )
            else cycles
        )

        self.sound = None
        if isinstance(sound, str):
            # If sound is set, get it's match
            self.sound = self.sound_lookup(sound.strip().lower())
            if self.sound is None:
                self.logger.warning(
                    f"An invalid LaMetric sound ({sound}) was specified."
                )
        return

    @staticmethod
    def sound_lookup(lookup):
        """A simple match function that takes string and returns the
        LametricSound object it was found in."""

        for x in LAMETRIC_SOUNDS:
            match = next((f for f in x[1] if f.startswith(lookup)), None)
            if match:
                # We're done
                return x

        # No match was found
        return None

    def _cloud_notification_payload(self, body, notify_type, headers):
        """Return URL and payload for cloud directed requests."""

        # Update header entries
        headers.update({
            "X-Access-Token": self.lametric_apikey,
        })

        if self.sound:
            self.logger.warning(
                "LaMetric sound setting is unavailable in Cloud mode"
            )

        if self.priority != self.template_args["priority"]["default"]:
            self.logger.warning(
                "LaMetric priority setting is unavailable in Cloud mode"
            )

        if self.icon_type != self.template_args["icon_type"]["default"]:
            self.logger.warning(
                "LaMetric icon_type setting is unavailable in Cloud mode"
            )

        if self.cycles != self.template_args["cycles"]["default"]:
            self.logger.warning(
                "LaMetric cycle settings is unavailable in Cloud mode"
            )

        # Assign our icon if the user specified a custom one, otherwise
        # choose from our pre-set list (based on notify_type)
        icon = (
            self.icon
            if self.icon
            else self.lametric_icon_id_mapping[notify_type]
        )

        # Our Payload
        # Cloud Notifications don't have as much functionality
        # You can not set priority and/or sound
        payload = {
            "frames": [{
                "icon": icon,
                "text": body,
                "index": 0,
            }]
        }

        # Prepare our Cloud Notify URL
        notify_url = self.cloud_notify_url.format(
            app_id=self.lametric_app_id, app_ver=self.lametric_app_ver
        )

        # Return request parameters
        return (notify_url, None, payload)

    def _device_notification_payload(self, body, notify_type, headers):
        """Return URL and Payload for Device directed requests."""

        # Assign our icon if the user specified a custom one, otherwise
        # choose from our pre-set list (based on notify_type)
        icon = (
            self.icon
            if self.icon
            else self.lametric_icon_id_mapping[notify_type]
        )

        # Our Payload
        payload = {
            # Priority of the message
            "priority": self.priority,
            # Icon Type: Represents the nature of notification
            "icon_type": self.icon_type,
            # The time notification lives in queue to be displayed in
            # milliseconds (ms). The default lifetime is 2 minutes (120000ms).
            # If notification stayed in queue for longer than lifetime
            # milliseconds - it will not be displayed.
            "lifetime": 120000,
            "model": {
                # cycles - the number of times message should be displayed. If
                # cycles is set to 0, notification will stay on the screen
                # until user dismisses it manually. By default it is set to 1.
                "cycles": self.cycles,
                "frames": [{
                    "icon": icon,
                    "text": body,
                }],
            },
        }

        if self.sound:
            # Sound was set, so add it to the payload
            payload["model"]["sound"] = {
                # The sound category
                "category": self.sound[0],
                # The first element of our tuple is always the id
                "id": self.sound[1][0],
                # repeat - defines the number of times sound must be played.
                # If set to 0 sound will be played until notification is
                # dismissed. By default the value is set to 1.
                "repeat": 1,
            }

        if not self.user:
            # Use default user if there wasn't one otherwise specified
            self.user = self.default_device_user

        # Prepare our authentication
        auth = (self.user, self.password)

        # Prepare our Direct Access Notify URL
        notify_url = self.device_notify_url.format(
            schema="https" if self.secure else "http",
            host=self.host,
            port=":{}".format(
                self.port
                if self.port
                else self.template_tokens["port"]["default"]
            ),
        )

        # Return request parameters
        return (notify_url, auth, payload)

    def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
        """Perform LaMetric Notification."""

        # Prepare our headers:
        headers = {
            "User-Agent": self.app_id,
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Cache-Control": "no-cache",
        }

        # Depending on the mode, the payload is gathered by
        # - _device_notification_payload()
        # - _cloud_notification_payload()
        (notify_url, auth, payload) = getattr(
            self, f"_{self.mode}_notification_payload"
        )(body=body, notify_type=notify_type, headers=headers)

        self.logger.debug(
            "LaMetric POST URL:"
            f" {notify_url} (cert_verify={self.verify_certificate!r})"
        )
        self.logger.debug(f"LaMetric Payload: {payload!s}")

        # Always call throttle before any remote server i/o is made
        self.throttle()

        try:
            r = requests.post(
                notify_url,
                data=dumps(payload),
                headers=headers,
                auth=auth,
                verify=self.verify_certificate,
                timeout=self.request_timeout,
            )
            # An ideal response would be:
            # {
            #   "success": {
            #     "id": "<notification id>"
            #   }
            # }

            if r.status_code not in (
                requests.codes.created,
                requests.codes.ok,
            ):
                # We had a problem
                status_str = NotifyLametric.http_response_code_lookup(
                    r.status_code
                )

                self.logger.warning(
                    "Failed to send LaMetric notification: "
                    "{}{}error={}.".format(
                        status_str, ", " if status_str else "", r.status_code
                    )
                )

                self.logger.debug(f"Response Details:\r\n{r.content}")

                # Return; we're done
                return False

            else:
                self.logger.info("Sent LaMetric notification.")

        except requests.RequestException as e:
            self.logger.warning(
                "A Connection error occurred sending LaMetric "
                f"notification to {self.host}."
            )
            self.logger.debug(f"Socket Exception: {e!s}")

            # Return; we're done
            return False

        return True

    @property
    def url_identifier(self):
        """Returns all of the identifiers that make this URL unique from
        another simliar one.

        Targets or end points should never be identified here.
        """
        if self.mode == LametricMode.DEVICE:
            return (
                self.secure_protocol if self.secure else self.protocol,
                self.user,
                self.lametric_apikey,
                self.host,
                (
                    self.port
                    if self.port
                    else (
                        443
                        if self.secure
                        else self.template_tokens["port"]["default"]
                    )
                ),
            )

        return (
            self.protocol,
            self.lametric_app_access_token,
            self.lametric_app_id,
            self.lametric_app_ver,
        )

    def url(self, privacy=False, *args, **kwargs):
        """Returns the URL built dynamically based on specified arguments."""

        # Define any URL parameters
        params = {
            "mode": self.mode,
        }

        # Extend our parameters
        params.update(self.url_parameters(privacy=privacy, *args, **kwargs))

        if self.icon:
            # Assign our icon IF one was specified
            params["icon"] = self.icon

        if self.mode == LametricMode.CLOUD:
            # Upstream/LaMetric App Return
            return "{schema}://{token}@{app_id}/{app_ver}/?{params}".format(
                schema=self.protocol,
                token=self.pprint(
                    self.lametric_app_access_token, privacy, safe=""
                ),
                app_id=self.pprint(self.lametric_app_id, privacy, safe=""),
                app_ver=NotifyLametric.quote(self.lametric_app_ver, safe=""),
                params=NotifyLametric.urlencode(params),
            )

        #
        # If we reach here then we're dealing with LametricMode.DEVICE
        #
        if self.priority != self.template_args["priority"]["default"]:
            params["priority"] = self.priority

        if self.icon_type != self.template_args["icon_type"]["default"]:
            params["icon_type"] = self.icon_type

        if self.cycles != self.template_args["cycles"]["default"]:
            params["cycles"] = self.cycles

        if self.sound:
            # Store our sound entry
            # The first element of our tuple is always the id
            params["sound"] = self.sound[1][0]

        auth = ""
        if self.user and self.password:
            auth = "{user}:{apikey}@".format(
                user=NotifyLametric.quote(self.user, safe=""),
                apikey=self.pprint(self.lametric_apikey, privacy, safe=""),
            )
        else:  # self.apikey is set
            auth = "{apikey}@".format(
                apikey=self.pprint(self.lametric_apikey, privacy, safe=""),
            )

        # Local Return
        return "{schema}://{auth}{hostname}{port}/?{params}".format(
            schema=self.secure_protocol if self.secure else self.protocol,
            auth=auth,
            # never encode hostname since we're expecting it to be a valid one
            hostname=self.host,
            port=(
                ""
                if self.port is None
                or self.port == self.template_tokens["port"]["default"]
                else f":{self.port}"
            ),
            params=NotifyLametric.urlencode(params),
        )

    @staticmethod
    def parse_url(url):
        """Parses the URL and returns enough arguments that can allow us to re-
        instantiate this object."""

        results = NotifyBase.parse_url(url, verify_host=False)
        if not results:
            # We're done early as we couldn't load the results
            return results

        if results.get("user") and not results.get("password"):
            # Handle URL like:
            # schema://user@host

            # This becomes the password
            results["password"] = results["user"]
            results["user"] = None

        # Get unquoted entries
        entries = NotifyLametric.split_path(results["fullpath"])

        # Priority Handling
        if "priority" in results["qsd"] and results["qsd"]["priority"]:
            results["priority"] = NotifyLametric.unquote(
                results["qsd"]["priority"].strip().lower()
            )

        # Icon Type
        if "icon" in results["qsd"] and results["qsd"]["icon"]:
            results["icon"] = NotifyLametric.unquote(
                results["qsd"]["icon"].strip().lower()
            )

        # Icon Type
        if "icon_type" in results["qsd"] and results["qsd"]["icon_type"]:
            results["icon_type"] = NotifyLametric.unquote(
                results["qsd"]["icon_type"].strip().lower()
            )

        # Sound
        if "sound" in results["qsd"] and results["qsd"]["sound"]:
            results["sound"] = NotifyLametric.unquote(
                results["qsd"]["sound"].strip().lower()
            )

        # API Key (Device Mode)
        if "apikey" in results["qsd"] and results["qsd"]["apikey"]:
            # Extract API Key from an argument
            results["apikey"] = NotifyLametric.unquote(
                results["qsd"]["apikey"]
            )

        # App ID
        if "app" in results["qsd"] and results["qsd"]["app"]:

            # Extract the App ID from an argument
            results["app_id"] = NotifyLametric.unquote(results["qsd"]["app"])

        # App Version
        if "app_ver" in results["qsd"] and results["qsd"]["app_ver"]:

            # Extract the App ID from an argument
            results["app_ver"] = NotifyLametric.unquote(
                results["qsd"]["app_ver"]
            )

        elif entries:
            # Store our app id
            results["app_ver"] = entries.pop(0)

        if "token" in results["qsd"] and results["qsd"]["token"]:
            # Extract Application Access Token from an argument
            results["app_token"] = NotifyLametric.unquote(
                results["qsd"]["token"]
            )

        # Mode override
        if "mode" in results["qsd"] and results["qsd"]["mode"]:
            results["mode"] = NotifyLametric.unquote(
                results["qsd"]["mode"].strip().lower()
            )
        else:
            # We can try to detect the mode based on the validity of the
            # hostname. We can also scan the validity of the Application
            # Access token
            #
            # This isn't a surfire way to do things though; it's best to
            # specify the mode= flag
            results["mode"] = (
                LametricMode.DEVICE
                if (
                    (
                        is_hostname(results["host"])
                        or is_ipaddr(results["host"])
                    )
                    and
                    # make sure password is not an Access Token
                    (
                        results["password"]
                        and not LAMETRIC_IS_APP_TOKEN.match(
                            results["password"]
                        )
                    )
                    and
                    # Scan for app_ flags
                    next((f for f in results if f.startswith("app_")), None)
                    is None
                )
                else LametricMode.CLOUD
            )

        # Handle defaults if not set
        if results["mode"] == LametricMode.DEVICE:
            # Device Mode Defaults
            if "apikey" not in results:
                results["apikey"] = NotifyLametric.unquote(results["password"])

        else:
            # CLOUD Mode Defaults
            if "app_id" not in results:
                results["app_id"] = NotifyLametric.unquote(results["host"])
            if "app_token" not in results:
                results["app_token"] = NotifyLametric.unquote(
                    results["password"]
                )

        # Set cycles
        with contextlib.suppress(TypeError, ValueError):
            results["cycles"] = abs(int(results["qsd"].get("cycles")))

        return results

    @staticmethod
    def parse_native_url(url):
        """
        Support
           https://developer.lametric.com/api/v1/dev/\
                   widget/update/com.lametric.{APP_ID}/1

           https://developer.lametric.com/api/v1/dev/\
                   widget/update/com.lametric.{APP_ID}/{APP_VER}
        """

        # If users do provide the Native URL they wll also want to add
        # ?token={APP_ACCESS_TOKEN} to the parameters at the end or the
        # URL will fail to load in later stages.
        result = re.match(
            r"^http(?P<secure>s)?://(?P<host>[^/]+)"
            r"/api/(?P<api_ver>v[1-9]*[0-9]+)"
            r"/dev/widget/update/"
            r"com\.lametric\.(?P<app_id>[0-9a-z.-]{1,64})"
            r"(/(?P<app_ver>[1-9][0-9]*))?/?"
            r"(?P<params>\?.+)?$",
            url,
            re.I,
        )

        if result:
            return NotifyLametric.parse_url(
                "{schema}://{app_id}{app_ver}/{params}".format(
                    schema=(
                        NotifyLametric.secure_protocol
                        if result.group("secure")
                        else NotifyLametric.protocol
                    ),
                    app_id=result.group("app_id"),
                    app_ver=(
                        "/{}".format(result.group("app_ver"))
                        if result.group("app_ver")
                        else ""
                    ),
                    params=(
                        ""
                        if not result.group("params")
                        else result.group("params")
                    ),
                )
            )

        return None
