<?php

namespace MintyPHP\App\Module;

use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\Errors\ValidationError;
use Opis\JsonSchema\JsonPointer;
use Opis\JsonSchema\Validator;
use RuntimeException;

final class ModuleManifestSchemaValidator
{
    private const DEFAULT_SCHEMA_PATH = '.agents/contracts/module-manifest.schema.json';

    /** @var array<string, object> */
    private static array $schemaCache = [];

    public static function assertValid(
        array $rawManifest,
        string $moduleId,
        ?string $manifestFile = null,
        ?string $schemaFile = null
    ): void {
        $schemaPath = self::resolveSchemaPath($schemaFile);
        $schema = self::loadSchema($schemaPath);

        $payload = json_decode((string) json_encode($rawManifest), false);
        if (!is_object($payload)) {
            throw new RuntimeException("Manifest schema validation failed for module '{$moduleId}': manifest payload is not serializable.");
        }

        $validator = new Validator();
        $result = $validator->validate($payload, $schema);
        if ($result->isValid()) {
            return;
        }

        $location = $manifestFile ?? ("modules/{$moduleId}/module.php");
        $message = self::formatValidationErrors($result->error());
        throw new RuntimeException(
            "Manifest schema validation failed for module '{$moduleId}' ({$location}): {$message}"
        );
    }

    private static function resolveSchemaPath(?string $schemaFile): string
    {
        if ($schemaFile !== null && trim($schemaFile) !== '') {
            return $schemaFile;
        }

        return dirname(__DIR__, 3) . '/' . self::DEFAULT_SCHEMA_PATH;
    }

    private static function loadSchema(string $schemaPath): object
    {
        $normalizedPath = str_replace('\\', '/', $schemaPath);
        if (isset(self::$schemaCache[$normalizedPath])) {
            return self::$schemaCache[$normalizedPath];
        }

        if (!is_file($schemaPath)) {
            throw new RuntimeException("Module manifest schema not found: {$schemaPath}");
        }

        $rawSchema = file_get_contents($schemaPath);
        if ($rawSchema === false) {
            throw new RuntimeException("Unable to read module manifest schema: {$schemaPath}");
        }

        $decodedSchema = json_decode($rawSchema);
        if (!is_object($decodedSchema)) {
            throw new RuntimeException("Invalid JSON schema in {$schemaPath}");
        }

        self::$schemaCache[$normalizedPath] = $decodedSchema;
        return $decodedSchema;
    }

    private static function formatValidationErrors(?ValidationError $error): string
    {
        if ($error === null) {
            return 'unknown schema validation error';
        }

        $formatter = new ErrorFormatter();
        $messages = $formatter->formatFlat(
            $error,
            static function (ValidationError $validationError) use ($formatter): string {
                $path = JsonPointer::pathToString($validationError->data()->fullPath());
                $normalizedPath = $path === '' ? '/' : $path;
                $message = $formatter->formatErrorMessage($validationError);

                return "{$normalizedPath}: {$message}";
            }
        );

        $messages = array_values(array_unique(array_filter(
            $messages,
            static fn ($message): bool => is_string($message) && trim($message) !== ''
        )));

        if ($messages === []) {
            return 'unknown schema validation error';
        }

        return implode('; ', array_slice($messages, 0, 3));
    }
}
