#!/usr/bin/env python3
"""
Throwaway-keychain provisioning for codesign / xcodebuild on a CI runner.

Creates a temporary keychain in ``$RUNNER_TEMP``, imports a PKCS12 cert
into it, and prepends it to the user's keychain search list so
``codesign`` and ``xcodebuild`` find the identity at archive time. The
keychain is purposely short-lived (default 6h auto-lock) and disposable
— each CI run re-creates it from scratch.

The user's *default* keychain is deliberately never pointed at the
throwaway keychain: the search list (plus the partition list set at
import) is what identity resolution actually uses. On a persistent
runner (a self-hosted Mac), claiming the default meant every credential
consumer on the host wrote into the throwaway keychain, and once its
temp dir was reclaimed the dangling default made macOS pop a
"Keychain ... cannot be found to store" dialog at every credential
write. Setup instead prunes search-list entries whose backing file is
gone and repoints a dangling default back at the login keychain.

The keychain password (``"ci"``) is intentionally hardcoded: the
keychain never leaves the runner (it lives in ``$RUNNER_TEMP`` which
GitHub deletes when the job ends), so encrypting it with a sourced
password adds bug surface (drift between import + unlock calls) without
raising the security floor. This is distinct from the persisted P12
password documented in ``creds_store.P12_PASSWORD``.
"""

from __future__ import annotations

import os
import subprocess
from pathlib import Path


_KEYCHAIN_FILENAME = "ci.keychain-db"
_KEYCHAIN_PASS = "ci"


def _create_keychain(keychain_path: str, keychain_pass: str) -> None:
    subprocess.run(
        ["security", "delete-keychain", keychain_path],
        check=False,
        capture_output=True,
    )
    subprocess.check_call(
        ["security", "create-keychain", "-p", keychain_pass, keychain_path]
    )
    subprocess.check_call(
        ["security", "set-keychain-settings", "-lut", "21600", keychain_path]
    )
    subprocess.check_call(
        ["security", "unlock-keychain", "-p", keychain_pass, keychain_path]
    )


def _import_p12(
    keychain_path: str, keychain_pass: str, p12_path: Path, p12_pass: str
) -> None:
    subprocess.check_call(
        [
            "security", "import", str(p12_path),
            "-P", p12_pass,
            "-A",
            "-t", "cert",
            "-f", "pkcs12",
            "-k", keychain_path,
        ]
    )
    subprocess.check_call(
        [
            "security", "set-key-partition-list",
            "-S", "apple-tool:,apple:,codesign:",
            "-s",
            "-k", keychain_pass,
            keychain_path,
        ]
    )


def _prepend_to_user_search_list(keychain_path: str) -> None:
    existing = subprocess.check_output(
        ["security", "list-keychains", "-d", "user"]
    ).decode()
    existing_list = [
        line.strip().strip('"')
        for line in existing.splitlines()
        if line.strip()
    ]
    # Prune entries whose file is gone: dangling throwaway keychains from
    # earlier runs on a persistent Mac would otherwise accumulate forever.
    new_list = [keychain_path] + [
        k for k in existing_list
        if k != keychain_path and os.path.exists(k)
    ]
    subprocess.check_call(
        ["security", "list-keychains", "-d", "user", "-s", *new_list]
    )
    _repair_default_keychain()


def _repair_default_keychain() -> None:
    """Repoint an unset or dangling default keychain at the login keychain.

    Heals hosts damaged by earlier releases (which made the throwaway CI
    keychain the default) without ever claiming the default for this run.
    A healthy default — whatever it points at — is left alone.
    """
    login = os.path.expanduser("~/Library/Keychains/login.keychain-db")
    if not os.path.exists(login):
        return
    probe = subprocess.run(
        ["security", "default-keychain"], capture_output=True, text=True
    )
    current = probe.stdout.strip().strip('"') if probe.returncode == 0 else ""
    if current and os.path.exists(current):
        return
    subprocess.check_call(["security", "default-keychain", "-s", login])


def setup_keychain(p12_path: Path, p12_pass: str, runner_temp: str) -> str:
    """Create + populate the throwaway CI keychain; return its path.

    ``runner_temp`` is the GitHub Actions temp directory (``$RUNNER_TEMP``);
    the keychain lives there so it auto-cleans when the job finishes.
    Returns the keychain path so callers can reference it for diagnostics.
    """
    keychain_path = os.path.join(runner_temp, _KEYCHAIN_FILENAME)
    _create_keychain(keychain_path, _KEYCHAIN_PASS)
    _import_p12(keychain_path, _KEYCHAIN_PASS, p12_path, p12_pass)
    _prepend_to_user_search_list(keychain_path)
    print(f"Keychain ready: {keychain_path}")
    return keychain_path
