using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;

namespace ZB.MOM.WW.ScadaBridge.InboundAPI;

/// <summary>
/// Validates a method script's return value against the method's
/// declared <c>ReturnDefinition</c>. <c>Component-InboundAPI.md</c> ("Return Value
/// Definition" / "Response Format") states the success body has "fields matching
/// the return value definition"; this is the response-side mirror of
/// <see cref="ParameterValidator"/>.
///
/// <para>
/// The return definition is JSON Schema (the canonical persisted format; the
/// legacy flat <c>[{name,type}]</c> array is still accepted for transition
/// safety). A method whose <c>ReturnDefinition</c> is null/empty is
/// unconstrained — its return value is serialized as-is (backward compatible).
/// </para>
///
/// <para>
/// Validation is RECURSIVE and type-aware — declared object
/// fields are validated against their declared (nested) types, list elements
/// against the declared element type, and scalars at any depth — with
/// path-qualified errors. The recursion is shared with
/// <see cref="ParameterValidator"/> via
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi.InboundApiSchema"/>,
/// so the inbound and outbound type checks cannot drift apart.
/// </para>
/// </summary>
public static class ReturnValueValidator
{
    /// <summary>
    /// Validates the serialized script result JSON against the method's return
    /// definition. Returns <see cref="ReturnValidationResult.Valid"/> when no
    /// definition is configured or the result conforms to it.
    /// </summary>
    /// <param name="resultJson">The JSON-serialized script return value to validate.</param>
    /// <param name="returnDefinition">JSON Schema describing the method's return value, or null/empty to skip validation. The legacy flat-array form is also accepted.</param>
    /// <param name="resolveRef">
    /// Optional JSON-Schema <c>$ref</c> resolution seam mapping a
    /// <c>{"$ref":"lib:Name"}</c> reference target's name to the referenced schema JSON
    /// (or <c>null</c> when the library entry does not exist). The executor pre-loads the
    /// shared-schema library (backed by <c>ISharedSchemaRepository</c>) and supplies it
    /// ONLY when the definition actually uses a <c>$ref</c>. <c>null</c> means no resolver:
    /// schemas with no <c>$ref</c> behave exactly as before; a <c>$ref</c> with no resolver
    /// (or a dangling one) is surfaced as a CLEAR invalid result naming the missing
    /// reference — NOT an opaque parse-error from a thrown <see cref="JsonException"/>.
    /// </param>
    /// <returns>A <see cref="ReturnValidationResult"/> indicating success or describing the validation failures.</returns>
    public static ReturnValidationResult Validate(
        string? resultJson,
        string? returnDefinition,
        Func<string, string?>? resolveRef = null)
    {
        if (string.IsNullOrWhiteSpace(returnDefinition))
        {
            // No declared return shape — the script's return value is unconstrained.
            return ReturnValidationResult.Valid();
        }

        // Parse through the ref-COLLECTING path so a {"$ref":"lib:Name"} the
        // resolver can satisfy is resolved inline, and a dangling/cyclic/over-depth ref is
        // surfaced as a descriptive "could not be resolved" message rather than an opaque
        // "Invalid return definition" from a swallowed JsonException.
        SchemaParseResult parsed;
        try
        {
            parsed = InboundApiSchema.ParseWithRefs(returnDefinition, resolveRef);
        }
        catch (JsonException)
        {
            return ReturnValidationResult.Invalid(
                "Invalid return definition in method configuration");
        }

        if (parsed.UnresolvedReferences.Count > 0)
        {
            return ReturnValidationResult.Invalid(
                $"Return definition references {ParameterValidator.DescribeUnresolved(parsed.UnresolvedReferences)} which could not be resolved");
        }

        InboundApiSchema? schema = parsed.Schema;

        // A schema that declares no constraints (e.g. an object schema with no
        // fields) leaves the return value unconstrained.
        if (schema is null || (schema.Type == "object" && schema.Fields.Count == 0))
        {
            return ReturnValidationResult.Valid();
        }

        // INTENTIONAL asymmetry with ParameterValidator:
        //
        // ParameterValidator has an early-return guard for "schema.Type != object"
        // because method parameters are ALWAYS a top-level JSON object (flat map of
        // name→value); a non-object parameter schema is treated as unconstrained.
        //
        // ReturnValueValidator does NOT guard on schema.Type here. A method may
        // declare a scalar return type (e.g. {"type":"string"} or {"type":"integer"})
        // and the script is expected to return exactly that scalar JSON value.
        // Guarding on type == "object" would silently bypass validation for scalar
        // and array return schemas — do NOT add that guard here.

        if (string.IsNullOrWhiteSpace(resultJson))
        {
            return ReturnValidationResult.Invalid(
                "Method declares a return structure but the script returned no value");
        }

        JsonElement root;
        JsonDocument doc;
        try
        {
            doc = JsonDocument.Parse(resultJson);
        }
        catch (JsonException)
        {
            return ReturnValidationResult.Invalid("Script return value is not valid JSON");
        }

        using (doc)
        {
            root = doc.RootElement;

            // A JSON null result against a declared structure is treated as
            // "no value returned" (preserves the prior contract).
            if (root.ValueKind == JsonValueKind.Null)
            {
                return ReturnValidationResult.Invalid(
                    "Method declares a return structure but the script returned no value");
            }

            var errors = new List<string>();
            schema.Validate(root, string.Empty, errors);

            return errors.Count > 0
                ? ReturnValidationResult.Invalid(
                    $"Return value does not match the declared return definition: {string.Join("; ", errors)}")
                : ReturnValidationResult.Valid();
        }
    }
}

/// <summary>
/// Result of validating a script return value against a method's return definition.
/// </summary>
public sealed class ReturnValidationResult
{
    /// <summary>True when the return value conforms to the declared return definition.</summary>
    public bool IsValid { get; private init; }
    /// <summary>Human-readable error message when <see cref="IsValid"/> is false; empty otherwise.</summary>
    public string ErrorMessage { get; private init; } = string.Empty;

    /// <summary>Returns a successful validation result.</summary>
    /// <returns>A <see cref="ReturnValidationResult"/> with <see cref="IsValid"/> set to <c>true</c>.</returns>
    public static ReturnValidationResult Valid() => new() { IsValid = true };

    /// <summary>Returns a failed validation result with the specified error message.</summary>
    /// <param name="message">Human-readable description of the validation failure.</param>
    /// <returns>A <see cref="ReturnValidationResult"/> with <see cref="IsValid"/> set to <c>false</c> and the given error message.</returns>
    public static ReturnValidationResult Invalid(string message) =>
        new() { IsValid = false, ErrorMessage = message };
}
