using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;

using AnthropicClient.Json;
using AnthropicClient.Models;

namespace AnthropicClient.Utils;

static class JsonSchemaGenerator
{
  private const string TypeKey = "type";
  private const string PropertiesKey = "properties";
  private const string RequiredPropertiesKey = "required";
  private const string DescriptionKey = "description";
  private const string DefinitionsKey = "definitions";
  private const string RefKey = "$ref";
  private const string ItemsKey = "items";
  private const string EnumKey = "enum";
  private const string FormatKey = "format";
  private const string ObjectType = "object";
  private const string StringType = "string";
  private const string ArrayType = "array";

  internal static JsonObject GenerateInputSchema(AnthropicFunction function)
  {
    var parameters = function.Method.GetParameters();
    var inputSchema = new JsonObject()
    {
      [TypeKey] = ObjectType
    };

    if (parameters.Length is 0)
    {
      return inputSchema;
    }

    var properties = new JsonObject();
    var requiredProperties = new JsonArray();

    foreach (var parameter in parameters)
    {
      if (parameter.ParameterType == typeof(CancellationToken))
      {
        continue;
      }

      var attribute = parameter.GetCustomAttribute<FunctionParameterAttribute>();
      var paramName = attribute is not null
        ? string.IsNullOrWhiteSpace(attribute.Name)
          ? parameter.Name
          : attribute.Name
        : parameter.Name;

      var paramDescription = attribute?.Description ?? string.Empty;
      var paramRequired = attribute?.Required ?? !parameter.HasDefaultValue;

      var paramSchema = GenerateParameterTypeSchema(parameter.ParameterType, inputSchema);
      paramSchema[DescriptionKey] = paramDescription;

      properties[paramName] = paramSchema;

      if (paramRequired)
      {
        requiredProperties.Add(paramName);
      }
    }

    inputSchema[PropertiesKey] = properties;
    inputSchema[RequiredPropertiesKey] = requiredProperties;
    return inputSchema;
  }

  private static JsonObject GenerateParameterTypeSchema(Type type, JsonObject inputSchema)
  {
    var definitions = inputSchema[DefinitionsKey] ?? new JsonObject();

    // Check if a definition for the type already exists
    // If it does, return a reference to the definition
    // no need to evaluate the type further
    if (definitions.AsObject().ContainsKey(type.FullName))
    {
      return new JsonObject()
      {
        [RefKey] = GetDefinitionPath(type)
      };
    }

    var paramSchema = type switch
    {
      var t when t == typeof(string) || t == typeof(char) => new JsonObject()
      {
        [TypeKey] = StringType
      },
      var t when
        t == typeof(int) ||
        t == typeof(uint) ||
        t == typeof(long) ||
        t == typeof(ulong) ||
        t == typeof(short) ||
        t == typeof(ushort) ||
        t == typeof(byte) ||
        t == typeof(sbyte) => new JsonObject()
        {
          [TypeKey] = "integer"
        },
      var t when t == typeof(bool) => new JsonObject()
      {
        [TypeKey] = "boolean"
      },
      var t when
        t == typeof(double) ||
        t == typeof(float) ||
        t == typeof(decimal) => new JsonObject()
        {
          [TypeKey] = "number"
        },
      var t when t == typeof(DateTime) || t == typeof(DateTimeOffset) => new JsonObject()
      {
        [TypeKey] = StringType,
        [FormatKey] = "date-time"
      },
      var t when t == typeof(Guid) => new JsonObject()
      {
        [TypeKey] = StringType,
        [FormatKey] = "uuid"
      },
      var t when t.IsEnum => new JsonObject()
      {
        [TypeKey] = StringType,
        [EnumKey] = Enum.GetNames(t).Aggregate(
          new JsonArray(), (acc, name) =>
          {
            acc.Add(name);
            return acc;
          }
        )
      },
      var t when t.IsArray => new JsonObject()
      {
        [TypeKey] = ArrayType,
        [ItemsKey] = GenerateParameterTypeSchema(t.GetElementType()!, inputSchema)
      },
      var t when t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>) => new JsonObject()
      {
        [TypeKey] = ArrayType,
        [ItemsKey] = GenerateParameterTypeSchema(t.GetGenericArguments()[0], inputSchema)
      },
      _ => GenerateTypeDefinitionSchema(type, inputSchema)
    };

    return paramSchema;
  }

  private static JsonObject GenerateTypeDefinitionSchema(Type type, JsonObject inputSchema)
  {
    var definitions = inputSchema[DefinitionsKey] ?? new JsonObject();
    inputSchema[DefinitionsKey] = definitions;

    var typeSchema = new JsonObject()
    {
      [TypeKey] = ObjectType
    };

    List<MemberInfo> members = [];
    var bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
    members.AddRange(type.GetProperties(bindingFlags));
    members.AddRange(type.GetFields(bindingFlags));

    var memberProperties = new JsonObject();
    var memberRequiredProperties = new JsonArray();

    foreach (var member in members)
    {
      JsonObject memberProperty;

      var memberType = member switch
      {
        PropertyInfo property => property.PropertyType,
        FieldInfo field => field.FieldType,
        _ => throw new InvalidOperationException("Member is not a property or field")
      };

      var attribute = member.GetCustomAttribute<FunctionPropertyAttribute>();

      var memberPropertyName = member.Name;
      var memberDescription = attribute?.Description ?? string.Empty;
      var memberRequired = attribute?.Required ?? Nullable.GetUnderlyingType(memberType) is null;

      memberProperty = definitions.AsObject().ContainsKey(memberType.FullName)
        ? new JsonObject()
        {
          [RefKey] = GetDefinitionPath(memberType)
        }
        : GenerateParameterTypeSchema(memberType, inputSchema);

      if (memberRequired)
      {
        memberRequiredProperties.Add(memberPropertyName);
      }

      JsonNode? defaultValue = null;

      if (attribute?.DefaultValue is not null)
      {
        defaultValue = JsonNode.Parse(JsonSerializer.Serialize(attribute.DefaultValue, JsonSerializationOptions.DefaultOptions));
        memberProperty["default"] = defaultValue;
      }

      if (attribute?.PossibleValues is { Length: > 0 })
      {
        var enumValues = new JsonArray();

        foreach (var value in attribute.PossibleValues)
        {
          var enumValue = JsonNode.Parse(JsonSerializer.Serialize(value, JsonSerializationOptions.DefaultOptions));

          if (defaultValue is null || JsonNode.DeepEquals(enumValue, defaultValue) is false)
          {
            enumValues.Add(enumValue);
          }
        }

        var containsDefaultValue = enumValues.Where(value => JsonNode.DeepEquals(value, defaultValue)).Any();

        if (defaultValue is not null && containsDefaultValue is false)
        {
          enumValues.Add(JsonNode.Parse(defaultValue.ToJsonString(JsonSerializationOptions.DefaultOptions)));
        }

        memberProperty[EnumKey] = enumValues;
      }

      memberProperty[DescriptionKey] = memberDescription;
      memberProperties[memberPropertyName] = memberProperty;
    }

    typeSchema[PropertiesKey] = memberProperties;
    typeSchema[RequiredPropertiesKey] = memberRequiredProperties;
    definitions[type.FullName] = typeSchema;
    inputSchema[DefinitionsKey] = definitions;
    return new JsonObject()
    {
      [RefKey] = GetDefinitionPath(type)
    };
  }

  private static string GetDefinitionPath(Type type) => $"#/definitions/{type.FullName}";
}