"""Shared device / request metadata helpers.

A single, dependency-free place to derive **coarse, non-identifying** device
information from a request: a browser/OS/device-type breakdown from the
User-Agent, and a privacy-masked form of the client IP. Reused by the
trusted-device service (which needs a display label) and the security-event
recorder (which needs the individual fields), so there is exactly one parser.

Deliberately heuristic and low-resolution — no version numbers, no fingerprinting.
This is display metadata only and never an authentication signal.
"""

from __future__ import annotations

# (user-agent token, display name) — order matters: more specific tokens first
# (e.g. Edge/Opera identify themselves *and* contain "chrome").
_BROWSERS = (
    ("edg", "Edge"),
    ("opr", "Opera"),
    ("samsungbrowser", "Samsung Internet"),
    ("chrome", "Chrome"),
    ("crios", "Chrome"),
    ("firefox", "Firefox"),
    ("fxios", "Firefox"),
    ("safari", "Safari"),
)

_OSES = (
    ("windows", "Windows"),
    ("android", "Android"),
    ("iphone", "iOS"),
    ("ipad", "iPadOS"),
    ("ipod", "iOS"),
    ("mac os", "macOS"),
    ("macintosh", "macOS"),
    ("cros", "ChromeOS"),
    ("linux", "Linux"),
)


def parse_user_agent(user_agent: str | None) -> dict:
    """Return ``{browser, os, device_type, label}`` from a User-Agent string.

    Unknown parts fall back to friendly placeholders; ``label`` is the
    "Browser on OS" form the trusted-device list shows."""
    ua = (user_agent or "").lower()

    browser = next((name for token, name in _BROWSERS if token in ua), "Browser")
    os_name = next((name for token, name in _OSES if token in ua), "Unknown OS")

    if any(t in ua for t in ("ipad", "tablet")):
        device_type = "Tablet"
    elif any(t in ua for t in ("mobi", "iphone", "ipod", "android")):
        # Android tablets omit "mobile"; treat "android" + "mobile" as phone.
        device_type = "Mobile" if ("mobi" in ua or "iphone" in ua or "ipod" in ua
                                    or ("android" in ua and "mobile" in ua)) else "Tablet"
    else:
        device_type = "Desktop"

    return {
        "browser": browser,
        "os": os_name,
        "device_type": device_type,
        "label": f"{browser} on {os_name}",
    }


def mask_ip(ip: str | None) -> str | None:
    """Return a privacy-masked IP for display (never the full address).

    IPv4 ``203.0.113.42`` → ``203.0.113.xxx``; IPv6 keeps the first two hextets
    (``2001:db8::…``). ``None``/blank → ``None``."""
    if not ip:
        return None
    ip = ip.strip()
    if ":" in ip:  # IPv6
        groups = ip.split(":")
        head = ":".join(g for g in groups[:2] if g)
        return f"{head}:…" if head else "…"
    parts = ip.split(".")
    if len(parts) == 4:
        return f"{parts[0]}.{parts[1]}.{parts[2]}.xxx"
    return ip  # unrecognised format — return as-is rather than guess
