<?php

class Meow_MWAI_Modules_Discussions {
  private $wpdb = null;
  private $core = null;
  public $table_chats = null;
  private $db_check = false;
  private $namespace_admin = 'mwai/v1';
  private $namespace_ui = 'mwai-ui/v1';

  public function __construct() {
    global $wpdb;
    $this->wpdb = $wpdb;
    global $mwai_core;
    $this->core = $mwai_core;
    $this->table_chats = $wpdb->prefix . 'mwai_chats';

    if ( $this->core->get_option( 'chatbot_discussions' ) ) {
      add_filter( 'mwai_chatbot_reply', [ $this, 'chatbot_reply' ], 10, 4 );
      add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );

      // Register task handler
      add_filter( 'mwai_task_cleanup_discussions', [ $this, 'handle_cleanup_task' ], 10, 2 );
    }
  }

  public function rest_api_init() {
    // Admin
    register_rest_route( $this->namespace_admin, '/discussions/list', [
      'methods' => 'POST',
      'callback' => [ $this, 'rest_discussions_list' ],
      'permission_callback' => [ $this->core, 'can_access_settings' ],
    ] );
    register_rest_route( $this->namespace_admin, '/discussions/delete', [
      'methods' => 'POST',
      'callback' => [ $this, 'rest_discussions_delete_admin' ],
      'permission_callback' => [ $this->core, 'can_access_settings' ],
    ] );
    register_rest_route( $this->namespace_admin, '/discussions/stats', [
      'methods' => 'GET',
      'callback' => [ $this, 'rest_discussions_stats' ],
      'permission_callback' => [ $this->core, 'can_access_settings' ],
    ] );

    // UI
    register_rest_route( $this->namespace_ui, '/discussions/list', [
      'methods' => 'POST',
      'callback' => [ $this, 'rest_discussions_ui_list' ],
      'permission_callback' => '__return_true'
    ] );
    register_rest_route( $this->namespace_ui, '/discussions/edit', [
      'methods' => 'POST',
      'callback' => [ $this, 'rest_discussions_ui_edit' ],
      'permission_callback' => '__return_true'
    ] );
    register_rest_route( $this->namespace_ui, '/discussions/delete', [
      'methods' => 'POST',
      'callback' => [ $this, 'rest_discussions_delete' ],
      'permission_callback' => [ $this, 'can_delete_discussion' ],
    ] );
    // Truncate a discussion to a client-provided (shorter) message set. Used by
    // edit-and-branch: when a user edits an earlier message, everything after
    // the edit point is dropped and the Responses API chain is reset so the
    // model continues from the fork, not the stale tail.
    register_rest_route( $this->namespace_ui, '/discussions/truncate', [
      'methods' => 'POST',
      'callback' => [ $this, 'rest_discussions_ui_truncate' ],
      'permission_callback' => '__return_true'
    ] );
  }

  public function can_delete_discussion( $request ) {
    $this->check_db();
    $params = $request->get_json_params();
    $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
    $userId = get_current_user_id();
    if ( !$userId ) {
      return false;
    }
    foreach ( $chatIds as $chatId ) {
      $chat = $this->wpdb->get_row(
        $this->wpdb->prepare(
          "SELECT * FROM $this->table_chats WHERE chatId = %s",
          $chatId
        )
      );
      if ( !$chat || (int) $chat->userId !== (int) $userId ) {
        return false;
      }
    }
    return true;
  }

  /**
   * Helper method to create REST responses with automatic token refresh
   *
   * @param array $data The response data
   * @param int $status HTTP status code
   * @return WP_REST_Response
   */
  protected function create_rest_response( $data, $status = 200 ) {
    // Always check if we need to provide a new nonce
    $current_nonce = $this->core->get_nonce( true );
    $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;

    // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
    // We'll refresh if the nonce is older than 10 hours to be safe
    $should_refresh = false;

    if ( $request_nonce ) {
      // Try to determine the age of the nonce
      // WordPress uses a tick system where each tick is 12 hours
      // If we're in the second half of the nonce's life, refresh it
      $time = time();
      $nonce_tick = wp_nonce_tick();

      // Verify if the nonce is still valid but getting old
      $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
      if ( $verify === 2 ) {
        // Nonce is valid but was generated 12-24 hours ago
        $should_refresh = true;
        // Log will be written when token is included in response
      }
    }

    // If the nonce has changed or should be refreshed, include the new one
    if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
      $data['new_token'] = $current_nonce;

      // Log if server debug mode is enabled
      if ( $this->core->get_option( 'server_debug_mode' ) ) {
        error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
      }
    }

    return new WP_REST_Response( $data, $status );
  }

  /**
  * Generate or update the title for a specific discussion
  * by calling the AI (if it meets the requirements).
  *
  * @param stdClass $discussion A row from the DB (object form).
  * @return void
  */
  private function generate_title_for_discussion( $discussion ) {
    // Check if there's already a title
    if ( !empty( $discussion->title ) ) {
      return; // Nothing to do if title is already set.
    }

    // Ensure it's not older than 10 days, or whatever logic you prefer
    $ten_days_ago = strtotime( '-10 days' );
    if ( strtotime( $discussion->updated ) < $ten_days_ago ) {
      return; // Skip if older than 10 days
    }

    // We expect JSON in the messages
    $messages = json_decode( $discussion->messages, true );
    if ( !is_array( $messages ) ) {
      return;
    }

    // Check for at least one user and one assistant message
    $has_user_message = false;
    $has_assistant_message = false;
    foreach ( $messages as $message ) {
      if ( isset( $message['role'] ) ) {
        if ( $message['role'] === 'user' ) {
          $has_user_message = true;
        }
        if ( $message['role'] === 'assistant' ) {
          $has_assistant_message = true;
        }
      }
      if ( $has_user_message && $has_assistant_message ) {
        break;
      }
    }

    if ( !( $has_user_message && $has_assistant_message ) ) {
      return; // If doesn't have both, skip
    }

    // Prepare the conversation text for the prompt
    $conversation_text = '';
    foreach ( $messages as $message ) {
      if ( isset( $message['role'] ) && isset( $message['content'] ) ) {
        $role = ucfirst( $message['role'] );
        $content = $message['content'];
        $conversation_text .= "$role: $content\n";
      }
    }

    $base_prompt = __( 'Based on the following conversation, generate a concise and specific title for the discussion, strictly less than 64 characters. Focus on the main topic, avoiding unnecessary words such as articles, pronouns, or adjectives. Do not include any punctuation at the end. Do not include anything else than the title itself, only one sentence, no line breaks, just the title.', 'ai-engine' ) . "\n\n" . __( 'Conversation:', 'ai-engine' ) . "\n$conversation_text\n";
    $prompt = apply_filters( 'mwai_discussions_title_prompt', $base_prompt, $conversation_text, $discussion );

    // Run the AI query using the fast environment
    global $mwai;
    $params = [ 'scope' => 'discussions' ];

    // Use simpleFastTextQuery which handles Fast Model configuration
    try {
      $answer = $mwai->simpleFastTextQuery( $prompt, $params );

      // Clean up the answer
      $title = trim( $answer );
      $title = rtrim( $title, '.!?:;,—–-–' ); // Remove trailing punctuation
      $title = substr( $title, 0, 64 ); // Ensure less than 64 characters
      if ( empty( $title ) ) {
        $title = __( 'Untitled', 'ai-engine' );
      }
    }
    catch ( Exception $e ) {
      // Handle content filter or other API errors
      $error_message = $e->getMessage();
      if ( strpos( $error_message, 'content_filter' ) !== false ||
           strpos( $error_message, 'ResponsibleAIPolicyViolation' ) !== false ) {
        error_log( "AI Engine: Content filter blocked title generation for discussion ID {$discussion->id}. Using fallback title." );
        $title = __( 'Discussion', 'ai-engine' ) . ' ' . date( 'Y-m-d H:i' );
      }
      else {
        error_log( "AI Engine: Failed to generate title for discussion ID {$discussion->id}: " . $error_message );
        $title = __( 'Untitled', 'ai-engine' );
      }
    }

    // Update the discussion with the title
    $updated = $this->wpdb->update(
      $this->table_chats,
      [ 'title' => $title ],
      [ 'id' => $discussion->id ]
    );
    if ( $updated === false ) {
      error_log( "Failed to update the title for discussion ID {$discussion->id}" );
    }
  }

  /**
  * Admin route for listing discussions. No forced logic here.
  */
  public function rest_discussions_list( $request ) {
    try {
      $params = $request->get_json_params();
      $offset = $params['offset'];
      $limit = $params['limit'];
      $filters = $params['filters'];
      $sort = $params['sort'];

      // Retrieve the chats
      $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );

      return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
    }
    catch ( Exception $e ) {
      return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
    }
  }

  /**
  * Admin route for discussion counts, used by the Dashboard usage widget.
  * Discussions are hard-deleted, so these counts never include deleted ones.
  */
  public function rest_discussions_stats( $request ) {
    try {
      $this->check_db();
      $days = (int) $request->get_param( 'days' );
      if ( $days < 1 || $days > 365 ) {
        $days = 7;
      }
      $since = date( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
      $count = (int) $this->wpdb->get_var(
        $this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->table_chats} WHERE created >= %s", $since )
      );
      $total = (int) $this->wpdb->get_var( "SELECT COUNT(*) FROM {$this->table_chats}" );
      return $this->create_rest_response( [ 'success' => true, 'count' => $count, 'total' => $total, 'days' => $days ], 200 );
    }
    catch ( Exception $e ) {
      return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
    }
  }

  public function rest_discussions_ui_edit( $request ) {
    try {
      $params = $request->get_json_params();
      $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
      $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;

      if ( is_null( $chatId ) || is_null( $title ) ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
      }

      $userId = get_current_user_id();
      if ( !$userId ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
      }

      // Update the discussion title for the current user
      $updated = $this->wpdb->update(
        $this->table_chats,
        [ 'title' => $title ],
        [ 'chatId' => $chatId, 'userId' => $userId ]
      );
      if ( $updated === false ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
      }

      return $this->create_rest_response( [ 'success' => true ], 200 );
    }
    catch ( Exception $e ) {
      return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
    }
  }

  /**
   * Overwrite a discussion's stored messages with a client-provided (shorter)
   * set, and clear the stored Responses API id so the next turn forks cleanly.
   * Ownership-guarded like the rest of the write path.
   */
  public function rest_discussions_ui_truncate( $request ) {
    try {
      $this->check_db();
      $params = $request->get_json_params();
      $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
      $messages = isset( $params['messages'] ) && is_array( $params['messages'] ) ? $params['messages'] : null;

      if ( is_null( $chatId ) || is_null( $messages ) ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and messages are required.' ], 400 );
      }

      $userId = get_current_user_id();
      if ( !$userId ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
      }

      $chat = $this->wpdb->get_row(
        $this->wpdb->prepare( "SELECT * FROM $this->table_chats WHERE chatId = %s", $chatId )
      );
      // Same ownership guard as chatbot_reply: never touch another user's row.
      if ( $chat && (int) $chat->userId !== 0 && (int) $chat->userId !== (int) $userId ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'Not allowed.' ], 403 );
      }

      // Keep only known fields so a client can't inject arbitrary data.
      // stopped and extra.model matter for stopped turns and model attribution.
      $clean = [];
      foreach ( $messages as $m ) {
        if ( !isset( $m['role'] ) ) {
          continue;
        }
        $entry = [ 'role' => sanitize_text_field( $m['role'] ), 'content' => $m['content'] ?? '' ];
        if ( isset( $m['who'] ) ) {
          $entry['who'] = sanitize_text_field( $m['who'] );
        }
        if ( !empty( $m['stopped'] ) ) {
          $entry['stopped'] = true;
        }
        if ( !empty( $m['timestamp'] ) && is_numeric( $m['timestamp'] ) ) {
          $entry['timestamp'] = (int) $m['timestamp'];
        }
        if ( isset( $m['extra'] ) && is_array( $m['extra'] ) ) {
          $extraEntry = [];
          if ( !empty( $m['extra']['model'] ) ) {
            $extraEntry['model'] = sanitize_text_field( $m['extra']['model'] );
          }
          // Embeddings and tool calls used to be dropped here, so truncating or
          // branching a discussion silently lost them on every surviving message.
          if ( !empty( $m['extra']['embeddings'] ) && is_array( $m['extra']['embeddings'] ) ) {
            $extraEntry['embeddings'] = $this->sanitize_message_extra( $m['extra']['embeddings'] );
          }
          if ( !empty( $m['extra']['toolCalls'] ) && is_array( $m['extra']['toolCalls'] ) ) {
            $extraEntry['toolCalls'] = $this->sanitize_message_extra( $m['extra']['toolCalls'] );
          }
          if ( !empty( $extraEntry ) ) {
            $entry['extra'] = $extraEntry;
          }
        }
        $clean[] = $entry;
      }

      // No row yet (e.g. the very first turn was stopped before completing):
      // create it, so the conversation survives a reload.
      if ( !$chat ) {
        $botId = isset( $params['botId'] ) ? sanitize_text_field( $params['botId'] ) : null;
        if ( !$botId || empty( $clean ) ) {
          return $this->create_rest_response( [ 'success' => true ], 200 );
        }
        $now = date( 'Y-m-d H:i:s' );
        $this->wpdb->insert( $this->table_chats, [
          'userId' => $userId,
          'ip' => $this->core->get_ip_address(),
          'messages' => json_encode( $clean ),
          'extra' => json_encode( [] ),
          'botId' => $botId,
          'chatId' => $chatId,
          'created' => $now,
          'updated' => $now,
        ] );
        return $this->create_rest_response( [ 'success' => true ], 200 );
      }

      $extra = json_decode( $chat->extra, true );
      $extra = is_array( $extra ) ? $extra : [];
      unset( $extra['responseId'], $extra['previousResponseId'], $extra['responseDate'], $extra['previousResponseDate'] );

      $this->wpdb->update(
        $this->table_chats,
        [ 'messages' => json_encode( $clean ), 'extra' => json_encode( $extra ), 'updated' => date( 'Y-m-d H:i:s' ) ],
        [ 'chatId' => $chatId ]
      );
      return $this->create_rest_response( [ 'success' => true ], 200 );
    }
    catch ( Exception $e ) {
      return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
    }
  }

  // Sanitize a client-supplied structure of unknown shape (embeddings list, tool call
  // arguments) before it goes back into the messages column. Keys and scalar leaves are
  // sanitized, with a depth cap so a crafted payload cannot recurse without end.
  private function sanitize_message_extra( $value, $depth = 0 ) {
    if ( $depth > 6 ) {
      return null;
    }
    if ( is_array( $value ) ) {
      $out = [];
      foreach ( $value as $key => $item ) {
        $out[sanitize_text_field( (string) $key )] = $this->sanitize_message_extra( $item, $depth + 1 );
      }
      return $out;
    }
    if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) || is_null( $value ) ) {
      return $value;
    }
    return sanitize_text_field( (string) $value );
  }

  public function cron_discussions() {
    // Track cron execution start
    $this->core->track_cron_start( 'mwai_discussions' );

    try {
      $this->check_db();

      // NEW CHECK: Only run if auto-titling is enabled
      if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
        $this->core->track_cron_end( 'mwai_discussions', 'success' );
        return;
      }
      // END NEW CHECK

      // Set the current user to the first admin to avoid guest limits
      $admin_users = get_users( [ 'role' => 'administrator', 'number' => 1 ] );
      if ( !empty( $admin_users ) ) {
        $admin_user = $admin_users[0];
        wp_set_current_user( $admin_user->ID );
      }

      $now = date( 'Y-m-d H:i:s' );
      $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );

      // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
      $query = $this->wpdb->prepare(
        "SELECT * FROM {$this->table_chats}
                                                                                                                                                                                  WHERE title IS NULL AND updated >= %s
                                                                                                                                                                                  ORDER BY updated DESC LIMIT 5",
        $ten_days_ago
      );
      $discussions = $this->wpdb->get_results( $query );
      if ( empty( $discussions ) ) {
        $this->core->track_cron_end( 'mwai_discussions', 'success' );
        return;
      }

      foreach ( $discussions as $discussion ) {
        $this->generate_title_for_discussion( $discussion );
      }

      $this->core->track_cron_end( 'mwai_discussions', 'success' );
    }
    catch ( Exception $e ) {
      $this->core->track_cron_end( 'mwai_discussions', 'error', $e->getMessage() );
    }
  }

  /**
  * UI route for listing discussions.
  * Here we add the "forced cron" logic for up to 5 discussions,
  * but only if auto-titling is enabled.
  */
  public function rest_discussions_ui_list( $request ) {
    try {
      $params = $request->get_json_params();
      $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
      // Get paging setting from options
      $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
      if ( $paging_option === 'None' ) {
        $default_limit = 999; // Show all discussions
      }
      else {
        $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
      }
      $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
      $botId = isset( $params['botId'] ) ? $params['botId'] : null;
      $customId = isset( $params['customId'] ) ? $params['customId'] : null;

      if ( !is_null( $customId ) ) {
        $botId = $customId;
      }
      if ( is_null( $botId ) ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
      }

      $userId = get_current_user_id();
      if ( !$userId ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
      }

      $filters = [
        [ 'accessor' => 'user',  'value' => $userId ],
        [ 'accessor' => 'botId', 'value' => $botId ],
      ];

      // Optional server-side search (content + title), used by the Workspace
      // sidebar and available to the chatbot discussions UI.
      $search = isset( $params['search'] ) ? sanitize_text_field( $params['search'] ) : '';
      if ( $search !== '' ) {
        $filters[] = [ 'accessor' => 'preview', 'value' => $search ];
      }

      // Retrieve the chats
      $chats = $this->chats_query( [], $offset, $limit, $filters );

      // NEW CHECK: only do forced titling if it's enabled
      if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
        // "Forced cron" logic: check up to 5 that have no title
        $counter = 0;
        foreach ( $chats['rows'] as &$chatRow ) {
          if ( $counter >= 5 ) {
            break;
          }
          if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
            $discussionObj = (object) $chatRow;
            $this->generate_title_for_discussion( $discussionObj );
            $counter++;
          }
        }
        // If you want the newly-updated titles to show up *immediately*:
        $chats = $this->chats_query( [], $offset, $limit, $filters );
      }
      // END NEW CHECK

      // Apply filters to discussion metadata
      foreach ( $chats['rows'] as &$chatRow ) {
        // Decode messages JSON to get the count
        $messages = json_decode( $chatRow['messages'], true );
        $message_count = is_array( $messages ) ? count( $messages ) : 0;

        // Add formatted metadata that can be filtered
        $chatRow['metadata_display'] = [
          'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
          'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
          'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
        ];
      }

      return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
    }
    catch ( Exception $e ) {
      return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
    }
  }

  public function rest_discussions_delete_admin( $request ) {
    try {
      $this->check_db();
      $params = $request->get_json_params();
      $chatsIds = $params['chatIds'];
      if ( is_array( $chatsIds ) ) {
        if ( count( $chatsIds ) === 0 ) {
          $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
        }
        foreach ( $chatsIds as $chatId ) {
          $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
        }
      }
      return $this->create_rest_response( [ 'success' => true ], 200 );
    }
    catch ( Exception $e ) {
      return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
    }
  }

  public function rest_discussions_delete( $request ) {
    try {
      $this->check_db();
      $params = $request->get_json_params();
      $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;

      if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
      }

      $userId = get_current_user_id();
      if ( !$userId ) {
        return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
      }

      foreach ( $chatIds as $chatId ) {
        $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
      }

      return $this->create_rest_response( [ 'success' => true ], 200 );
    }
    catch ( Exception $e ) {
      return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
    }
  }

  // Get latest discussion for the given parameter
  public function get_discussion( $botId, $chatId ) {
    $this->check_db();
    $chat = $this->wpdb->get_row(
      $this->wpdb->prepare(
        "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
        $chatId,
        $botId
      ),
      ARRAY_A
    );
    if ( $chat ) {
      $chat['messages'] = json_decode( $chat['messages'] );
      return $chat;
    }
    return null;
  }

  public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
    $this->check_db();
    $offset = !empty( $offset ) ? intval( $offset ) : 0;
    $limit = !empty( $limit ) ? intval( $limit ) : 5;
    $filters = !empty( $filters ) ? $filters : [];
    $this->core->sanitize_sort( $sort, 'updated', 'DESC' );

    $where_clauses = [];
    $where_values = [];

    if ( is_array( $filters ) ) {
      foreach ( $filters as $filter ) {
        $value = $filter['value'];
        if ( is_null( $value ) || $value === '' ) {
          continue;
        }
        switch ( $filter['accessor'] ) {
          case 'user':
            $isIP = filter_var( $value, FILTER_VALIDATE_IP );
            if ( $isIP ) {
              $where_clauses[] = 'ip = %s';
              $where_values[] = $value;
            }
            else {
              $where_clauses[] = 'userId = %d';
              $where_values[] = intval( $value );
            }
            break;
          case 'botId':
            $where_clauses[] = 'botId = %s';
            $where_values[] = $value;
            break;
          case 'preview':
            $like = '%' . $this->wpdb->esc_like( $value ) . '%';
            // Match the message content AND the (AI or user) title.
            $where_clauses[] = '(messages LIKE %s OR title LIKE %s)';
            $where_values[] = $like;
            $where_values[] = $like;
            break;
            // Add other cases as needed
        }
      }
    }

    $where_sql = '';
    if ( !empty( $where_clauses ) ) {
      $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
    }
    $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );

    $limit_sql = '';
    if ( $limit > 0 ) {
      $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
    }

    $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
    $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );

    // Get the total count
    $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
    $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );

    return $chats;
  }

  public function chatbot_reply( $rawText, $reply, $params, $extra ) {
    global $mwai_core;
    $query = $reply->query;
    $userIp = $mwai_core->get_ip_address();
    $userId = $mwai_core->get_user_id();
    $botId = isset( $params['botId'] ) ? $params['botId'] : null;
    $chatId = $this->core->fix_chat_id( $query, $params );
    $customId = isset( $params['customId'] ) ? $params['customId'] : null;
    $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
    $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
    $now = date( 'Y-m-d H:i:s' );

    if ( !empty( $customId ) ) {
      $botId = $customId;
    }
    $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
    $shortcutName = isset( $params['shortcutName'] ) ? $params['shortcutName'] : null;

    // If there are images, add them to the message for display purposes
    $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
    foreach ( $attachments as $attachedFile ) {
      if ( $attachedFile->is_image() && $attachedFile->get_type() === 'url' ) {
        $newMessage = "![Uploaded Image]({$attachedFile->get_url()})\n" . $newMessage;
      }
    }

    $this->check_db();
    $chat = $this->wpdb->get_row(
      $this->wpdb->prepare(
        "SELECT * FROM $this->table_chats WHERE chatId = %s",
        $chatId
      )
    );

    // Ownership guard: never append to, or take over, a discussion owned by a
    // different logged-in user. Without this, anyone holding a valid REST nonce
    // could submit another user's chatId, have the row reassigned to them (the
    // userId update below) and then read the victim's history through
    // discussions/list. A guest-owned row (userId 0) stays claimable so a
    // visitor can keep their conversation after logging in.
    // Cross-user takeover reported by Shivamani Vastrala (via WPScan/Automattic),
    // fixed in 3.5.5. Keep this on the write path; do not owner-scope the read
    // path (get_discussion), which would drop legitimately shared history.
    if ( $chat && (int) $chat->userId !== 0 && (int) $chat->userId !== (int) $userId ) {
      return $rawText;
    }

    $messageExtra = [
      'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null,
      // Tools executed to produce this reply (name, arguments, truncated result).
      'toolCalls' => isset( $extra['toolCalls'] ) ? $extra['toolCalls'] : null,
      // The model that produced this reply, so UIs can attribute each message
      // even after the user switches models mid-conversation.
      'model' => $query->model ?? null,
    ];
    $chatExtra = [
      'session' => $query->session,
      'model' => $query->model,
    ];
    if ( isset( $query->temperature ) ) {
      $chatExtra['temperature'] = $query->temperature;
    }
    if ( !empty( $query->context ) ) {
      $chatExtra['context'] = $query->context;
    }
    if ( !empty( $params['parentBotId'] ) ) {
      $chatExtra['parentBotId'] = $params['parentBotId'];
    }
    if ( $query instanceof Meow_MWAI_Query_Assistant ) {
      $chatExtra['assistantId'] = $query->assistantId;
      $chatExtra['threadId'] = $query->threadId;
      $chatExtra['storeId'] = $query->storeId;
    }

    // Store response ID and date for Responses API.
    // The extra of an existing discussion is MERGED, not replaced, so an absent key keeps
    // whatever was stored before. That is the right default, but it means a caller cannot
    // clear the chain by simply omitting the id. An explicit null does clear it, which is
    // how a turn ending on an unanswered client-side function call unsticks itself.
    if ( !empty( $extra['responseId'] ) ) {
      $chatExtra['previousResponseId'] = $extra['responseId'];
      $chatExtra['previousResponseDate'] = $now;
    }
    else if ( array_key_exists( 'responseId', $extra ) ) {
      $chatExtra['previousResponseId'] = null;
      $chatExtra['previousResponseDate'] = null;
    }

    $nowMs = (int) round( microtime( true ) * 1000 );
    if ( $chat ) {
      $chat->messages = json_decode( $chat->messages );
      $userMessage = [ 'role' => 'user', 'content' => $newMessage, 'timestamp' => $nowMs ];
      if ( $shortcutName ) {
        $userMessage['shortcutName'] = $shortcutName;
        $userMessage['shortcutPrompt'] = $query->get_message();
      }
      $chat->messages[] = $userMessage;
      $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra, 'timestamp' => $nowMs ];
      $chat->messages = json_encode( $chat->messages );

      // Update or merge extra data
      $existingExtra = json_decode( $chat->extra, true ) ?: [];
      $mergedExtra = array_merge( $existingExtra, $chatExtra );

      $this->wpdb->update(
        $this->table_chats,
        [
          'userId' => $userId,
          'messages' => $chat->messages,
          'extra' => json_encode( $mergedExtra ),
          'updated' => $now
        ],
        [ 'id' => $chat->id ]
      );
    }
    else {
      $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
      $messages = [];
      if ( !empty( $startSentence ) ) {
        $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
      }
      $userMessage = [ 'role' => 'user', 'content' => $newMessage, 'timestamp' => $nowMs ];
      if ( $shortcutName ) {
        $userMessage['shortcutName'] = $shortcutName;
        $userMessage['shortcutPrompt'] = $query->get_message();
      }
      $messages[] = $userMessage;
      $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra, 'timestamp' => $nowMs ];
      $chat = [
        'userId' => $userId,
        'ip' => $userIp,
        'messages' => json_encode( $messages ),
        'extra' => json_encode( $chatExtra ),
        'botId' => $botId,
        'chatId' => $chatId,
        'threadId' => $threadId,
        'storeId' => $storeId,
        'created' => $now,
        'updated' => $now
      ];
      $this->wpdb->insert( $this->table_chats, $chat );
    }
    return $rawText;
  }

  public function format_messages( $json, $format = 'html' ) {
    $html = '';
    if ( $format === 'html' ) {
      try {
        $conversation = json_decode( $json, true );
        if ( json_last_error() !== JSON_ERROR_NONE ) {
          return 'Invalid JSON format';
        }
        foreach ( $conversation as $message ) {
          $role = ucfirst( $message['role'] );
          $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
        }
      }
      catch ( Exception $e ) {
        error_log( $e->getMessage() );
        return 'Error while formatting the message';
      }
    }
    $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
    return $html;
  }

  /**
  * Commits a discussion into the database (create or update if the same chatId is found).
  *
  * @param Meow_MWAI_Discussion $discussionObject
  * @return bool True if success, false if error
  */
  public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
    $this->check_db();

    // 1. Check if a discussion with the same chatId already exists
    $chat = $this->wpdb->get_row(
      $this->wpdb->prepare(
        "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
        $discussionObject->chatId
      ),
      ARRAY_A
    );

    // 2. Prepare data for DB
    $userIp = $this->core->get_ip_address();
    $userId = $this->core->get_user_id();
    $now = date( 'Y-m-d H:i:s' );

    $data = [
      'userId' => $userId,
      'ip' => $userIp,
      'botId' => $discussionObject->botId,
      'chatId' => $discussionObject->chatId,
      'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
      'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
      'updated' => $now,
    ];

    // 3. Update if found, otherwise insert a new row
    if ( $chat ) {
      $updateRes = $this->wpdb->update(
        $this->table_chats,
        $data,
        [ 'id' => $chat['id'] ]
      );
      if ( $updateRes === false ) {
        error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
        return false;
      }
    }
    else {
      // For insertion, also set "created"
      $data['created'] = $now;
      $insertRes = $this->wpdb->insert( $this->table_chats, $data );
      if ( $insertRes === false ) {
        error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
        return false;
      }
    }

    return true;
  }

  public function check_db() {
    if ( $this->db_check ) {
      return true;
    }

    // Per-module version check: skip SHOW TABLES if already verified for this version.
    if ( get_option( 'mwai_db_version_discussions' ) === MWAI_VERSION ) {
      $this->db_check = true;
      return true;
    }

    $this->db_check = !(
      strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
          != strtolower( $this->table_chats )
    );
    if ( !$this->db_check ) {
      $this->create_db();
      $this->db_check = !(
        strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
            != strtolower( $this->table_chats )
      );
    }

    if ( $this->db_check ) {
      // TODO: Remove after 2026-10-15. Upgrade legacy TEXT (64KB) messages column to MEDIUMTEXT (16MB)
      // so long conversations aren't truncated on insert.
      $column_info = $this->wpdb->get_row( "SHOW COLUMNS FROM $this->table_chats WHERE Field = 'messages'" );
      if ( $column_info && stripos( $column_info->Type, 'mediumtext' ) === false
        && stripos( $column_info->Type, 'longtext' ) === false ) {
        $this->wpdb->query( "ALTER TABLE $this->table_chats MODIFY COLUMN messages MEDIUMTEXT NOT NULL" );
      }
      update_option( 'mwai_db_version_discussions', MWAI_VERSION, true );
    }

    return $this->db_check;
  }

  public function create_db() {
    $charset_collate = $this->wpdb->get_charset_collate();
    $sqlLogs = "CREATE TABLE $this->table_chats (
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              id BIGINT(20) NOT NULL AUTO_INCREMENT,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                userId BIGINT(20) NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  ip VARCHAR(64) NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    title VARCHAR(64) NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      messages MEDIUMTEXT NOT NULL NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      extra LONGTEXT NOT NULL NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      botId VARCHAR(64) NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        chatId VARCHAR(64) NOT NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          threadId VARCHAR(64) NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            storeId VARCHAR(64) NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              created DATETIME NOT NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              updated DATETIME NOT NULL,
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              PRIMARY KEY  (id),
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                INDEX chatId (chatId)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                ) $charset_collate;";
    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sqlLogs );
  }

  /**
   * Handle cleanup task for discussions
   */
  public function handle_cleanup_task( $result, $job ) {
    $start = microtime( true );
    $retention_option = $this->core->get_option( 'chatbot_discussions_retention_days' );
    // "Never" (or 0 / negative) disables the cleanup entirely.
    if ( $retention_option === 'Never' || (int) $retention_option <= 0 ) {
      return [
        'ok' => true,
        'done' => true,
        'message' => 'Discussions cleanup disabled (retention set to Never)',
      ];
    }
    $retention_days = (int) apply_filters( 'mwai_discussions_retention_days', (int) $retention_option );
    if ( $retention_days <= 0 ) {
      return [
        'ok' => true,
        'done' => true,
        'message' => 'Discussions cleanup disabled by filter',
      ];
    }
    $cutoff = date( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );

    // Check if discussions table exists
    $table_exists = $this->wpdb->get_var( "SHOW TABLES LIKE '{$this->table_chats}'" );
    if ( !$table_exists ) {
      return [
        'ok' => true,
        'done' => true,
        'message' => 'Discussions table does not exist yet',
      ];
    }

    // Get current progress
    $deleted_total = isset( $job['meta']['deleted_total'] ) ? (int) $job['meta']['deleted_total'] : 0;
    $last_id = isset( $job['meta']['last_id'] ) ? (int) $job['meta']['last_id'] : 0;

    // Delete in batches
    $batch_size = 100;
    $deleted_batch = 0;

    $old_discussions = $this->wpdb->get_results( $this->wpdb->prepare(
      "SELECT id FROM {$this->table_chats} 
       WHERE updated < %s AND id > %d 
       ORDER BY id ASC 
       LIMIT %d",
      $cutoff,
      $last_id,
      $batch_size
    ) );

    if ( !empty( $old_discussions ) ) {
      $ids = wp_list_pluck( $old_discussions, 'id' );
      $ids_string = implode( ',', array_map( 'intval', $ids ) );

      $deleted_batch = $this->wpdb->query(
        "DELETE FROM {$this->table_chats} WHERE id IN ($ids_string)"
      );

      $deleted_total += $deleted_batch;
      $last_id = end( $ids );
    }

    // Check if we have more to process or time is running out
    $has_more = count( $old_discussions ) === $batch_size;
    $time_elapsed = microtime( true ) - $start;

    if ( $has_more && $time_elapsed < 8 ) {
      // Continue processing
      return [
        'ok' => true,
        'done' => false,
        'message' => sprintf( 'Deleted %d discussions (total: %d)', $deleted_batch, $deleted_total ),
        'meta' => [
          'deleted_total' => $deleted_total,
          'last_id' => $last_id,
        ],
        'step' => $job['step'] + 1,
        'step_name' => 'batch_' . ( $job['step'] + 1 ),
      ];
    }

    // Completed
    return [
      'ok' => true,
      'done' => true,
      'message' => sprintf( 'Cleanup complete. Deleted %d discussions older than %d days', $deleted_total, $retention_days ),
      'meta' => [
        'deleted_total' => 0,
        'last_id' => 0,
      ],
    ];
  }
}
