using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using Json.Schema;
using ZstdSharp;
using {{ namespace }}.Core;

namespace {{ namespace }}.Validation;

/// <summary>
/// `JsonSchema.Net`-based input/output validation. Schemas themselves are
/// NOT embedded in this file — they're loaded from the co-located
/// `GeneratedSchemas.json.zst` embedded resource (written directly by the
/// Rust generator, not templated, keeping this file's size independent
/// of how many operations the source spec declares) — mirrors
/// `targets::rust::templates::validation::validator.rs`'s `include_bytes!`
/// approach: baked into the compiled assembly rather than read from disk
/// at runtime.
///
/// The asset is zstd-compressed: every operation's schema embeds a full
/// copy of the spec's `$defs` library (a deliberate generator tradeoff —
/// see `mcpify`'s `openapi::schema_resolve` — simpler than cross-schema
/// `$ref` resolution, at the cost of file size), and a long-distance-
/// matching compressor collapses that duplication (measured ~190 MB down
/// to tens of KB on a real spec) without requiring any change to the JSON
/// Schema shape itself. Decompressed once per version, lazily, in
/// <see cref="LoadSchemas"/>.
/// </summary>
public static class Validator
{
    // mcpify:versions:begin
    private static readonly Dictionary<string, string> VersionSchemasFiles = new()
    {
{%- for v in version_entries %}
        ["{{ v.label }}"] = "{{ v.schemas_file }}",
{%- endfor %}
    };
    // mcpify:versions:end

    private static readonly Dictionary<string, Dictionary<string, (JsonSchema Input, JsonSchema Output)>> SchemasByVersion = [];
    private static readonly Lock SchemasByVersionLock = new();

    /// <summary>Throws <see cref="ValidationException"/> if <paramref name="data"/> doesn't satisfy <paramref name="operationId"/>'s input schema, for the given <paramref name="apiVersion"/>.</summary>
    public static void ValidateInput(string apiVersion, string operationId, JsonNode? data)
    {
        var errors = ValidateAgainst(apiVersion, operationId, data, useInputSchema: true);
        if (errors.Count > 0)
        {
            throw new ValidationException($"invalid input for '{operationId}'", errors);
        }
    }

    /// <summary>
    /// Throws <see cref="ValidationException"/> if <paramref name="data"/>
    /// doesn't satisfy <paramref name="operationId"/>'s output schema —
    /// surfaces upstream API drift as a structured error rather than
    /// silently returning a mismatched response (architecture.md's `call`
    /// pipeline). Validates against the given <paramref name="apiVersion"/>'s schema.
    /// </summary>
    public static void ValidateOutput(string apiVersion, string operationId, JsonNode? data)
    {
        var errors = ValidateAgainst(apiVersion, operationId, data, useInputSchema: false);
        if (errors.Count > 0)
        {
            throw new ValidationException($"unexpected response shape for '{operationId}'", errors);
        }
    }

    private static List<string> ValidateAgainst(string apiVersion, string operationId, JsonNode? data, bool useInputSchema)
    {
        if (!SchemasFor(apiVersion).TryGetValue(operationId, out var pair))
        {
            return [];
        }

        var schema = useInputSchema ? pair.Input : pair.Output;
        var instance = (data?.Deserialize<JsonElement>()) ?? default;
        var results = schema.Evaluate(instance, new EvaluationOptions { OutputFormat = OutputFormat.List });
        if (results.IsValid)
        {
            return [];
        }

        return (results.Details ?? [])
            .Where(d => d.Errors is not null)
            .SelectMany(d => d.Errors!.Values)
            .ToList();
    }

    private static Dictionary<string, (JsonSchema Input, JsonSchema Output)> SchemasFor(string apiVersion)
    {
        lock (SchemasByVersionLock)
        {
            if (SchemasByVersion.TryGetValue(apiVersion, out var cached))
            {
                return cached;
            }

            var loaded = VersionSchemasFiles.TryGetValue(apiVersion, out var resourceSuffix)
                ? LoadSchemas(resourceSuffix)
                : [];
            SchemasByVersion[apiVersion] = loaded;
            return loaded;
        }
    }

    private static Dictionary<string, (JsonSchema Input, JsonSchema Output)> LoadSchemas(string resourceSuffix)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = assembly.GetManifestResourceNames()
            .FirstOrDefault(name => name.EndsWith(resourceSuffix, StringComparison.Ordinal));
        if (resourceName is null)
        {
            return [];
        }

        using var stream = assembly.GetManifestResourceStream(resourceName)!;
        using var decompressionStream = new DecompressionStream(stream);
        // Deliberately not disposed: `JsonSchema.Build` keeps a live
        // reference into this document's buffer rather than copying it,
        // so disposing here (even via `using`) throws
        // `ObjectDisposedException` the first time `Evaluate` runs after
        // `LoadSchemas` returns — the built schemas need this document to
        // stay alive for as long as `SchemasByVersion`'s cached entry does
        // (the process lifetime). `JsonDocument.Parse` fully buffers the
        // decompressed content before returning, so it's safe for
        // `decompressionStream`/`stream` themselves to be disposed once
        // this method returns.
        var document = JsonDocument.Parse(decompressionStream);

        var result = new Dictionary<string, (JsonSchema, JsonSchema)>();
        foreach (var property in document.RootElement.EnumerateObject())
        {
            var inputSchema = JsonSchema.Build(property.Value.GetProperty("inputSchema"));
            var outputSchema = JsonSchema.Build(property.Value.GetProperty("outputSchema"));
            result[property.Name] = (inputSchema, outputSchema);
        }
        return result;
    }
}
