<?php
/**
 * JSON-RPC 2.0 Validator
 * Validates JSON-RPC 2.0 requests and responses using JSON Schema
 * 
 * @package PortalPeople\mcp\components
 * @author Portal People Development Team
 * @link https://www.jsonrpc.org/specification
 */

namespace PortalPeople\mcp\components;

use Yii;
use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;
use yii\base\Component;

/**
 * JsonRpc2Validator validates JSON-RPC 2.0 messages
 * 
 * Usage:
 * ```php
 * $response = ['jsonrpc' => '2.0', 'result' => ['price' => 99.99], 'id' => 1];
 * $validation = JsonRpc2Validator::validateResponse($response);
 * if ($validation['valid']) {
 *     echo json_encode($response);
 * } else {
 *     Yii::error($validation['errors']);
 * }
 * ```
 */
class JsonRpc2Validator extends Component
{
    /**
     * Safely stringify schema constraint details for logs/errors.
     *
     * @param mixed $constraint
     * @return string
     */
    private static function formatConstraint($constraint): string
    {
        if ($constraint === null || $constraint === '') {
            return '';
        }

        if (is_scalar($constraint)) {
            return (string)$constraint;
        }

        if (is_array($constraint) || is_object($constraint)) {
            $encoded = json_encode($constraint);
            return $encoded !== false ? $encoded : gettype($constraint);
        }

        return gettype($constraint);
    }

    /**
     * JSON-RPC 2.0 Response Schema
     * Based on https://www.jsonrpc.org/specification
     */
    private const RESPONSE_SCHEMA = [
        '$schema' => 'http://json-schema.org/draft-04/schema#',
        'title' => 'JSON-RPC 2.0 Response',
        'type' => 'object',
        'required' => ['jsonrpc'],
        'properties' => [
            'jsonrpc' => [
                'type' => 'string',
                'enum' => ['2.0'],
                'description' => 'A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".'
            ],
            'result' => [
                'description' => 'This member is REQUIRED on success. This member MUST NOT exist if there was an error invoking the method.'
            ],
            'error' => [
                'type' => 'object',
                'required' => ['code', 'message'],
                'properties' => [
                    'code' => [
                        'type' => 'integer',
                        'description' => 'A Number that indicates the error type that occurred.'
                    ],
                    'message' => [
                        'type' => 'string',
                        'description' => 'A String providing a short description of the error.'
                    ],
                    'data' => [
                        'description' => 'A Primitive or Structured value that contains additional information about the error. This may be omitted.'
                    ]
                ],
                'description' => 'This member is REQUIRED on error. This member MUST NOT exist if there was no error triggered during invocation.'
            ],
            'id' => [
                'description' => 'This member is REQUIRED. It MUST be the same as the value of the id member in the Request Object.',
                'oneOf' => [
                    ['type' => 'string'],
                    ['type' => 'number'],
                    ['type' => 'null']
                ]
            ]
        ],
        'oneOf' => [
            [
                'required' => ['result', 'id']
            ],
            [
                'required' => ['error', 'id']
            ]
        ]
    ];

    /**
     * JSON-RPC 2.0 Request Schema
     */
    private const REQUEST_SCHEMA = [
        '$schema' => 'http://json-schema.org/draft-04/schema#',
        'title' => 'JSON-RPC 2.0 Request',
        'type' => 'object',
        'required' => ['jsonrpc', 'method'],
        'properties' => [
            'jsonrpc' => [
                'type' => 'string',
                'enum' => ['2.0'],
                'description' => 'A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".'
            ],
            'method' => [
                'type' => 'string',
                'description' => 'A String containing the name of the method to be invoked.'
            ],
            'params' => [
                'description' => 'A Structured value that holds the parameter values to be used during the invocation of the method.',
                'oneOf' => [
                    ['type' => 'array'],
                    ['type' => 'object']
                ]
            ],
            'id' => [
                'description' => 'An identifier established by the Client. If not included it is assumed to be a notification.',
                'oneOf' => [
                    ['type' => 'string'],
                    ['type' => 'number'],
                    ['type' => 'null']
                ]
            ]
        ]
    ];

    /**
     * Standard JSON-RPC 2.0 Error Codes
     */
    public const ERROR_PARSE = -32700;
    public const ERROR_INVALID_REQUEST = -32600;
    public const ERROR_METHOD_NOT_FOUND = -32601;
    public const ERROR_INVALID_PARAMS = -32602;
    public const ERROR_INTERNAL = -32603;
    
    /**
     * Validate a JSON-RPC 2.0 Response
     * 
     * @param array|object|string $data The response data to validate
     * @param bool $strict Whether to use strict validation
     * @return array ['valid' => bool, 'errors' => array (optional)]
     */
    public static function validateResponse($data, bool $strict = true): array
    {
        try {
            // Convert to object if needed
            if (is_string($data)) {
                $data = json_decode($data);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    return [
                        'valid' => false,
                        'errors' => ['Invalid JSON: ' . json_last_error_msg()]
                    ];
                }
            } elseif (is_array($data)) {
                $data = json_decode(json_encode($data));
            }

            $validator = new Validator();
            $schema = json_decode(json_encode(self::RESPONSE_SCHEMA));
            
            $checkMode = $strict 
                ? Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_VALIDATE_SCHEMA
                : Constraint::CHECK_MODE_COERCE_TYPES;
                
            $validator->validate($data, $schema, $checkMode);

            if ($validator->isValid()) {
                return ['valid' => true];
            }

            $errors = [];
            foreach ($validator->getErrors() as $error) {
                $constraint = self::formatConstraint($error['constraint'] ?? null);
                $errors[] = sprintf(
                    "[%s] %s%s",
                    $error['property'],
                    $error['message'],
                    $constraint !== '' ? " (constraint: {$constraint})" : ''
                );
            }

            return [
                'valid' => false,
                'errors' => $errors
            ];
            
        } catch (\Exception $e) {
            Yii::error($e->getMessage(), __METHOD__);
            return [
                'valid' => false,
                'errors' => ['Exception: ' . $e->getMessage()]
            ];
        }
    }

    /**
     * Validate a JSON-RPC 2.0 Request
     * 
     * @param array|object|string $data The request data to validate
     * @param bool $strict Whether to use strict validation
     * @return array ['valid' => bool, 'errors' => array (optional)]
     */
    public static function validateRequest($data, bool $strict = true): array
    {
        try {
            // Convert to object if needed
            if (is_string($data)) {
                $data = json_decode($data);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    return [
                        'valid' => false,
                        'errors' => ['Invalid JSON: ' . json_last_error_msg()]
                    ];
                }
            } elseif (is_array($data)) {
                $data = json_decode(json_encode($data));
            }

            $validator = new Validator();
            $schema = json_decode(json_encode(self::REQUEST_SCHEMA));
            
            $checkMode = $strict 
                ? Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_VALIDATE_SCHEMA
                : Constraint::CHECK_MODE_COERCE_TYPES;
                
            $validator->validate($data, $schema, $checkMode);

            if ($validator->isValid()) {
                return ['valid' => true];
            }

            $errors = [];
            foreach ($validator->getErrors() as $error) {
                $constraint = self::formatConstraint($error['constraint'] ?? null);
                $errors[] = sprintf(
                    "[%s] %s%s",
                    $error['property'],
                    $error['message'],
                    $constraint !== '' ? " (constraint: {$constraint})" : ''
                );
            }

            return [
                'valid' => false,
                'errors' => $errors
            ];
            
        } catch (\Exception $e) {
            Yii::error($e->getMessage(), __METHOD__);
            return [
                'valid' => false,
                'errors' => ['Exception: ' . $e->getMessage()]
            ];
        }
    }

    /**
     * Create a valid JSON-RPC 2.0 success response
     * 
     * @param mixed $result The result data
     * @param string|int|null $id The request ID
     * @return array
     */
    public static function createSuccessResponse($result, $id): array
    {
        return [
            'jsonrpc' => '2.0',
            'result' => $result,
            'id' => $id
        ];
    }

    /**
     * Create a valid JSON-RPC 2.0 error response
     * 
     * @param int $code Error code
     * @param string $message Error message
     * @param mixed $data Additional error data (optional)
     * @param string|int|null $id The request ID
     * @return array
     */
    public static function createErrorResponse(int $code, string $message, $data = null, $id = null): array
    {
        $error = [
            'code' => $code,
            'message' => $message
        ];
        
        if ($data !== null) {
            $error['data'] = $data;
        }
        
        return [
            'jsonrpc' => '2.0',
            'error' => $error,
            'id' => $id
        ];
    }

    /**
     * Validate and return formatted response
     * 
     * @param array $response The response to validate and format
     * @param bool $throw Whether to throw exception on validation failure
     * @return array|false Returns formatted response or false on failure
     * @throws \yii\base\InvalidArgumentException
     */
    public static function validateAndFormat(array $response, bool $throw = false)
    {
        $validation = self::validateResponse($response);
        
        if (!$validation['valid']) {
            $errorMsg = 'Invalid JSON-RPC 2.0 Response: ' . implode(', ', $validation['errors']);
            Yii::error($errorMsg, __METHOD__);
            
            if ($throw) {
                throw new \yii\base\InvalidArgumentException($errorMsg);
            }
            
            return false;
        }
        
        return $response;
    }

    /**
     * Check if response is an error response
     * 
     * @param array $response
     * @return bool
     */
    public static function isError(array $response): bool
    {
        return isset($response['error']);
    }

    /**
     * Get standard error message for error code
     * 
     * @param int $code
     * @return string
     */
    public static function getStandardErrorMessage(int $code): string
    {
        $messages = [
            self::ERROR_PARSE => 'Parse error',
            self::ERROR_INVALID_REQUEST => 'Invalid Request',
            self::ERROR_METHOD_NOT_FOUND => 'Method not found',
            self::ERROR_INVALID_PARAMS => 'Invalid params',
            self::ERROR_INTERNAL => 'Internal error',
        ];
        
        return $messages[$code] ?? 'Server error';
    }
}
