using System.Text.Json;

namespace ScadaLink.InboundAPI;

/// <summary>
/// InboundAPI-014: 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 a JSON array of <see cref="ReturnFieldDefinition"/>
/// (the same <c>{name,type}</c> shape as a parameter definition). A method whose
/// <c>ReturnDefinition</c> is null/empty is unconstrained — its return value is
/// serialized as-is (backward compatible). Primitive fields (Boolean / Integer /
/// Float / String) are type-checked; the extended <c>Object</c>/<c>List</c> types
/// are shape-checked only (object vs. array), consistent with how
/// <see cref="ParameterValidator"/> treats inbound extended types.
/// </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>
    public static ReturnValidationResult Validate(string? resultJson, string? returnDefinition)
    {
        if (string.IsNullOrWhiteSpace(returnDefinition))
        {
            // No declared return shape — the script's return value is unconstrained.
            return ReturnValidationResult.Valid();
        }

        List<ReturnFieldDefinition> fields;
        try
        {
            fields = JsonSerializer.Deserialize<List<ReturnFieldDefinition>>(
                returnDefinition,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
                ?? [];
        }
        catch (JsonException)
        {
            return ReturnValidationResult.Invalid(
                "Invalid return definition in method configuration");
        }

        if (fields.Count == 0)
        {
            return ReturnValidationResult.Valid();
        }

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

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

        if (root.ValueKind != JsonValueKind.Object)
        {
            return ReturnValidationResult.Invalid(
                "Method declares a return structure but the script did not return an object");
        }

        var errors = new List<string>();
        foreach (var field in fields)
        {
            if (!root.TryGetProperty(field.Name, out var value))
            {
                errors.Add($"missing return field '{field.Name}'");
                continue;
            }

            var typeError = CheckFieldType(value, field.Type, field.Name);
            if (typeError != null)
                errors.Add(typeError);
        }

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

    private static string? CheckFieldType(JsonElement value, string declaredType, string fieldName)
    {
        // A null value satisfies any field type — the script may legitimately omit
        // optional data; only a missing field (handled by the caller) is an error.
        if (value.ValueKind == JsonValueKind.Null)
            return null;

        var ok = declaredType.ToLowerInvariant() switch
        {
            "boolean" => value.ValueKind is JsonValueKind.True or JsonValueKind.False,
            "integer" => value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out _),
            "float" => value.ValueKind == JsonValueKind.Number,
            "string" => value.ValueKind == JsonValueKind.String,
            "object" => value.ValueKind == JsonValueKind.Object,
            "list" => value.ValueKind == JsonValueKind.Array,
            _ => true, // unknown declared type — do not block the response
        };

        return ok ? null : $"return field '{fieldName}' must be {declaredType}";
    }
}

/// <summary>
/// InboundAPI-014: one field of a method's declared return structure — the
/// deserialized form of an entry in <c>ApiMethod.ReturnDefinition</c>. Defined in
/// this module (not Commons) because the inbound API is currently its only consumer.
/// </summary>
public sealed class ReturnFieldDefinition
{
    public string Name { get; set; } = string.Empty;
    public string Type { get; set; } = "String";
}

/// <summary>
/// Result of validating a script return value against a method's return definition.
/// </summary>
public sealed class ReturnValidationResult
{
    public bool IsValid { get; private init; }
    public string ErrorMessage { get; private init; } = string.Empty;

    public static ReturnValidationResult Valid() => new() { IsValid = true };

    public static ReturnValidationResult Invalid(string message) =>
        new() { IsValid = false, ErrorMessage = message };
}
