#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# External Dependencies
import kivy

kivy.require("2.3.1")
# Standard Library
import asyncio
import html
import logging
import traceback
import xml.etree.ElementTree as ET
from glob import glob
from os.path import basename, dirname, join
from urllib.parse import parse_qs, urlparse

from . import utils

# External Dependencies
from aiohttp import ClientSession, web

SSDP_PORT = 1900
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
SSDP_ADDR = "239.255.255.250"
UUID = utils.get_uuid()
SERV_IP = utils.get_ip_address()
SERV_PORT = 8008
NAME = utils.get_friendly_name()
XML_DOCS = {}
ROOT_NAMESPACES = {
    "dc": "http://purl.org/dc/elements/1.1/",
    "": "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
    "upnp": "urn:schemas-upnp-org:metadata-1-0/upnp/",
}
METADATA_NAMESPACES = {"s": "http://schemas.xmlsoap.org/soap/envelope/"}
xml_headers = {"Content-Type": "text/xml", "Access-Control-Allow-Origin": "*"}
PREVIEW_FILE = join(dirname(__file__), "img", "preview.png")

for filename in glob(join(dirname(__file__), "xml", "*.xml")):
    with open(filename, "r", encoding="utf-8") as file:
        key = basename(filename).replace(".xml", "")
        XML_DOCS[key] = file.read()


routes = web.RouteTableDef()
event_clients = {}


@web.middleware
async def error_middleware(request, handler):
    try:
        response = await handler(request)
        # this is needed to handle ``return web.HTTPNotFound()`` case
        if response.status == 404:
            return web.Response(status=404)
        return response
    except web.HTTPException as ex:
        # this is needed to handle ``raise web.HTTPNotFound()`` case
        logger.info(repr(request))
        logger.info(repr(ex))
        if ex.status == 404:
            if "/favicon.ico" not in str(request.url):
                logger.info("second")
                text = await request.read()
                logger.info(repr(text))
            return web.Response(status=404)
        raise
    # this is needed to handle non-HTTPException
    except Exception:  # pylint: disable=broad-exception-caught
        logger.error(traceback.format_exc())
        return web.Response(status=500)


def template_response(template, **kwargs):
    with open(join(dirname(__file__), "xml", template), "r", encoding="utf-8") as f:
        data = f.read()
    data = data.format(**kwargs)
    return web.Response(text=data, headers=xml_headers)


@routes.get("/dlna/info.xml")
def handle_desc(_request):
    icon = XML_DOCS["rootDescLogo"].format("image/png", "/img/logo.png")
    services = []
    services.append(
        XML_DOCS["rootDescService"].format(
            "AVTransport",
            "1",
            "AVTransport",
            "/dlna/AVTransport_scpd.xml",
            "/dlna/_urn:schemas-upnp-org:service:AVTransport_control",
            "/dlna/_urn:schemas-upnp-org:service:AVTransport_event",
        )
    )
    services.append(
        XML_DOCS["rootDescService"].format(
            "RenderingControl",
            "1",
            "RenderingControl",
            "/dlna/RenderingControl_scpd.xml",
            "/dlna/_urn:schemas-upnp-org:service:RenderingControl_control",
            "/dlna/_urn:schemas-upnp-org:service:RenderingControl_event",
        )
    )
    text = XML_DOCS["rootDesc"].format(
        "MediaRenderer",
        "1",
        NAME,
        "IronCast Project",
        "",
        "IronCast v1",
        "IronCast",
        "1",
        "",
        "0",
        UUID,
        icon,
        "\n".join(services),
        "",
        SERV_IP,
        SERV_PORT,
    )
    return web.Response(text=text, headers=xml_headers)


@routes.get("/img/logo.png")
def handle_logo(_request):
    return web.FileResponse(join(dirname(__file__), "img", "icon.png"), headers=xml_headers)


@routes.get("/dlna/AVTransport_scpd.xml")
def handle_avtransport_scpd(_request):
    return web.FileResponse(join(dirname(__file__), "xml", "AVTransport.xml"), headers=xml_headers)


@routes.get("/dlna/RenderingControl_scpd.xml")
def handle_renderingcontrol_scpd(_request):
    return web.FileResponse(join(dirname(__file__), "xml", "RenderingControl.xml"), headers=xml_headers)


@routes.post("/dlna/_urn:schemas-upnp-org:service:AVTransport_control")
async def handle_avtransport_control(request):
    data = await request.read()
    transport_control_mapping = {
        b"GetTransportInfo": handle_get_transport_info,
        b"GetPositionInfo": handle_get_position_info,
        b"u:Stop": handle_stop,
        b"u:SetAVTransportURI": handle_set_avtransport_uri,
        b"u:Play": handle_play,
        b"u:SetPlayMode": handle_set_play_mode,
        b"u:Pause": handle_pause,
        b"u:Seek": handle_seek,
    }
    for action, handler in transport_control_mapping.items():
        if action in data:
            return handler(request, data)
    logger.error("SERVER: Unknown request type:")
    logger.error(repr(data))
    return web.Response(status=404)


def handle_get_transport_info(request, data):
    # Possible options are STOPPED, PLAYING, PAUSED_PLAYBACK, TRANSITIONING, and NO_MEDIA_PRESENT
    state = utils.ironcast.state
    dur = utils.to_time(utils.ironcast.duration)
    pos = utils.to_time(utils.ironcast.position)
    logger.debug("SERVER: TransportInfo %s  - -  %s / %s", repr(state), pos, dur)
    return template_response("AVTransport_control_GetTransportInfo.xml", TRANSPORT_STATE=state)


def handle_get_position_info(request, data):
    conf = {
        "INDEX": 0,
        "DURATION": utils.to_time(utils.ironcast.duration),
        "URI": utils.ironcast.uri,
        "TIME": utils.to_time(utils.ironcast.position),
    }
    logger.debug("SERVER: PositionInfo %s", repr(conf))
    return template_response("AVTransport_control_GetPositionInfo.xml", **conf)


def handle_stop(request, data):
    logger.debug("SERVER: Stop")
    utils.ironcast.gui_queue.put_nowait(("stop", ()))
    return template_response("AVTransport_control_Stop.xml")


async def set_preview_image(request, uri):
    async with request.app.session.get(uri) as resp:
        with open(PREVIEW_FILE, "wb") as f:
            f.write(await resp.read())


def handle_set_avtransport_uri(request, data):
    logger.debug("SERVER: SetAVTransportURI")
    logger.debug(data)
    root = ET.fromstring(data)
    uri = root.findtext("s:Body//CurrentURI", None, METADATA_NAMESPACES)
    if not uri:
        return web.Response(status=404)
    metadata = html.unescape(root.findtext("s:Body//CurrentURIMetaData", None, METADATA_NAMESPACES))
    metadata = metadata.replace("&", "&amp;")
    preview = ""
    try:
        metadata = ET.fromstring(metadata)
        duration = metadata.find(".//res", ROOT_NAMESPACES).get("duration", "00:00:00")
        if preview := metadata.findtext(".//upnp:albumArtURI", None, ROOT_NAMESPACES):
            asyncio.create_task(set_preview_image(request, preview))
    except ET.ParseError:
        duration = "00:00:00"

    utils.ironcast.uri = uri
    utils.ironcast.duration = utils.from_time(duration)

    # Try to guess what the offset is if there is one.
    qs = parse_qs(urlparse(uri).query)
    if "s" in qs:
        utils.ironcast.current_offset = int(qs["s"][0])
    else:
        utils.ironcast.current_offset = 0

    utils.ironcast.gui_queue.put_nowait(("preview", (PREVIEW_FILE if preview else "",)))
    utils.ironcast.gui_queue.put_nowait(("source", (uri,)))
    utils.ironcast.state = "STOPPED"
    utils.ironcast.video_loaded = False
    utils.ironcast.cec_queue.put_nowait(("power_on", ()))
    return template_response("AVTransport_control_SetAVTransportURI.xml")


def handle_play(request, data):
    logger.debug("SERVER: Play")
    utils.ironcast.gui_queue.put_nowait(("play", ()))
    utils.ironcast.cec_queue.put_nowait(("is_active", ()))
    return template_response("AVTransport_control_Play.xml")


def handle_set_play_mode(request, data):
    logger.debug("SERVER: SetPlayMode")
    return template_response("AVTransport_control_SetPlayMode.xml")


def handle_pause(request, data):
    logger.debug("SERVER: Pause")
    utils.ironcast.gui_queue.put_nowait(("pause", ()))
    return template_response("AVTransport_control_Pause.xml")


def handle_seek(request, data):
    logger.debug("SERVER: Seek")
    root = ET.fromstring(data)
    pos = root.findtext("s:Body//Target", None, METADATA_NAMESPACES)
    utils.ironcast.current_offset = utils.from_time(pos)
    utils.ironcast.gui_queue.put_nowait(("seek", (utils.from_time(pos),)))
    return template_response("AVTransport_control_Seek.xml")


@routes.route("SUBSCRIBE", "/dlna/{uri}")
async def handle_subscribe(request):
    if client := request.headers.get("callback", None):
        client = client.replace(">", "").replace("<", "")
        sid = utils.make_uuid()
        event_clients[client] = {"sid": sid, "seq": 0}
    else:
        sid = request.headers.get("sid")
        try:
            client = list(event_clients.keys())[list(event_clients.values()).index(sid)]
        except IndexError:
            return web.Response(status=404)

    asyncio.create_task(last_change_render_control(request, client))

    return web.Response(
        headers={
            "timeout": "Second-3600",
            "content-type": 'text/html; charset="utf-8"',
            "SID": sid,
            "content-length": "0",
        }
    )


@routes.route("UNSUBSCRIBE", "/dlna/{uri}")
async def handle_unsubscribe(request):
    sid = request.headers.get("sid")
    if sid in event_clients:
        del event_clients[sid]
    return web.Response(headers={"content-length": "0"})


async def client_session_ctx(app):
    """
    Cleanup context async generator to create and properly close aiohttp ClientSession
    """
    logger.debug("Creating ClientSession")
    app.session = ClientSession()

    yield

    logger.debug("Closing ClientSession")
    await app.session.close()


def create_web_server(access_log=None):
    web_server = web.Application(middlewares=[error_middleware])
    web_server.add_routes(routes)
    web_server.cleanup_ctx.append(client_session_ctx)
    return web._run_app(web_server, port=SERV_PORT, access_log=access_log)  # pylint: disable=protected-access


async def last_change_render_control(request, client_url):
    is_muted = "1" if utils.ironcast.is_muted.is_set() else "0"
    volume = utils.ironcast.volume

    headers = {
        "content-type": 'text/xml; charset="utf-8"',
        "nt": "upnp:event",
        "nts": "upnp:propchange",
        "sid": event_clients[client_url]["sid"],
        "seq": str(event_clients[client_url]["seq"]),
    }
    event_clients[client_url]["seq"] += 1

    callback = f"""<Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
    <InstanceID val="0">
        <Volume channel="Master" val="{volume}"/>
        <Mute channel="Master" val="{is_muted}"/>
    </InstanceID>
</Event>"""
    async with request.app.session.request(
        "NOTIFY", client_url, data=callback.encode("utf-8"), headers=headers
    ) as resp:
        logger.info(await resp.text())
