#!/usr/bin/env python3
"""Validate MCP tool manifest files for common contract issues.

Input sources:
- --input <manifest.json>
- stdin JSON

Validation domains:
- structural correctness
- naming hygiene
- schema consistency
- descriptive completeness
"""

import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


TOOL_NAME_RE = re.compile(r"^[a-z0-9_]{3,64}$")


class CLIError(Exception):
    """Raised for expected CLI failures."""


@dataclass
class ValidationResult:
    errors: List[str]
    warnings: List[str]
    tool_count: int


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Validate MCP tool definitions.")
    parser.add_argument("--input", help="Path to manifest JSON file. If omitted, reads from stdin.")
    parser.add_argument("--strict", action="store_true", help="Exit non-zero when errors are found.")
    parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
    return parser.parse_args()


def load_manifest(input_path: Optional[str]) -> Dict[str, Any]:
    if input_path:
        try:
            data = Path(input_path).read_text(encoding="utf-8")
        except Exception as exc:
            raise CLIError(f"Failed reading --input: {exc}") from exc
    else:
        if sys.stdin.isatty():
            raise CLIError("No input provided. Use --input or pipe manifest JSON via stdin.")
        data = sys.stdin.read().strip()
        if not data:
            raise CLIError("Empty stdin.")

    try:
        payload = json.loads(data)
    except json.JSONDecodeError as exc:
        raise CLIError(f"Invalid JSON input: {exc}") from exc

    if not isinstance(payload, dict):
        raise CLIError("Manifest root must be a JSON object.")
    return payload


def validate_schema(tool_name: str, schema: Dict[str, Any]) -> Tuple[List[str], List[str]]:
    errors: List[str] = []
    warnings: List[str] = []

    if schema.get("type") != "object":
        errors.append(f"{tool_name}: inputSchema.type must be 'object'.")

    props = schema.get("properties", {})
    if not isinstance(props, dict):
        errors.append(f"{tool_name}: inputSchema.properties must be an object.")
        props = {}

    required = schema.get("required", [])
    if not isinstance(required, list):
        errors.append(f"{tool_name}: inputSchema.required must be an array.")
        required = []

    prop_keys = set(props.keys())
    for req in required:
        if req not in prop_keys:
            errors.append(f"{tool_name}: required field '{req}' is not defined in properties.")

    if not props:
        warnings.append(f"{tool_name}: no input properties declared.")

    for pname, pdef in props.items():
        if not isinstance(pdef, dict):
            errors.append(f"{tool_name}: property '{pname}' must be an object.")
            continue
        ptype = pdef.get("type")
        if not ptype:
            warnings.append(f"{tool_name}: property '{pname}' has no explicit type.")

    return errors, warnings


def validate_manifest(payload: Dict[str, Any]) -> ValidationResult:
    errors: List[str] = []
    warnings: List[str] = []

    tools = payload.get("tools")
    if not isinstance(tools, list):
        raise CLIError("Manifest must include a 'tools' array.")

    seen_names = set()
    for idx, tool in enumerate(tools):
        if not isinstance(tool, dict):
            errors.append(f"tool[{idx}] is not an object.")
            continue

        name = str(tool.get("name", "")).strip()
        desc = str(tool.get("description", "")).strip()
        schema = tool.get("inputSchema")

        if not name:
            errors.append(f"tool[{idx}] missing name.")
            continue

        if name in seen_names:
            errors.append(f"duplicate tool name: {name}")
        seen_names.add(name)

        if not TOOL_NAME_RE.match(name):
            warnings.append(
                f"{name}: non-standard naming; prefer lowercase snake_case (3-64 chars, [a-z0-9_])."
            )

        if len(desc) < 10:
            warnings.append(f"{name}: description too short; provide actionable purpose.")

        if not isinstance(schema, dict):
            errors.append(f"{name}: missing or invalid inputSchema object.")
            continue

        schema_errors, schema_warnings = validate_schema(name, schema)
        errors.extend(schema_errors)
        warnings.extend(schema_warnings)

    return ValidationResult(errors=errors, warnings=warnings, tool_count=len(tools))


def to_text(result: ValidationResult) -> str:
    lines = [
        "MCP manifest validation",
        f"- tools: {result.tool_count}",
        f"- errors: {len(result.errors)}",
        f"- warnings: {len(result.warnings)}",
    ]
    if result.errors:
        lines.append("Errors:")
        lines.extend([f"- {item}" for item in result.errors])
    if result.warnings:
        lines.append("Warnings:")
        lines.extend([f"- {item}" for item in result.warnings])
    return "\n".join(lines)


def main() -> int:
    args = parse_args()
    payload = load_manifest(args.input)
    result = validate_manifest(payload)

    if args.format == "json":
        print(json.dumps(asdict(result), indent=2))
    else:
        print(to_text(result))

    if args.strict and result.errors:
        return 1
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except CLIError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        raise SystemExit(2)
