/*
 * Copyright 2025 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace Firebase.AI {

/// <summary>
/// A `Schema` object allows the definition of input and output data types.
///
/// These types can be objects, but also primitives and arrays. Represents a select subset of an
/// [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema).
/// </summary>
public class Schema {

  /// <summary>
  /// The value type of a `Schema`.
  /// </summary>
  public enum SchemaType {
    String,
    Number,
    Integer,
    Boolean,
    Array,
    Object,
  }

  private string TypeAsString =>
    Type switch {
      SchemaType.String => "STRING",
      SchemaType.Number => "NUMBER",
      SchemaType.Integer => "INTEGER",
      SchemaType.Boolean => "BOOLEAN",
      SchemaType.Array => "ARRAY",
      SchemaType.Object => "OBJECT",
      null => null,
      _ => throw new ArgumentOutOfRangeException(nameof(Type), Type, "Invalid SchemaType value")
    };

  /// <summary>
  /// Modifiers describing the expected format of a string `Schema`.
  /// </summary>
  public readonly struct StringFormat {
    internal string Format { get; }

    private StringFormat(string format) {
      Format = format;
    }

    /// <summary>
    /// A custom string format.
    /// </summary>
    public static StringFormat Custom(string format) {
      return new StringFormat(format);
    }
  }

  /// <summary>
  /// The data type.
  /// </summary>
  public SchemaType? Type { get; }
  /// <summary>
  /// A human-readable explanation of the purpose of the schema or property. While not strictly
  /// enforced on the value itself, good descriptions significantly help the model understand the
  /// context and generate more relevant and accurate output.
  /// </summary>
  public string Description { get; }
  /// <summary>
  /// A human-readable name/summary for the schema or a specific property. This helps document the
  /// schema's purpose but doesn't typically constrain the generated value. It can subtly guide the
  /// model by clarifying the intent of a field.
  /// </summary>
  public string Title { get; }
  /// <summary>
  /// Indicates if the value may be null.
  /// </summary>
  public bool? Nullable { get; }
  /// <summary>
  /// The format of the data.
  /// </summary>
  public string Format { get; }

  private readonly ReadOnlyCollection<string> _enumValues;
  /// <summary>
  /// Possible values of the element of type "String" with "enum" format.
  /// </summary>
  public IEnumerable<string> EnumValues { get { return _enumValues; } }
  /// <summary>
  /// Schema of the elements of type "Array".
  /// </summary>
  public Schema Items { get; }
  /// <summary>
  /// An integer specifying the minimum number of items the generated "Array" must contain.
  /// </summary>
  public int? MinItems { get; }
  /// <summary>
  /// An integer specifying the maximum number of items the generated "Array" must contain.
  /// </summary>
  public int? MaxItems { get; }

  /// <summary>
  /// The minimum value of a numeric type.
  /// </summary>
  public double? Minimum { get; }
  /// <summary>
  /// The maximum value of a numeric type.
  /// </summary>
  public double? Maximum { get; }

  /// <summary>
  /// Properties of type "Object".
  /// </summary>
  public IReadOnlyDictionary<string, Schema> Properties { get; }

  private readonly ReadOnlyCollection<string> _requiredProperties;
  /// <summary>
  /// Required properties of type "Object".
  /// </summary>
  public IEnumerable<string> RequiredProperties { get { return _requiredProperties; } }

  private readonly ReadOnlyCollection<string> _propertyOrdering;
  /// <summary>
  /// A specific hint provided to the Gemini model, suggesting the order in which the keys should
  /// appear in the generated JSON string. Important: Standard JSON objects are inherently unordered
  /// collections of key-value pairs. While the model will try to respect PropertyOrdering in its
  /// textual JSON output, subsequent parsing into native C# objects (like Dictionaries) might
  /// not preserve this order. This parameter primarily affects the raw JSON string
  /// serialization.
  /// </summary>
  public IEnumerable<string> PropertyOrdering { get { return _propertyOrdering; } }

  private readonly ReadOnlyCollection<Schema> _anyOf;
  /// <summary>
  /// An array of `Schema` objects. The generated data must be valid against *any* (one or more)
  /// of the schemas listed in this array. This allows specifying multiple possible structures or
  /// types for a single field.
  ///
  /// For example, a value could be either a `String` or an `Int`:
  /// ```
  /// Schema.AnyOf(new [] { Schema.String(), Schema.Int() })
  /// ```
  /// </summary>
  public IEnumerable<Schema> AnyOfSchemas { get { return _anyOf; } }

  private Schema(
      SchemaType? type,
      string description = null,
      string title = null,
      bool? nullable = null,
      string format = null,
      IEnumerable<string> enumValues = null,
      Schema items = null,
      int? minItems = null,
      int? maxItems = null,
      double? minimum = null,
      double? maximum = null,
      IDictionary<string, Schema> properties = null,
      IEnumerable<string> requiredProperties = null,
      IEnumerable<string> propertyOrdering = null,
      IEnumerable<Schema> anyOf = null) {
    Type = type;
    Description = description;
    Title = title;
    Nullable = nullable;
    Format = format;
    _enumValues = enumValues?.ToList().AsReadOnly();
    Items = items;
    MinItems = minItems;
    MaxItems = maxItems;
    Minimum = minimum;
    Maximum = maximum;
    Properties = (properties == null) ? null : new Dictionary<string, Schema>(properties);
    _requiredProperties = requiredProperties?.ToList().AsReadOnly();
    _propertyOrdering = propertyOrdering?.ToList().AsReadOnly();
    _anyOf = anyOf?.ToList().AsReadOnly();
  }

  /// <summary>
  /// Returns a `Schema` representing a boolean value.
  /// </summary>
  /// <param name="description">An optional description of what the boolean should contain or represent.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  public static Schema Boolean(
      string description = null,
      bool nullable = false) {
    return new Schema(SchemaType.Boolean,
        description: description,
        nullable: nullable
      );
  }

  /// <summary>
  /// Returns a `Schema` for a 32-bit signed integer number.
  /// 
  /// **Important:** This `Schema` provides a hint to the model that it should generate a 32-bit
  ///   integer, but only guarantees that the value will be an integer. Therefore it's *possible*
  ///   that decoding it as an `int` could overflow.
  /// </summary>
  /// <param name="description">An optional description of what the integer should contain or represent.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  /// <param name="minimum">If specified, instructs the model that the value should be greater than or
  ///   equal to the specified minimum.</param>
  /// <param name="maximum">If specified, instructs the model that the value should be less than or
  ///   equal to the specified maximum.</param>
  public static Schema Int(
      string description = null,
      bool nullable = false,
      int? minimum = null,
      int? maximum = null) {
    return new Schema(SchemaType.Integer,
        description: description,
        nullable: nullable,
        format: "int32",
        minimum: minimum,
        maximum: maximum
      );
  }

  /// <summary>
  /// Returns a `Schema` for a 64-bit signed integer number.
  /// </summary>
  /// <param name="description">An optional description of what the number should contain or represent.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  /// <param name="minimum">If specified, instructs the model that the value should be greater than or
  ///   equal to the specified minimum.</param>
  /// <param name="maximum">If specified, instructs the model that the value should be less than or
  ///   equal to the specified maximum.</param>
  public static Schema Long(
      string description = null,
      bool nullable = false,
      long? minimum = null,
      long? maximum = null) {
    return new Schema(SchemaType.Integer,
        description: description,
        nullable: nullable,
        minimum: minimum,
        maximum: maximum
      );
  }

  /// <summary>
  /// Returns a `Schema` for a double-precision floating-point number.
  /// </summary>
  /// <param name="description">An optional description of what the number should contain or represent.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  /// <param name="minimum">If specified, instructs the model that the value should be greater than or
  ///   equal to the specified minimum.</param>
  /// <param name="maximum">If specified, instructs the model that the value should be less than or
  ///   equal to the specified maximum.</param>
  public static Schema Double(
      string description = null,
      bool nullable = false,
      double? minimum = null,
      double? maximum = null) {
    return new Schema(SchemaType.Number,
        description: description,
        nullable: nullable,
        minimum: minimum,
        maximum: maximum
      );
  }

  /// <summary>
  /// Returns a `Schema` for a single-precision floating-point number.
  ///
  /// **Important:** This `Schema` provides a hint to the model that it should generate a
  ///   single-precision floating-point number, but only guarantees that the value will be a number.
  ///   Therefore it's *possible* that decoding it as a `float` could overflow.
  /// </summary>
  /// <param name="description">An optional description of what the number should contain or represent.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  /// <param name="minimum">If specified, instructs the model that the value should be greater than or
  ///   equal to the specified minimum.</param>
  /// <param name="maximum">If specified, instructs the model that the value should be less than or
  ///   equal to the specified maximum.</param>
  public static Schema Float(
      string description = null,
      bool nullable = false,
      float? minimum = null,
      float? maximum = null) {
    return new Schema(SchemaType.Number,
        description: description,
        nullable: nullable,
        format: "float",
        minimum: minimum,
        maximum: maximum
      );
  }

  /// <summary>
  /// Returns a `Schema` for a string.
  /// </summary>
  /// <param name="description">An optional description of what the string should contain or represent.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  /// <param name="format">An optional pattern that values need to adhere to.</param>
  public static Schema String(
      string description = null,
      bool nullable = false,
      StringFormat? format = null) {
    return new Schema(SchemaType.String,
        description: description,
        nullable: nullable,
        format: format?.Format
      );
  }

  /// <summary>
  /// Returns a `Schema` representing an object.
  ///
  /// This schema instructs the model to produce data of type "Object", which has keys of type
  /// "String" and values of any other data type (including nested "Objects"s).
  ///
  /// **Example:** A `City` could be represented with the following object `Schema`.
  /// ```
  /// Schema.Object(properties: new Dictionary() {
  ///   { "name", Schema.String() },
  ///   { "population", Schema.Integer() }
  /// })
  /// ```
  /// </summary>
  /// <param name="properties">The map of the object's property names to their `Schema`s.</param>
  /// <param name="optionalProperties">The list of optional properties. They must correspond to the keys
  ///   provided in the `properties` map. By default it's empty, signaling the model that all
  ///   properties are to be included.</param>
  /// <param name="propertyOrdering">An optional hint to the model suggesting the order for keys in the
  ///   generated JSON string.</param>
  /// <param name="description">An optional description of what the object represents.</param>
  /// <param name="title">An optional human-readable name/summary for the object schema.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  public static Schema Object(
      IDictionary<string, Schema> properties,
      IEnumerable<string> optionalProperties = null,
      IEnumerable<string> propertyOrdering = null,
      string description = null,
      string title = null,
      bool nullable = false) {
    if (properties == null) {
      throw new ArgumentNullException(nameof(properties));
    }

    if (optionalProperties != null) {
      // Check for invalid optional properties
      var invalidOptionalProperties = optionalProperties.Except(properties.Keys).ToList();
      if (invalidOptionalProperties.Any()) {
        throw new ArgumentException(
            "The following optional properties are not present in the properties dictionary: " +
              $"{string.Join(", ", invalidOptionalProperties)}",
            nameof(optionalProperties));
      }
    }

    // Determine required properties
    var required =
        properties.Keys.Except(optionalProperties ?? Enumerable.Empty<string>()).ToList();

    foreach (string key in properties.Keys) {
      if (optionalProperties == null || !optionalProperties.Contains(key)) {
        required.Add(key);
      }
    }

    return new Schema(SchemaType.Object,
        description: description,
        title: title,
        nullable: nullable,
        properties: properties,
        requiredProperties: required,
        propertyOrdering: propertyOrdering
      );
  }

  /// <summary>
  /// Returns a `Schema` for an array.
  /// </summary>
  /// <param name="items">The `Schema` of the elements stored in the array.</param>
  /// <param name="description">An optional description of what the array represents.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  /// <param name="minItems">Instructs the model to produce at least the specified minimum number of elements
  ///   in the array.</param>
  /// <param name="maxItems">Instructs the model to produce at most the specified minimum number of elements
  ///   in the array.</param>
  public static Schema Array(
      Schema items,
      string description = null,
      bool nullable = false,
      int? minItems = null,
      int? maxItems = null) {
    return new Schema(SchemaType.Array,
        description: description,
        nullable: nullable,
        items: items,
        minItems: minItems,
        maxItems: maxItems
      );
  }

  /// <summary>
  /// Returns a `Schema` for an enumeration.
  ///
  /// For example, the cardinal directions can be represented as:
  /// ```
  /// Schema.Enum(new string[]{ "North", "East", "South", "West" }, "Cardinal directions")
  /// ```
  /// </summary>
  /// <param name="values">The list of valid values for this enumeration.</param>
  /// <param name="description">An optional description of what the enum represents.</param>
  /// <param name="nullable">Indicates whether the value can be `null`. Defaults to `false`.</param>
  public static Schema Enum(
      IEnumerable<string> values,
      string description = null,
      bool nullable = false) {
    return new Schema(SchemaType.String,
        description: description,
        nullable: nullable,
        enumValues: values,
        format: "enum"
      );
  }

  /// <summary>
  /// Returns a `Schema` representing a value that must conform to *any* (one or more) of the
  /// provided sub-schemas.
  ///
  /// This schema instructs the model to produce data that is valid against at least one of the
  /// schemas listed in the `schemas` array. This is useful when a field can accept multiple
  /// distinct types or structures.
  /// </summary>
  /// <param name="schemas">An array of `Schema` objects. The generated data must be valid against at least
  ///   one of these schemas. The array must not be empty.</param>
  public static Schema AnyOf(
      IEnumerable<Schema> schemas) {
    if (schemas == null || !schemas.Any()) {
      throw new ArgumentException("The `AnyOf` schemas array cannot be empty.");
    }

    // The backend doesn't define a SchemaType for AnyOf, instead using the existence of 'anyOf'.
    return new Schema(null,
        anyOf: schemas
      );
  }

  /// <summary>
  /// Intended for internal use only.
  /// This method is used for serializing the object to JSON for the API request.
  /// </summary>
  internal Dictionary<string, object> ToJson() {
    Dictionary<string, object> json = new() {
      { "type", TypeAsString }
    };
    if (!string.IsNullOrWhiteSpace(Description)) {
      json["description"] = Description;
    }
    if (!string.IsNullOrWhiteSpace(Title)) {
      json["title"] = Title;
    }
    if (Nullable.HasValue) {
      json["nullable"] = Nullable.Value;
    }
    if (EnumValues != null && EnumValues.Any()) {
      json["enum"] = EnumValues.ToList();
    }
    if (Items != null) {
      json["items"] = Items.ToJson();
    }
    if (MinItems != null) {
      json["minItems"] = MinItems.Value;
    }
    if (MaxItems != null) {
      json["maxItems"] = MaxItems.Value;
    }
    if (Minimum != null) {
      json["minimum"] = Minimum.Value;
    }
    if (Maximum != null) {
      json["maximum"] = Maximum.Value;
    }
    if (Properties != null && Properties.Any()) {
      Dictionary<string, object> propertiesJson = new();
      foreach (var kvp in Properties) {
        propertiesJson[kvp.Key] = kvp.Value.ToJson();
      }
      json["properties"] = propertiesJson;
      
    }
    if (RequiredProperties != null && RequiredProperties.Any()) {
      json["required"] = RequiredProperties.ToList();
    }
    if (PropertyOrdering != null && PropertyOrdering.Any()) {
      json["propertyOrdering"] = PropertyOrdering.ToList();
    }
    if (AnyOfSchemas != null && AnyOfSchemas.Any()) {
      json["anyOf"] = AnyOfSchemas.Select(s => s.ToJson()).ToList();
    }

    return json;
  }
}

}
