<?php

class Meow_MWAI_Query_Base implements JsonSerializable {
  // Environment
  public ?string $session = null;
  public ?string $chatId = null;
  public string $scope = '';
  private $core = null;

  // Core Content
  public ?string $instructions = null;
  public array $messages = [];
  public ?string $context = null;
  public string $message = '';

  // Parameters
  public int $maxMessages = 15;
  public int $maxResults = 1;
  public ?string $model = null;
  public string $feature = 'completion';

  // Functions
  public array $functions = [];
  public ?string $functionCall = null;

  // MCP Servers
  public array $mcpServers = [];

  // Tools (for Responses API)
  public array $tools = [];

  // History strategy for Responses API
  public ?string $historyStrategy = null;
  public ?string $previousResponseId = null;

  // Overrides for env
  public array $envSettings = [];
  public string $envId = '';
  public ?string $apiKey = null;

  // Seem to be only used by the Assistants, to get the current thread/discussion.
  // Maybe we should try to move this to the assistant class, or use it as ExtraParams.
  public ?string $botId = null;
  // Identifier for ad-hoc/custom chatbots (distinct from registered botId)
  public ?string $customId = null;

  // Embeddings configuration
  public ?string $embeddingsEnvId = null;

  // Extra Parameters (used by specific services, or for statistics, etc)
  public array $extraParams = [];

  /**
   * Internal state tracking for function call recursion.
   * Stores the configured max depth to prevent re-applying the filter on recursive calls.
   * Set once by engines/core.php during the first run() call.
   * @internal
   */
  public $_maxDepthConfigured = null;

  // Options
  // Engine will either upload or share an URL to the image, for Vision, for example.
  // Having this here allows other services to override it if needed (Ollama needs it false).
  public ?string $image_remote_upload = null;

  #region Constructors, Serialization

  public function __construct( $message = '' ) {
    global $mwai_core;
    if ( is_string( $message ) ) {
      $this->set_message( $message );
    }
    $this->session = $mwai_core->get_session_id();
    $this->core = $mwai_core;
    $this->image_remote_upload = $this->core->get_option( 'image_remote_upload' );
  }

  #[\ReturnTypeWillChange]
  public function jsonSerialize(): array {
    $json = [
      'message' => $this->message,
      'instructions' => $this->instructions,

      'ai' => [
        'model' => $this->model,
        'feature' => $this->feature,
      ],

      'system' => [
        'class' => get_class( $this ),
        'envId' => $this->envId,
        'scope' => $this->scope,
        'session' => $this->session,
        'customId' => $this->customId,
        'maxMessages' => $this->maxMessages,
      ]
    ];

    if ( !empty( $this->context ) ) {
      $json['context']['content'] = $this->context;
    }

    return $json;
  }

  #endregion

  #region Functions

  public function add_function( Meow_MWAI_Query_Function $function ): void {
    $this->functions[] = $function;
    $this->functionCall = 'auto';
  }

  public function set_functions( array $functions ): void {
    $this->functions = $functions;
    $this->functionCall = 'auto';
  }

  public function set_tools( array $tools ): void {
    $this->tools = $tools;
  }

  public function set_mcp_servers( array $mcpServers ): void {
    $this->mcpServers = $mcpServers;
  }

  #endregion

  #region Helpers

  public function replace( $search, $replace ) {
    $this->message = str_replace( $search, $replace, $this->message );
  }

  #endregion

  public function get_message(): string {
    return $this->message;
  }

  public function get_in_tokens(): int {
    $text = '';
    foreach ( $this->messages as $message ) {
      $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
      $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
    }
    $text .= $this->message;
    $text .= $this->context ?? '';
    return Meow_MWAI_Core::estimate_tokens( $text );
  }

  /**
  * The environment, like "chatbot", "imagesbot", "chatbot-007", "textwriter", etc...
  * Used for statistics, mainly.
  * @param string $env The environment.
  */
  public function set_scope( string $scope ): void {
    $this->scope = $scope;
  }

  /**
  * The environment ID for AI services.
  * Used for statistics, mainly.
  * @param string $envId The environment ID.
  */
  public function set_env_id( string $envId ): void {
    $this->envId = $envId;
  }

  /**
  * ID of the model to use.
  * @param string $model ID of the model to use.
  */
  public function set_model( string $model ) {
    $this->model = $model;
  }

  public function get_model() {
    return $this->model;
  }

  /**
  * The chat ID to use.
  * @param string $chatId The chat ID.
  */
  public function set_chat_id( string $chatId ) {
    $this->chatId = $chatId;
  }

  public function get_chat_id() {
    return $this->chatId;
  }

  /**
  * The instructions are used to define the personality of the AI, and to give it some context.
  * @param string $instructions The instructions.
  */
  public function set_instructions( string $instructions ): void {
    global $mwai_core;

    // Decode HTML entities in case the instructions were sanitized at the UI level
    // and ended up encoded when reaching the server.
    $instructions = html_entity_decode( $instructions );

    // Apply filters first, so developers can add their own placeholders
    $filtered = apply_filters( 'mwai_ai_context', $instructions, $this );
    if ( $filtered !== $instructions ) {
      Meow_MWAI_Logging::deprecated( '"mwai_ai_context" filter is deprecated. Please use "mwai_ai_instructions" instead.' );
    }
    $filtered = apply_filters( 'mwai_ai_instructions', $filtered, $this );

    // Apply placeholders (e.g., {DATE_TIME}, {DISPLAY_NAME}, etc.) after filters,
    // so custom placeholders added by developers are also processed.
    if ( $mwai_core ) {
      $filtered = $mwai_core->do_placeholders( $filtered );
    }

    $this->instructions = $filtered;
  }

  /**
  * Given a message, the model will return one or more predicted completions.
  * It can also return the probabilities of alternative tokens at each position.
  * @param string $message The message to generate completions.
  */
  public function set_message( string $message ) {
    $this->message = $message;
  }

  /**
  * Similar to the prompt, but use an array of messages instead.
  * @param string $messages The messages to generate completions.
  */
  public function set_messages( array $messages ) {
    $messages = array_map( function ( $message ) {
      if ( is_array( $message ) ) {
        return [ 'role' => $message['role'], 'content' => $message['content'] ];
      }
      else if ( is_object( $message ) ) {
        return [ 'role' => $message->role, 'content' => $message->content ];
      }
      else {
        throw new InvalidArgumentException( 'Unsupported message type.' );
      }
    }, $messages );
    $this->messages = $messages;
  }

  /**
  * The context can be used to add additional information that is likely to be relevant to the model.
  * @param string $context The context.
  */
  public function set_context( string $context ): void {
    $this->context = $context;
  }

  /**
  * The API key to use.
  * @param string $apiKey The API key.
  */
  public function set_api_key( string $apiKey ) {
    $this->apiKey = $apiKey;
  }

  /**
  * The session ID to use.
  * @param string $session The session ID.
  */
  public function set_session( string $session ) {
    $this->session = $session;
  }

  /**
  * The bot ID to use.
  * @param string $botId The bot ID.
  */
  public function set_bot_id( string $botId ) {
    $this->botId = $botId;
  }

  /**
  * The custom ID to use for ad-hoc chatbots (shortcode/overrides).
  * @param string $customId The custom chatbot ID.
  */
  public function set_custom_id( string $customId ) {
    $this->customId = $customId;
  }

  /**
  * The embeddings environment ID to use.
  * @param string $embeddingsEnvId The embeddings environment ID.
  */
  public function set_embeddings_env_id( string $embeddingsEnvId ) {
    $this->embeddingsEnvId = $embeddingsEnvId;
  }

  /**
  * How many completions to generate for each prompt.
  * Because this parameter generates many completions, it can quickly consume your token quota.
  * Use carefully and ensure that you have reasonable settings for max_tokens and stop.
  * @param float $maxResults Number of completions.
  */
  public function set_max_results( int $maxResults ) {
    $this->maxResults = $maxResults;
  }

  /**
  * Set the history strategy for Responses API.
  * @param string $historyStrategy The history strategy ('internal', 'response_id', or null).
  */
  public function set_history_strategy( ?string $historyStrategy ) {
    $this->historyStrategy = $historyStrategy;
  }

  /**
  * Set the previous response ID for Responses API.
  * @param string $previousResponseId The previous response ID.
  */
  public function set_previous_response_id( ?string $previousResponseId ) {
    $this->previousResponseId = $previousResponseId;
  }

  /**
  * This is run at the end of the process, to do some final checks.
  */
  public function final_checks() {
    if ( !empty( $this->maxMessages ) ) {
      $context = array_shift( $this->messages );
      if ( !empty( $this->messages ) ) {
        $this->messages = array_slice( $this->messages, -$this->maxMessages );
      }
      else {
        $this->messages = [];
      }
      if ( !empty( $context ) ) {
        array_unshift( $this->messages, $context );
      }
    }
  }

  public function set_max_messages( int $maxMessages ): void {
    if ( !empty( $maxMessages ) ) {
      $this->maxMessages = intval( $maxMessages );
    }
  }

  protected function convert_keys( $params ) {
    $newParams = [];
    foreach ( $params as $key => $value ) {
      // Skip non-string keys (numeric indices, booleans, etc.)
      if ( !is_string( $key ) ) {
        continue;
      }
      $newKey = '';
      $capitalizeNextChar = false;
      for ( $i = 0; $i < strlen( $key ); $i++ ) {
        if ( $key[$i] == '_' ) {
          $capitalizeNextChar = true;
        }
        else {
          $newKey .= $capitalizeNextChar ? strtoupper( $key[$i] ) : $key[$i];
          $capitalizeNextChar = false;
        }
      }
      $newParams[$newKey] = $value;
    }
    return $newParams;
  }

  public function toJson() {
    return json_encode( $this );
  }

  #region Extra Params
  public function setExtraParam( string $key, $value ): void {
    $this->extraParams[$key] = $value;
  }

  public function getExtraParam( string $key ) {
    // Only if it exists
    if ( !isset( $this->extraParams[$key] ) ) {
      return null;
    }
    $value = $this->extraParams[$key];
    return $value;
  }
  #endregion Extra Params

  // Based on the params of the query, update the attributes
  public function inject_params( array $params ): void {
    // Those are for the keys passed directly by the shortcode.
    $params = $this->convert_keys( $params );

    if ( !empty( $params['instructions'] ) ) {
      $this->set_instructions( $params['instructions'] );
    }
    // Do not allow external params to clobber an already-set message
    // The message is typically set via constructor (e.g., Chatbot newMessage).
    // Some UIs may accidentally send a top-level 'message' param (e.g., from
    // unrelated settings like verbosity defaults), which would override the
    // real user input here. Only set it if it's not already set.
    if ( !empty( $params['message'] ) && $this->message === '' ) {
      $this->set_message( $params['message'] );
    }
    if ( !empty( $params['messages'] ) ) {
      $this->set_messages( $params['messages'] );
    }
    if ( !empty( $params['maxMessages'] ) && intval( $params['maxMessages'] ) > 0 ) {
      $this->set_max_messages( intval( $params['maxMessages'] ) );
    }
    if ( !empty( $params['maxResults'] ) ) {
      $this->set_max_results( $params['maxResults'] );
    }
    if ( !empty( $params['scope'] ) ) {
      $this->set_scope( $params['scope'] );
    }
    if ( !empty( $params['session'] ) ) {
      $this->set_session( $params['session'] );
    }
    if ( !empty( $params['apiKey'] ) ) {
      $this->set_api_key( $params['apiKey'] );
    }
    if ( !empty( $params['botId'] ) ) {
      $this->set_bot_id( $params['botId'] );
    }
    if ( !empty( $params['customId'] ) ) {
      $this->set_custom_id( $params['customId'] );
    }
    if ( !empty( $params['envId'] ) ) {
      $this->set_env_id( $params['envId'] );
    }
    if ( !empty( $params['model'] ) ) {
      $this->set_model( $params['model'] );
    }
    if ( !empty( $params['chatId'] ) ) {
      $this->set_chat_id( $params['chatId'] );
    }
    if ( !empty( $params['tools'] ) && is_array( $params['tools'] ) ) {
      $this->set_tools( $params['tools'] );
    }
    if ( isset( $params['historyStrategy'] ) ) {
      $this->set_history_strategy( $params['historyStrategy'] );
    }
    if ( !empty( $params['previousResponseId'] ) ) {
      $this->set_previous_response_id( $params['previousResponseId'] );
    }
    if ( !empty( $params['embeddingsEnvId'] ) ) {
      $this->set_embeddings_env_id( $params['embeddingsEnvId'] );
    }
    if ( !empty( $params['mcpServers'] ) ) {
      // Handle both JSON string and array formats
      $mcpServers = $params['mcpServers'];
      if ( is_string( $mcpServers ) ) {
        $mcpServers = json_decode( $mcpServers, true );
      }
      if ( is_array( $mcpServers ) ) {
        $this->set_mcp_servers( $mcpServers );
      }
    }
  }
}
