#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Standard Library
import asyncio
import logging
import random
import socket

from . import utils

SSDP_MULTICAST_IP = "239.255.255.250"
SSDP_PORT = 1900
WEB_PORT = 8008
logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)

NT_MEDIARENDERER = "urn:schemas-upnp-org:service:MediaRenderer:1"
NT_AVTRANSPORT = "urn:schemas-upnp-org:service:AVTransport:1"

NTS_ALIVE = "ssdp:alive"
NTS_BYEBYE = "ssdp:byebye"


class SSDPTransport:
    def __init__(self):
        self.loop = asyncio.get_event_loop()
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton("0.0.0.0"))
        self.sock.setsockopt(
            socket.IPPROTO_IP,
            socket.IP_ADD_MEMBERSHIP,
            socket.inet_aton(SSDP_MULTICAST_IP) + socket.inet_aton("0.0.0.0"),
        )
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.setblocking(0)
        self.sock.bind(("0.0.0.0", SSDP_PORT))

    async def sendto(self, message, addr):
        return await self.loop.sock_sendto(self.sock, message, addr)

    async def recvfrom(self):
        return await self.loop.sock_recvfrom(self.sock, 1024)

    def close(self):
        return self.sock.close()


class SSDPProtocol(asyncio.DatagramProtocol):
    transport = None

    def datagram_received(self, data, addr):
        if not b"M-SEARCH" in data or b"ssdp:discover" not in data:
            return

        data = {
            line.split(":", 1)[0].strip().upper(): line.split(":", 1)[1].strip()
            for line in data.decode("utf-8").split("\r\n")
            if line and ":" in line
        }

        timing = {
            "max_wait": int(data.get("MX", "0")),
            "min_wait": int(data.get("MM", "0")),
        }
        sid = data.get("S", "")

        if "ssdp:discover" not in data.get("MAN", ""):
            return

        notification_types = []
        if NT_MEDIARENDERER in data.get("ST", "") or "ssdp:all" in data.get("ST", ""):
            notification_types.append(NT_MEDIARENDERER)
        if NT_AVTRANSPORT in data.get("ST", "") or "ssdp:all" in data.get("ST", ""):
            notification_types.append(NT_AVTRANSPORT)

        for nt in notification_types:
            asyncio.create_task(self.send_m_search(addr, nt, timing, sid))

    async def send_notify(self, notification_type, notification_type_sub):
        logger.debug("SSDP: NOTIFY %s, %s", notification_type, notification_type_sub)
        text = "\r\n".join(
            [
                "NOTIFY * HTTP/1.1",
                f"HOST: {SSDP_MULTICAST_IP}:{SSDP_PORT}",
                "CACHE-CONTROL: max-age=30",
                f"LOCATION: http://{utils.get_ip_address()}:{WEB_PORT}/dlna/info.xml",
                f"NT: {notification_type}",
                f"NTS: {notification_type_sub}",
                f"SERVER: {utils.get_friendly_name()}",
                f"USN: uuid:{utils.get_uuid()}::{notification_type}",
                "",
                "",
            ]
        )
        addr = (SSDP_MULTICAST_IP, SSDP_PORT)
        await self.transport.sendto(text.encode("utf-8"), addr)

    async def send_m_search(self, addr, notification_type, timing, sid):
        logger.info("SSDP: M-SEARCH %s, %s, %s", notification_type, timing["min_wait"], timing["max_wait"])
        text = "\r\n".join(
            [
                "HTTP/1.1 200 OK",
                f"ST: {notification_type}",
                f"USN: uuid:{utils.get_uuid()}::{notification_type}",
                f"LOCATION: http://{utils.get_ip_address()}:{WEB_PORT}/dlna/info.xml",
                f"AL: http://{utils.get_ip_address()}:{WEB_PORT}/dlna/info.xml",
                f"S: {sid}",
                "EXT: ",
                f"SERVER: {utils.get_friendly_name()}",
                "CACHE-CONTROL: max-age=30",
                "",
                "",
            ]
        )
        await asyncio.sleep(random.uniform(timing["min_wait"], timing["max_wait"]))
        await self.transport.sendto(text.encode("utf-8"), addr)

    def connection_made(self, transport):
        self.transport = transport
        asyncio.create_task(self._connection_made())

    async def _connection_made(self):
        async with asyncio.TaskGroup() as tg:
            tg.create_task(self.send_notify(NT_AVTRANSPORT, NTS_ALIVE))
            tg.create_task(self.send_notify(NT_MEDIARENDERER, NTS_ALIVE))

    async def close(self, *args, **kwargs):
        async with asyncio.TaskGroup() as tg:
            tg.create_task(self.send_notify(NT_AVTRANSPORT, NTS_BYEBYE))
            tg.create_task(self.send_notify(NT_MEDIARENDERER, NTS_BYEBYE))
        self.transport.close()


async def create_ssdp_server():
    try:
        proto = SSDPProtocol()
        transport = SSDPTransport()

        proto.connection_made(transport)

        while True:  # not utils.ironcast.is_closing.is_set():
            data, remote_addr = await transport.recvfrom()
            proto.datagram_received(data, remote_addr)
    except asyncio.CancelledError:
        logger.info("SSDP: cancelled")
    finally:
        logger.info("SSDP: Closing SSDP server.")
        await proto.close()
