#!/usr/bin/env python3
"""
SIP server (UDP + TCP port 5060).
Parses SIP REGISTER and INVITE messages, logs credentials from the
Authorization header and call metadata, then responds with 401 Unauthorized.
"""

import asyncio
import os
import re
from typing import cast
from syslog_bridge import (
    classify_authorization,
    forward_syslog,
    syslog_line,
    write_syslog_file,
)

NODE_NAME = os.environ.get("NODE_NAME", "pbx")
SERVICE_NAME   = "sip"
LOG_TARGET = os.environ.get("LOG_TARGET", "")

_401 = (
    "SIP/2.0 401 Unauthorized\r\n"
    "Via: {via}\r\n"
    "From: {from_}\r\n"
    "To: {to}\r\n"
    "Call-ID: {call_id}\r\n"
    "CSeq: {cseq}\r\n"
    'WWW-Authenticate: Digest realm="{host}", nonce="{nonce}", algorithm=MD5\r\n'
    "Content-Length: 0\r\n\r\n"
)




def _log(event_type: str, severity: int = 6, **kwargs) -> None:
    line = syslog_line(SERVICE_NAME, NODE_NAME, event_type, severity, **kwargs)
    write_syslog_file(line)
    forward_syslog(line, LOG_TARGET)


def _parse_headers(msg: str) -> dict:
    headers = {}
    for line in msg.splitlines()[1:]:
        if ":" in line:
            k, _, v = line.partition(":")
            headers[k.strip().lower()] = v.strip()
    return headers


def _handle_message(data: bytes, src_addr) -> bytes | None:
    try:
        msg = data.decode(errors="replace")
    except Exception:
        return None
    first_line = msg.splitlines()[0] if msg else ""
    method = first_line.split()[0] if first_line else "UNKNOWN"
    headers = _parse_headers(msg)

    auth_header = headers.get("authorization", "")
    username = ""
    if auth_header:
        m = re.search(r'username="([^"]+)"', auth_header)
        username = m.group(1) if m else ""

    # SIP Digest is the same shape as HTTP Digest (RFC 7616 derived from
    # RFC 2617). classify_authorization handles it identically — emits
    # secret_kind="http_digest_md5", which is correct: the cred is the
    # MD5 hash response, regardless of whether it rode in over SIP or
    # HTTP. Reuse-analytics correlates across both.
    cred = classify_authorization(auth_header)

    _log(
        "request",
        src=src_addr[0],
        src_port=src_addr[1],
        method=method,
        from_=headers.get("from", ""),
        to=headers.get("to", ""),
        username=username,
        auth=auth_header[:256],
        **(cred or {}),
    )

    if method in ("REGISTER", "INVITE", "OPTIONS"):
        nonce = os.urandom(8).hex()
        response = _401.format(
            via=headers.get("via", ""),
            from_=headers.get("from", ""),
            to=headers.get("to", ""),
            call_id=headers.get("call-id", ""),
            cseq=headers.get("cseq", ""),
            host=NODE_NAME,
            nonce=nonce,
        )
        return response.encode()
    return None


class SIPUDPProtocol(asyncio.DatagramProtocol):
    _transport: asyncio.DatagramTransport | None = None

    def __init__(self):
        self._transport = None

    def connection_made(self, transport: asyncio.BaseTransport) -> None:
        self._transport = cast(asyncio.DatagramTransport, transport)

    def datagram_received(self, data, addr):
        response = _handle_message(data, addr)
        if response and self._transport:
            self._transport.sendto(response, addr)


class SIPTCPProtocol(asyncio.Protocol):
    _transport: asyncio.Transport | None = None
    _peer: tuple[str, int] | None = None

    def __init__(self):
        self._transport = None
        self._peer = None
        self._buf = b""

    def connection_made(self, transport: asyncio.BaseTransport) -> None:
        self._transport = cast(asyncio.Transport, transport)
        self._peer = cast(tuple[str, int], self._transport.get_extra_info("peername", ("?", 0)))

    def data_received(self, data: bytes) -> None:
        self._buf += data
        if b"\r\n\r\n" in self._buf or b"\n\n" in self._buf:
            response = _handle_message(self._buf, self._peer)
            self._buf = b""
            if response and self._transport:
                self._transport.write(response)

    def connection_lost(self, exc):
        pass


async def main():
    _log("startup", msg=f"SIP server starting as {NODE_NAME}")
    loop = asyncio.get_running_loop()
    udp_transport, _ = await loop.create_datagram_endpoint(
        SIPUDPProtocol, local_addr=("0.0.0.0", 5060)  # nosec B104
    )
    tcp_server = await loop.create_server(SIPTCPProtocol, "0.0.0.0", 5060)  # nosec B104
    async with tcp_server:
        await tcp_server.serve_forever()
    udp_transport.close()


if __name__ == "__main__":
    asyncio.run(main())
