from __future__ import annotations

import gzip
import socket
from datetime import datetime, timezone
from pathlib import Path


def rotate_log_file(output_path: Path, *, max_archives: int) -> Path | None:
    """Archive the active syslog file as gzip and retain only recent archives."""
    if not output_path.exists() or output_path.stat().st_size == 0:
        return None

    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    archive = output_path.with_name(f"{output_path.name}.{timestamp}.gz")
    suffix = 1
    while archive.exists():
        archive = output_path.with_name(f"{output_path.name}.{timestamp}.{suffix}.gz")
        suffix += 1

    with output_path.open("rb") as source, gzip.open(archive, "wb") as destination:
        while chunk := source.read(1024 * 1024):
            destination.write(chunk)
    output_path.unlink()

    archives = sorted(output_path.parent.glob(f"{output_path.name}.*.gz"), key=lambda path: path.stat().st_mtime, reverse=True)
    for stale_archive in archives[max(0, max_archives):]:
        stale_archive.unlink()
    return archive


def listen_udp_syslog(
    host: str,
    port: int,
    output: str,
    *,
    max_bytes: int = 65535,
    rotate_bytes: int = 25 * 1024 * 1024,
    rotate_count: int = 14,
    quiet: bool = False,
) -> None:
    output_path = Path(output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if rotate_bytes < 0 or rotate_count < 0:
        raise ValueError("rotation settings must be zero or positive")

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((host, port))

    print(f"Listening for UDP syslog on {host}:{port}")
    print(f"Writing logs to {output_path}")
    if rotate_bytes:
        print(f"Rotation: {rotate_bytes} bytes, retaining {rotate_count} gzip archives")
    with output_path.open("a", encoding="utf-8", buffering=1) as handle:
        while True:
            data, address = sock.recvfrom(max_bytes)
            message = data.decode("utf-8", errors="replace").strip()
            if not message:
                continue
            line = f"{message}\n"
            line_bytes = len(line.encode("utf-8"))
            if rotate_bytes and output_path.stat().st_size + line_bytes > rotate_bytes:
                handle.close()
                archive = rotate_log_file(output_path, max_archives=rotate_count)
                if archive and not quiet:
                    print(f"Rotated syslog to {archive}")
                handle = output_path.open("a", encoding="utf-8", buffering=1)
            handle.write(line)
            if not quiet:
                print(f"{address[0]}:{address[1]} {message}")
