#' @title Interaccion con modelos de IA usando TogetherAI
#' @description
#' Funcion para interactuar con modelos de IA a traves de la API de TogetherAI.
#' TogetherAI proporciona acceso a modelos open-source de alta calidad como
#' Llama, Qwen, Mistral, DeepSeek y muchos otros. Soporta JSON mode para
#' respuestas estructuradas. La API es compatible con el formato de OpenAI,
#' lo que facilita la integracion.
#'
#' @param texto Texto a analizar. Puede ser una noticia, tweet, documento, etc.
#' @param instrucciones Instrucciones en lenguaje natural que indican al modelo que hacer
#'   con el texto. Ejemplo: "Extrae todas las entidades nombradas", "Clasifica el sentimiento".
#' @param modelo Modelo a utilizar. Ejemplos populares:
#'   - Moonshot: `"moonshotai/Kimi-K2-Instruct-0905"` (128K context)
#'   - Meta Llama: `"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo"`, `"meta-llama/Llama-3.3-70B-Instruct-Turbo"`
#'   - Qwen: `"Qwen/Qwen2.5-72B-Instruct-Turbo"`, `"Qwen/QwQ-32B-Preview"`
#'   - Mistral: `"mistralai/Mixtral-8x22B-Instruct-v0.1"`, `"mistralai/Mistral-7B-Instruct-v0.3"`
#'   - DeepSeek: `"deepseek-ai/DeepSeek-V3"`, `"deepseek-ai/DeepSeek-R1"`
#'   - Google: `"google/gemma-2-27b-it"`, `"google/gemma-2-9b-it"`
#'   Por defecto: `"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"`.
#'   Ver lista completa: https://docs.together.ai/docs/chat-models
#' @param api_key Clave de API de TogetherAI. Si no se proporciona, busca la variable de
#'   entorno `TOGETHER_API_KEY`. Para obtener una clave: https://api.together.xyz/settings/api-keys
#' @param schema Esquema JSON que define la estructura de la respuesta. Puede usar
#'   `acep_gpt_schema()` para obtener esquemas predefinidos o crear uno personalizado.
#'   Si es `NULL`, usa un esquema simple con campo "respuesta".
#'   NOTA: TogetherAI soporta JSON mode con \code{response_format: \{type: "json_object"\}} para
#'   modelos compatibles. Consulta la lista de modelos soportados en:
#'   https://docs.together.ai/docs/json-mode
#' @param parse_json Logico. Si `TRUE` (por defecto), parsea automaticamente el JSON
#'   a un objeto R (lista o data frame). Si `FALSE`, devuelve el JSON como string.
#' @param temperature Parametro de temperatura (0-2). Valores bajos (0-0.3) generan
#'   respuestas mas deterministas. Valores altos (0.7-1) mas creativas. Por defecto: 0.
#' @param max_tokens Numero maximo de tokens en la respuesta. Por defecto: 2000.
#' @param top_p Parametro top-p para nucleus sampling (0-1). Por defecto: 0.2.
#' @param top_k Parametro top-k para muestreo. Limita las opciones a los k tokens mas probables.
#'   Por defecto: 50. Usar 0 o -1 para desactivar.
#' @param repetition_penalty Penalizacion por repeticion de tokens (0.1-2.0).
#'   Valores > 1 penalizan repeticiones. Por defecto: 1.
#' @param stop Secuencias de parada opcionales. Vector de strings que detienen la generacion.
#'   Por defecto: NULL.
#' @param prompt_system Prompt del sistema que define el comportamiento del modelo. Opciones:
#'   - `"json"` (por defecto): Usa un prompt estructurado que instruye al modelo a responder
#'     SOLO en formato JSON siguiendo el esquema proporcionado. Agrega \code{response_format: \{type: "json_object"\}}
#'   - `"texto"`: Usa un prompt simple para respuestas en texto plano sin estructura.
#'     Elimina automaticamente el contenido de pensamiento (<think>...</think>) de modelos como Qwen3-Thinking
#'   - String personalizado: Cualquier texto que definas como prompt del sistema
#'
#' @return Si `parse_json=TRUE`, devuelve una lista o data frame con la respuesta
#'   estructurada segun el esquema. Si `parse_json=FALSE`, devuelve un string JSON.
#'
#' @details
#' **Sobre TogetherAI:**
#'
#' TogetherAI es una plataforma especializada en modelos open-source que ofrece:
#' - Precios competitivos y modelos gratuitos
#' - Alta velocidad de inferencia optimizada
#' - Acceso a modelos de ultima generacion (Llama, Qwen, DeepSeek, etc.)
#' - API compatible con formato OpenAI
#'
#' **JSON Mode:**
#'
#' La funcion utiliza JSON mode de TogetherAI para obtener respuestas estructuradas.
#' Cuando `prompt_system = "json"`, la funcion:
#' 1. Incluye el esquema JSON en el prompt del sistema (REQUERIDO por TogetherAI)
#' 2. Agrega \code{response_format: \{type: "json_object"\}} al body de la peticion
#' 3. Instruye explicitamente al modelo a responder SOLO en JSON
#'
#' Esta combinacion de esquema textual + \code{response_format} asegura respuestas JSON validas
#' y consistentes en cada llamada.
#'
#' **Modelos compatibles con JSON mode:**
#'
#' Los modelos mas recientes que soportan JSON mode incluyen:
#' - Qwen3, Qwen2.5 (Instruct, Coder, VL, Thinking)
#' - DeepSeek-R1, DeepSeek-V3
#' - Meta Llama 3.1, 3.3, 4
#' - Mistral 7B Instruct
#' - Google Gemma
#'
#' Ver lista completa: https://docs.together.ai/docs/json-mode
#'
#' **Validaciones:**
#'
#' La funcion incluye validacion de limite de tokens. Si la respuesta es truncada
#' por `max_tokens`, devuelve un mensaje claro indicando que se necesitan mas tokens.
#'
#' @export
#' @examples
#' \dontrun{
#' # Configurar API key
#' Sys.setenv(TOGETHER_API_KEY = "tu-api-key")
#'
#' # Usar Llama 3.1 70B (rapido y potente)
#' texto <- "El SUTEBA convoco a un paro en Buenos Aires el 15 de marzo."
#' resultado <- acep_together(texto, "Extrae las entidades nombradas",
#'                            modelo = "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
#'                            schema = acep_gpt_schema("extraccion_entidades"))
#'
#' # Usar Qwen para analisis de sentimiento
#' resultado_qwen <- acep_together(texto, "Clasifica el sentimiento",
#'                                 modelo = "Qwen/Qwen2.5-72B-Instruct-Turbo",
#'                                 schema = acep_gpt_schema("sentimiento"))
#'
#' # Usar DeepSeek-V3
#' resultado_ds <- acep_together(texto, "Analiza el texto",
#'                               modelo = "deepseek-ai/DeepSeek-V3",
#'                               schema = acep_gpt_schema("clasificacion"))
#'
#' # Usar Moonshot Kimi con 128K context
#' resultado_kimi <- acep_together(texto, "Resume el texto",
#'                                 modelo = "moonshotai/Kimi-K2-Instruct-0905",
#'                                 schema = acep_gpt_schema("resumen"))
#'
#' # Usar modo texto plano (sin estructura JSON)
#' resultado_texto <- acep_together(texto, "Resume este texto en una frase",
#'                                  modelo = "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
#'                                  prompt_system = "texto",
#'                                  parse_json = FALSE)
#' print(resultado_texto)  # Devuelve string de texto plano
#'
#' # Usar prompt del sistema personalizado
#' resultado_custom <- acep_together(
#'   texto,
#'   "Analiza el sentimiento",
#'   modelo = "Qwen/Qwen2.5-72B-Instruct-Turbo",
#'   prompt_system = paste(
#'     "Eres un experto en analisis de sentimientos politicos.",
#'     "Se objetivo y neutral."
#'   ),
#'   parse_json = FALSE
#' )
#' }
acep_together <- function(texto,
                          instrucciones,
                          modelo = "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
                          api_key = Sys.getenv("TOGETHER_API_KEY"),
                          schema = NULL,
                          parse_json = TRUE,
                          temperature = 0,
                          max_tokens = 2000,
                          top_p = 0.2,
                          top_k = 50,
                          repetition_penalty = 1,
                          stop = NULL,
                          prompt_system = "json") {

  .acep_provider_validate_request_inputs(texto, instrucciones, api_key, "TOGETHER_API_KEY")

  # Construir prompt del sistema segun el tipo especificado
  if (prompt_system == "json") {
    # Modo JSON: Fuerza respuestas estructuradas en JSON

    # Esquema por defecto si no se proporciona uno
    if (is.null(schema)) {
      schema <- .acep_provider_default_schema()
    }

    # Construir descripcion de campos esperados del esquema
    # Extraer nombres y descripciones de los campos del schema
    campos_descripciones <- .acep_provider_schema_field_descriptions(schema)
    campos_texto <- paste(campos_descripciones, collapse = "\n")

    # Prompt del sistema con instrucciones para JSON
    # TogetherAI usa JSON mode basico (type: json_object), no Structured Outputs estrictos
    # NO incluimos el esquema completo para evitar que el modelo lo devuelva como respuesta
    system_prompt <- sprintf(
      "Eres un asistente experto en analisis de texto. Debes responder SIEMPRE en formato JSON valido con los siguientes campos:\n\n%s\n\nSe preciso, conciso y basa tus respuestas unicamente en el texto proporcionado. Responde UNICAMENTE con el JSON de datos, sin texto adicional antes o despues.",
      campos_texto
    )

  } else if (prompt_system == "texto") {
    # Modo texto plano: Respuestas sin estructura especifica
    system_prompt <- "Eres un asistente experto en analisis de texto. Responde de manera clara, precisa y concisa. Basa tus respuestas unicamente en el texto proporcionado."

  } else {
    # Prompt personalizado: Usa el string proporcionado directamente
    system_prompt <- prompt_system
  }

  # Construir prompt del usuario
  user_prompt <- .acep_provider_user_prompt(texto, instrucciones)

  # Construir body de la peticion
  body <- list(
    model = modelo,
    messages = list(
      list(role = "system", content = system_prompt),
      list(role = "user", content = user_prompt)
    ),
    temperature = temperature,
    max_tokens = max_tokens,
    top_p = top_p,
    top_k = top_k,
    repetition_penalty = repetition_penalty
  )

  # Agregar response_format para JSON mode solo si prompt_system es "json"
  # TogetherAI soporta response_format segun https://docs.together.ai/docs/json-mode
  if (prompt_system == "json") {
    body$response_format <- list(
      type = "json_object"
    )
  }

  # Agregar secuencias de parada si se especifican
  if (!is.null(stop)) {
    body$stop <- stop
  }

  # Construir headers
  headers_call <- .acep_provider_auth_headers("together", api_key)

  # Realizar peticion a la API
  tryCatch({
    output <- httr::POST(
      url = .acep_provider_endpoint("together"),
      do.call(httr::add_headers, headers_call),
      body = jsonlite::toJSON(body, auto_unbox = TRUE, pretty = FALSE),
      encode = "raw"
    )

    # Verificar codigo HTTP
    if (httr::status_code(output) != 200) {
      error_content <- httr::content(output, as = "parsed")
      error_msg <- if (!is.null(error_content$error$message)) {
        error_content$error$message
      } else if (!is.null(error_content$error)) {
        as.character(error_content$error)
      } else {
        "Error desconocido"
      }
      stop(sprintf("Error HTTP %d: %s", httr::status_code(output), error_msg))
    }

    # Extraer respuesta
    respuesta_parsed <- httr::content(output, as = "parsed")
    respuesta_json <- .acep_provider_extract_chat_content(respuesta_parsed)

    # Verificar si la respuesta fue truncada por limite de tokens
    finish_reason <- respuesta_parsed$choices[[1]]$finish_reason
    if (!is.null(finish_reason) && finish_reason == "length") {
      stop("La respuesta fue truncada debido al limite de max_tokens. Se necesitan mas tokens para obtener una respuesta valida. Aumenta el valor de max_tokens.")
    }

    # Procesar respuesta segun el modo
    if (prompt_system == "json") {
      # Modo JSON: Limpiar markdown y parsear
      # Algunos modelos envuelven el JSON en ```json...``` o ```...```
      # Parsear JSON si se solicita
      return(.acep_provider_parse_json_response(respuesta_json, parse_json = parse_json))
    } else {
      # Modo texto plano o personalizado: Limpiar y devolver
      # Eliminar contenido de pensamiento (thinking) si existe
      # Algunos modelos como Qwen3-Thinking generan contenido entre <think>...</think>
      # La etiqueta inicial puede estar o no, pero la de cierre siempre está
      respuesta_limpia <- gsub("(<think>)?[\\s\\S]*?</think>\\s*", "", respuesta_json, perl = TRUE, ignore.case = TRUE)
      respuesta_limpia <- trimws(respuesta_limpia)

      return(respuesta_limpia)
    }

  }, error = function(e) {
    stop(sprintf("Error al interactuar con la API de TogetherAI: %s", conditionMessage(e)))
  })
}
