#!/usr/bin/env python3
"""Secure JSON payload preflight checker.

This script is a vendor-neutral companion to secure C# JSON deserialization
workflows. It validates a JSON payload against a narrow expected contract,
optionally rejects unknown properties, and reports common issues before the
payload is passed to application logic.

Intended use:
- Validate sample payloads during development.
- Add a simple guard step in CI or local testing.
- Demonstrate secure-deserialization principles without hard-coding secrets
  or environment-specific values.

This script does not execute any business logic, connect to services, or modify
files. It only reads JSON and reports validation results.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, List, Optional, Sequence, Tuple


TITLE_MIN_LEN = 3
TITLE_MAX_LEN = 100
DESCRIPTION_MIN_LEN = 1
DESCRIPTION_MAX_LEN = 2000
PRIORITY_MIN = 1
PRIORITY_MAX = 5

TITLE_RE = re.compile(r"^.{3,100}$", re.DOTALL)
DESCRIPTION_RE = re.compile(r"^.{1,2000}$", re.DOTALL)


@dataclass
class ValidationIssue:
    path: str
    message: str


def load_json(source: str) -> Any:
    """Load JSON from a file path or raw JSON string."""
    try:
        if source == "-":
            return json.load(sys.stdin)

        try:
            with open(source, "r", encoding="utf-8") as f:
                return json.load(f)
        except FileNotFoundError:
            # Treat as raw JSON if the path does not exist.
            return json.loads(source)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid JSON: {exc.msg} at line {exc.lineno}, column {exc.colno}") from exc


def ensure_object(value: Any) -> Dict[str, Any]:
    if not isinstance(value, dict):
        raise ValueError("Top-level JSON value must be an object.")
    return value


def check_unknown_fields(obj: Dict[str, Any], allowed_fields: Sequence[str]) -> List[ValidationIssue]:
    issues: List[ValidationIssue] = []
    allowed = set(allowed_fields)
    for key in obj.keys():
        if key not in allowed:
            issues.append(ValidationIssue(path=key, message="Unknown property present."))
    return issues


def validate_string_field(
    obj: Dict[str, Any],
    field: str,
    min_len: int,
    max_len: int,
    required: bool = True,
) -> List[ValidationIssue]:
    issues: List[ValidationIssue] = []
    if field not in obj:
        if required:
            issues.append(ValidationIssue(path=field, message="Missing required field."))
        return issues

    value = obj[field]
    if not isinstance(value, str):
        issues.append(ValidationIssue(path=field, message="Must be a string."))
        return issues

    if len(value) < min_len:
        issues.append(ValidationIssue(path=field, message=f"Must be at least {min_len} characters."))
    if len(value) > max_len:
        issues.append(ValidationIssue(path=field, message=f"Must be at most {max_len} characters."))
    return issues


def validate_int_field(
    obj: Dict[str, Any],
    field: str,
    min_value: int,
    max_value: int,
    required: bool = True,
) -> List[ValidationIssue]:
    issues: List[ValidationIssue] = []
    if field not in obj:
        if required:
            issues.append(ValidationIssue(path=field, message="Missing required field."))
        return issues

    value = obj[field]
    if not isinstance(value, int) or isinstance(value, bool):
        issues.append(ValidationIssue(path=field, message="Must be an integer."))
        return issues

    if value < min_value or value > max_value:
        issues.append(ValidationIssue(path=field, message=f"Must be between {min_value} and {max_value}."))
    return issues


def validate_payload(obj: Dict[str, Any], reject_unknown: bool) -> List[ValidationIssue]:
    issues: List[ValidationIssue] = []
    allowed_fields = ["title", "description", "priority"]

    if reject_unknown:
        issues.extend(check_unknown_fields(obj, allowed_fields))

    issues.extend(validate_string_field(obj, "title", TITLE_MIN_LEN, TITLE_MAX_LEN, required=True))
    issues.extend(validate_string_field(obj, "description", DESCRIPTION_MIN_LEN, DESCRIPTION_MAX_LEN, required=True))
    issues.extend(validate_int_field(obj, "priority", PRIORITY_MIN, PRIORITY_MAX, required=True))

    return issues


def format_issues(issues: Sequence[ValidationIssue]) -> str:
    lines = []
    for issue in issues:
        lines.append(f"- {issue.path}: {issue.message}")
    return "\n".join(lines)


def main(argv: Optional[Sequence[str]] = None) -> int:
    parser = argparse.ArgumentParser(
        description="Validate a JSON payload against a narrow contract before deserialization in application code."
    )
    parser.add_argument(
        "input",
        help="Path to a JSON file, raw JSON string, or '-' to read from stdin.",
    )
    parser.add_argument(
        "--allow-unknown",
        action="store_true",
        help="Allow unknown properties instead of rejecting them.",
    )
    parser.add_argument(
        "--show-parsed",
        action="store_true",
        help="Print the parsed payload when validation succeeds.",
    )

    args = parser.parse_args(argv)

    try:
        data = load_json(args.input)
        obj = ensure_object(data)
        issues = validate_payload(obj, reject_unknown=not args.allow_unknown)
    except ValueError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2

    if issues:
        print("Validation failed:", file=sys.stderr)
        print(format_issues(issues), file=sys.stderr)
        return 1

    print("Validation passed.")
    if args.show_parsed:
        print(json.dumps(obj, indent=2, sort_keys=True))

    # A small operational hint: validation succeeded, but application code still
    # needs authorization checks, schema review, and safe mapping into internal
    # models before the data is used.
    return 0


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