<?php
/**
 * KooKoa Folders — Bulk postmeta INSERT helper.
 *
 * Replaces an N-iteration `update_post_meta()` loop with a single multi-row
 * INSERT. Used by Dynamic smart-folder size resolution and the PRO Stats
 * indexers, both of which previously paid 2N queries to populate the
 * `_wp_attached_file_size` / `_kookoawpf_dup_md5` / `_kookoawpf_dup_filesize`
 * postmeta entries.
 *
 * Caller contract: every row passed in is for a (post_id, meta_key) pair
 * that is KNOWN NOT TO EXIST in `wp_postmeta`. The helper does not check
 * — callers are expected to do an `update_meta_cache()` warm + a
 * `get_post_meta()` empty-check before adding the row to the batch.
 * Passing an already-existing pair would result in a duplicate postmeta
 * row, which `update_post_meta()` would have avoided.
 *
 * @package KookoaWPF
 * @license GPL-2.0-or-later
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Multi-row postmeta INSERT helper.
 *
 * @since 1.1.2
 */
class KookoaWPF_Postmeta_Bulk {

	/**
	 * Rows per single INSERT statement. Bounded so we never push past
	 * MySQL's `max_allowed_packet` (default 16–64 MB) on installs with
	 * unusually wide meta values.
	 */
	private const CHUNK_SIZE = 500;

	/**
	 * Bulk-INSERT a batch of postmeta rows.
	 *
	 * @param array<int, array{post_id:int, key:string, value:int|string}> $rows Rows to insert; each must carry post_id (positive int), key (non-empty meta key string), value (int or string meta value, written as-is, NOT serialized).
	 * @return int Total rows actually written across all chunks. 0 on empty input.
	 */
	public static function insert( array $rows ): int {
		if ( empty( $rows ) ) {
			return 0;
		}

		global $wpdb;
		$total          = 0;
		$cache_post_ids = array();

		foreach ( array_chunk( $rows, self::CHUNK_SIZE ) as $chunk ) {
			$placeholders = array();
			$values       = array();
			foreach ( $chunk as $row ) {
				if ( ! isset( $row['post_id'], $row['key'] ) ) {
					continue;
				}
				$post_id = (int) $row['post_id'];
				$key     = (string) $row['key'];
				if ( $post_id <= 0 || '' === $key ) {
					continue;
				}
				$placeholders[]   = '(%d, %s, %s)';
				$values[]         = $post_id;
				$values[]         = $key;
				$values[]         = (string) ( $row['value'] ?? '' );
				$cache_post_ids[] = $post_id;
			}
			if ( empty( $placeholders ) ) {
				continue;
			}

			// Build the prepared statement inline so PHPCS sees a single
			// `$wpdb->prepare()` call with a static template + placeholder
			// block. Both the table name (from $wpdb) and the placeholder
			// list (literal '(%d, %s, %s)' repeated by array_fill above)
			// are static — no user data lands in the SQL string.
			$prepared = $wpdb->prepare(
				'INSERT INTO ' . $wpdb->postmeta . ' (post_id, meta_key, meta_value) VALUES ' . implode( ',', $placeholders ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholder block is a literal '(%d, %s, %s)' repeated; $values are passed to prepare() below.
				$values
			);

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared -- Bulk multi-row INSERT replaces N update_post_meta() calls; $prepared is the return value of $wpdb->prepare() above; postmeta cache is invalidated immediately below via wp_cache_delete().
			$result = $wpdb->query( $prepared );

			if ( false !== $result ) {
				$total += (int) $result;
			}

			// Invalidate the WP postmeta object cache for every post id in
			// this chunk, so subsequent get_post_meta() reads in the same
			// request see the freshly INSERTed values instead of the cached
			// empty placeholder. Lives inside the chunk loop (not after it)
			// so the NoCaching sniff considers it adjacent to the query.
			foreach ( $chunk as $row ) {
				if ( isset( $row['post_id'] ) && (int) $row['post_id'] > 0 ) {
					wp_cache_delete( (int) $row['post_id'], 'post_meta' );
				}
			}
		}

		return $total;
	}
}
