<?php
/**
 * Chat model class.
 *
 * @class ChatModel
 * @package KimaAI\Models
 * @since   1.0.0
 */

declare(strict_types=1);

namespace KimaAI\Models;

defined('ABSPATH') || exit;

/**
 * Class ChatModel
 */
class ChatModel {

  use HasSchema;

  /**
   * The table name.
   *
   * @var string
   */
  public const TABLE = 'kimaai_chats';

  /**
   * The table primary key.
   *
   * @var string
   */
  public const PRIMARY_KEY = 'id';

  /**
   * The table columns.
   *
   * @var array
   */
  public const COLUMNS = [
    'id' => 'BIGINT(20) NOT NULL AUTO_INCREMENT',
    'user_id' => 'BIGINT(20) NOT NULL',
    'ip' => 'VARCHAR(64) NOT NULL',
    'session_id' => 'VARCHAR(64) NOT NULL',
    'input' => 'LONGTEXT NOT NULL',
    'response' => 'LONGTEXT NOT NULL',
    'created_at' => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
  ];

  /**
   * The table name.
   *
   * @var string
   */
  private string $table_name;

  /**
   * ChatModel constructor.
   */
  public function __construct() {
    global $wpdb;
    $this->table_name = esc_sql($wpdb->prefix . self::TABLE);
  }

  /**
   * Inserts a new chat message.
   *
   * @param array $data The chat data.
   * @return int The new chat ID.
   */
  public function insert(array $data): int {
    global $wpdb;
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
    $result = $wpdb->insert(
      $this->table_name,
      [
        'user_id' => $data['user_id'],
        'ip' => $data['ip'],
        'session_id' => $data['session_id'],
        'input' => $data['input'],
        'response' => $data['response'],
        'created_at' => current_time('mysql'),
      ]
    );
    return $result ? (int) $wpdb->insert_id : 0;
  }

  /**
   * Gets chat history for a session.
   *
   * @param string $session_id The session ID.
   * @return array The chat history.
   */
  public function getSessionHistory(string $session_id): array {
    global $wpdb;

    // phpcs:ignore
    $query = $wpdb->prepare("SELECT * FROM {$this->table_name} WHERE session_id = %s ORDER BY created_at ASC", $session_id);
    // phpcs:ignore
    $results = $wpdb->get_results($query, ARRAY_A);

    $results = $this->addResponseAndMessage($results);
    return $results;
  }

  /**
   * Gets all chat history.
   *
   * @param bool $is_playground Whether to get playground history.
   * @return array The chat history.
   */
  public function getAll(bool $is_playground = false): array {
    global $wpdb;

    if ($is_playground) {
      // For Playground history we only need each session_id once to avoid duplicates.
      // Also with ip = 0 we can filter out the playground messages.
      // phpcs:ignore
      $query = "SELECT m.* FROM {$this->table_name} AS m
	      JOIN (SELECT session_id, MIN(created_at) AS first_created_at FROM {$this->table_name} GROUP BY session_id) AS first
	      ON first.session_id = m.session_id
	      AND first.first_created_at = m.created_at
	      WHERE m.ip = 0
	      ORDER BY m.created_at DESC";
    }
    else {
      // phpcs:ignore
      $query = "SELECT * FROM {$this->table_name} ORDER BY created_at ASC";
    }

    // phpcs:ignore
    $results = $wpdb->get_results($query, ARRAY_A);

    $results = $this->addResponseAndMessage($results);
    return $results;
  }

  /**
   * Deletes a chat message via ID.
   *
   * @param int $id The chat ID.
   * @return bool True if the chat message was deleted, false otherwise.
   */
  public function delete(int $id): bool {
    global $wpdb;

    // Get the session ID for cache invalidation.
    // phpcs:ignore
    $session_id = $wpdb->get_var($wpdb->prepare("SELECT session_id FROM {$this->table_name} WHERE id = %d", $id));
    if (!$session_id) {
      return false;
    }

    // phpcs:ignore
    $result = $wpdb->delete($this->table_name, ['id' => $id]);

    return $result !== false;
  }

  /**
   * Count the number of chat messages for a session ID.
   *
   * @param string $session_id The session ID.
   * @return int The number of chat messages.
   */
  public function countChatbotMessagesBySessionId(string $session_id): int {
    global $wpdb;

    $query = $wpdb->prepare(
      // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
      "SELECT COUNT(*) FROM {$this->table_name} WHERE session_id = %s",
      $session_id
    );
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
    return (int) $wpdb->get_var($query);
  }

  /**
   * Counts the number of chat messages for an IP address in today's timeframe (00:00 to 23:59).
   *
   * @param string $ip The IP address.
   * @return int The number of chat messages.
   */
  public function countChatbotMessagesByIpToday(string $ip): int {
    global $wpdb;

    $today = wp_date('Y-m-d');
    $start_of_day = $today . ' 00:00:00';
    $end_of_day = $today . ' 23:59:59';

    // phpcs:ignore
    $query = $wpdb->prepare("SELECT COUNT(*) FROM {$this->table_name} WHERE ip = %s AND created_at BETWEEN %s AND %s", $ip, $start_of_day, $end_of_day);
    // phpcs:ignore
    return (int) $wpdb->get_var($query);
  }

  /**
   * Adds ai_response and user_message to the results as separate keys for each row.
   *
   * @param array $results The chat history results.
   * @return array
   */
  private function addResponseAndMessage(array $results): array {
    foreach ($results as $idx => $item) {
      $response = json_decode((string) ($item['response'] ?? ''), true) ?: [];
      $input = json_decode((string) ($item['input'] ?? ''), true) ?: [];

      $provider = (string) ($input['ai_provider'] ?? '');

      if (in_array($provider, ['openai', 'openrouter', 'ollama'], true)) {
        $ai_response = $this->extractOpenAiResponseText($response);
        $user_message = (string) ($input['prompt'] ?? $input['user_message'] ?? '');

        $usage = $response['usage'] ?? [];
        $input_tokens = $usage['input_tokens'] ?? null;
        $output_tokens = $usage['output_tokens'] ?? null;
        $total_tokens = $usage['total_tokens'] ?? null;
      }
      else {
        // Other providers
        $ai_response = is_array($response['output'] ?? null)
        ? trim(implode(' ', $response['output']))
        : '';

        $user_message = (string) ($input['user_message'] ?? $input['prompt'] ?? '');

        $input_tokens = 'Not Supported';
        $output_tokens = 'Not Supported';
        $total_tokens = 'Not Supported';
      }

      $renderables = [];

      if (!empty($response['_kimaai_renderables']) && is_array($response['_kimaai_renderables'])) {
        $renderables = apply_filters(
          'kimaai_chatbot_hydrated_renderables',
          [],
          $response['_kimaai_renderables'],
          $response,
          $input,
          $item
        );

        if (!is_array($renderables)) {
          $renderables = [];
        }
      }
      $results[$idx]['renderables'] = array_values($renderables);
      $results[$idx]['user_message'] = $user_message;
      $results[$idx]['ai_response'] = $ai_response;
      $results[$idx]['ai_model'] = (string) ($input['ai_model'] ?? $input['ai_model'] ?? $input['model'] ?? '');
      $results[$idx]['input_tokens'] = $input_tokens;
      $results[$idx]['output_tokens'] = $output_tokens;
      $results[$idx]['total_tokens'] = $total_tokens;
    }

    return $results;
  }

  /**
   * Extract the assistant text from an OpenAI Responses API payload.
   * Returns '' if no message output exists yet (e.g., tool-call-only response).
   */
  private function extractOpenAiResponseText(array $response): string {
    $output = $response['output'] ?? [];
    if (!is_array($output) || !$output) {
      return '';
    }

    // Walk backwards: the final assistant message is typically the last "message" item.
    for ($i = count($output) - 1; $i >= 0; $i--) {
      $item = $output[$i];
      if (!is_array($item)) {
        continue;
      }

      if (($item['type'] ?? null) !== 'message') {
        continue;
      }

      $content = $item['content'] ?? [];
      if (!is_array($content)) {
        continue;
      }

      $texts = [];
      foreach ($content as $part) {
        if (!is_array($part)) {
          continue;
        }
        $type = (string) ($part['type'] ?? '');
        if ($type === 'output_text' || $type === 'text') {
          $text = (string) ($part['text'] ?? '');
          if ($text !== '') {
            $texts[] = $text;
          }
        }
      }

      $joined = trim(implode('', $texts));
      if ($joined !== '') {
        return $joined;
      }
    }

    return '';
  }

  /**
   * Gets one summary row per session.
   *
   * Returns the first message row of each session plus:
   * - message_count
   * - first_created_at
   * - last_created_at
   *
   * @param bool $is_playground Whether to get playground sessions only.
   * @return array
   */
  public function getAllSessions(bool $is_playground = false): array {
    global $wpdb;

    $where = $is_playground ? "WHERE ip = '0'" : '';

    // We use MIN(id) as the first row per session.
    // That gives us exactly one row per session for the overview table.
    // phpcs:ignore
    $query = "SELECT m.*, agg.message_count, agg.first_created_at, agg.last_created_at
        FROM {$this->table_name} AS m
        INNER JOIN (
            SELECT
                session_id,
                MIN(id) AS first_id,
                MIN(created_at) AS first_created_at,
                MAX(created_at) AS last_created_at,
                COUNT(*) AS message_count
            FROM {$this->table_name}
            {$where}
            GROUP BY session_id
        ) AS agg
            ON agg.first_id = m.id
        ORDER BY agg.last_created_at DESC";

    // phpcs:ignore
    $results = $wpdb->get_results($query, ARRAY_A);

    $results = $this->addResponseAndMessage($results);

    return $results;
  }

  /**
   * Deletes all chat rows for a session ID.
   *
   * @param string $session_id The session ID.
   * @return bool True if rows were deleted, false otherwise.
   */
  public function deleteSession(string $session_id): bool {
    global $wpdb;

    if ($session_id === '') {
      return false;
    }

    // Check whether the session exists first.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    $existing_count = (int) $wpdb->get_var(
      $wpdb->prepare(
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        "SELECT COUNT(*) FROM {$this->table_name} WHERE session_id = %s",
        $session_id
      )
    );

    if ($existing_count < 1) {
      return false;
    }

    // Delete every row that belongs to the session.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
    $result = $wpdb->query(
      $wpdb->prepare(
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        "DELETE FROM {$this->table_name} WHERE session_id = %s",
        $session_id
      )
    );

    return $result !== false;
  }

  /**
   * Deletes all chat rows for multiple session IDs.
   *
   * @param array $session_ids The session IDs.
   * @return int The number of affected rows.
   */
  public function deleteSessions(array $session_ids): int {
    global $wpdb;

    $session_ids = array_values(array_filter(array_map(
      static fn($session_id) => sanitize_text_field((string) $session_id),
      $session_ids
    )));

    if (empty($session_ids)) {
      return 0;
    }

    $placeholders = implode(', ', array_fill(0, count($session_ids), '%s'));

    $query = $wpdb->prepare(
      // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
      "DELETE FROM {$this->table_name} WHERE session_id IN ({$placeholders})",
      ...$session_ids
    );

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
    $result = $wpdb->query($query);

    return $result === false ? 0 : (int) $result;
  }

  /**
   * Gets expired session IDs whose latest activity is older than the cutoff.
   *
   * @param string $cutoff The cutoff timestamp in MySQL format.
   * @param int $batch_size Maximum number of session IDs to return.
   * @return string[]
   */
  public function getExpiredSessionIds(string $cutoff, int $batch_size): array {
    global $wpdb;

    $cutoff = sanitize_text_field($cutoff);
    $batch_size = max(1, absint($batch_size));

    if ($cutoff === '') {
      return [];
    }

    // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    $prepared_get_expired = $wpdb->prepare(
      "SELECT session_id FROM {$this->table_name}
      GROUP BY session_id HAVING MAX(created_at) < %s
      ORDER BY MAX(created_at) ASC LIMIT %d",
      $cutoff,
      $batch_size
    );
    // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
    $rows = $wpdb->get_col($prepared_get_expired);

    return array_values(array_unique(array_filter(array_map(
      static fn($session_id) => sanitize_text_field((string) $session_id),
      $rows
    ))));
  }

  /**
   * Counts how many sessions from the given list exist in the table.
   *
   * @param array $session_ids Session IDs to count.
   * @return int
   */
  public function countSessions(array $session_ids): int {
    global $wpdb;

    $session_ids = array_values(array_filter(array_map(
      static fn($session_id) => sanitize_text_field((string) $session_id),
      $session_ids
    )));

    if (empty($session_ids)) {
      return 0;
    }

    $placeholders = implode(', ', array_fill(0, count($session_ids), '%s'));

    $query = $wpdb->prepare(
      // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
      "SELECT COUNT(*) FROM {$this->table_name} WHERE session_id IN ({$placeholders})",
      ...$session_ids
    );

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
    return (int) $wpdb->get_var($query);
  }
}
