"""Implementation of the RTSP protocol.

This is a simple implementation of the RTSP protocol used by Apple (with its quirks
and all). It is somewhat generalized to support both AirPlay 1 and 2.
"""
import asyncio
from hashlib import md5
import logging
import plistlib
from random import randrange
from typing import Any, Dict, Mapping, NamedTuple, Optional, Tuple, Union

import async_timeout

from pyatv.protocols.dmap import tags
from pyatv.support.http import HttpConnection, HttpResponse, decode_bplist_from_body
from pyatv.support.metadata import MediaMetadata

_LOGGER = logging.getLogger(__name__)

FRAMES_PER_PACKET = 352
USER_AGENT = "AirPlay/550.10"
HTTP_PROTOCOL = "HTTP/1.1"

ANNOUNCE_PAYLOAD = (
    "v=0\r\n"
    + "o=iTunes {session_id} 0 IN IP4 {local_ip}\r\n"
    + "s=iTunes\r\n"
    + "c=IN IP4 {remote_ip}\r\n"
    + "t=0 0\r\n"
    + "m=audio 0 RTP/AVP 96\r\n"
    + "a=rtpmap:96 L16/44100/2\r\n"
    + f"a=fmtp:96 {FRAMES_PER_PACKET} 0 "
    + "{bits_per_channel} 40 10 14 {channels} 255 0 0 {sample_rate}\r\n"
)

# Used to signal that traffic is to be unencrypted
AUTH_SETUP_UNENCRYPTED = b"\x01"

# Just a static Curve25519 public key used to satisfy the auth-setup step for devices
# requiring that (e.g. AirPort Express). We never verify anything. Source:
# https://github.com/owntone/owntone-server/blob/
# c1db4d914f5cd8e7dbe6c1b6478d68a4c14824af/src/outputs/raop.c#L276
CURVE25519_PUB_KEY = (
    b"\x59\x02\xed\xe9\x0d\x4e\xf2\xbd"
    b"\x4c\xb6\x8a\x63\x30\x03\x82\x07"
    b"\xa9\x4d\xbd\x50\xd8\xaa\x46\x5b"
    b"\x5d\x8c\x01\x2a\x0c\x7e\x1d\x4e"
)

BPLIST_CONTENT_TYPE = "application/x-apple-binary-plist"


class DigestInfo(NamedTuple):
    """
    OAuth information used for password protected devices.
    """

    username: str
    realm: str
    password: str
    nonce: str


def get_digest_payload(method, uri, user, realm, pwd, nonce):
    """Return the Authorization payload for Apples OAuth."""
    payload = (
        'Digest username="{0}", realm="{1}", nonce="{2}", uri="{3}", response="{4}"'
    )
    ha1 = md5(f"{user}:{realm}:{pwd}".encode("utf-8")).hexdigest()
    ha2 = md5(f"{method}:{uri}".encode("utf-8")).hexdigest()
    di_response = md5(f"{ha1}:{nonce}:{ha2}".encode("utf-8")).hexdigest()
    return payload.format(user, realm, nonce, uri, di_response)


class RtspSession:
    """Representation of an RTSP session."""

    def __init__(self, connection: HttpConnection) -> None:
        """Initialize a new RtspSession."""
        super().__init__()
        self.connection = connection
        self.requests: Dict[int, Tuple[asyncio.Event, Optional[HttpResponse]]] = {}

        self.digest_info: Optional[DigestInfo] = None  # Password authentication
        self.cseq = 0
        self.session_id: int = randrange(2**32)
        self.dacp_id: str = f"{randrange(2 ** 64):X}"
        self.active_remote: int = randrange(2**32)

    @property
    def uri(self) -> str:
        """Return URI used for session requests."""
        return f"rtsp://{self.connection.local_ip}/{self.session_id}"

    @staticmethod
    def error_received(exc) -> None:
        """Handle a connection error."""
        _LOGGER.error("Error received: %s", exc)

    async def info(self) -> Dict[str, object]:
        """Return device information."""
        device_info = await self.exchange("GET", "/info", allow_error=True)

        # If not supported, just return an empty dict
        if device_info.code != 200:
            _LOGGER.debug("Device does not support /info")
            return {}

        return decode_bplist_from_body(device_info)

    async def auth_setup(self) -> HttpResponse:
        """Send auth-setup message."""
        # Payload to say that we want to proceed unencrypted
        body = AUTH_SETUP_UNENCRYPTED + CURVE25519_PUB_KEY

        return await self.exchange(
            "POST",
            "/auth-setup",
            content_type="application/octet-stream",
            body=body,
            protocol=HTTP_PROTOCOL,
        )

    # This method is only used by AirPlay 1 and is very specific (e.g. does not support
    # announcing arbitrary audio formats) and should probably move to the AirPlay 1
    # specific RAOP implementation. It will however live here for now until something
    # motivates that.
    async def announce(
        self,
        bytes_per_channel: int,
        channels: int,
        sample_rate: int,
        password: Optional[str],
    ) -> HttpResponse:
        """Send ANNOUNCE message."""
        body = ANNOUNCE_PAYLOAD.format(
            session_id=self.session_id,
            local_ip=self.connection.local_ip,
            remote_ip=self.connection.remote_ip,
            bits_per_channel=8 * bytes_per_channel,
            channels=channels,
            sample_rate=sample_rate,
        )

        requires_password: bool = password is not None

        response = await self.exchange(
            "ANNOUNCE",
            content_type="application/sdp",
            body=body,
            allow_error=requires_password,
        )

        # Save the necessary data for password authentication
        www_authenticate = response.headers.get("www-authenticate", None)
        if response.code == 401 and www_authenticate and requires_password:
            _, realm, _, nonce, _ = www_authenticate.split('"')
            info = DigestInfo("pyatv", realm, password, nonce)  # type: ignore
            self.digest_info = info

            response = await self.exchange(
                "ANNOUNCE",
                content_type="application/sdp",
                body=body,
            )

        return response

    async def setup(
        self,
        headers: Optional[Dict[str, Any]] = None,
        body: Optional[Union[str, bytes, dict]] = None,
    ) -> HttpResponse:
        """Send SETUP message."""
        return await self.exchange("SETUP", headers=headers, body=body)

    async def record(
        self,
        headers: Optional[Dict[str, Any]] = None,
        body: Optional[Union[str, bytes]] = None,
    ) -> HttpResponse:
        """Send RECORD message."""
        return await self.exchange("RECORD", headers=headers, body=body)

    async def flush(
        self,
        headers: Optional[Dict[str, Any]] = None,
        body: Optional[Union[str, bytes]] = None,
    ) -> HttpResponse:
        """Send RECORD message."""
        return await self.exchange("FLUSH", headers=headers, body=body)

    async def set_parameter(self, parameter: str, value: str) -> HttpResponse:
        """Send SET_PARAMETER message."""
        return await self.exchange(
            "SET_PARAMETER",
            content_type="text/parameters",
            body=f"{parameter}: {value}",
        )

    async def set_metadata(
        self,
        rtsp_session: int,
        rtpseq: int,
        rtptime: int,
        metadata: MediaMetadata,
    ) -> HttpResponse:
        """Change metadata for what is playing."""
        payload = b""
        if metadata.title:
            payload += tags.string_tag("minm", metadata.title)
        if metadata.album:
            payload += tags.string_tag("asal", metadata.album)
        if metadata.artist:
            payload += tags.string_tag("asar", metadata.artist)

        return await self.exchange(
            "SET_PARAMETER",
            content_type="application/x-dmap-tagged",
            headers={
                "Session": rtsp_session,
                "RTP-Info": f"seq={rtpseq};rtptime={rtptime}",
            },
            body=tags.container_tag("mlit", payload),
        )

    async def set_artwork(
        self,
        rtsp_session: int,
        rtpseq: int,
        rtptime: int,
        artwork: bytes,
    ) -> HttpResponse:
        """Change artwork for what is playing."""
        return await self.exchange(
            "SET_PARAMETER",
            content_type="image/jpeg",
            headers={
                "Session": rtsp_session,
                "RTP-Info": f"seq={rtpseq};rtptime={rtptime}",
            },
            body=artwork,
        )

    async def feedback(self, allow_error=False) -> HttpResponse:
        """Send SET_PARAMETER message."""
        return await self.exchange("POST", uri="/feedback", allow_error=allow_error)

    async def teardown(self, rtsp_session) -> HttpResponse:
        """Send TEARDOWN message."""
        return await self.exchange("TEARDOWN", headers={"Session": rtsp_session})

    async def exchange(  # pylint: disable=too-many-locals
        self,
        method: str,
        uri: Optional[str] = None,
        content_type: Optional[str] = None,
        headers: Optional[Mapping[str, object]] = None,
        body: Optional[Union[str, bytes, dict]] = None,
        allow_error: bool = False,
        protocol: str = "RTSP/1.0",
    ) -> HttpResponse:
        """Send a RTSP message and return response."""
        cseq = self.cseq
        self.cseq += 1

        hdrs = {
            "CSeq": cseq,
            "DACP-ID": self.dacp_id,
            "Active-Remote": self.active_remote,
            "Client-Instance": self.dacp_id,
        }

        # Add the password authentication if required
        if self.digest_info:
            hdrs["Authorization"] = get_digest_payload(
                method, uri or self.uri, *self.digest_info
            )

        if headers:
            hdrs.update(headers)

        # If body is a dict, assume that payload should be sent as a binary plist
        if isinstance(body, dict):
            hdrs["Content-Type"] = BPLIST_CONTENT_TYPE
            body = plistlib.dumps(
                body, fmt=plistlib.FMT_BINARY  # pylint: disable=no-member
            )

        # Map an asyncio Event to current CSeq and make the request
        self.requests[cseq] = (asyncio.Event(), None)
        resp = await self.connection.send_and_receive(
            method,
            uri or self.uri,
            protocol=protocol,
            user_agent=USER_AGENT,
            content_type=content_type,
            headers=hdrs,
            body=body,
            allow_error=allow_error,
        )

        # The response most likely contains a CSeq and it is also very likely to be
        # the one we expect, but it could be for someone else. So set the correct event
        # and save response.
        resp_cseq = int(resp.headers.get("CSeq", "-1"))
        if resp_cseq in self.requests:
            # Insert response for correct CSeq and activate event
            event, _ = self.requests[resp_cseq]
            self.requests[resp_cseq] = (event, resp)
            event.set()

        # Wait for response to the CSeq we expect
        try:
            async with async_timeout.timeout(4):
                await self.requests[cseq][0].wait()
            response = self.requests[cseq][1]
        except asyncio.TimeoutError as ex:
            raise TimeoutError(
                f"no response to CSeq {cseq} ({uri or self.uri})"
            ) from ex
        finally:
            del self.requests[cseq]

        # Programming error: forgot to store response before activating event
        if response is None:
            raise RuntimeError(f"no response was saved for {cseq}")

        return response
