/*
 * 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.Linq;
using System.Reflection;
using Firebase.AI.Internal;

namespace Firebase.AI
{
  /// <summary>
  /// Structured representation of a function declaration.
  ///
  /// This `FunctionDeclaration` is a representation of a block of code that can be used
  /// as a `Tool` by the model and executed by the client.
  /// 
  /// Function calling can be used to provide data to the model that was not known at the time it
  /// was trained (for example, the current date or weather conditions) or to allow it to interact
  /// with external systems (for example, making an API request or querying/updating a database).
  /// For more details and use cases, see [Introduction to function
  /// calling](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling).
  /// </summary>
  public readonly struct FunctionDeclaration
  {
    // No public properties, on purpose since it is meant for user input only

    private string Name { get; }
    private string Description { get; }
    private Schema Parameters { get; }
    private JsonSchema JsonParameters { get; }

    /// <summary>
    /// Constructs a new `FunctionDeclaration`.
    /// </summary>
    /// <param name="name">The name of the function; must be a-z, A-Z, 0-9, or contain
    ///   underscores and dashes, with a maximum length of 63.</param>
    /// <param name="description">A brief description of the function.</param>
    /// <param name="parameters">Describes the parameters to this function.</param>
    /// <param name="optionalParameters">The names of parameters that may be omitted by the model
    ///   in function calls; by default, all parameters are considered required.</param>
    public FunctionDeclaration(string name, string description,
        IDictionary<string, Schema> parameters,
        IEnumerable<string> optionalParameters = null)
    {
      Name = name;
      Description = description;
      Parameters = Schema.Object(parameters, optionalParameters);
      JsonParameters = null;
    }

    /// <summary>
    /// Constructs a new `FunctionDeclaration`.
    /// </summary>
    /// <param name="name">The name of the function; must be a-z, A-Z, 0-9, or contain
    ///   underscores and dashes, with a maximum length of 63.</param>
    /// <param name="description">A brief description of the function.</param>
    /// <param name="parameters">Describes the parameters to this function.</param>
    /// <param name="optionalParameters">The names of parameters that may be omitted by the model
    ///   in function calls; by default, all parameters are considered required.</param>
    public FunctionDeclaration(string name, string description,
        IDictionary<string, JsonSchema> parameters,
        IEnumerable<string> optionalParameters = null)
    {
      Name = name;
      Description = description;
      Parameters = null;
      JsonParameters = JsonSchema.Object(parameters, optionalParameters);
    }

    /// <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()
    {
      var json = new Dictionary<string, object>() {
        { "name", Name },
        { "description", Description },
      };
      // Only one of these will likely be set, but just check
      if (JsonParameters != null)
      {
        json["parametersJsonSchema"] = JsonParameters.ToJson();
      }
      if (Parameters != null)
      {
        json["parameters"] = Parameters.ToJson();
      }

      return json;
    }
  }

  /// <summary>
  /// Structured representation of a function declaration, designed
  /// to be automatically handled when using Chat, instead of requiring
  /// manual handling.
  /// 
  /// See `FunctionDeclaration` for more information.
  /// </summary>
  public class AutoFunctionDeclaration : BaseAutoFunctionDeclaration
  {
    /// <summary>
    /// Constructs a new `AutoFunctionDeclaration`
    /// </summary>
    /// <param name="callable">The delegate that will be called automatically when requested by the model.</param>
    /// <param name="description">A brief description of the function.</param>
    /// <param name="name">Optional name to use for the function, used to overwrite the delegate name.</param>
    public AutoFunctionDeclaration(Delegate callable, string description,
        string name = null)
        : base(callable, description, name)
    { }
  }

  /// <summary>
  /// A tool that allows the generative model to connect to Google Search to access and incorporate
  /// up-to-date information from the web into its responses.
  ///
  /// > Important: When using this feature, you are required to comply with the
  /// "Grounding with Google Search" usage requirements for your chosen API provider:
  /// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
  /// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
  /// section within the Service Specific Terms).
  /// </summary>
  public readonly struct GoogleSearch { }

  /// <summary>
  /// A tool that allows the model to execute code.
  ///
  /// This tool can be used to solve complex problems, for example, by generating and executing Python
  /// code to solve a math problem.
  /// </summary>
  public readonly struct CodeExecution { }

  /// <summary>
  /// A tool that allows the model to use Grounding with Google Maps.
  ///
  /// Grounding with Google Maps can be used to allow the model to connect to Google
  /// Maps to incorporate location-based information into its responses.
  ///
  /// When using this feature, you are required to comply with the "Grounding with
  /// Google Maps" usage requirements for your chosen API provider:
  /// {@link https://ai.google.dev/gemini-api/terms#grounding-with-google-maps |
  /// Gemini Developer API} or Vertex AI Gemini API (see 
  /// {@link https://cloud.google.com/terms/service-terms | Service Terms} section 
  /// within the Service Specific Terms).
  /// </summary>
  public readonly struct GoogleMaps { }

  /// <summary>
  /// A helper tool that the model may use when generating responses.
  ///
  /// A `Tool` is a piece of code that enables the system to interact with external systems to
  /// perform an action, or set of actions, outside of knowledge and scope of the model.
  /// </summary>
  public readonly struct Tool
  {
    // No public properties, on purpose since it is meant for user input only

    private List<FunctionDeclaration> FunctionDeclarations { get; }
    internal List<AutoFunctionDeclaration> AutoFunctionDeclarations { get; }
    private GoogleSearch? GoogleSearch { get; }
    private CodeExecution? CodeExecution { get; }
    private UrlContext? UrlContext { get; }
    private GoogleMaps? GoogleMaps { get; }

    /// <summary>
    /// Creates a tool that allows the model to perform function calling.
    /// </summary>
    /// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
    ///   that can be used for function calling.</param>
    public Tool(params FunctionDeclaration[] functionDeclarations)
    {
      FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
      AutoFunctionDeclarations = null;
      GoogleSearch = null;
      CodeExecution = null;
      UrlContext = null;
      GoogleMaps = null;
    }
    /// <summary>
    /// Creates a tool that allows the model to perform function calling.
    /// </summary>
    /// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
    ///   that can be used for function calling.</param>
    public Tool(IEnumerable<FunctionDeclaration> functionDeclarations)
    {
      FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
      AutoFunctionDeclarations = null;
      GoogleSearch = null;
      CodeExecution = null;
      UrlContext = null;
      GoogleMaps = null;
    }

    /// <summary>
    /// Creates a tool that allows the model to perform function calling.
    /// </summary>
    /// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
    ///   that can be used for function calling.</param>
    public Tool(params AutoFunctionDeclaration[] functionDeclarations)
    {
      FunctionDeclarations = null;
      AutoFunctionDeclarations = new List<AutoFunctionDeclaration>(functionDeclarations);
      GoogleSearch = null;
      CodeExecution = null;
      UrlContext = null;
      GoogleMaps = null;
    }
    /// <summary>
    /// Creates a tool that allows the model to perform function calling.
    /// </summary>
    /// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
    ///   that can be used for function calling.</param>
    public Tool(IEnumerable<AutoFunctionDeclaration> functionDeclarations)
    {
      FunctionDeclarations = null;
      AutoFunctionDeclarations = new List<AutoFunctionDeclaration>(functionDeclarations);
      GoogleSearch = null;
      CodeExecution = null;
      UrlContext = null;
      GoogleMaps = null;
    }

    /// <summary>
    /// Creates a tool that allows the model to use Grounding with Google Search.
    /// </summary>
    /// <param name="googleSearch">An empty `GoogleSearch` object. The presence of this object
    ///     in the list of tools enables the model to use Google Search.</param>
    public Tool(GoogleSearch googleSearch)
    {
      FunctionDeclarations = null;
      AutoFunctionDeclarations = null;
      GoogleSearch = googleSearch;
      CodeExecution = null;
      UrlContext = null;
      GoogleMaps = null;
    }

    /// <summary>
    /// Creates a tool that allows the model to use Code Execution.
    /// </summary>
    /// <param name="codeExecution">An empty `CodeExecution` object. The presence of this object
    ///     in the list of tools enables the model to use Code Execution.</param>
    public Tool(CodeExecution codeExecution)
    {
      FunctionDeclarations = null;
      AutoFunctionDeclarations = null;
      GoogleSearch = null;
      CodeExecution = codeExecution;
      UrlContext = null;
      GoogleMaps = null;
    }

    /// <summary>
    /// Creates a tool that allows you to provide additional context to the models in the form of
    /// public web URLs.
    /// </summary>
    /// <param name="urlContext">An empty `UrlContext` object. The presence of this object
    ///     in the list of tools enables the model to use Url Contexts.</param>
    public Tool(UrlContext urlContext)
    {
      FunctionDeclarations = null;
      AutoFunctionDeclarations = null;
      GoogleSearch = null;
      CodeExecution = null;
      UrlContext = urlContext;
      GoogleMaps = null;
    }

    /// <summary>
    /// Creates a tool that allows the model to use Grounding for Google Maps.
    /// </summary>
    /// <param name="googleMaps">The Grounding for Google Maps configuration. The
    /// presence of this object in the list of tools enables the model to use
    /// Grounding for Google Maps.</param>
    public Tool(GoogleMaps googleMaps)
    {
      FunctionDeclarations = null;
      AutoFunctionDeclarations = null;
      GoogleSearch = null;
      CodeExecution = null;
      UrlContext = null;
      GoogleMaps = googleMaps;
    }

    /// <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()
    {
      var json = new Dictionary<string, object>();
      List<Dictionary<string, object>> functionDeclarations = new();
      if (FunctionDeclarations != null && FunctionDeclarations.Any())
      {
        functionDeclarations.AddRange(FunctionDeclarations.Select(f => f.ToJson()));
      }
      if (AutoFunctionDeclarations != null && AutoFunctionDeclarations.Any())
      {
        functionDeclarations.AddRange(AutoFunctionDeclarations.Select(f => f.ToJson()));
      }
      if (functionDeclarations.Count > 0)
      {
        json["functionDeclarations"] = functionDeclarations;
      }
      if (GoogleSearch.HasValue)
      {
        json["googleSearch"] = new Dictionary<string, object>();
      }
      if (CodeExecution.HasValue)
      {
        json["codeExecution"] = new Dictionary<string, object>();
      }
      if (UrlContext.HasValue)
      {
        json["urlContext"] = new Dictionary<string, object>();
      }
      if (GoogleMaps.HasValue)
      {
        json["googleMaps"] = new Dictionary<string, object>();
      }
      return json;
    }
  }

  /// <summary>
  /// Tool configuration for any `Tool` specified in the request.
  /// </summary>
  public readonly struct ToolConfig
  {
    // No public properties, on purpose since it is meant for user input only

    private FunctionCallingConfig? FunctionConfig { get; }
    private RetrievalConfig? RetrievalConfig { get; }

    /// <summary>
    /// Constructs a new `ToolConfig`.
    /// </summary>
    /// <param name="functionCallingConfig">Configures how the model should use the
    ///   provided functions.</param>
    /// <param name="retrievalConfig">Configures how the model should use
    ///   the provided retrieval options.</param>
    public ToolConfig(FunctionCallingConfig? functionCallingConfig = null,
        RetrievalConfig? retrievalConfig = null)
    {
      FunctionConfig = functionCallingConfig;
      RetrievalConfig = retrievalConfig;
    }

    /// <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()
    {
      var json = new Dictionary<string, object>();
      if (FunctionConfig.HasValue)
      {
        json["functionCallingConfig"] = FunctionConfig?.ToJson();
      }
      if (RetrievalConfig.HasValue)
      {
        json["retrievalConfig"] = RetrievalConfig?.ToJson();
      }
      return json;
    }
  }

  /// <summary>
  /// Configuration for specifying function calling behavior.
  /// </summary>
  public readonly struct FunctionCallingConfig
  {
    // No public properties, on purpose since it is meant for user input only

    private string Mode { get; }
    private List<string> AllowedFunctionNames { get; }

    private FunctionCallingConfig(string mode, IEnumerable<string> allowedFunctionNames = null)
    {
      Mode = mode;
      if (allowedFunctionNames != null)
      {
        AllowedFunctionNames = new List<string>(allowedFunctionNames);
      }
      else
      {
        AllowedFunctionNames = null;
      }
    }

    /// <summary>
    /// Creates a function calling config where the model calls functions at its discretion.
    ///
    /// > Note: This is the default behavior.
    /// </summary>
    public static FunctionCallingConfig Auto()
    {
      return new FunctionCallingConfig("AUTO");
    }

    /// <summary>
    /// Creates a function calling config where the model will always call a provided function.
    /// </summary>
    /// <param name="allowedFunctionNames">A set of function names that, when provided, limits the
    ///   function that the model will call.</param>
    public static FunctionCallingConfig Any(params string[] allowedFunctionNames)
    {
      return new FunctionCallingConfig("ANY", allowedFunctionNames);
    }
    /// <summary>
    /// Creates a function calling config where the model will always call a provided function.
    /// </summary>
    /// <param name="allowedFunctionNames">A set of function names that, when provided, limits the
    ///   function that the model will call.</param>
    public static FunctionCallingConfig Any(IEnumerable<string> allowedFunctionNames)
    {
      return new FunctionCallingConfig("ANY", allowedFunctionNames);
    }

    /// Creates a function calling config where the model will never call a function.
    ///
    /// > Note: This can also be achieved by not passing any `FunctionDeclaration` tools when
    ///     instantiating the model.
    public static FunctionCallingConfig None()
    {
      return new FunctionCallingConfig("NONE");
    }

    /// <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()
    {
      var json = new Dictionary<string, object>() {
      { "mode", Mode }
    };
      if (AllowedFunctionNames != null)
      {
        json["allowedFunctionNames"] = AllowedFunctionNames;
      }
      return json;
    }
  }

  /// <summary>
  /// An struct that represents a latitude/longitude pair.
  /// </summary>
  public readonly struct LatLng
  {
    private double Latitude { get; }
    private double Longitude { get; }

    /// <summary>
    /// Constructs a LatLng object with the given coordinates.
    /// </summary>
    public LatLng(double latitude, double longitude)
    {
      Latitude = latitude;
      Longitude = longitude;
    }

    /// <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()
    {
      return new()
      {
        { "latitude", Latitude },
        { "longitude", Longitude }
      };
    }
  }

  /// <summary>
  /// Information which can be used by tools during inference calls.
  /// </summary>
  public readonly struct RetrievalConfig
  {
    private LatLng? LatLng { get; }
    private string LanguageCode { get; }

    /// <summary>
    /// Creates a retrieval configuration.
    /// </summary>
    /// <param name="latLng">An object that represents a latitude/longitude pair.</param>
    /// <param name="languageCode">The language code of the user.</param>
    public RetrievalConfig(LatLng? latLng = null, string languageCode = null)
    {
      LatLng = latLng;
      LanguageCode = languageCode;
    }

    /// <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()
    {
      var dict = new Dictionary<string, object>();
      if (LatLng.HasValue)
      {
        dict["latLng"] = LatLng?.ToJson();
      }
      dict.AddIfHasValue("languageCode", LanguageCode);
      return dict;
    }
  }

  /// <summary>
  /// Attribute that can be attached to parameters to give them a description,
  /// used when using `AutoFunctionDeclaration`.
  /// </summary>
  [AttributeUsage(AttributeTargets.Parameter)]
  public class AutoFunctionDescriptionAttribute : Attribute
  {
    public AutoFunctionDescriptionAttribute(string description)
    {
      Description = description;
    }

    public string Description { get; set; } = null;
  }

#if !DOXYGEN
  public abstract class BaseAutoFunctionDeclaration
  {
    internal string Name { get; }
    internal string Description { get; }
    internal JsonSchema Parameters { get; }

    internal Delegate Callable { get; }

    public BaseAutoFunctionDeclaration(Delegate callable, string description,
        string name = null)
    {
      if (callable == null)
      {
        throw new ArgumentNullException(nameof(callable));
      }
      Name = name ?? callable.Method.Name;
      Description = description;

      Dictionary<string, JsonSchema> parameters = new();
      List<string> optionalParameters = new();
      // Construct the Parameters based on the given Delegate
      foreach (var pInfo in callable.Method.GetParameters())
      {
        var attr = pInfo.GetCustomAttribute<AutoFunctionDescriptionAttribute>();
        parameters.Add(pInfo.Name,
            JsonSchema.FromType(pInfo.ParameterType, attr?.Description));
        if (pInfo.HasDefaultValue)
        {
          optionalParameters.Add(pInfo.Name);
        }
      }
      Parameters = JsonSchema.Object(parameters, optionalParameters);

      Callable = callable;
    }

    /// <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()
    {
      var json = new Dictionary<string, object>() {
        { "name", Name },
        { "parametersJsonSchema", Parameters.ToJson() }
      };
      json.AddIfHasValue("description", Description);

      return json;
    }
  }
#endif // !DOXYGEN

}
