#!/usr/bin/env python3
"""
On-disk + plist-decode helpers for ``.mobileprovision`` files.

Split from ``profile_manager.py`` so the ASC-API-facing module stays
focused on lifecycle (create / delete / install) and this module owns
the file-format gymnastics (CMS envelope decode via ``security cms -D``,
plist date normalisation).

Nothing in this module talks to App Store Connect. Callers pass raw DER
bytes and receive parsed Python primitives.
"""

from __future__ import annotations

import plistlib
import subprocess
from datetime import datetime, timezone
from pathlib import Path


# Apple guarantees ``ExpirationDate`` in every issued profile; if it is
# missing or non-datetime the profile is malformed — treat as already-
# expired so callers never reuse it.
_EXPIRED_SENTINEL = datetime(1970, 1, 1, tzinfo=timezone.utc)


def decode_profile_plist(profile_der: bytes, scratch_dir: Path) -> dict:
    """Run ``security cms -D`` against ``profile_der`` and parse the plist.

    The decoded payload's only authoritative source is the macOS
    ``security`` binary; ``plistlib`` alone can't parse the CMS envelope.
    ``scratch_dir`` is used for a temporary file (``security`` only
    accepts file paths, not stdin); it is created if missing.

    May raise ``subprocess.CalledProcessError`` (decode failure),
    ``ValueError`` (plist parse failure), or ``OSError`` (filesystem
    failure). Callers that want graceful cache fallback must catch.
    """
    scratch_dir.mkdir(parents=True, exist_ok=True)
    tmp_path = scratch_dir / "tmp.mobileprovision"
    tmp_path.write_bytes(profile_der)
    try:
        decoded = subprocess.check_output(
            ["security", "cms", "-D", "-i", str(tmp_path)]
        )
    finally:
        tmp_path.unlink(missing_ok=True)
    return plistlib.loads(decoded)


def ensure_utc(value) -> datetime:
    """Normalise a plist datetime to a UTC-aware ``datetime``.

    Plist dates parsed by ``plistlib`` come back as naive UTC; explicitly
    attach ``timezone.utc`` so downstream comparisons stay timezone-safe.
    Anything that is not a ``datetime`` returns the expired sentinel so
    a malformed profile is never treated as reusable.
    """
    if not isinstance(value, datetime):
        return _EXPIRED_SENTINEL
    if value.tzinfo is None:
        return value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)
