#!/usr/bin/env python3
from __future__ import annotations

import argparse
import hashlib
import json
import shutil
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
DEFAULT_KEYCHAIN_PROFILE = "sixxie-notary"


def main() -> int:
    parser = argparse.ArgumentParser(description="Submit a macOS DMG or app to Apple notarization and staple it.")
    parser.add_argument("artifact", type=Path, help="Path to a signed .dmg or .app artifact")
    parser.add_argument("--keychain-profile", default=DEFAULT_KEYCHAIN_PROFILE)
    parser.add_argument("--gatekeeper-type", choices=["install", "exec"], default="install")
    parser.add_argument("--output-report", type=Path)
    parser.add_argument("--dry-run", action="store_true", help="Validate local inputs and tool availability without contacting Apple")
    args = parser.parse_args()

    report = notarize_artifact(
        artifact=args.artifact.resolve(),
        keychain_profile=args.keychain_profile,
        gatekeeper_type=args.gatekeeper_type,
        dry_run=args.dry_run,
    )
    if args.output_report:
        args.output_report.parent.mkdir(parents=True, exist_ok=True)
        args.output_report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 0 if report["ok"] else 1


def notarize_artifact(
    *,
    artifact: Path,
    keychain_profile: str,
    gatekeeper_type: str,
    dry_run: bool = False,
) -> dict[str, Any]:
    if not artifact.exists():
        raise FileNotFoundError(f"artifact not found: {artifact}")
    if not keychain_profile.strip():
        raise ValueError("keychain profile is required")
    if gatekeeper_type not in {"install", "exec"}:
        raise ValueError("gatekeeper type must be install or exec")

    tool_check = {
        "xcrun": shutil.which("xcrun") is not None,
        "spctl": shutil.which("spctl") is not None,
    }
    base_report: dict[str, Any] = {
        "artifact": str(artifact),
        "artifact_sha256": _sha256_file(artifact) if artifact.is_file() else None,
        "keychain_profile": keychain_profile,
        "gatekeeper_type": gatekeeper_type,
        "tool_check": tool_check,
        "dry_run": dry_run,
        "created_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
    }
    if dry_run:
        return {
            **base_report,
            "ok": all(tool_check.values()),
            "notarytool_submit": None,
            "stapler_staple": None,
            "stapler_validate": None,
            "gatekeeper_assessment": None,
        }

    submit = _run(
        [
            "xcrun",
            "notarytool",
            "submit",
            str(artifact),
            "--wait",
            "--keychain-profile",
            keychain_profile,
            "--output-format",
            "json",
        ]
    )
    staple = _run(["xcrun", "stapler", "staple", str(artifact)]) if submit["returncode"] == 0 else _skipped_result()
    validate = _run(["xcrun", "stapler", "validate", str(artifact)]) if staple["returncode"] == 0 else _skipped_result()
    gatekeeper = (
        _run(["spctl", "-a", "-vv", "-t", gatekeeper_type, str(artifact)])
        if validate["returncode"] == 0
        else _skipped_result()
    )
    return {
        **base_report,
        "ok": (
            submit["returncode"] == 0
            and staple["returncode"] == 0
            and validate["returncode"] == 0
            and gatekeeper["returncode"] == 0
        ),
        "notarytool_submit": submit,
        "stapler_staple": staple,
        "stapler_validate": validate,
        "gatekeeper_assessment": gatekeeper,
    }


def _run(command: list[str]) -> dict[str, Any]:
    completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False)
    return {
        "command": command,
        "returncode": completed.returncode,
        "stdout": completed.stdout.strip(),
        "stderr": completed.stderr.strip(),
    }


def _skipped_result() -> dict[str, Any]:
    return {"command": [], "returncode": 1, "stdout": "", "stderr": "skipped because a previous step failed"}


def _sha256_file(path: Path) -> str:
    hasher = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            hasher.update(chunk)
    return hasher.hexdigest()


if __name__ == "__main__":
    raise SystemExit(main())
