#!/usr/bin/env python3
"""Verify declared App Group identifiers against Apple's issued profiles.

Registration and assignment belong to the account's Developer console. This
module never guesses groups, posts an unsupported ASC operation or changes a
certificate. It rejects profiles that would break an app/extension shared container.
"""
from __future__ import annotations

import json
import plistlib
import re
from pathlib import Path

from profile_io import decode_profile_plist

ENTITLEMENT = "com.apple.security.application-groups"
_IDENTIFIER = re.compile(r"group\.[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*")


def declared_by_bundle(project: str, targets: list[dict]) -> dict[str, set[str]]:
    """Read literal group values from the selected archive target entitlements."""
    result: dict[str, set[str]] = {}
    for target in targets:
        relative = target.get("entitlements")
        if not relative:
            continue
        path = Path(project).parent / relative
        try:
            data = plistlib.loads(path.read_bytes())
        except (OSError, ValueError, plistlib.InvalidFileException):
            raise SystemExit("::error::Cannot verify App Groups: target entitlements are unreadable") from None
        if not isinstance(data, dict):
            raise SystemExit("::error::Cannot verify App Groups: target entitlements must be a dictionary")
        if ENTITLEMENT not in data:
            continue
        values = data[ENTITLEMENT]
        if not isinstance(values, list) or any(
            not isinstance(value, str) or not _IDENTIFIER.fullmatch(value) for value in values
        ):
            raise SystemExit("::error::App Groups must contain literal group identifiers, without build variables")
        result.setdefault(target["bundle_id"], set()).update(values)
    return result


def missing(profile_entitlements: dict, required: set[str]) -> set[str]:
    """An unrelated group, malformed value or wildcard never grants a required group."""
    if not isinstance(profile_entitlements, dict):
        return set(required)
    granted = profile_entitlements.get(ENTITLEMENT)
    if not isinstance(granted, list) or any(not isinstance(value, str) for value in granted):
        return set(required)
    return required - set(granted)


def validate_requirements(keys_by_bundle: dict, groups_by_bundle: dict) -> None:
    """Refuse legacy key-only inputs rather than silently ignoring group values."""
    for bundle, keys in keys_by_bundle.items():
        if ENTITLEMENT in keys and bundle not in groups_by_bundle:
            raise SystemExit("::error::App Group profile verification requires declared identifier values")


def assert_profile(profile_der: bytes, required: set[str], bundle: str, scratch: Path) -> None:
    """A fresh profile must contain every declared group before installation."""
    if not required:
        return
    try:
        profile = decode_profile_plist(profile_der, scratch)
    except Exception:
        raise SystemExit("::error::Cannot verify App Groups in the issued profile") from None
    if not isinstance(profile, dict):
        raise SystemExit("::error::Cannot verify App Groups in the issued profile")
    absent = missing(profile.get("Entitlements") or {}, required)
    if not absent:
        return
    receipt = {
        "schema": "gowalk-cicd/apple-app-groups-required.v1",
        "bundle_id": bundle,
        "missing_group_identifiers": sorted(absent),
        "recovery": "Assign the declared groups to this App ID through the account console, then rerun CI",
    }
    raise SystemExit("::error title=apple_app_groups_required::" + json.dumps(receipt, separators=(",", ":")))
