<?php
/**
 * BitForgeNet Guest Tickets - Event model.
 *
 * @package BitForgeNet_Guest_Tickets
 * @since   1.0.0
 */

defined( 'ABSPATH' ) || exit;

// This file talks to the plugin's own tables. WordPress has no API for them --
// they are not posts and not meta -- so $wpdb is the only way in. The rows
// change on every booking and every scan, which makes an object cache wrong
// more often than it is right. Table names are built from $wpdb->prefix and
// never from a request, so interpolating one carries nothing to inject; every
// value goes through $wpdb->prepare(), and the few identifiers that cannot
// (ORDER BY columns) are matched against a whitelist first.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter


/**
 * Data access for events.
 */
class BFNET_Event {

	/**
	 * Table name.
	 *
	 * @return string
	 */
	public static function table() {
		return BFNET_Install::table( 'events' );
	}

	/**
	 * Fetch one event by id.
	 *
	 * @param int $id Event id.
	 * @return object|null
	 */
	public static function get( $id ) {
		global $wpdb;
		$table = self::table();
		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", (int) $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Fetch one event by slug.
	 *
	 * @param string $slug Event slug.
	 * @return object|null
	 */
	public static function get_by_slug( $slug ) {
		global $wpdb;
		$table = self::table();
		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE slug = %s", (string) $slug ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Fetch one event by uuid.
	 *
	 * @param string $uuid Event uuid.
	 * @return object|null
	 */
	public static function get_by_uuid( $uuid ) {
		global $wpdb;
		$table = self::table();
		return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE uuid = %s", (string) $uuid ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Query events.
	 *
	 * @param array $args Query arguments.
	 * @return array
	 */
	public static function query( $args = array() ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'status'       => '',
				'statuses'     => array(),
				'organizer_id' => 0,
				'series'       => '',
				'search'       => '',
				'upcoming'     => false,
				'past'         => false,
				'orderby'      => 'starts_at',
				'order'        => 'DESC',
				'limit'        => 50,
				'offset'       => 0,
			)
		);

		$table  = self::table();
		$where  = array( '1=1' );
		$params = array();

		if ( $args['status'] ) {
			$where[]  = 'status = %s';
			$params[] = $args['status'];
		}
		if ( ! empty( $args['statuses'] ) ) {
			$in      = implode( ',', array_fill( 0, count( $args['statuses'] ), '%s' ) );
			$where[] = "status IN ({$in})";
			$params  = array_merge( $params, array_map( 'strval', $args['statuses'] ) );
		}
		if ( $args['organizer_id'] ) {
			$where[]  = 'organizer_id = %d';
			$params[] = (int) $args['organizer_id'];
		}
		if ( $args['series'] ) {
			$where[]  = 'series = %s';
			$params[] = $args['series'];
		}
		if ( '' !== $args['search'] ) {
			$like     = '%' . $wpdb->esc_like( $args['search'] ) . '%';
			$where[]  = '(title LIKE %s OR series LIKE %s OR subtitle LIKE %s)';
			$params[] = $like;
			$params[] = $like;
			$params[] = $like;
		}
		if ( $args['upcoming'] ) {
			$where[]  = 'starts_at >= %s';
			$params[] = current_time( 'mysql' );
		}
		if ( $args['past'] ) {
			$where[]  = 'starts_at < %s';
			$params[] = current_time( 'mysql' );
		}

		$allowed_orderby = array( 'starts_at', 'title', 'created_at', 'id', 'sold_count' );
		$orderby         = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'starts_at';
		$order           = ( 'ASC' === strtoupper( $args['order'] ) ) ? 'ASC' : 'DESC';

		$params[] = max( 1, (int) $args['limit'] );
		$params[] = max( 0, (int) $args['offset'] );

		$sql = "SELECT * FROM {$table} WHERE " . implode( ' AND ', $where ) . " ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";

		return $wpdb->get_results( $wpdb->prepare( $sql, $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Count events matching a status.
	 *
	 * @param string $status Optional status filter.
	 * @return int
	 */
	public static function count( $status = '' ) {
		global $wpdb;
		$table = self::table();
		if ( $status ) {
			return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", $status ) ); // phpcs:ignore WordPress.DB.PreparedSQL
		}
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Insert or update an event.
	 *
	 * @param array $data Field values; include 'id' to update.
	 * @return int|WP_Error Event id or error.
	 */
	public static function save( array $data ) {
		global $wpdb;

		$id    = isset( $data['id'] ) ? (int) $data['id'] : 0;
		$table = self::table();
		$now   = current_time( 'mysql' );

		$title = isset( $data['title'] ) ? sanitize_text_field( $data['title'] ) : '';
		if ( '' === trim( $title ) ) {
			return new WP_Error( 'bfnet_event_title', __( 'The event needs a title.', 'bitforgenet-guest-tickets' ) );
		}

		$starts_at = isset( $data['starts_at'] ) ? self::normalise_datetime( $data['starts_at'] ) : '';
		if ( ! $starts_at ) {
			return new WP_Error( 'bfnet_event_date', __( 'The event needs a valid start date and time.', 'bitforgenet-guest-tickets' ) );
		}

		$slug = isset( $data['slug'] ) && '' !== trim( (string) $data['slug'] ) ? sanitize_title( $data['slug'] ) : sanitize_title( $title );
		$slug = self::unique_slug( $slug, $id );

		$fields = array(
			'title'                  => $title,
			'slug'                   => $slug,
			'subtitle'               => isset( $data['subtitle'] ) ? sanitize_text_field( $data['subtitle'] ) : '',
			'series'                 => isset( $data['series'] ) ? sanitize_text_field( $data['series'] ) : '',
			'edition'                => isset( $data['edition'] ) ? sanitize_text_field( $data['edition'] ) : '',
			'description'            => isset( $data['description'] ) ? wp_kses_post( $data['description'] ) : '',
			'short_description'      => isset( $data['short_description'] ) ? sanitize_textarea_field( $data['short_description'] ) : '',
			'cover_id'               => isset( $data['cover_id'] ) ? (int) $data['cover_id'] : 0,
			'gallery'                => wp_json_encode( self::clean_gallery( isset( $data['gallery'] ) ? $data['gallery'] : array() ) ),
			'organizer_id'           => isset( $data['organizer_id'] ) ? (int) $data['organizer_id'] : 0,
			'venue_id'               => isset( $data['venue_id'] ) ? (int) $data['venue_id'] : 0,
			'template_id'            => isset( $data['template_id'] ) ? (int) $data['template_id'] : 0,
			'starts_at'              => $starts_at,
			'ends_at'                => isset( $data['ends_at'] ) ? self::normalise_datetime( $data['ends_at'] ) : null,
			'doors_open_at'          => isset( $data['doors_open_at'] ) ? self::normalise_datetime( $data['doors_open_at'] ) : null,
			'timezone'               => self::clean_timezone( isset( $data['timezone'] ) ? $data['timezone'] : '' ),
			'age_check_mode'         => self::clean_age_mode( isset( $data['age_check_mode'] ) ? $data['age_check_mode'] : 'off' ),
			'terms_page_id'          => isset( $data['terms_page_id'] ) ? (int) $data['terms_page_id'] : 0,
			'regulation_page_id'     => isset( $data['regulation_page_id'] ) ? (int) $data['regulation_page_id'] : 0,
			'lineup'                 => wp_json_encode( self::clean_lineup( isset( $data['lineup'] ) ? $data['lineup'] : array() ) ),
			'dress_code'             => isset( $data['dress_code'] ) ? sanitize_text_field( $data['dress_code'] ) : '',
			'min_age'                => isset( $data['min_age'] ) ? max( 0, min( 99, (int) $data['min_age'] ) ) : 0,
			'tables_mode'            => self::clean_tables_mode( isset( $data['tables_mode'] ) ? $data['tables_mode'] : 'manual' ),
			'tables_from'            => isset( $data['tables_from'] ) ? max( 1, (int) $data['tables_from'] ) : 1,
			'tables_to'              => isset( $data['tables_to'] ) ? max( 0, (int) $data['tables_to'] ) : 0,
			'table_seats'            => isset( $data['table_seats'] ) ? max( 0, (int) $data['table_seats'] ) : 0,
			'table_zones'            => self::clean_zones( isset( $data['table_zones'] ) ? $data['table_zones'] : array() ),
			'sponsors_image'         => isset( $data['sponsors_image'] ) ? max( 0, (int) $data['sponsors_image'] ) : 0,
			'menu_enabled'           => ! empty( $data['menu_enabled'] ) ? 1 : 0,
			'menu_title'             => isset( $data['menu_title'] ) ? sanitize_text_field( $data['menu_title'] ) : '',
			'menu_body'              => isset( $data['menu_body'] ) ? wp_kses_post( $data['menu_body'] ) : '',
			'capacity'               => isset( $data['capacity'] ) ? max( 0, (int) $data['capacity'] ) : 0,
			'price_cents'            => isset( $data['price_cents'] ) ? max( 0, (int) $data['price_cents'] ) : 0,
			'currency'               => isset( $data['currency'] ) ? strtoupper( substr( sanitize_text_field( $data['currency'] ), 0, 3 ) ) : (string) BFNET_Settings::get( 'currency', 'CAD' ),
			'max_per_order'          => isset( $data['max_per_order'] ) ? max( 1, min( 50, (int) $data['max_per_order'] ) ) : 10,
			'collect_guest_names'    => ! empty( $data['collect_guest_names'] ) ? 1 : 0,
			'ticket_prefix'          => isset( $data['ticket_prefix'] ) ? strtoupper( preg_replace( '/[^A-Za-z0-9\-]/', '', (string) $data['ticket_prefix'] ) ) : '',
			'status'                 => self::clean_status( isset( $data['status'] ) ? $data['status'] : 'draft' ),
			'registration_opens_at'  => isset( $data['registration_opens_at'] ) ? self::normalise_datetime( $data['registration_opens_at'] ) : null,
			'registration_closes_at' => isset( $data['registration_closes_at'] ) ? self::normalise_datetime( $data['registration_closes_at'] ) : null,
			'whatsapp_number'        => isset( $data['whatsapp_number'] ) ? BFNET_Security::clean_phone( $data['whatsapp_number'] ) : '',
			'payment_instructions'   => isset( $data['payment_instructions'] ) ? wp_kses_post( $data['payment_instructions'] ) : '',
			'terms'                  => isset( $data['terms'] ) ? wp_kses_post( $data['terms'] ) : '',
			'settings'               => wp_json_encode( isset( $data['settings'] ) && is_array( $data['settings'] ) ? $data['settings'] : array() ),
			'updated_at'             => $now,
		);

		if ( $id ) {
			$wpdb->update( $table, BFNET_Install::writable( $table, $fields ), array( 'id' => $id ), null, array( '%d' ) );
			BFNET_Log::audit( 'event', $id, 'event_updated', array( 'title' => $title ) );
		} else {
			$fields['uuid']       = BFNET_Util::uuid();
			$fields['created_by'] = get_current_user_id();
			$fields['created_at'] = $now;
			$wpdb->insert( $table, BFNET_Install::writable( $table, $fields ) );
			$id = (int) $wpdb->insert_id;

			if ( $id ) {
				/**
				 * An event has just come into existence.
				 *
				 * Distinct from bfnet_event_saved: a module that needs a row to
				 * exist from the very first moment — a default price tier, say —
				 * creates it here, whether the event came from the wizard, the
				 * editor or an import.
				 *
				 * @param int   $id     The new event.
				 * @param array $fields What it was created with.
				 */
				do_action( 'bfnet_event_created', $id, $fields );

				BFNET_Log::audit( 'event', $id, 'event_created', array( 'title' => $title ) );
			}
		}

		if ( ! $id ) {
			return new WP_Error( 'bfnet_event_save', __( 'The event could not be saved. Please check your database connection.', 'bitforgenet-guest-tickets' ) );
		}

		return $id;
	}

	/**
	 * Delete an event and all of its dependent rows.
	 *
	 * @param int $id Event id.
	 * @return bool
	 */
	public static function delete( $id ) {
		global $wpdb;

		$id = (int) $id;
		if ( ! $id ) {
			return false;
		}

		foreach ( array( 'scans', 'tickets', 'attendees', 'orders', 'ticket_types' ) as $child ) {
			$table = BFNET_Install::table( $child );
			$wpdb->delete( $table, array( 'event_id' => $id ), array( '%d' ) );
		}

		$wpdb->delete( self::table(), array( 'id' => $id ), array( '%d' ) );
		BFNET_Log::audit( 'event', $id, 'event_deleted', array(), 'warning' );

		return true;
	}

	/* --------------------------------------------------------------------- */
	/* Derived state                                                         */
	/* --------------------------------------------------------------------- */

	/**
	 * Number of tickets sold (paid orders only).
	 *
	 * @param int $event_id Event id.
	 * @return int
	 */
	public static function sold( $event_id ) {
		global $wpdb;
		$orders = BFNET_Install::table( 'orders' );
		return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(quantity),0) FROM {$orders} WHERE event_id = %d AND payment_status = 'paid'", (int) $event_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Number of seats reserved but not yet paid.
	 *
	 * @param int $event_id Event id.
	 * @return int
	 */
	public static function pending( $event_id ) {
		global $wpdb;
		$orders = BFNET_Install::table( 'orders' );
		return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(quantity),0) FROM {$orders} WHERE event_id = %d AND payment_status = 'pending'", (int) $event_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL
	}

	/**
	 * Remaining seats, or null when the event has no capacity limit.
	 *
	 * @param object $event Event row.
	 * @return int|null
	 */
	public static function seats_left( $event ) {
		$capacity = (int) $event->capacity;
		if ( $capacity <= 0 ) {
			return null;
		}
		$taken = self::sold( $event->id ) + self::pending( $event->id );
		return max( 0, $capacity - $taken );
	}

	/**
	 * Whether registration is currently accepting new orders.
	 *
	 * @param object $event Event row.
	 * @return bool
	 */
	public static function registration_open( $event ) {
		if ( ! $event || 'open' !== $event->status ) {
			return false;
		}

		$now = time();

		$opens  = BFNET_Util::event_ts( $event, 'registration_opens_at' );
		$closes = BFNET_Util::event_ts( $event, 'registration_closes_at' );
		$starts = BFNET_Util::event_ts( $event, 'starts_at' );

		if ( null !== $opens && $now < $opens ) {
			return false;
		}
		if ( null !== $closes && $now > $closes ) {
			return false;
		}
		if ( null !== $starts && $now > $starts + DAY_IN_SECONDS ) {
			return false;
		}

		$left = self::seats_left( $event );
		if ( null !== $left && $left <= 0 ) {
			return false;
		}

		return true;
	}

	/**
	 * Human explanation of why registration is unavailable.
	 *
	 * @param object $event Event row.
	 * @return string
	 */
	public static function closed_reason( $event ) {
		if ( ! $event ) {
			return __( 'This event is not available.', 'bitforgenet-guest-tickets' );
		}
		$left = self::seats_left( $event );
		if ( null !== $left && $left <= 0 ) {
			return __( 'This event is sold out.', 'bitforgenet-guest-tickets' );
		}
		$now    = time();
		$opens  = BFNET_Util::event_ts( $event, 'registration_opens_at' );
		$starts = BFNET_Util::event_ts( $event, 'starts_at' );

		if ( null !== $opens && $now < $opens ) {
			/* translators: %s: date registration opens. */
			return sprintf( __( 'Registration opens on %s.', 'bitforgenet-guest-tickets' ), BFNET_Util::event_date( $event, 'registration_opens_at' ) );
		}
		if ( null !== $starts && $now > $starts ) {
			return __( 'This event has already taken place.', 'bitforgenet-guest-tickets' );
		}
		return __( 'Registration for this event is currently closed.', 'bitforgenet-guest-tickets' );
	}

	/**
	 * Decode the line-up JSON.
	 *
	 * @param object $event Event row.
	 * @return array List of ['role' => ..., 'name' => ..., 'note' => ...].
	 */
	public static function lineup( $event ) {
		$items = BFNET_Util::json( isset( $event->lineup ) ? $event->lineup : '' );
		return is_array( $items ) ? $items : array();
	}

	/**
	 * Cover image URL with fallback.
	 *
	 * @param object $event Event row.
	 * @param string $size  Image size.
	 * @return string
	 */
	public static function cover_url( $event, $size = 'large' ) {
		if ( ! empty( $event->cover_id ) ) {
			$url = wp_get_attachment_image_url( (int) $event->cover_id, $size );
			if ( $url ) {
				return $url;
			}
		}
		return '';
	}

	/* --------------------------------------------------------------------- */
	/* Internals                                                             */
	/* --------------------------------------------------------------------- */

	/**
	 * Sanitise a gallery list, accepting either an array or a stored JSON string.
	 *
	 * @param mixed $raw Raw gallery input.
	 * @return array List of attachment ids.
	 */
	private static function clean_gallery( $raw ) {
		if ( is_string( $raw ) ) {
			$raw = BFNET_Util::json( $raw );
		}
		if ( ! is_array( $raw ) ) {
			return array();
		}
		return array_values( array_filter( array_map( 'intval', $raw ) ) );
	}

	/**
	 * Sanitise the line-up structure.
	 *
	 * @param mixed $raw Raw line-up input.
	 * @return array
	 */
	private static function clean_lineup( $raw ) {
		if ( is_string( $raw ) ) {
			$raw = BFNET_Util::json( $raw );
		}
		if ( ! is_array( $raw ) ) {
			return array();
		}

		$out = array();
		foreach ( $raw as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$name = isset( $item['name'] ) ? sanitize_text_field( $item['name'] ) : '';
			if ( '' === trim( $name ) ) {
				continue;
			}
			$out[] = array(
				'role' => isset( $item['role'] ) ? sanitize_text_field( $item['role'] ) : 'artist',
				'name' => $name,
				'note' => isset( $item['note'] ) ? sanitize_text_field( $item['note'] ) : '',
			);
			if ( count( $out ) >= 30 ) {
				break;
			}
		}
		return $out;
	}

	/**
	 * Normalise a datetime input to MySQL format.
	 *
	 * @param string $value Raw input (datetime-local or MySQL).
	 * @return string|null
	 */
	private static function normalise_datetime( $value ) {
		$value = trim( str_replace( 'T', ' ', (string) $value ) );

		if ( '' === $value || 0 === strpos( $value, '0000-00-00' ) ) {
			return null;
		}

		// Store the wall clock exactly as typed. Converting here is what used to make an
		// event booked for 21:00 come back out as 15:00.
		if ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})[ ](\d{1,2}):(\d{2})(?::(\d{2}))?$/', $value, $m ) ) {
			return sprintf(
				'%04d-%02d-%02d %02d:%02d:%02d',
				(int) $m[1],
				(int) $m[2],
				(int) $m[3],
				(int) $m[4],
				(int) $m[5],
				isset( $m[6] ) ? (int) $m[6] : 0
			);
		}

		$ts = strtotime( $value );
		return $ts ? gmdate( 'Y-m-d H:i:s', $ts ) : null;
	}

	/**
	 * Validate a timezone identifier, falling back to the site's.
	 *
	 * @param string $tz Raw timezone.
	 * @return string
	 */
	private static function clean_timezone( $tz ) {
		$tz = trim( (string) $tz );
		if ( $tz && in_array( $tz, timezone_identifiers_list(), true ) ) {
			return $tz;
		}
		return BFNET_Util::site_timezone();
	}

	/**
	 * Restrict the age-verification mode to a known value.
	 *
	 * @param string $mode Raw mode.
	 * @return string
	 */
	/**
	 * Keep the table mode to one the plugin understands.
	 *
	 * @param string $mode Raw value.
	 * @return string
	 */
	private static function clean_tables_mode( $mode ) {
		$mode = sanitize_key( (string) $mode );

		/**
		 * The seating arrangements on offer.
		 *
		 * Core knows one, and it is the one that needs nothing configured: none.
		 * A module that places guests adds its own here, so an event can only be
		 * set to an arrangement something can actually carry out.
		 *
		 * @param array $modes Mode key => label.
		 */
		$modes = apply_filters(
			'bfnet_table_modes',
			array( 'off' => __( 'No — this event has no table numbers', 'bitforgenet-guest-tickets' ) )
		);

		// Falling back to "off" rather than "manual" is deliberate: a build with
		// nothing to place guests must not record an event as seated by hand,
		// because no hand on that build can do it.
		return array_key_exists( $mode, $modes ) ? $mode : 'off';
	}

	/**
	 * Tidy the zone repeater into storable JSON.
	 *
	 * A zone with no range is not a zone, so rows left blank simply disappear
	 * rather than becoming an empty band nobody can be seated in.
	 *
	 * @param mixed $zones Raw repeater value.
	 * @return string
	 */
	private static function clean_zones( $zones ) {
		if ( is_string( $zones ) ) {
			$zones = BFNET_Util::json( $zones );
		}

		$clean = array();

		foreach ( (array) $zones as $zone ) {
			$from = isset( $zone['from'] ) ? (int) $zone['from'] : 0;
			$to   = isset( $zone['to'] ) ? (int) $zone['to'] : 0;

			if ( $from <= 0 || $to < $from ) {
				continue;
			}

			$clean[] = array(
				'name'  => isset( $zone['name'] ) ? sanitize_text_field( $zone['name'] ) : '',
				'from'  => $from,
				'to'    => $to,
				'seats' => max( 1, isset( $zone['seats'] ) ? (int) $zone['seats'] : 8 ),
			);
		}

		return $clean ? wp_json_encode( $clean ) : '';
	}

	private static function clean_age_mode( $mode ) {
		$mode = sanitize_key( $mode );
		return in_array( $mode, array( 'off', 'declare', 'dob' ), true ) ? $mode : 'off';
	}

	/**
	 * Age-verification modes offered per event.
	 *
	 * @return array
	 */
	public static function age_modes() {
		return array(
			'off'     => __( 'No age check', 'bitforgenet-guest-tickets' ),
			'declare' => __( 'Ask the booker to confirm every guest meets the minimum age', 'bitforgenet-guest-tickets' ),
			'dob'     => __( 'Require each guest\'s date of birth and verify it', 'bitforgenet-guest-tickets' ),
		);
	}

	/**
	 * Restrict the status to a known value.
	 *
	 * @param string $status Raw status.
	 * @return string
	 */
	private static function clean_status( $status ) {
		$status = sanitize_key( $status );
		return array_key_exists( $status, BFNET_Util::event_statuses() ) ? $status : 'draft';
	}

	/**
	 * Ensure the slug is unique across events.
	 *
	 * @param string $slug    Candidate slug.
	 * @param int    $exclude Event id to ignore.
	 * @return string
	 */
	private static function unique_slug( $slug, $exclude = 0 ) {
		global $wpdb;

		$table = self::table();
		$slug  = $slug ? $slug : 'event';
		$base  = $slug;
		$i     = 2;

		while ( true ) {
			$found = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE slug = %s AND id != %d", $slug, (int) $exclude ) ); // phpcs:ignore WordPress.DB.PreparedSQL
			if ( ! $found ) {
				return $slug;
			}
			$slug = $base . '-' . $i;
			$i++;
			if ( $i > 200 ) {
				return $base . '-' . wp_generate_password( 5, false, false );
			}
		}
	}
}
