#!/usr/bin/env python3
"""
Derive the Apple developer team identifier from an App Store Connect API key.

Why:
    ``ci.config.yaml`` lists ``app.team_id`` as optional because the ASC API
    key is always bound to exactly one team — so it's derivable. This module
    implements that derivation via GET-only endpoints so the first CI run on
    a fresh repo works without pre-seeding the team id.

Strategy (GET-only, no resource creation):
    1. ``GET /v1/certificates?limit=1&sort=-id`` — any Distribution /
       Development cert carries the team id in its Subject's Organizational
       Unit (OU) attribute. Most teams already have at least one.
    2. If no certs exist: ``GET /v1/profiles?limit=1`` — the attached
       ``profileContent`` (base64-encoded ``.mobileprovision``) contains the
       ``TeamIdentifier`` array in its plist payload. Profiles are CMS-signed
       in production, so we fall back to ``security cms -D`` when
       ``plistlib.loads`` refuses the raw bytes. When ``security`` isn't
       available (unit tests), the test substitutes its own plist.
    3. If neither yields a team, return "" — the caller decides whether to
       fail with an actionable message or defer to ``prepare_signing.py``'s
       post-profile-install fallback.

The returned team id is a 10-character alphanumeric string (Apple's format).
"""

from __future__ import annotations

import base64
import plistlib
import subprocess
from typing import Any

from asc_common import get_json
from cryptography import x509
from cryptography.x509.oid import NameOID


def _team_from_cert_der(cert_der: bytes) -> str:
    """Return OU attribute from the cert's Subject, or '' if absent/malformed."""
    try:
        cert = x509.load_der_x509_certificate(cert_der)
    except (ValueError, TypeError):
        return ""
    ou_attrs = cert.subject.get_attributes_for_oid(NameOID.ORGANIZATIONAL_UNIT_NAME)
    if not ou_attrs:
        return ""
    value = ou_attrs[0].value
    if isinstance(value, bytes):
        value = value.decode(errors="replace")
    return str(value).strip()


def _team_from_asc_certificates(token: str) -> str:
    """Try GET /certificates and parse OU from the first cert's DER."""
    data = get_json(
        "/certificates",
        token,
        params={"limit": "1", "sort": "-id"},
    )
    items = data.get("data", []) if isinstance(data, dict) else []
    if not items:
        return ""
    first = items[0]
    attrs = first.get("attributes") or {}
    b64 = attrs.get("certificateContent")
    if not b64:
        return ""
    try:
        cert_der = base64.b64decode(b64)
    except (ValueError, TypeError):
        return ""
    return _team_from_cert_der(cert_der)


def _team_from_profile_plist(profile_bytes: bytes) -> str:
    """Extract TeamIdentifier[0] from a mobileprovision plist payload."""
    plist: Any
    try:
        plist = plistlib.loads(profile_bytes)
    except (plistlib.InvalidFileException, ValueError, OSError):
        # Real profiles are CMS-signed — strip the envelope via `security cms`.
        try:
            decoded = subprocess.check_output(
                ["security", "cms", "-D", "-i", "/dev/stdin"],
                input=profile_bytes,
            )
        except (subprocess.CalledProcessError, FileNotFoundError, OSError):
            return ""
        try:
            plist = plistlib.loads(decoded)
        except (plistlib.InvalidFileException, ValueError, OSError):
            return ""
    if not isinstance(plist, dict):
        return ""
    teams = plist.get("TeamIdentifier") or []
    if isinstance(teams, list) and teams:
        return str(teams[0]).strip()
    return ""


def _team_from_asc_profiles(token: str) -> str:
    """Try GET /profiles and parse TeamIdentifier from the first profile's plist."""
    data = get_json("/profiles", token, params={"limit": "1"})
    items = data.get("data", []) if isinstance(data, dict) else []
    if not items:
        return ""
    attrs = items[0].get("attributes") or {}
    b64 = attrs.get("profileContent")
    if not b64:
        return ""
    try:
        profile_bytes = base64.b64decode(b64)
    except (ValueError, TypeError):
        return ""
    return _team_from_profile_plist(profile_bytes)


def derive_team_id(token: str) -> str:
    """Derive the developer team id bound to the ASC API key.

    Returns the 10-char team identifier, or "" when neither a certificate
    nor a provisioning profile exposes it. Never raises on missing data;
    only propagates network / auth failures from the underlying ASC client.
    """
    team = _team_from_asc_certificates(token)
    if team:
        return team
    return _team_from_asc_profiles(token)
