#!/usr/bin/env python3
"""Checkmk special agent for Hetzner Storage Boxes."""

from __future__ import annotations

import argparse
import fcntl
import hashlib
import json
import os
import socket
import sys
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Iterable
from pathlib import Path
from typing import Any

DEFAULT_API_URL = "https://api.hetzner.com/v1"
SECTION_NAME = "hetzner_storagebox"
USER_AGENT = "checkmk-hetzner-storagebox/0.1"
MAX_PAGES = 10000
DEFAULT_CACHE_TTL_SECONDS = 3600
DEFAULT_CACHE_LOCK_TIMEOUT = 1.0
CACHE_SCHEMA_VERSION = 1
CACHE_CLEANUP_RETENTION_SECONDS = 30 * 24 * 60 * 60
CACHE_CLEANUP_SUFFIXES = {".json", ".lock"}


class AgentError(Exception):
    """Structured error that can be emitted as agent data."""

    def __init__(self, code: str, message: str) -> None:
        super().__init__(message)
        self.code = code
        self.message = message

    def as_dict(self) -> dict[str, str]:
        return {"code": self.code, "message": self.message}


def parse_arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--api-token", required=True, help="Hetzner Console API token")
    parser.add_argument("--api-url", default=DEFAULT_API_URL, help=f"API base URL, default {DEFAULT_API_URL}")
    parser.add_argument("--timeout", type=int, default=10, help="HTTP timeout in seconds")
    parser.add_argument(
        "--box-id",
        action="append",
        default=[],
        help="Storage Box ID filter. Can be specified repeatedly or as a comma-separated list.",
    )
    parser.add_argument(
        "--fetch-subaccounts",
        dest="fetch_subaccounts",
        action="store_true",
        default=True,
        help="Fetch subaccounts per Storage Box to expose subaccounts_count.",
    )
    parser.add_argument(
        "--no-fetch-subaccounts",
        dest="fetch_subaccounts",
        action="store_false",
        help="Do not fetch subaccounts per Storage Box.",
    )
    parser.add_argument(
        "--cache-enabled",
        dest="cache_enabled",
        action="store_true",
        default=True,
        help="Enable the site-local result cache.",
    )
    parser.add_argument(
        "--no-cache-enabled",
        "--disable-cache",
        dest="cache_enabled",
        action="store_false",
        help="Disable the site-local result cache.",
    )
    parser.add_argument(
        "--cache-ttl",
        type=int,
        default=DEFAULT_CACHE_TTL_SECONDS,
        help=f"Result cache time-to-live in seconds, default {DEFAULT_CACHE_TTL_SECONDS}.",
    )
    parser.add_argument(
        "--cache-stale-on-error",
        dest="cache_stale_on_error",
        action="store_true",
        default=True,
        help="Use an expired cached result when fresh collection fails.",
    )
    parser.add_argument(
        "--no-cache-stale-on-error",
        dest="cache_stale_on_error",
        action="store_false",
        help="Do not use expired cache entries after collection failures.",
    )
    parser.add_argument("--cache-dir", default=None, help=argparse.SUPPRESS)
    parser.add_argument("--debug", action="store_true", help="Include additional safe error context")
    return parser.parse_args()


def emit_section(
    storage_boxes: list[dict[str, Any]],
    errors: list[dict[str, str]],
    cache_info: dict[str, Any] | None = None,
) -> None:
    payload = {"storage_boxes": storage_boxes, "errors": errors}
    if cache_info is not None:
        payload["cache"] = cache_info
    print(f"<<<{SECTION_NAME}:sep(0)>>>")
    print(json.dumps(payload, sort_keys=True, separators=(",", ":")))


def resolve_api_token(raw_token: str) -> str:
    """Resolve Checkmk password-store references while accepting plain tokens."""

    if ":" not in raw_token:
        return raw_token

    password_id, password_file = raw_token.split(":", 1)
    if not password_id or not password_file.startswith("/"):
        return raw_token

    try:
        from cmk.utils.password_store import lookup

        return lookup(Path(password_file), password_id)
    except Exception as exc:
        raise AgentError("secret_error", "Unable to resolve API token from the Checkmk password store") from exc


def sanitize_message(message: str, api_token: str) -> str:
    if api_token:
        return message.replace(api_token, "<redacted>")
    return message


def normalize_box_ids(raw_values: list[str]) -> set[str]:
    box_ids: set[str] = set()
    for raw_value in raw_values:
        for part in raw_value.split(","):
            normalized = part.strip()
            if normalized:
                box_ids.add(normalized)
    return box_ids


def request_json(url: str, api_token: str, timeout: int) -> dict[str, Any] | list[Any]:
    request = urllib.request.Request(
        url,
        headers={
            "Accept": "application/json",
            "Authorization": f"Bearer {api_token}",
            "User-Agent": USER_AGENT,
        },
        method="GET",
    )

    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            raw_body = response.read()
    except urllib.error.HTTPError as exc:
        body = _read_http_error_body(exc)
        reason = exc.reason or "HTTP error"
        detail = f": {body}" if body else ""
        code = "auth_error" if exc.code in (401, 403) else "http_error"
        raise AgentError(code, f"HTTP {exc.code} {reason} while fetching {url}{detail}") from exc
    except urllib.error.URLError as exc:
        reason = getattr(exc, "reason", None)
        code = "timeout" if isinstance(reason, (socket.timeout, TimeoutError)) else "network_error"
        raise AgentError(code, f"Unable to fetch {url}: {exc}") from exc
    except (socket.timeout, TimeoutError) as exc:
        code = "timeout"
        raise AgentError(code, f"Unable to fetch {url}: {exc}") from exc

    try:
        decoded = raw_body.decode("utf-8")
    except UnicodeDecodeError as exc:
        raise AgentError("decode_error", f"API response from {url} is not valid UTF-8") from exc

    try:
        parsed: dict[str, Any] | list[Any] = json.loads(decoded)
    except json.JSONDecodeError as exc:
        raise AgentError("json_error", f"API response from {url} is not valid JSON: {exc}") from exc

    return parsed


def _read_http_error_body(exc: urllib.error.HTTPError) -> str:
    try:
        raw_body = exc.read()
    except Exception:
        return ""

    try:
        body = raw_body.decode("utf-8", errors="replace").strip()
    except Exception:
        return ""

    if not body:
        return ""

    try:
        parsed = json.loads(body)
    except json.JSONDecodeError:
        return body[:300]

    if isinstance(parsed, dict):
        for key in ("message", "error", "detail"):
            value = parsed.get(key)
            if isinstance(value, str) and value:
                return value[:300]
    return body[:300]


def extract_collection(
    payload: dict[str, Any] | list[Any],
    field_name: str,
    resource_name: str,
    fallback_field_names: tuple[str, ...] = (),
    missing_field_is_empty: bool = True,
) -> list[dict[str, Any]]:
    if isinstance(payload, list):
        raw_entries = payload
    elif isinstance(payload, dict):
        raw_entries: Any = None
        for candidate in (field_name, *fallback_field_names):
            if candidate in payload:
                raw_entries = payload.get(candidate, [])
                break
        if raw_entries is None:
            if not missing_field_is_empty:
                raise AgentError("api_response_error", f"API response field '{field_name}' is missing")
            raw_entries = []
    else:
        raise AgentError("api_response_error", "API response has an unexpected top-level type")

    if not isinstance(raw_entries, list):
        raise AgentError("api_response_error", f"API response field '{field_name}' is not a list")

    entries: list[dict[str, Any]] = []
    for index, raw_entry in enumerate(raw_entries):
        if not isinstance(raw_entry, dict):
            raise AgentError("api_response_error", f"{resource_name} entry at index {index} is not an object")
        entries.append(raw_entry)
    return entries


def next_page_from_payload(payload: dict[str, Any] | list[Any], current_page: int) -> int | None:
    if not isinstance(payload, dict):
        return None

    pagination = payload.get("pagination")
    meta = payload.get("meta")
    if isinstance(meta, dict):
        pagination = meta.get("pagination", pagination)

    if not isinstance(pagination, dict):
        return None

    next_page = _to_int(pagination.get("next_page"))
    if next_page is not None:
        return next_page

    last_page = _to_int(pagination.get("last_page"))
    page = _to_int(pagination.get("page")) or current_page
    if last_page is not None and page < last_page:
        return page + 1

    return None


def _to_int(value: Any) -> int | None:
    if isinstance(value, bool) or value is None:
        return None
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def with_page_parameter(url: str, page: int) -> str:
    parsed_url = urllib.parse.urlparse(url)
    query = dict(urllib.parse.parse_qsl(parsed_url.query, keep_blank_values=True))
    query["page"] = str(page)
    return urllib.parse.urlunparse(parsed_url._replace(query=urllib.parse.urlencode(query)))


def fetch_collection(
    api_url: str,
    path: str,
    field_name: str,
    resource_name: str,
    api_token: str,
    timeout: int,
    fallback_field_names: tuple[str, ...] = (),
    missing_field_is_empty: bool = True,
) -> tuple[list[dict[str, Any]], list[dict[str, str]]]:
    endpoint = f"{api_url.rstrip('/')}/{path.lstrip('/')}"
    entries: list[dict[str, Any]] = []
    errors: list[dict[str, str]] = []
    seen_pages: set[int] = set()
    page = 1

    while True:
        if page in seen_pages:
            errors.append(AgentError("pagination_error", f"Pagination loop detected at page {page}").as_dict())
            break
        if len(seen_pages) >= MAX_PAGES:
            errors.append(AgentError("pagination_error", f"Pagination exceeded {MAX_PAGES} pages").as_dict())
            break

        seen_pages.add(page)
        url = endpoint if page == 1 else with_page_parameter(endpoint, page)

        try:
            payload = request_json(url, api_token, timeout)
            entries.extend(
                extract_collection(
                    payload,
                    field_name,
                    resource_name,
                    fallback_field_names,
                    missing_field_is_empty,
                )
            )
        except AgentError as exc:
            errors.append(exc.as_dict())
            break

        next_page = next_page_from_payload(payload, page)
        if next_page is None:
            break
        page = next_page

    return entries, errors


def fetch_storage_boxes(api_url: str, api_token: str, timeout: int) -> tuple[list[dict[str, Any]], list[dict[str, str]]]:
    return fetch_collection(
        api_url=api_url,
        path="storage_boxes",
        field_name="storage_boxes",
        resource_name="Storage Box",
        api_token=api_token,
        timeout=timeout,
    )


def fetch_subaccounts(
    api_url: str,
    api_token: str,
    timeout: int,
    storage_box_id: str,
) -> tuple[list[dict[str, Any]], list[dict[str, str]]]:
    quoted_id = urllib.parse.quote(storage_box_id, safe="")
    return fetch_collection(
        api_url=api_url,
        path=f"storage_boxes/{quoted_id}/subaccounts",
        field_name="subaccounts",
        resource_name="Storage Box subaccount",
        api_token=api_token,
        timeout=timeout,
        fallback_field_names=("storage_box_subaccounts",),
        missing_field_is_empty=False,
    )


def enrich_storage_boxes_with_subaccount_counts(
    storage_boxes: list[dict[str, Any]],
    api_url: str,
    api_token: str,
    timeout: int,
) -> None:
    for storage_box in storage_boxes:
        storage_box_id = storage_box.get("id")
        if storage_box_id in (None, ""):
            storage_box["subaccounts_error"] = {
                "code": "subaccounts_error",
                "message": "Unable to fetch subaccounts: Storage Box ID is missing",
            }
            continue

        subaccounts, errors = fetch_subaccounts(api_url, api_token, timeout, str(storage_box_id))
        if errors:
            storage_box["subaccounts_error"] = _subaccounts_error(str(storage_box_id), errors)
            continue

        storage_box["subaccounts_count"] = len(subaccounts)


def _subaccounts_error(storage_box_id: str, errors: list[dict[str, str]]) -> dict[str, str]:
    first_error = errors[0] if errors else {"code": "error", "message": "Unknown subaccount API error"}
    code = first_error.get("code") or "error"
    message = first_error.get("message") or "Unknown subaccount API error"
    suffix = f" (+{len(errors) - 1} more)" if len(errors) > 1 else ""
    return {
        "code": f"subaccounts_{code}",
        "message": f"Unable to fetch subaccounts for Storage Box {storage_box_id}: {message}{suffix}",
    }


def sanitize_storage_box_errors(storage_boxes: list[dict[str, Any]], api_token: str) -> None:
    for storage_box in storage_boxes:
        raw_error = storage_box.get("subaccounts_error")
        if not isinstance(raw_error, dict):
            continue
        raw_error["code"] = str(raw_error.get("code") or "subaccounts_error")
        raw_error["message"] = sanitize_message(str(raw_error.get("message") or "Subaccount API error"), api_token)


def filter_storage_boxes(storage_boxes: list[dict[str, Any]], box_ids: set[str]) -> list[dict[str, Any]]:
    if not box_ids:
        return storage_boxes

    return [storage_box for storage_box in storage_boxes if str(storage_box.get("id", "")) in box_ids]


def collect_payload(args: argparse.Namespace, api_token: str) -> dict[str, Any]:
    storage_boxes, errors = fetch_storage_boxes(args.api_url, api_token, args.timeout)
    storage_boxes = filter_storage_boxes(storage_boxes, normalize_box_ids(args.box_id))
    if args.fetch_subaccounts:
        enrich_storage_boxes_with_subaccount_counts(storage_boxes, args.api_url, api_token, args.timeout)
    sanitize_storage_box_errors(storage_boxes, api_token)
    return {
        "storage_boxes": storage_boxes,
        "errors": [
            {"code": error.get("code", "error"), "message": sanitize_message(error.get("message", ""), api_token)}
            for error in errors
        ],
    }


def default_cache_directory() -> Path:
    omd_root = os.environ.get("OMD_ROOT")
    if omd_root:
        return Path(omd_root) / "var/check_mk/cache/hetzner_storagebox"
    return Path("~/var/check_mk/cache/hetzner_storagebox").expanduser()


def cache_paths(args: argparse.Namespace, api_token: str) -> tuple[Path, Path, Path, dict[str, Any], str]:
    directory = Path(args.cache_dir).expanduser() if args.cache_dir else default_cache_directory()
    cache_key = {
        "schema_version": CACHE_SCHEMA_VERSION,
        "api_url": str(args.api_url).rstrip("/"),
        "api_token_hash": hashlib.sha256(api_token.encode("utf-8")).hexdigest(),
        "timeout": int(args.timeout),
        "box_ids": sorted(normalize_box_ids(args.box_id)),
        "fetch_subaccounts": bool(args.fetch_subaccounts),
    }
    serialized_key = json.dumps(cache_key, sort_keys=True, separators=(",", ":"))
    cache_hash = hashlib.sha256(serialized_key.encode("utf-8")).hexdigest()
    return (
        directory,
        directory / f"storagebox_{cache_hash}.json",
        directory / f"storagebox_{cache_hash}.lock",
        cache_key,
        cache_hash,
    )


def collect_payload_with_cache(
    args: argparse.Namespace,
    api_token: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
    ttl = max(0, int(args.cache_ttl))
    directory, cache_file, lock_file, cache_key, cache_hash = cache_paths(args, api_token)

    try:
        directory.mkdir(parents=True, exist_ok=True)
    except OSError:
        payload = collect_payload(args, api_token)
        return payload, cache_info_for_collection(ttl, "unavailable")

    cleanup_cache_directory(directory, ttl, protected_paths=(cache_file, lock_file))

    now = time.time()
    cached = read_cache_entry(cache_file, cache_hash)
    if cached is not None and cache_entry_is_fresh(cached, ttl, now):
        return payload_from_cache_entry(cached), cache_info_from_entry("hit", cached, ttl)

    lock_handle = acquire_cache_lock(lock_file)
    if lock_handle is None:
        cached_after_wait = read_cache_entry(cache_file, cache_hash)
        if cached_after_wait is not None:
            status = "hit" if cache_entry_is_fresh(cached_after_wait, ttl, time.time()) else "stale_lock"
            return payload_from_cache_entry(cached_after_wait), cache_info_from_entry(status, cached_after_wait, ttl)
        payload = collect_payload(args, api_token)
        return payload, cache_info_for_collection(ttl, "lock_unavailable")

    try:
        cached_after_lock = read_cache_entry(cache_file, cache_hash)
        if cached_after_lock is not None and cache_entry_is_fresh(cached_after_lock, ttl, time.time()):
            return payload_from_cache_entry(cached_after_lock), cache_info_from_entry("hit", cached_after_lock, ttl)

        stale_candidate = cached_after_lock or cached
        try:
            payload = collect_payload(args, api_token)
        except Exception as exc:
            if args.cache_stale_on_error and stale_candidate is not None:
                error_message = exception_message(exc, args, api_token)
                return (
                    payload_from_cache_entry(stale_candidate),
                    cache_info_from_entry("stale_on_error", stale_candidate, ttl, error=error_message),
                )
            raise

        collection_error = collection_error_message(payload)
        if collection_error:
            if args.cache_stale_on_error and stale_candidate is not None:
                return (
                    payload_from_cache_entry(stale_candidate),
                    cache_info_from_entry("stale_on_error", stale_candidate, ttl, error=collection_error),
                )
            return payload, cache_info_for_collection(ttl, "refresh_failed", error=collection_error)

        collected_at = time.time()
        write_cache_entry(cache_file, cache_key, cache_hash, payload, collected_at)
        return payload, cache_info_for_collection(ttl, "refresh", collected_at=collected_at)
    finally:
        release_cache_lock(lock_handle)


def payload_from_cache_entry(entry: dict[str, Any]) -> dict[str, Any]:
    payload = entry.get("payload")
    if not isinstance(payload, dict):
        return {"storage_boxes": [], "errors": [{"code": "cache_error", "message": "Cached payload is invalid"}]}
    return {
        "storage_boxes": payload.get("storage_boxes", []),
        "errors": payload.get("errors", []),
    }


def collection_error_message(payload: dict[str, Any]) -> str | None:
    errors = payload.get("errors")
    if not isinstance(errors, list) or not errors:
        return None
    return format_errors_for_cache(errors)


def format_errors_for_cache(errors: list[Any]) -> str:
    if not errors:
        return "Unknown collection error"
    first_error = errors[0]
    if isinstance(first_error, dict):
        code = str(first_error.get("code") or "error")
        message = str(first_error.get("message") or "Collection failed")
    else:
        code = "error"
        message = str(first_error)
    suffix = f" (+{len(errors) - 1} more)" if len(errors) > 1 else ""
    return f"{code}: {message}{suffix}"


def exception_message(exc: Exception, args: argparse.Namespace, api_token: str) -> str:
    if isinstance(exc, AgentError):
        return sanitize_message(exc.message, api_token)
    message = f"{type(exc).__name__}: {exc}" if args.debug else str(exc)
    return sanitize_message(message or "Unexpected agent error", api_token)


def read_cache_entry(cache_file: Path, expected_hash: str) -> dict[str, Any] | None:
    try:
        with cache_file.open("r", encoding="utf-8") as handle:
            entry = json.load(handle)
    except (OSError, json.JSONDecodeError, TypeError, UnicodeDecodeError):
        return None

    if not isinstance(entry, dict):
        return None
    if entry.get("schema_version") != CACHE_SCHEMA_VERSION or entry.get("cache_hash") != expected_hash:
        return None
    if not isinstance(entry.get("collected_at"), (int, float)):
        return None
    payload = entry.get("payload")
    if not isinstance(payload, dict):
        return None
    if not isinstance(payload.get("storage_boxes"), list) or not isinstance(payload.get("errors", []), list):
        return None
    return entry


def cache_entry_is_fresh(entry: dict[str, Any], ttl: int, now: float) -> bool:
    if ttl <= 0:
        return False
    collected_at = entry.get("collected_at")
    if not isinstance(collected_at, (int, float)):
        return False
    return now - float(collected_at) <= ttl


def cache_file_is_valid_for_ttl(cache_file: Path, ttl: int, now: float) -> bool:
    if ttl <= 0:
        return False
    try:
        with cache_file.open("r", encoding="utf-8") as handle:
            entry = json.load(handle)
    except (OSError, json.JSONDecodeError, TypeError, UnicodeDecodeError):
        return False

    if not isinstance(entry, dict) or entry.get("schema_version") != CACHE_SCHEMA_VERSION:
        return False
    collected_at = entry.get("collected_at")
    if not isinstance(collected_at, (int, float)):
        return False
    return now - float(collected_at) <= ttl


def cleanup_cache_directory(
    directory: Path,
    ttl: int,
    *,
    protected_paths: Iterable[Path] = (),
) -> None:
    now = time.time()
    cutoff = now - CACHE_CLEANUP_RETENTION_SECONDS
    protected = set(protected_paths)

    try:
        candidates = list(directory.iterdir())
    except OSError:
        return

    for candidate in candidates:
        try:
            if candidate in protected or candidate.suffix not in CACHE_CLEANUP_SUFFIXES:
                continue
            if not candidate.is_file() or candidate.stat().st_mtime >= cutoff:
                continue
            if candidate.suffix == ".json" and cache_file_is_valid_for_ttl(candidate, ttl, now):
                continue
            candidate.unlink()
        except OSError:
            continue


def write_cache_entry(
    cache_file: Path,
    cache_key: dict[str, Any],
    cache_hash: str,
    payload: dict[str, Any],
    collected_at: float,
) -> None:
    entry = {
        "schema_version": CACHE_SCHEMA_VERSION,
        "created_at": collected_at,
        "collected_at": collected_at,
        "cache_key": cache_key,
        "cache_hash": cache_hash,
        "payload": payload,
    }

    temp_name: str | None = None
    try:
        with tempfile.NamedTemporaryFile(
            "w",
            encoding="utf-8",
            dir=str(cache_file.parent),
            prefix=f".{cache_file.name}.",
            suffix=".tmp",
            delete=False,
        ) as handle:
            temp_name = handle.name
            json.dump(entry, handle, sort_keys=True, separators=(",", ":"))
            handle.write("\n")
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temp_name, cache_file)
    except OSError:
        if temp_name:
            try:
                os.unlink(temp_name)
            except OSError:
                pass


def acquire_cache_lock(lock_file: Path, timeout: float = DEFAULT_CACHE_LOCK_TIMEOUT) -> Any | None:
    try:
        handle = lock_file.open("a", encoding="utf-8")
    except OSError:
        return None

    deadline = time.monotonic() + max(0.0, timeout)
    while True:
        try:
            fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
            handle.seek(0)
            handle.truncate()
            handle.write(f"pid={os.getpid()} acquired_at={time.time():.3f}\n")
            handle.flush()
            return handle
        except BlockingIOError:
            if time.monotonic() >= deadline:
                handle.close()
                return None
            time.sleep(0.05)
        except OSError:
            handle.close()
            return None


def release_cache_lock(handle: Any | None) -> None:
    if handle is None:
        return
    try:
        fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
    except OSError:
        pass
    try:
        handle.close()
    except OSError:
        pass


def cache_info_from_entry(
    status: str,
    entry: dict[str, Any],
    ttl: int,
    *,
    error: str | None = None,
) -> dict[str, Any]:
    now = time.time()
    collected_at = float(entry["collected_at"])
    age = max(0.0, now - collected_at)
    stale = status in {"stale_on_error", "stale_lock"} or not cache_entry_is_fresh(entry, ttl, now)
    messages = {
        "hit": "fresh result cache hit",
        "stale_on_error": "stale result cache used after refresh failure",
        "stale_lock": "stale result cache used while refresh is already running",
    }
    info: dict[str, Any] = {
        "enabled": True,
        "status": status,
        "stale": stale,
        "age_seconds": round(age, 3),
        "ttl_seconds": ttl,
        "collected_at": collected_at,
        "message": f"{messages.get(status, 'result cache used')} (age {int(age)}s, ttl {ttl}s)",
    }
    if error:
        info["error"] = error
    return info


def cache_info_for_collection(
    ttl: int,
    status: str,
    *,
    collected_at: float | None = None,
    error: str | None = None,
) -> dict[str, Any]:
    collected_at = time.time() if collected_at is None else collected_at
    labels = {
        "refresh": "fresh collection stored in result cache",
        "refresh_failed": "fresh collection failed and no stale cache was used",
        "lock_unavailable": "fresh collection completed without updating result cache",
        "unavailable": "result cache unavailable, using fresh collection",
    }
    info: dict[str, Any] = {
        "enabled": True,
        "status": status,
        "stale": False,
        "age_seconds": 0,
        "ttl_seconds": ttl,
        "collected_at": collected_at,
        "message": f"{labels.get(status, 'fresh collection')} (age 0s, ttl {ttl}s)",
    }
    if error:
        info["error"] = error
    return info


def disabled_cache_info() -> dict[str, Any]:
    return {"enabled": False, "status": "disabled", "stale": False}


def main() -> int:
    args = parse_arguments()
    api_token = ""
    cache_info: dict[str, Any] | None = None
    errors: list[dict[str, str]] = []
    storage_boxes: list[dict[str, Any]] = []

    try:
        api_token = resolve_api_token(args.api_token)
        if args.cache_enabled:
            payload, cache_info = collect_payload_with_cache(args, api_token)
        else:
            payload = collect_payload(args, api_token)
            cache_info = disabled_cache_info()
        storage_boxes = payload["storage_boxes"]
        errors = payload["errors"]
    except AgentError as exc:
        errors = [{"code": exc.code, "message": sanitize_message(exc.message, api_token)}]
    except Exception as exc:
        message = f"{type(exc).__name__}: {exc}" if args.debug else str(exc)
        message = sanitize_message(message, api_token)
        errors = [{"code": "unexpected_error", "message": message or "Unexpected agent error"}]

    emit_section(storage_boxes, errors, cache_info)
    return 0


if __name__ == "__main__":
    sys.exit(main())
