# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2026, 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.

#
# API: https://dot.mindreset.tech/docs/service/studio/api/text_api
#      https://dot.mindreset.tech/docs/service/studio/api/image_api
#
# Text API Fields:
#   - refreshNow (bool, optional, default true): controls display timing.
#   - deviceId (string, required): unique device serial.
#   - title (string, optional): title text shown on screen.
#   - message (string, optional): body text shown on screen.
#   - signature (string, optional): footer/signature text.
#   - icon (string, optional): base64 PNG icon (40px x 40px).
#   - link (string, optional): tap-to-interact target URL.
#
# Image API Fields:
#   - refreshNow (bool, optional, default true): controls display timing.
#   - deviceId (string, required): unique device serial.
#   - image (string, required): base64 PNG image (296px x 152px).
#   - link (string, optional): tap-to-interact target URL.
#   - border (number, optional, default 0): 0=white, 1=black frame.
#   - ditherType (string, optional, default DIFFUSION): dithering mode.
#   - ditherKernel (string, optional, default FLOYD_STEINBERG):
#     dithering kernel.

from contextlib import suppress
import json
import logging

import requests

from ..common import NotifyImageSize, NotifyType
from ..locale import gettext_lazy as _
from ..url import PrivacyMode
from ..utils.parse import parse_bool
from ..utils.sanitize import sanitize_payload
from .base import NotifyBase

# Supported Dither Types
DOT_DITHER_TYPES = (
    "DIFFUSION",
    "ORDERED",
    "NONE",
)

# Supported Dither Kernels
DOT_DITHER_KERNELS = (
    "THRESHOLD",
    "ATKINSON",
    "BURKES",
    "FLOYD_STEINBERG",
    "SIERRA2",
    "STUCKI",
    "JARVIS_JUDICE_NINKE",
    "DIFFUSION_ROW",
    "DIFFUSION_COLUMN",
    "DIFFUSION_2D",
)


class NotifyDot(NotifyBase):
    """A wrapper for Dot. Notifications."""

    # The default descriptive name associated with the Notification
    service_name = "Dot."
    # Alias: devices marketed as "Quote/0" remain discoverable.

    # The services URL
    service_url = "https://dot.mindreset.tech"

    # All notification requests are secure
    secure_protocol = "dot"

    # A URL that takes you to the setup/help of the specific protocol
    setup_url = "https://appriseit.com/services/dot/"

    # Allows the user to specify the NotifyImageSize object
    image_size = NotifyImageSize.XY_128

    # Support Attachments
    attachment_support = True

    # Supported API modes
    SUPPORTED_MODES = ("text", "image")

    DEFAULT_MODE = "text"

    # Define object templates
    templates = ("{schema}://{apikey}@{device_id}/{mode}/",)

    # Define our template arguments
    template_tokens = dict(
        NotifyBase.template_tokens,
        **{
            "apikey": {
                "name": _("API Key"),
                "type": "string",
                "required": True,
                "private": True,
            },
            "device_id": {
                "name": _("Device Serial Number"),
                "type": "string",
                "required": True,
                "map_to": "device_id",
            },
            "mode": {
                "name": _("API Mode"),
                "type": "choice:string",
                "values": SUPPORTED_MODES,
                "default": DEFAULT_MODE,
                "required": True,
                "map_to": "mode",
            },
        },
    )

    # Define our template arguments
    template_args = dict(
        NotifyBase.template_args,
        **{
            "refresh": {
                "name": _("Refresh Now"),
                "type": "bool",
                "default": True,
                "map_to": "refresh_now",
            },
            "signature": {
                "name": _("Text Signature"),
                "type": "string",
            },
            "icon": {
                "name": _("Icon Base64 (Text API)"),
                "type": "string",
            },
            "image": {
                "name": _("Image Base64 (Image API)"),
                "type": "string",
                "map_to": "image_data",
            },
            "link": {
                "name": _("Link"),
                "type": "string",
            },
            "border": {
                "name": _("Border"),
                "type": "int",
                "min": 0,
                "max": 1,
                "default": 0,
            },
            "dither_type": {
                "name": _("Dither Type"),
                "type": "choice:string",
                "values": DOT_DITHER_TYPES,
                "default": "DIFFUSION",
            },
            "dither_kernel": {
                "name": _("Dither Kernel"),
                "type": "choice:string",
                "values": DOT_DITHER_KERNELS,
                "default": "FLOYD_STEINBERG",
            },
        },
    )
    # Note:
    # - icon (Text API): base64 PNG icon (40px x 40px) in lower-left corner.
    #   Can be provided via `icon` parameter or first attachment.
    # - image (Image API): base64 PNG image (296px x 152px) supplied via
    #   configuration `image` parameter or first attachment.
    # - Only the first attachment is used; multiple attachments trigger a
    #   warning.

    def __init__(
        self,
        apikey=None,
        device_id=None,
        mode=DEFAULT_MODE,
        refresh_now=True,
        signature=None,
        icon=None,
        link=None,
        border=None,
        dither_type=None,
        dither_kernel=None,
        image_data=None,
        **kwargs,
    ):
        """Initialize Notify Dot Object."""
        super().__init__(**kwargs)

        # API Key (from user)
        self.apikey = apikey

        # Device ID tracks the Dot hardware serial.
        self.device_id = device_id

        # Refresh Now flag: True shows content immediately (default).
        self.refresh_now = parse_bool(refresh_now, default=True)

        # API mode ("text" or "image")
        self.mode = (
            mode.lower()
            if isinstance(mode, str) and mode.lower() in self.SUPPORTED_MODES
            else self.DEFAULT_MODE
        )
        if (
            not isinstance(mode, str)
            or mode.lower() not in self.SUPPORTED_MODES
        ):
            self.logger.warning(
                "Unsupported Dot mode (%s) specified; defaulting to '%s'.",
                mode,
                self.mode,
            )

        # Signature text used by the Text API footer.
        self.signature = signature if isinstance(signature, str) else None

        # Icon for the Text API (base64 PNG 40x40, lower-left corner).
        # Note: distinct from the Image API "image" field.
        self.icon = icon if isinstance(icon, str) else None

        # Image payload for the Image API (base64 PNG 296x152).
        self.image_data = image_data if isinstance(image_data, str) else None
        if self.mode == "text" and self.image_data:
            self.logger.warning(
                "Image data provided in text mode; ignoring configurable"
                " image payload."
            )
            self.image_data = None

        # Link for tap-to-interact navigation.
        self.link = link if isinstance(link, str) else None

        # Border for the Image API
        self.border = border

        # Dither type for Image API
        self.dither_type = dither_type

        # Dither kernel for the Image API
        self.dither_kernel = dither_kernel

        # Text API endpoint
        self.text_api_url = "https://dot.mindreset.tech/api/open/text"

        # Image API endpoint
        self.image_api_url = "https://dot.mindreset.tech/api/open/image"

        return

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

        if not self.apikey:
            self.logger.warning("No API key was specified")
            return False

        if not self.device_id:
            self.logger.warning("No device ID was specified")
            return False

        # Prepare our headers
        headers = {
            "Authorization": f"Bearer {self.apikey}",
            "Content-Type": "application/json",
            "User-Agent": self.app_id,
        }

        if self.mode == "image":
            if title or body:
                self.logger.warning(
                    "Title and body are not supported in image mode "
                    "and will be ignored."
                )

            image_data = (
                self.image_data if isinstance(self.image_data, str) else None
            )

            # Use first attachment as image if no image_data provided
            # attachment.base64() returns base64-encoded string for API
            if not image_data and attach and self.attachment_support:
                if len(attach) > 1:
                    self.logger.warning(
                        "Multiple attachments provided; only the first "
                        "one will be used as image."
                    )
                try:
                    attachment = attach[0]
                    if attachment:
                        # Convert attachment to base64-encoded string
                        image_data = attachment.base64()
                except Exception as e:
                    self.logger.warning(f"Failed to process attachment: {e!s}")

            if not image_data:
                self.logger.warning(
                    "Image API mode selected but no image data was provided."
                )
                return False

            # Use Image API
            # Image API payload:
            #   refreshNow: display timing control.
            #   deviceId: Dot device serial (required).
            #   image: base64 PNG 296x152 (required).
            #   link: optional tap target.
            #   border: optional frame color.
            #   ditherType: optional dithering mode.
            #   ditherKernel: optional dithering kernel.
            payload = {
                "refreshNow": self.refresh_now,
                "deviceId": self.device_id,
                "image": image_data,  # Image payload shown on screen
            }

            if self.link:
                payload["link"] = self.link

            if self.border is not None:
                payload["border"] = self.border

            if self.dither_type is not None:
                payload["ditherType"] = self.dither_type

            if self.dither_kernel is not None:
                payload["ditherKernel"] = self.dither_kernel

            api_url = self.image_api_url

        else:
            # Use Text API
            # Text API payload:
            #   refreshNow: display timing control.
            #   deviceId: Dot device serial (required).
            #   title: optional title on screen.
            #   message: optional body on screen.
            #   signature: optional footer text.
            #   icon: optional base64 PNG icon (40x40).
            #   link: optional tap target.
            payload = {
                "refreshNow": self.refresh_now,
                "deviceId": self.device_id,
            }

            if title:
                payload["title"] = title

            if body:
                payload["message"] = body

            if self.signature:
                payload["signature"] = (
                    self.signature
                )  # Footer/signature displayed on screen

            # Use first attachment as icon if no icon provided
            # attachment.base64() returns base64-encoded string for API
            icon_data = self.icon
            if not icon_data and attach and self.attachment_support:
                if len(attach) > 1:
                    self.logger.warning(
                        "Multiple attachments provided; only the first "
                        "one will be used as icon."
                    )
                try:
                    attachment = attach[0]
                    if attachment:
                        # Convert attachment to base64-encoded string
                        icon_data = attachment.base64()
                except Exception as e:
                    self.logger.warning(f"Failed to process attachment: {e!s}")

            if icon_data:
                # Text API icon payload
                payload["icon"] = icon_data

            if self.link:
                payload["link"] = self.link

            api_url = self.text_api_url

        # Some Debug Logging
        if self.logger.isEnabledFor(logging.DEBUG):
            # Due to attachments; output can be quite heavy and io intensive
            # To accommodate this, we only show our debug payload information
            # if required.
            self.logger.debug(
                "Dot POST URL:"
                f" {api_url} (cert_verify={self.verify_certificate!r})"
            )
            self.logger.debug("Dot Payload %s", sanitize_payload(payload))

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

        try:
            r = requests.post(
                api_url,
                data=json.dumps(payload),
                headers=headers,
                verify=self.verify_certificate,
                timeout=self.request_timeout,
            )

            if r.status_code == requests.codes.ok:
                self.logger.info(f"Sent Dot notification to {self.device_id}.")
                return True

            # We had a problem
            status_str = NotifyDot.http_response_code_lookup(r.status_code)

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

            self.logger.debug(
                "Response Details:\r\n%r", (r.content or b"")[:2000])

            return False

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

            return False

    @property
    def url_identifier(self):
        """Returns all of the identifiers that make this URL unique from
        another similar one.
        """
        return (
            self.secure_protocol,
            self.apikey,
            self.device_id,
            self.mode,
        )

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

        # Define any URL parameters
        params = {
            "refresh": "yes" if self.refresh_now else "no",
        }

        if self.mode == "text":
            if self.signature:
                params["signature"] = self.signature

            if self.icon:
                params["icon"] = self.icon

            if self.link:
                params["link"] = self.link

        else:  # image mode
            if self.image_data:
                params["image"] = self.image_data

            if self.link:
                params["link"] = self.link

            if self.border is not None:
                params["border"] = str(self.border)

            if self.dither_type and self.dither_type != "DIFFUSION":
                params["dither_type"] = self.dither_type

            if self.dither_kernel and self.dither_kernel != "FLOYD_STEINBERG":
                params["dither_kernel"] = self.dither_kernel

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

        mode_segment = f"/{self.mode}/"

        return "{schema}://{apikey}@{device_id}{mode}?{params}".format(
            schema=self.secure_protocol,
            apikey=self.pprint(
                self.apikey, privacy, mode=PrivacyMode.Secret, safe=""
            ),
            device_id=NotifyDot.quote(self.device_id, safe=""),
            mode=mode_segment,
            params=NotifyDot.urlencode(params),
        )

    def __len__(self):
        """Returns the number of targets associated with this notification."""
        return 1 if self.device_id else 0

    @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)
        if not results:
            # We're done early as we couldn't load the results
            return results

        # Determine API mode from path (default to text)
        mode = NotifyDot.DEFAULT_MODE
        path_tokens = NotifyDot.split_path(results.get("fullpath"))
        if path_tokens:
            candidate = path_tokens.pop(0).lower()
            if candidate in NotifyDot.SUPPORTED_MODES:
                mode = candidate
            else:
                NotifyDot.logger.warning(
                    "Unsupported Dot mode (%s) detected; defaulting to '%s'.",
                    candidate,
                    NotifyDot.DEFAULT_MODE,
                )
        results["mode"] = mode
        remaining_path = "/".join(path_tokens)
        results["fullpath"] = "/" + remaining_path if remaining_path else "/"
        results["path"] = remaining_path

        # Extract API key from user
        user = results.get("user")
        if user:
            results["apikey"] = NotifyDot.unquote(user)

        # Extract device ID from hostname
        host = results.get("host")
        if host:
            results["device_id"] = NotifyDot.unquote(host)

        # Refresh Now
        refresh_value = results["qsd"].get("refresh")
        if refresh_value:
            results["refresh_now"] = parse_bool(refresh_value.strip())

        # Signature
        signature_value = results["qsd"].get("signature")
        if signature_value:
            results["signature"] = NotifyDot.unquote(signature_value.strip())

        # Icon
        icon_value = results["qsd"].get("icon")
        if icon_value:
            results["icon"] = NotifyDot.unquote(icon_value.strip())

        # Link
        link_value = results["qsd"].get("link")
        if link_value:
            results["link"] = NotifyDot.unquote(link_value.strip())

        # Border
        border_value = results["qsd"].get("border")
        if border_value:
            with suppress(TypeError, ValueError):
                results["border"] = int(border_value.strip())

        # Dither Type
        dither_type_value = results["qsd"].get("dither_type")
        if dither_type_value:
            results["dither_type"] = NotifyDot.unquote(
                dither_type_value.strip()
            )

        # Dither Kernel
        dither_kernel_value = results["qsd"].get("dither_kernel")
        if dither_kernel_value:
            results["dither_kernel"] = NotifyDot.unquote(
                dither_kernel_value.strip()
            )

        # Image (Image API)
        image_value = results["qsd"].get("image")
        if image_value:
            results["image_data"] = NotifyDot.unquote(image_value.strip())

        return results
