//
//  ResponseModel.swift
//  SwiftOpenAI
//
//  Created by James Rochabrun on 3/15/25.
//

import Foundation

/// The Response object returned when retrieving a model response
/// [Get a model response](https://platform.openai.com/docs/api-reference/responses/get)
public struct ResponseModel: Decodable {
  /// The status of the response generation.
  public enum Status: String, Decodable {
    case completed
    case failed
    case inProgress = "in_progress"
    case cancelled
    case queued
    case incomplete
  }

  public struct ErrorObject: Decodable {
    /// The error code for the response.
    public let code: String

    /// A human-readable description of the error.
    public let message: String
  }

  /// Incomplete details structure
  public struct IncompleteDetails: Decodable {
    /// The reason why the response is incomplete
    public let reason: String
  }

  /// Input tokens details
  public struct InputTokensDetails: Decodable {
    /// Number of cached tokens
    public let cachedTokens: Int

    enum CodingKeys: String, CodingKey {
      case cachedTokens = "cached_tokens"
    }
  }

  /// Output tokens details
  public struct OutputTokensDetails: Decodable {
    /// Number of reasoning tokens
    public let reasoningTokens: Int

    enum CodingKeys: String, CodingKey {
      case reasoningTokens = "reasoning_tokens"
    }
  }

  /// Instructions type - can be a string, an array of strings, or an array of messages (for reusable prompts)
  public enum InstructionsType: Decodable {
    case string(String)
    case array([String])
    case messages([InputMessage])

    public init(from decoder: Decoder) throws {
      let container = try decoder.singleValueContainer()

      if let stringValue = try? container.decode(String.self) {
        self = .string(stringValue)
      } else if let arrayValue = try? container.decode([String].self) {
        self = .array(arrayValue)
      } else if let messagesValue = try? container.decode([InputMessage].self) {
        self = .messages(messagesValue)
      } else {
        throw DecodingError.dataCorruptedError(
          in: container,
          debugDescription: "Expected String, [String], or [InputMessage] for instructions")
      }
    }
  }

  /// Whether to run the model response in the background. Learn more.
  public let background: Bool?

  /// The conversation that this response belongs to. Input items and output items from this response are automatically added to this conversation.
  public let conversation: Conversation?

  /// Unix timestamp (in seconds) of when this Response was created.
  public let createdAt: Int

  /// An error object returned when the model fails to generate a Response.
  public let error: ErrorObject?

  /// Unique identifier for this Response.
  public let id: String

  /// Details about why the response is incomplete.
  public let incompleteDetails: IncompleteDetails?

  /// A system (or developer) message inserted into the model's context.
  /// When using along with previous_response_id, the instructions from a previous response will not be carried over to the next response.
  /// This makes it simple to swap out system (or developer) messages in new responses.
  public let instructions: InstructionsType?

  /// An upper bound for the number of tokens that can be generated for a response, including visible output tokens
  /// and [reasoning tokens](https://platform.openai.com/docs/guides/reasoning).
  public let maxOutputTokens: Int?

  /// The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.
  public let maxToolCalls: Int?

  /// Set of 16 key-value pairs that can be attached to an object.
  /// This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard.
  /// Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters.
  public let metadata: [String: String]

  /// Model ID used to generate the response, like gpt-4o or o1.
  /// OpenAI offers a wide range of models with different capabilities, performance characteristics, and price points.
  /// Refer to the [model guide](https://platform.openai.com/docs/models) to browse and compare available models.
  public let model: String

  /// The object type of this resource - always set to response.
  public let object: String

  /// An array of content items generated by the model.
  public let output: [OutputItem]

  /// Whether to allow the model to run tool calls in parallel.
  public let parallelToolCalls: Bool

  /// The unique ID of the previous response to the model. Use this to create multi-turn conversations.
  ///  Learn more about [conversation state](https://platform.openai.com/docs/guides/conversation-state).
  public let previousResponseId: String?

  /// Reference to a prompt template and its variables. Learn more.
  public let prompt: Prompt?

  /// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field. Learn more.
  public let promptCacheKey: String?

  /// Configuration options for reasoning models.
  public let reasoning: Reasoning?

  /// A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. The IDs should be a string that uniquely identifies each user. We recommend hashing their username or email address, in order to avoid sending us any identifying information. Learn more.
  public let safetyIdentifier: String?

  /// Specifies the latency tier to use for processing the request.
  /// This parameter is relevant for customers subscribed to the scale tier service:
  /// - If set to 'auto', and the Project is Scale tier enabled, the system will utilize scale tier credits until they are exhausted.
  /// - If set to 'auto', and the Project is not Scale tier enabled, the request will be processed using the default service tier with a lower uptime SLA and no latency guarantee.
  /// - If set to 'default', the request will be processed using the default service tier with a lower uptime SLA and no latency guarantee.
  /// - If set to 'flex', the request will be processed with the Flex Processing service tier. [Learn more](https://platform.openai.com/docs/guides/flex-processing).
  /// When not set, the default behavior is 'auto'.
  /// When this parameter is set, the response body will include the service_tier utilized.
  public let serviceTier: String?

  /// The status of the response generation. One of completed, failed, in_progress, cancelled, queued, or incomplete.
  public let status: Status?

  /// Whether to store the message. Used for model improvement.
  public let store: Bool?

  /// What sampling temperature to use, between 0 and 2.
  /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
  /// We generally recommend altering this or top_p but not both.
  public let temperature: Double?

  /// Configuration options for a text response from the model.
  public let text: TextConfiguration?

  /// How the model should select which tool (or tools) to use when generating a response.
  /// See the tools parameter to see how to specify which tools the model can call.
  public let toolChoice: ToolChoiceMode?

  /// An array of tools the model may call while generating a response. You can specify which tool to use by setting the tool_choice parameter.
  /// The two categories of tools you can provide the model are:
  /// Built-in tools: Tools that are provided by OpenAI that extend the model's capabilities, like [web search](https://platform.openai.com/docs/guides/tools-web-search) or [file search](https://platform.openai.com/docs/guides/tools-file-search0. Learn more about [built-in tools](https://platform.openai.com/docs/guides/tools).
  /// Function calls (custom tools): Functions that are defined by you, enabling the model to call your own code. Learn more about [function calling.](https://platform.openai.com/docs/guides/function-calling)
  public let tools: [Tool]?

  /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass.
  /// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
  /// We generally recommend altering this or temperature but not both.
  public let topP: Double?

  /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability.
  public let topLogprobs: Int?

  /// The truncation strategy to use for the model response.
  public let truncation: String?

  /// Represents token usage details.
  public let usage: Usage?

  /// A unique identifier representing your end-user.
  public let user: String?

  /// Convenience property that aggregates all text output from output_text items in the output array.
  /// Similar to the outputText property in Python and JavaScript SDKs.
  public var outputText: String? {
    let outputTextItems = output.compactMap { outputItem -> String? in
      switch outputItem {
      case .message(let message):
        return message.content.compactMap { contentItem -> String? in
          switch contentItem {
          case .outputText(let outputText):
            return outputText.text
          case .refusal:
            return nil
          }
        }.joined()

      default:
        return nil
      }
    }

    return outputTextItems.isEmpty ? nil : outputTextItems.joined()
  }

  enum CodingKeys: String, CodingKey {
    case background
    case conversation
    case id
    case object
    case createdAt = "created_at"
    case status
    case error
    case incompleteDetails = "incomplete_details"
    case instructions
    case maxOutputTokens = "max_output_tokens"
    case maxToolCalls = "max_tool_calls"
    case model
    case output
    case parallelToolCalls = "parallel_tool_calls"
    case previousResponseId = "previous_response_id"
    case prompt
    case promptCacheKey = "prompt_cache_key"
    case reasoning
    case safetyIdentifier = "safety_identifier"
    case serviceTier = "service_tier"
    case store
    case temperature
    case text
    case toolChoice = "tool_choice"
    case tools
    case topP = "top_p"
    case topLogprobs = "top_logprobs"
    case truncation
    case usage
    case user
    case metadata
  }
}
