#!/usr/bin/env python3
"""Store one current proof receipt under an exact project-supplied key."""

from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import sys
import tempfile
from typing import Any


SCHEMA = 1


class ReceiptError(Exception):
    pass


def miss(reason: str) -> int:
    print(f"receipt=miss reason={reason}")
    return 1


def read(path: Path) -> dict[str, Any] | None:
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return None
    except json.JSONDecodeError:
        return {}
    except (OSError, UnicodeError) as error:
        raise ReceiptError(f"read-failed: {error}") from error
    return value if isinstance(value, dict) else {}


def write(path: Path, value: dict[str, Any]) -> None:
    temporary: Path | None = None
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        handle, temporary_name = tempfile.mkstemp(
            dir=path.parent,
            prefix=f".{path.name}.",
            suffix=".tmp",
            text=True,
        )
        temporary = Path(temporary_name)
        with os.fdopen(handle, "w", encoding="utf-8") as output:
            json.dump(value, output, indent=2, sort_keys=True)
            output.write("\n")
        os.replace(temporary, path)
        temporary = None
    except OSError as error:
        raise ReceiptError(f"write-failed: {error}") from error
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)


def get_receipt(args: argparse.Namespace) -> int:
    receipt = read(Path(args.file))
    if receipt is None:
        return miss("missing")
    if (
        receipt.get("schema") != SCHEMA
        or not isinstance(receipt.get("key"), str)
        or not isinstance(receipt.get("value"), dict)
    ):
        return miss("invalid")
    if receipt["key"] != args.key:
        return miss("key")
    print(json.dumps(receipt, sort_keys=True, separators=(",", ":")))
    return 0


def set_receipt(args: argparse.Namespace) -> int:
    if not args.key:
        raise ReceiptError("key-empty")
    try:
        value = json.loads(args.value)
    except json.JSONDecodeError as error:
        raise ReceiptError(f"value-invalid: {error.msg}") from error
    if not isinstance(value, dict):
        raise ReceiptError("value-must-be-object")
    write(Path(args.file), {"schema": SCHEMA, "key": args.key, "value": value})
    print("receipt=set")
    return 0


def delete_receipt(args: argparse.Namespace) -> int:
    try:
        Path(args.file).unlink(missing_ok=True)
    except OSError as error:
        raise ReceiptError(f"delete-failed: {error}") from error
    print("receipt=deleted")
    return 0


def parser() -> argparse.ArgumentParser:
    result = argparse.ArgumentParser(
        description="Get, set, or delete one exact-key proof receipt."
    )
    commands = result.add_subparsers(dest="command", required=True)

    get_command = commands.add_parser("get")
    get_command.add_argument("--file", required=True)
    get_command.add_argument("--key", required=True)
    get_command.set_defaults(handler=get_receipt)

    set_command = commands.add_parser("set")
    set_command.add_argument("--file", required=True)
    set_command.add_argument("--key", required=True)
    set_command.add_argument("--value", required=True, help="JSON object")
    set_command.set_defaults(handler=set_receipt)

    delete_command = commands.add_parser("delete")
    delete_command.add_argument("--file", required=True)
    delete_command.set_defaults(handler=delete_receipt)
    return result


def main() -> int:
    arguments = parser().parse_args()
    try:
        return arguments.handler(arguments)
    except ReceiptError as error:
        print(f"receipt=error reason={error}", file=sys.stderr)
        return 2


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