#!/usr/bin/env python3
"""Parse JSON with type hints, validation, and safe fallbacks.

This script demonstrates a practical workflow for:
- loading JSON from a string, file, or stdin
- separating JSON syntax errors from validation errors
- validating objects and arrays of objects
- converting raw data into typed runtime structures
- using explicit, safe checks before trusting parsed values

Usage examples:
  python parse_json_typed.py --json '{"service":"auth","retries":3,"enabled":true}'
  python parse_json_typed.py --input-file payload.json
  cat payload.json | python parse_json_typed.py --stdin

The script is intentionally vendor-neutral and avoids any external dependencies.
"""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import dataclass
from typing import Any, Optional, TypedDict, TypeGuard


class ServiceConfigDict(TypedDict):
    service: str
    retries: int
    enabled: bool


@dataclass(frozen=True)
class ServiceConfig:
    service: str
    retries: int
    enabled: bool


class ValidationError(ValueError):
    """Raised when decoded JSON does not match the expected shape."""


def load_json_text(args: argparse.Namespace) -> str:
    """Load JSON text from --json, --input-file, or stdin."""
    sources_selected = sum(
        1
        for value in (args.json, args.input_file, args.stdin)
        if value is not None and value is not False
    )
    if sources_selected == 0:
        raise ValidationError("Provide exactly one input source: --json, --input-file, or --stdin.")
    if sources_selected > 1:
        raise ValidationError("Use only one input source at a time.")

    if args.json is not None:
        return args.json

    if args.input_file is not None:
        with open(args.input_file, "r", encoding="utf-8") as f:
            return f.read()

    return sys.stdin.read()


def parse_json(text: str) -> Any:
    """Parse JSON text and separate syntax errors from schema validation."""
    try:
        return json.loads(text)
    except json.JSONDecodeError as exc:
        raise ValidationError(f"Invalid JSON syntax: {exc.msg} (line {exc.lineno}, column {exc.colno})") from exc


def is_service_config_dict(value: Any) -> TypeGuard[dict[str, Any]]:
    """Return True when value looks like a valid service config dictionary."""
    return (
        isinstance(value, dict)
        and isinstance(value.get("service"), str)
        and isinstance(value.get("retries"), int)
        and isinstance(value.get("enabled"), bool)
    )


def to_service_config(raw: dict[str, Any]) -> ServiceConfig:
    """Convert validated raw dict into a typed dataclass."""
    return ServiceConfig(
        service=raw["service"],
        retries=raw["retries"],
        enabled=raw["enabled"],
    )


def validate_service_config(raw: Any) -> ServiceConfig:
    """Validate a single JSON object and return a typed config."""
    if not is_service_config_dict(raw):
        raise ValidationError(
            "Expected an object with keys: service (str), retries (int), enabled (bool)."
        )
    return to_service_config(raw)


def validate_service_config_list(raw: Any) -> list[ServiceConfig]:
    """Validate a JSON array of service config objects."""
    if not isinstance(raw, list):
        raise ValidationError("Expected a JSON array of service config objects.")

    configs: list[ServiceConfig] = []
    for index, item in enumerate(raw):
        if not is_service_config_dict(item):
            raise ValidationError(
                f"Item {index} is invalid. Expected service (str), retries (int), enabled (bool)."
            )
        configs.append(to_service_config(item))
    return configs


def print_result_single(config: ServiceConfig) -> None:
    print("Validated service config:")
    print(f"  service : {config.service}")
    print(f"  retries : {config.retries}")
    print(f"  enabled : {config.enabled}")


def print_result_list(configs: list[ServiceConfig]) -> None:
    print(f"Validated {len(configs)} service config item(s):")
    for idx, config in enumerate(configs, start=1):
        print(f"  [{idx}] service={config.service!r}, retries={config.retries}, enabled={config.enabled}")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Parse JSON with type hints and explicit validation."
    )
    source = parser.add_argument_group("input source")
    source.add_argument(
        "--json",
        dest="json",
        help="JSON text to parse directly.",
    )
    source.add_argument(
        "--input-file",
        help="Path to a file containing JSON text.",
    )
    source.add_argument(
        "--stdin",
        action="store_true",
        help="Read JSON text from standard input.",
    )

    parser.add_argument(
        "--expect-array",
        action="store_true",
        help="Validate the payload as a list of service config objects instead of a single object.",
    )
    return parser


def main(argv: Optional[list[str]] = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    try:
        text = load_json_text(args)
        raw = parse_json(text)

        if args.expect_array:
            configs = validate_service_config_list(raw)
            print_result_list(configs)
        else:
            config = validate_service_config(raw)
            print_result_single(config)

        return 0

    except ValidationError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 2
    except OSError as exc:
        print(f"File error: {exc}", file=sys.stderr)
        return 2


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