"""Helper methods to handle the time in Home Assistant."""

from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable, Mapping
import datetime as dt
import logging
from logging import Logger
import random
import ssl
from typing import Any, TypedDict, TypeVar

import ciso8601
from icmplib import Host, ICMPLibError, SocketPermissionError, async_multiping
import jwt

from .exceptions import NabuCasaBaseError

CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable)  # pylint: disable=invalid-name
UTC = dt.UTC

_LOGGER = logging.getLogger(__name__)


class CheckLatencyError(NabuCasaBaseError):
    """Error to indicate a ping failure."""


class CheckLatencyInsufficientPrivileges(CheckLatencyError):
    """Error to indicate insufficient privileges for pinging."""


class CheckLatencyHostResult(TypedDict):
    """Result of a latency check for a single host."""

    address: str
    avg_rtt: float
    is_alive: bool
    max_rtt: float
    min_rtt: float


def utcnow() -> dt.datetime:
    """Get now in UTC time."""
    return dt.datetime.now(UTC)


def utc_from_timestamp(timestamp: float) -> dt.datetime:
    """Return a UTC time from a timestamp."""
    return dt.datetime.fromtimestamp(timestamp, UTC)


def parse_date(dt_str: str) -> dt.date | None:
    """Convert a date string to a date object."""
    try:
        return ciso8601.parse_datetime(dt_str).date()
    except ValueError:  # If dt_str did not match our format
        return None


def seconds_as_dhms(seconds: float) -> str:
    """Convert seconds to a DDd:HHh:MMm:SSs string."""
    days, seconds = divmod(int(seconds), 86400)
    hours, seconds = divmod(seconds, 3600)
    minutes, seconds = divmod(seconds, 60)

    parts = []
    if days > 0:
        parts.append(f"{days}d")
    if hours > 0 or (parts and (minutes > 0 or seconds > 0)):
        parts.append(f"{hours}h")
    if minutes > 0 or (parts and seconds > 0):
        parts.append(f"{minutes}m")
    if seconds > 0 or not parts:
        parts.append(f"{seconds}s")
    return ":".join(parts)


def expiration_from_token(token: str | None) -> int | None:
    """Return the expiration time from a token."""
    if not token:
        return None

    try:
        decoded_token: Mapping[str, Any] = jwt.decode(
            token,
            options={"verify_signature": False},
        )
        return int(decoded_token["exp"])
    except jwt.DecodeError, KeyError:
        return None


def server_context_modern() -> ssl.SSLContext:
    """
    Return an SSL context following the Mozilla recommendations.

    TLS configuration follows the best-practice guidelines specified here:
    https://wiki.mozilla.org/Security/Server_Side_TLS
    Modern guidelines are followed.
    """
    context = ssl.SSLContext(ssl.PROTOCOL_TLS)  # pylint: disable=no-member

    context.options |= (
        ssl.OP_NO_SSLv2
        | ssl.OP_NO_SSLv3
        | ssl.OP_NO_TLSv1
        | ssl.OP_NO_TLSv1_1
        | ssl.OP_CIPHER_SERVER_PREFERENCE
    )
    if hasattr(ssl, "OP_NO_COMPRESSION"):
        context.options |= ssl.OP_NO_COMPRESSION

    context.set_ciphers(
        "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:"
        "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:"
        "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:"
        "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:"
        "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256",
    )

    return context


def next_midnight() -> float:
    """Return the seconds till next local midnight."""
    midnight = dt.datetime.now().replace(
        hour=0,
        minute=0,
        second=0,
        microsecond=0,
    ) + dt.timedelta(days=1)
    return (midnight - dt.datetime.now()).total_seconds()


async def async_check_latency(
    addresses: list[str],
    *,
    count: int = 1,
    ping_timeout: float = 5,
    privileged: bool = True,
) -> list[CheckLatencyHostResult]:
    """Check latency to a list of IP addresses and return them.

    Args:
        addresses: List of IP addresses to ping.
        count: Number of ping packets to send to each address.
        ping_timeout: Timeout in seconds for each ping.
        privileged: Whether to use privileged (raw socket) mode.

    Returns:
        List of CheckLatencyHostResult dicts.

    """
    if not addresses:
        raise CheckLatencyError("No addresses provided")

    hosts: list[Host]
    try:
        hosts = await async_multiping(
            addresses=addresses,
            count=count,
            timeout=ping_timeout,
            privileged=privileged,
        )
    except SocketPermissionError as err:
        if not privileged:
            raise CheckLatencyInsufficientPrivileges(
                "Insufficient privileges to perform ICMP ping."
            ) from err
        _LOGGER.info(
            "Ping failed due to insufficient privileges, "
            "retrying without privileged mode"
        )
        return await async_check_latency(
            addresses,
            count=count,
            ping_timeout=ping_timeout,
            privileged=False,
        )
    except ICMPLibError as err:
        raise CheckLatencyError("ICMP ping failed") from err

    return [
        CheckLatencyHostResult(
            address=host.address,
            is_alive=host.is_alive,
            avg_rtt=host.avg_rtt,
            max_rtt=host.max_rtt,
            min_rtt=host.min_rtt,
        )
        for host in hosts
    ]


def jitter(minimum: float, maximum: float) -> float:
    """Return a random float between minimum and maximum for backoff jitter."""
    return random.uniform(minimum, maximum)


async def gather_callbacks(
    logger: Logger,
    name: str,
    callbacks: list[Callable[[], Awaitable[None]]],
) -> None:
    """Gather callbacks and log exceptions."""
    results = await asyncio.gather(*[cb() for cb in callbacks], return_exceptions=True)
    for result, callback in zip(results, callbacks, strict=False):
        if not isinstance(result, Exception):
            continue
        logger.error(
            "Unexpected error in %s callback %s", name, callback, exc_info=result
        )


class Registry(dict):
    """Registry of items."""

    def register(self, name: str) -> Callable[[CALLABLE_T], CALLABLE_T]:
        """Return decorator to register item with a specific name."""

        def decorator(func: CALLABLE_T) -> CALLABLE_T:
            """Register decorated function."""
            self[name] = func
            return func

        return decorator
