# Copyright (C) 2026 GoodData Corporation

"""Shared helpers for external JSON runners.

External runners are invoked via validation registry CommandSpec and must print a single JSON
payload to stdout (one line).
"""

from __future__ import annotations

import asyncio
import sys
from collections.abc import Callable, Coroutine
from typing import Any, cast

import orjson


def json_dump(obj: object) -> str:
    """Serialize an object as compact JSON (UTF-8 str)."""
    return orjson.dumps(obj).decode("utf-8")


def bool_from_str(s: str | None, *, default: bool = False) -> bool:
    """Parse a bool from common string forms.

    This is useful because registry argv often passes booleans as strings to avoid shell conditionals.
    """
    if s is None:
        return default
    v = s.strip().lower()
    if v in {"1", "true", "yes", "on"}:
        return True
    if v in {"0", "false", "no", "off"}:
        return False
    return default


def run_external_json_runner(
    *,
    tool: str,
    run: Callable[[], Coroutine[Any, Any, dict[str, object]]],
    extra_error_fields: dict[str, object] | None = None,
) -> int:
    """Run an async runner, print JSON payload, and return a process exit code.

    Exit codes:
    - 0: success=True
    - 1: success=False
    - 2: runner exception (payload printed with success=False)
    """
    try:
        payload = asyncio.run(run())
    except Exception as exc:
        err: dict[str, object] = {
            "tool": tool,
            "success": False,
            "text": f"External runner exception: {exc}",
            "error_excerpt": repr(exc),
            "error_count": 1,
        }
        if extra_error_fields:
            err.update(extra_error_fields)
        sys.stdout.write(json_dump(err))
        sys.stdout.write("\n")
        return 2

    if not isinstance(payload.get("tool"), str):
        payload["tool"] = tool

    # Keep exit-code semantics stable for external_json processors.
    sys.stdout.write(json_dump(payload))
    sys.stdout.write("\n")
    success = payload.get("success") is True
    return 0 if success else 1


def split_csv(raw: str | None) -> list[str] | None:
    """Return a list of non-empty CSV items, or None if input is empty."""
    if not isinstance(raw, str) or not raw.strip():
        return None
    parts = [s for s in (part.strip() for part in raw.split(",")) if s]
    return parts or None


def as_str(value: object, *, default: str = "") -> str:
    if isinstance(value, str):
        return value
    return default


def as_dict_str_object(value: object) -> dict[str, object]:
    if not isinstance(value, dict):
        return {}
    return cast(dict[str, object], cast(dict[Any, Any], value))
