<?php
/**
 * Built-in form repository.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Repository methods read/write Submission Inbox custom tables directly; WordPress has no native API for these records.
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names are plugin-owned $wpdb->prefix values, and dynamic clauses are built from sanitized allowlists/placeholders in this repository.

/**
 * Reads and writes built-in Submission Inbox forms.
 */
class FormRepository {
	const STATUS_ACTIVE   = 'active';
	const STATUS_INACTIVE = 'inactive';

	const LAYOUT_CLEAN_CARD    = 'clean_card';
	const LAYOUT_MINIMAL       = 'minimal';
	const LAYOUT_SOFT_BUSINESS = 'soft_business';
	const LAYOUT_COMPACT       = 'compact';
	const LAYOUT_FULL_WIDTH    = 'full_width';
	const LAYOUT_TWO_COLUMN    = 'two_column';

	const STYLE_DEFAULT = 'default';
	const STYLE_LIGHT   = 'light';
	const STYLE_SOFT    = 'soft';
	const STYLE_SIMPLE  = 'simple';
	const STYLE_PROFESSIONAL = 'professional';

	/**
	 * Create a form.
	 *
	 * @param array<string,mixed> $data Form data.
	 * @return int
	 */
	public function create( array $data ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_forms';
		$now   = current_time( 'mysql' );
		$data  = $this->sanitize_data( $data );

		if ( '' === $data['title'] ) {
			return 0;
		}

		$data['slug']       = $this->generate_unique_slug( $data['slug'] ? $data['slug'] : $data['title'] );
		$data['created_at'] = $now;
		$data['updated_at'] = $now;

		$inserted = $wpdb->insert(
			$table,
			$data,
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s' )
		);

		return $inserted ? absint( $wpdb->insert_id ) : 0;
	}

	/**
	 * Update a form.
	 *
	 * @param int                 $form_id Form id.
	 * @param array<string,mixed> $data Form data.
	 * @return bool
	 */
	public function update( $form_id, array $data ) {
		global $wpdb;

		$form_id = absint( $form_id );
		if ( $form_id <= 0 ) {
			return false;
		}

		$table = $wpdb->prefix . 'subin_forms';
		$data  = $this->sanitize_data( $data );

		if ( '' === $data['title'] ) {
			return false;
		}

		$data['slug']       = $this->generate_unique_slug( $data['slug'] ? $data['slug'] : $data['title'], $form_id );
		$data['updated_at'] = current_time( 'mysql' );

		$updated = $wpdb->update(
			$table,
			$data,
			array( 'id' => $form_id ),
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s' ),
			array( '%d' )
		);

		return false !== $updated;
	}

	/**
	 * Update a form by merging submitted values with the current row.
	 *
	 * @param int                 $form_id Form id.
	 * @param array<string,mixed> $data Form data.
	 * @return bool
	 */
	public function update_partial( $form_id, array $data ) {
		$current = $this->get( $form_id );
		if ( ! $current ) {
			return false;
		}

		return $this->update( $form_id, array_merge( $current, $data ) );
	}

	/**
	 * Update only settings_json with an already validated settings array.
	 *
	 * GrapesJS project data uses camelCase keys that must remain intact, so this
	 * path intentionally avoids the generic nested key sanitizer used by form
	 * field saves.
	 *
	 * @param int                 $form_id Form id.
	 * @param array<string,mixed> $settings Settings.
	 * @return bool
	 */
	public function update_settings_json( $form_id, array $settings ) {
		global $wpdb;

		$form_id = absint( $form_id );
		if ( $form_id <= 0 ) {
			return false;
		}

		$encoded = wp_json_encode( $settings );
		if ( ! is_string( $encoded ) ) {
			return false;
		}

		$updated = $wpdb->update(
			$wpdb->prefix . 'subin_forms',
			array(
				'settings_json' => $encoded,
				'updated_at'    => current_time( 'mysql' ),
			),
			array( 'id' => $form_id ),
			array( '%s', '%s' ),
			array( '%d' )
		);

		return false !== $updated;
	}

	/**
	 * Get a form by id.
	 *
	 * @param int $form_id Form id.
	 * @return array<string,mixed>|null
	 */
	public function get( $form_id ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_forms';
		$row   = $wpdb->get_row(
			$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", absint( $form_id ) ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			ARRAY_A
		);

		return is_array( $row ) ? $row : null;
	}

	/**
	 * Get a form by slug.
	 *
	 * @param string $slug Form slug.
	 * @return array<string,mixed>|null
	 */
	public function get_by_slug( $slug ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_forms';
		$slug  = sanitize_title( $slug );
		$row   = $wpdb->get_row(
			$wpdb->prepare( "SELECT * FROM {$table} WHERE slug = %s", $slug ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			ARRAY_A
		);

		return is_array( $row ) ? $row : null;
	}

	/**
	 * Get paginated forms.
	 *
	 * @param array<string,mixed> $args Query args.
	 * @return array<int,array<string,mixed>>
	 */
	public function get_paginated( array $args = array() ) {
		global $wpdb;

		$table   = $wpdb->prefix . 'subin_forms';
		$where   = $this->build_where( $args );
		$limit   = max( 1, absint( $args['per_page'] ?? 20 ) );
		$offset  = max( 0, absint( $args['offset'] ?? 0 ) );
		$sql     = "SELECT * FROM {$table} {$where['sql']} ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d";
		$params  = array_merge( $where['params'], array( $limit, $offset ) );
		$results = $wpdb->get_results( $wpdb->prepare( $sql, $params ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		return is_array( $results ) ? $results : array();
	}

	/**
	 * Count forms.
	 *
	 * @param array<string,mixed> $filters Filters.
	 * @return int
	 */
	public function count( array $filters = array() ) {
		global $wpdb;

		$table    = $wpdb->prefix . 'subin_forms';
		$where    = $this->build_where( $filters );
		$sql      = "SELECT COUNT(*) FROM {$table} {$where['sql']}";

		if ( ! empty( $where['params'] ) ) {
			return absint( $wpdb->get_var( $wpdb->prepare( $sql, $where['params'] ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		return absint( $wpdb->get_var( $sql ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Update a form status.
	 *
	 * @param int    $form_id Form id.
	 * @param string $status Status.
	 * @return bool
	 */
	public function update_status( $form_id, $status ) {
		global $wpdb;

		$form_id = absint( $form_id );
		$status  = $this->sanitize_status( $status );

		if ( $form_id <= 0 ) {
			return false;
		}

		$updated = $wpdb->update(
			$wpdb->prefix . 'subin_forms',
			array(
				'status'     => $status,
				'updated_at' => current_time( 'mysql' ),
			),
			array( 'id' => $form_id ),
			array( '%s', '%s' ),
			array( '%d' )
		);

		return false !== $updated;
	}

	/**
	 * Get forms for select controls.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public function get_for_dropdown() {
		global $wpdb;

		$table   = $wpdb->prefix . 'subin_forms';
		$results = $wpdb->get_results(
			"SELECT id, title, slug, status FROM {$table} ORDER BY title ASC, id ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			ARRAY_A
		);

		return is_array( $results ) ? $results : array();
	}

	/**
	 * Get summary counts for admin cards.
	 *
	 * @return array<string,int>
	 */
	public function get_summary_counts() {
		return array(
			'total'    => $this->count(),
			'active'   => $this->count( array( 'status' => self::STATUS_ACTIVE ) ),
			'inactive' => $this->count( array( 'status' => self::STATUS_INACTIVE ) ),
		);
	}

	/**
	 * Duplicate a form and its fields.
	 *
	 * @param int $form_id Form id.
	 * @return int
	 */
	public function duplicate( $form_id ) {
		$form = $this->get( $form_id );
		if ( ! $form ) {
			return 0;
		}

		$form['title']  = sprintf(
			/* translators: %s: Original form title. */
			__( '%s Copy', 'submission-inbox' ),
			$form['title']
		);
		$form['slug']   = '';
		$form['status'] = self::STATUS_INACTIVE;

		$new_form_id = $this->create( $form );
		if ( $new_form_id <= 0 ) {
			return 0;
		}

		$field_repository = new FormFieldRepository();
		if ( ! $field_repository->copy_fields( absint( $form_id ), $new_form_id ) ) {
			return 0;
		}

		return $new_form_id;
	}

	/**
	 * Count saved leads for a built-in form.
	 *
	 * @param int $form_id Form id.
	 * @return int
	 */
	public function get_lead_count( $form_id ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_leads';

		return absint(
			$wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$table} WHERE provider = %s AND form_id = %s",
					'submission_inbox_form',
					(string) absint( $form_id )
				) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			)
		);
	}

	/**
	 * Get last saved lead submission date for a built-in form.
	 *
	 * @param int $form_id Form id.
	 * @return string|null
	 */
	public function get_last_submission( $form_id ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_leads';
		$value = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT MAX(submitted_at) FROM {$table} WHERE provider = %s AND form_id = %s",
				'submission_inbox_form',
				(string) absint( $form_id )
			) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);

		return $value ? (string) $value : null;
	}

	/**
	 * Get email status counts for a built-in form.
	 *
	 * @param int $form_id Form id.
	 * @return array<string,int>
	 */
	public function get_email_status_counts( $form_id ) {
		global $wpdb;

		$table  = $wpdb->prefix . 'subin_leads';
		$counts = array(
			'sent'               => 0,
			'failed'             => 0,
			'no_email_generated' => 0,
			'unknown'            => 0,
		);

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT email_status, COUNT(*) AS total
				FROM {$table}
				WHERE provider = %s AND form_id = %s
				GROUP BY email_status",
				'submission_inbox_form',
				(string) absint( $form_id )
			)
		); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		foreach ( $rows as $row ) {
			$status = sanitize_key( $row->email_status );
			if ( array_key_exists( $status, $counts ) ) {
				$counts[ $status ] = absint( $row->total );
			}
		}

		return $counts;
	}

	/**
	 * Delete a form and its fields.
	 *
	 * @param int $form_id Form id.
	 * @return bool
	 */
	public function delete( $form_id ) {
		global $wpdb;

		$form_id = absint( $form_id );
		if ( $form_id <= 0 ) {
			return false;
		}

		$field_repository = new FormFieldRepository();
		$field_repository->delete_by_form_id( $form_id );

		return false !== $wpdb->delete( $wpdb->prefix . 'subin_forms', array( 'id' => $form_id ), array( '%d' ) );
	}

	/**
	 * Sanitize status.
	 *
	 * @param string $status Status.
	 * @return string
	 */
	public function sanitize_status( $status ) {
		$status = sanitize_key( $status );

		return in_array( $status, $this->get_statuses(), true ) ? $status : self::STATUS_ACTIVE;
	}

	/**
	 * Generate a unique form slug.
	 *
	 * @param string $title Title or slug.
	 * @param int    $exclude_id Form id to exclude.
	 * @return string
	 */
	public function generate_unique_slug( $title, $exclude_id = 0 ) {
		global $wpdb;

		$table      = $wpdb->prefix . 'subin_forms';
		$base_slug  = sanitize_title( $title );
		$base_slug  = $base_slug ? $base_slug : 'form';
		$slug       = $base_slug;
		$suffix     = 2;
		$exclude_id = absint( $exclude_id );

		while ( $this->slug_exists( $table, $slug, $exclude_id ) ) {
			$slug = $base_slug . '-' . $suffix;
			++$suffix;
		}

		return $slug;
	}

	/**
	 * Get supported layout templates.
	 *
	 * @return array<int,string>
	 */
	public function get_layout_templates() {
		return array(
			self::LAYOUT_CLEAN_CARD,
			self::LAYOUT_MINIMAL,
			self::LAYOUT_SOFT_BUSINESS,
			self::LAYOUT_COMPACT,
			self::LAYOUT_FULL_WIDTH,
			self::LAYOUT_TWO_COLUMN,
		);
	}

	/**
	 * Get supported style presets.
	 *
	 * @return array<int,string>
	 */
	public function get_style_presets() {
		return array(
			self::STYLE_DEFAULT,
			self::STYLE_LIGHT,
			self::STYLE_SOFT,
			self::STYLE_SIMPLE,
			self::STYLE_PROFESSIONAL,
		);
	}

	/**
	 * Decode form settings JSON.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @return array<string,mixed>
	 */
	public function decode_settings( array $form ) {
		if ( empty( $form['settings_json'] ) || ! is_string( $form['settings_json'] ) ) {
			return array();
		}

		$settings = json_decode( $form['settings_json'], true );

		return is_array( $settings ) ? $settings : array();
	}

	/**
	 * Get supported statuses.
	 *
	 * @return array<int,string>
	 */
	private function get_statuses() {
		return array( self::STATUS_ACTIVE, self::STATUS_INACTIVE );
	}

	/**
	 * Sanitize form data.
	 *
	 * @param array<string,mixed> $data Raw data.
	 * @return array<string,mixed>
	 */
	private function sanitize_data( array $data ) {
		$layout_template = sanitize_key( $data['layout_template'] ?? self::LAYOUT_CLEAN_CARD );
		$style_preset    = sanitize_key( $data['style_preset'] ?? self::STYLE_DEFAULT );

		if ( ! in_array( $layout_template, $this->get_layout_templates(), true ) ) {
			$layout_template = self::LAYOUT_CLEAN_CARD;
		}

		if ( ! in_array( $style_preset, $this->get_style_presets(), true ) ) {
			$style_preset = self::STYLE_DEFAULT;
		}

		return array(
			'title'                => sanitize_text_field( $data['title'] ?? '' ),
			'slug'                 => sanitize_title( $data['slug'] ?? '' ),
			'status'               => $this->sanitize_status( $data['status'] ?? self::STATUS_ACTIVE ),
			'template_key'         => sanitize_key( $data['template_key'] ?? '' ),
			'description'          => sanitize_textarea_field( $data['description'] ?? '' ),
			'layout_template'      => $layout_template,
			'style_preset'         => $style_preset,
			'header_title'         => sanitize_text_field( $data['header_title'] ?? '' ),
			'header_description'   => sanitize_textarea_field( $data['header_description'] ?? '' ),
			'footer_note'          => sanitize_textarea_field( $data['footer_note'] ?? '' ),
			'privacy_text'         => sanitize_textarea_field( $data['privacy_text'] ?? '' ),
			'success_message'      => sanitize_textarea_field( $data['success_message'] ?? '' ),
			'submit_button_text'   => sanitize_text_field( $data['submit_button_text'] ?? __( 'Send', 'submission-inbox' ) ),
			'notification_email'   => sanitize_email( $data['notification_email'] ?? '' ),
			'notification_subject' => sanitize_text_field( $data['notification_subject'] ?? '' ),
			'notification_enabled' => array_key_exists( 'notification_enabled', $data ) && empty( $data['notification_enabled'] ) ? 0 : 1,
			'save_leads'           => array_key_exists( 'save_leads', $data ) && empty( $data['save_leads'] ) ? 0 : 1,
			'settings_json'        => $this->sanitize_settings_json( $data['settings_json'] ?? array() ),
		);
	}

	/**
	 * Sanitize settings JSON input.
	 *
	 * @param mixed $settings Settings.
	 * @return string|null
	 */
	private function sanitize_settings_json( $settings ) {
		if ( is_string( $settings ) ) {
			$decoded = json_decode( $settings, true );
			$settings = is_array( $decoded ) ? $decoded : array();
		}

		if ( ! is_array( $settings ) || empty( $settings ) ) {
			return null;
		}

		$sanitized = $this->sanitize_nested_value( $settings );

		$encoded = wp_json_encode( $sanitized );

		return is_string( $encoded ) ? $encoded : null;
	}

	/**
	 * Sanitize nested values for JSON storage.
	 *
	 * @param mixed $value Value.
	 * @return mixed
	 */
	private function sanitize_nested_value( $value ) {
		if ( is_array( $value ) ) {
			$clean = array();
			foreach ( $value as $key => $item ) {
				$clean_key = sanitize_key( (string) $key );
				if ( 'advanced_html' === $clean_key && is_array( $item ) ) {
					$clean[ $clean_key ] = $this->sanitize_advanced_html_settings( $item );
					continue;
				}
				if ( 'design_blocks' === $clean_key && is_array( $item ) ) {
					$clean[ $clean_key ] = $this->sanitize_design_blocks( $item );
					continue;
				}
				$clean[ $clean_key ] = $this->sanitize_nested_value( $item );
			}

			return $clean;
		}

		if ( is_bool( $value ) ) {
			return $value;
		}

		if ( is_numeric( $value ) ) {
			return 0 + $value;
		}

		return sanitize_text_field( (string) $value );
	}

	/**
	 * Sanitize Advanced HTML settings while preserving allowed markup.
	 *
	 * @param array<string,mixed> $settings Settings.
	 * @return array<string,string>
	 */
	private function sanitize_advanced_html_settings( array $settings ) {
		return array(
			'html_before_form'    => $this->sanitize_custom_html( $settings['html_before_form'] ?? '' ),
			'html_after_form'     => $this->sanitize_custom_html( $settings['html_after_form'] ?? '' ),
			'html_before_submit'  => $this->sanitize_custom_html( $settings['html_before_submit'] ?? '' ),
			'html_after_submit'   => $this->sanitize_custom_html( $settings['html_after_submit'] ?? '' ),
			'custom_wrapper_class' => $this->sanitize_class_list( $settings['custom_wrapper_class'] ?? '' ),
			'custom_form_class'    => $this->sanitize_class_list( $settings['custom_form_class'] ?? '' ),
			'custom_css'           => $this->sanitize_custom_css_storage( $settings['custom_css'] ?? '' ),
		);
	}

	/**
	 * Sanitize Design Studio blocks while preserving safe HTML.
	 *
	 * @param array<int,array<string,mixed>> $blocks Blocks.
	 * @return array<int,array<string,mixed>>
	 */
	private function sanitize_design_blocks( array $blocks ) {
		$clean = array();
		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}
			$type = $this->sanitize_choice( $block['type'] ?? '', array( 'text', 'html', 'notice', 'divider', 'privacy' ), 'text' );
			$style_allowed = 'divider' === $type ? array( 'line', 'space' ) : ( 'notice' === $type ? array( 'neutral', 'info', 'success', 'warning' ) : array( 'neutral' ) );
			$clean[] = array(
				'id'           => sanitize_key( $block['id'] ?? '' ),
				'type'         => $type,
				'title'        => sanitize_text_field( $block['title'] ?? '' ),
				'content'      => 'html' === $type ? $this->sanitize_custom_html( $block['content'] ?? '' ) : sanitize_textarea_field( $block['content'] ?? '' ),
				'position'     => $this->sanitize_choice( $block['position'] ?? '', array( 'before_form', 'after_header', 'before_fields', 'after_fields', 'before_submit', 'after_submit', 'before_footer', 'after_form' ), 'before_form' ),
				'style'        => $this->sanitize_choice( $block['style'] ?? '', $style_allowed, 'divider' === $type ? 'line' : 'neutral' ),
				'custom_class' => 'html' === $type ? $this->sanitize_class_list( $block['custom_class'] ?? '' ) : '',
				'sort_order'   => absint( $block['sort_order'] ?? 0 ),
				'enabled'      => ! array_key_exists( 'enabled', $block ) || (bool) $block['enabled'],
			);
		}

		return $clean;
	}

	/**
	 * Sanitize stored Custom CSS without adding new selectors.
	 *
	 * Full form-scope validation happens before admin save and again before
	 * rendering because repository sanitization does not know the form ID.
	 *
	 * @param mixed $css CSS.
	 * @return string
	 */
	private function sanitize_custom_css_storage( $css ) {
		$css = trim( sanitize_textarea_field( (string) $css ) );
		if ( '' === $css ) {
			return '';
		}

		if ( preg_match( '/(<|>|@|url\s*\(|expression\s*\(|javascript\s*:|data\s*:|behavior\s*:|-moz-binding|<\/?style|<\/?script)/i', $css ) ) {
			return '';
		}

		return $css;
	}

	/**
	 * Sanitize a custom HTML block.
	 *
	 * @param mixed $html HTML.
	 * @return string
	 */
	private function sanitize_custom_html( $html ) {
		return wp_kses( (string) $html, $this->custom_html_allowed_tags(), array( 'http', 'https', 'mailto', 'tel' ) );
	}

	/**
	 * Sanitize a space-separated class list.
	 *
	 * @param mixed $classes Classes.
	 * @return string
	 */
	private function sanitize_class_list( $classes ) {
		$clean = array();
		foreach ( preg_split( '/\s+/', (string) $classes ) as $class ) {
			$class = sanitize_html_class( $class );
			if ( '' !== $class ) {
				$clean[] = $class;
			}
		}

		return implode( ' ', array_unique( $clean ) );
	}

	/**
	 * Sanitize an allowlisted choice.
	 *
	 * @param mixed             $value Value.
	 * @param array<int,string> $allowed Allowed values.
	 * @param string            $fallback Fallback.
	 * @return string
	 */
	private function sanitize_choice( $value, array $allowed, $fallback ) {
		$value = sanitize_key( (string) $value );
		return in_array( $value, $allowed, true ) ? $value : $fallback;
	}

	/**
	 * Get allowed tags for custom HTML blocks.
	 *
	 * @return array<string,array<string,bool>>
	 */
	private function custom_html_allowed_tags() {
		$global = array(
			'class'      => true,
			'aria-label' => true,
		);

		return array(
			'p'      => $global,
			'br'     => array(),
			'strong' => $global,
			'em'     => $global,
			'b'      => $global,
			'i'      => $global,
			'small'  => $global,
			'ul'     => $global,
			'ol'     => $global,
			'li'     => $global,
			'span'   => $global,
			'div'    => $global,
			'a'      => array(
				'href'       => true,
				'title'      => true,
				'target'     => true,
				'rel'        => true,
				'class'      => true,
				'aria-label' => true,
			),
			'hr'     => $global,
		);
	}

	/**
	 * Check whether a slug exists.
	 *
	 * @param string $table Table name.
	 * @param string $slug Slug.
	 * @param int    $exclude_id Form id to exclude.
	 * @return bool
	 */
	private function slug_exists( $table, $slug, $exclude_id ) {
		global $wpdb;

		if ( $exclude_id > 0 ) {
			$count = $wpdb->get_var(
				$wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE slug = %s AND id != %d", $slug, $exclude_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			);
		} else {
			$count = $wpdb->get_var(
				$wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE slug = %s", $slug ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			);
		}

		return absint( $count ) > 0;
	}

	/**
	 * Build WHERE clause.
	 *
	 * @param array<string,mixed> $args Query args.
	 * @return array{sql:string,params:array<int,mixed>}
	 */
	private function build_where( array $args ) {
		global $wpdb;

		$where  = array();
		$params = array();

		if ( ! empty( $args['status'] ) ) {
			$where[]  = 'status = %s';
			$params[] = $this->sanitize_status( $args['status'] );
		}

		if ( ! empty( $args['template_key'] ) ) {
			$where[]  = 'template_key = %s';
			$params[] = sanitize_key( $args['template_key'] );
		}

		if ( ! empty( $args['search'] ) ) {
			$search   = '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%';
			$where[]  = '(title LIKE %s OR slug LIKE %s OR description LIKE %s)';
			$params[] = $search;
			$params[] = $search;
			$params[] = $search;
		}

		return array(
			'sql'    => $where ? 'WHERE ' . implode( ' AND ', $where ) : '',
			'params' => $params,
		);
	}
}
