"""Incremental Apple upload receipt and the typed, credential-free failure annotation."""
from __future__ import annotations

import json
from pathlib import Path

RECEIPT_SCHEMA = "gowalk-cicd/apple-build-upload-receipt.v1"
FAILURE_SCHEMA = "gowalk-cicd/apple-build-upload-failed.v2"
ACTION = "retain the IPA and receipt; read provider upload state before retrying"
# Safe receipt fields the annotation republishes so recovery needs no log parsing.
PUBLISHED = ("cfBundleVersion", "cfBundleShortVersionString", "upload_id", "file_id", "state",
             "provider_status", "provider_code", "provider_pointer")
FALLBACK_CATEGORIES = ((SystemExit, "provider_request_failed"), (KeyError, "provider_response_shape"),
                       (OSError, "local_io"), (ValueError, "invalid_state"), (RuntimeError, "upload_failed"))


class UploadError(RuntimeError):
    """A classified failure: `category` and `details` are safe to publish; the message never is."""

    def __init__(self, category: str, message: str, **details):
        super().__init__(message)
        self.category = category
        self.details = details


class Receipt:
    """The upload receipt written after every stage so a failed job still explains itself."""

    def __init__(self, path: Path):
        self.path = path
        self.data = {"schema": RECEIPT_SCHEMA, "proxy_required": True, "stage": "start"}
        if path.is_file():
            try:
                previous = json.loads(path.read_text())
            except ValueError:
                previous = None
            if isinstance(previous, dict):
                self.data.update(previous)

    def update(self, stage: str, **fields) -> dict:
        self.data.update(fields)
        self.data["stage"] = stage
        self.path.write_text(json.dumps(self.data, indent=2))
        return dict(self.data)


def classify(exc: BaseException) -> str:
    if isinstance(exc, UploadError):
        return exc.category
    for kind, category in FALLBACK_CATEGORIES:
        if isinstance(exc, kind):
            return category
    return "unexpected"


def record_failure(report: Path, exc: BaseException) -> dict:
    """Write the classified failure into the receipt and return the annotation payload.

    Neither carries exception text: provider bodies and transport errors may quote
    request details, so only the category, stage, exception class and the receipt's
    own identifiers are published.
    """
    receipt = Receipt(report)
    category = classify(exc)
    details = dict(getattr(exc, "details", {}) or {})
    data = receipt.update(receipt.data["stage"], **details,
                          failure={"category": category, "exception": type(exc).__name__})
    published = {key: data[key] for key in PUBLISHED if data.get(key) not in (None, "")}
    return {"schema": FAILURE_SCHEMA, "category": category, "stage": data["stage"],
            "receipt_available": report.is_file(), **published, "action": ACTION}
