"""JSON contract validation CLI and helpers."""

from __future__ import annotations

import argparse
import importlib
import json
import sys
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Protocol

from x_make_common_x.json_contracts import validate_schema as _common_validate_schema

SCHEMA_VERSION = "x_make_contract_validators_x.run/1.0"


class _DraftValidatorProtocol(Protocol):
    @classmethod
    def check_schema(cls, schema: Mapping[str, object]) -> None: ...

    def __init__(self, schema: Mapping[str, object]) -> None: ...

    def iter_errors(self, instance: object) -> Iterable[object]: ...


def _load_validator() -> type[_DraftValidatorProtocol]:
    validators_module = importlib.import_module("jsonschema.validators")
    return validators_module.Draft202012Validator


_DRAFT_VALIDATOR = _load_validator()


@dataclass(slots=True)
class ValidationIssue:
    message: str
    path: tuple[object, ...]
    schema_path: tuple[object, ...]

    def to_dict(self) -> dict[str, object]:
        return {
            "message": self.message,
            "path": list(self.path),
            "schema_path": list(self.schema_path),
        }


@dataclass(slots=True)
class ValidationResult:
    success: bool
    issues: tuple[ValidationIssue, ...]

    def to_dict(self) -> dict[str, object]:
        return {
            "success": self.success,
            "issues": [issue.to_dict() for issue in self.issues],
        }


class SchemaValidationError(RuntimeError):
    def __init__(self, message: str) -> None:
        super().__init__(message)
        self.message = message


class ContractValidationError(RuntimeError):
    def __init__(self, issues: Sequence[ValidationIssue]) -> None:
        message = issues[0].message if issues else "Payload failed validation"
        super().__init__(message)
        self.issues = tuple(issues)


def validate_schema(schema: Mapping[str, object]) -> None:
    try:
        _common_validate_schema(schema)
    except Exception as exc:
        raise SchemaValidationError(str(exc)) from exc


def validate_payload(payload: object, schema: Mapping[str, object]) -> ValidationResult:
    validator = _DRAFT_VALIDATOR(dict(schema))
    issues = tuple(_build_issue(err) for err in validator.iter_errors(payload))
    if issues:
        raise ContractValidationError(issues)
    return ValidationResult(success=True, issues=())


def _build_issue(error: object) -> ValidationIssue:
    message = getattr(error, "message", "Validation failed")
    path = tuple(getattr(error, "path", ()))
    schema_path = tuple(getattr(error, "schema_path", ()))
    return ValidationIssue(message=message, path=path, schema_path=schema_path)


def run(payload: Mapping[str, object]) -> dict[str, object]:
    parameters_obj = payload.get("parameters")
    if not isinstance(parameters_obj, Mapping):
        raise TypeError("Payload parameters must be a mapping")
    schema = _resolve_schema(parameters_obj)
    try:
        validate_schema(schema)
    except SchemaValidationError as exc:
        return _failure_result("schema", str(exc), ())
    candidate = _resolve_payload(parameters_obj)
    try:
        result = validate_payload(candidate, schema)
    except ContractValidationError as exc:
        return _failure_result("payload", str(exc), exc.issues)
    return {
        "status": "success",
        "schema_version": SCHEMA_VERSION,
        "issues": [issue.to_dict() for issue in result.issues],
        "message": "Payload matches schema",
    }


def _failure_result(
    error_type: str,
    message: str,
    issues: Sequence[ValidationIssue],
) -> dict[str, object]:
    return {
        "status": "failure",
        "schema_version": SCHEMA_VERSION,
        "error_type": error_type,
        "message": message,
        "issues": [issue.to_dict() for issue in issues],
    }


def _resolve_schema(parameters: Mapping[str, object]) -> Mapping[str, object]:
    if "schema" in parameters:
        schema_obj = parameters["schema"]
        if not isinstance(schema_obj, Mapping):
            raise TypeError("Inline schema must be a mapping")
        return schema_obj
    schema_path_obj = parameters.get("schema_path")
    if schema_path_obj is None:
        raise ValueError("Schema data must be provided via 'schema' or 'schema_path'")
    schema_data = _load_json(Path(str(schema_path_obj)), expect_mapping=True)
    return schema_data


def _resolve_payload(parameters: Mapping[str, object]) -> object:
    if "payload" in parameters:
        return parameters["payload"]
    payload_path_obj = parameters.get("payload_path")
    if payload_path_obj is None:
        raise ValueError("Payload data must be provided via 'payload' or 'payload_path'")
    return _load_json(Path(str(payload_path_obj)), expect_mapping=False)


def _load_json(path: Path, *, expect_mapping: bool) -> Any:
    with path.open("r", encoding="utf-8") as handle:
        data = json.load(handle)
    if expect_mapping and not isinstance(data, Mapping):
        raise TypeError("JSON file must contain an object at the root")
    return data


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Validate JSON payloads against a schema")
    parser.add_argument("--schema", required=True, help="Path to JSON schema file")
    parser.add_argument("--payload", required=True, help="Path to JSON payload file")
    parser.add_argument("--json", action="store_true", help="Emit JSON summary")
    args = parser.parse_args(list(argv) if argv is not None else None)

    payload = {
        "command": "x_make_contract_validators_x",
        "parameters": {
            "schema_path": args.schema,
            "payload_path": args.payload,
        },
    }
    result = run(payload)
    if args.json:
        json.dump(result, sys.stdout, indent=2)
        sys.stdout.write("\n")
    else:
        print(result.get("message", ""))
        issues_obj = result.get("issues")
        if isinstance(issues_obj, list):
            for issue in issues_obj:
                message = issue.get("message") if isinstance(issue, dict) else issue
                path = issue.get("path") if isinstance(issue, dict) else "?"
                print(f" - {message} @ path {path}")
    return 0 if result["status"] == "success" else 1


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