#!/usr/bin/env python3
"""Persistent provisioning-profile cache under an app's creds directory."""
from __future__ import annotations

import json
import os
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path

RENEW_THRESHOLD_DAYS = 30
_MANIFEST = "profiles.manifest.json"
_PROFILES_SUBDIR = "profiles"


@dataclass
class ProfileEntry:
    bundle_id: str
    name: str
    uuid: str
    filename: str
    expiration: datetime


def _atomic_write(path: Path, data: bytes) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temp = path.with_suffix(path.suffix + ".tmp")
    temp.write_bytes(data)
    os.replace(temp, path)


def _parse_iso(value: str) -> datetime:
    if value.endswith("Z"):
        value = value[:-1] + "+00:00"
    parsed = datetime.fromisoformat(value)
    return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)


def profile_path(creds_dir: Path, uuid: str) -> Path:
    return creds_dir / _PROFILES_SUBDIR / f"{uuid}.mobileprovision"


def load_profile_manifest(creds_dir: Path) -> dict:
    path = creds_dir / _MANIFEST
    if not path.exists():
        return {"cert_id": None, "profiles": []}
    try:
        data = json.loads(path.read_text())
    except (OSError, ValueError) as exc:
        print(f"::warning::profiles.manifest.json unreadable ({exc}); resetting")
        return {"cert_id": None, "profiles": []}
    data.setdefault("cert_id", None)
    data.setdefault("profiles", [])
    return data


def write_profile_manifest(creds_dir: Path, manifest: dict) -> None:
    profiles = sorted(manifest.get("profiles", []),
                      key=lambda entry: entry.get("bundle_id", ""))
    payload = {"cert_id": manifest.get("cert_id"), "profiles": profiles}
    _atomic_write(creds_dir / _MANIFEST, json.dumps(payload, indent=2).encode())


def find_reusable_profile(manifest: dict, bundle_id: str, cert_id: str,
                          creds_dir: Path) -> ProfileEntry | None:
    if manifest.get("cert_id") != cert_id:
        return None
    deadline = datetime.now(timezone.utc) + timedelta(days=RENEW_THRESHOLD_DAYS)
    for raw in manifest.get("profiles", []):
        if raw.get("bundle_id") != bundle_id:
            continue
        try:
            expiration, uuid = _parse_iso(raw["expiration"]), raw["uuid"]
        except (KeyError, ValueError):
            return None
        if expiration <= deadline or not profile_path(creds_dir, uuid).exists():
            return None
        return ProfileEntry(
            bundle_id=bundle_id, name=raw.get("name", ""), uuid=uuid,
            filename=raw.get("filename", f"{uuid}.mobileprovision"),
            expiration=expiration,
        )
    return None


def write_cached_profile(creds_dir: Path, uuid: str, profile_der: bytes) -> None:
    _atomic_write(profile_path(creds_dir, uuid), profile_der)
