<?php
/**
 * Sequential Order Numbers module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Orders;

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

/**
 * Sequential Order Numbers module.
 *
 * Replaces WooCommerce's default order numbering (post ID / HPOS ID) with
 * configurable sequential order numbers using atomic SQL — no race conditions.
 *
 * @since 1.0.0
 */
final class Sequential_Numbers extends \Kramar\Module {

	/**
	 * WP option name used as the atomic counter.
	 *
	 * @since 1.0.0
	 */
	private const COUNTER_OPTION = 'kramar_next_order_number';

	/**
	 * Most orders a single order-number search will resolve.
	 *
	 * The lookup is a single "contains" query ranked so exact matches come first,
	 * then prefix matches — so if a very broad term does reach this cap, it is the
	 * least relevant rows that fall off. Generous enough that a realistic search is
	 * never truncated, bounded so a one-character term cannot build an IN () list
	 * of every order on the shop.
	 *
	 * @since 1.0.3
	 */
	private const SEARCH_RESULT_LIMIT = 500;

	/**
	 * {@inheritDoc}
	 */
	public function get_slug(): string {
		return 'sequential-numbers';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_category(): string {
		return 'orders';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_name(): string {
		return __( 'Sequential Order Numbers', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Replace WooCommerce\'s default order IDs with clean, sequential order numbers. Supports prefix, suffix, zero-padding, and date tokens.', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_default_settings(): array {
		return [
			'enabled'      => false,
			'start_number' => 1000,
			'prefix'       => '',
			'suffix'       => '',
			'padding'      => 0,
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		$new_start = max( 1, absint( $input['start_number'] ?? 1000 ) );

		$old_start = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'start_number' );
		if ( $new_start !== $old_start ) {
			// Forward-only: raising the start jumps the counter forward, but
			// lowering it must never rewind past already-issued numbers (which
			// would re-issue duplicate order numbers). Mirror init_counter().
			$current    = (int) get_option( self::COUNTER_OPTION, (string) ( $new_start - 1 ) );
			$new_seed   = max( $current, $new_start - 1 );
			update_option( self::COUNTER_OPTION, (string) $new_seed, false );
		}

		return [
			'enabled'      => ! empty( $input['enabled'] ),
			'start_number' => $new_start,
			'prefix'       => sanitize_text_field( $input['prefix'] ?? '' ),
			'suffix'       => sanitize_text_field( $input['suffix'] ?? '' ),
			'padding'      => min( 20, absint( $input['padding'] ?? 0 ) ),
		];
	}

	/**
	 * {@inheritDoc}
	 */
	protected function run(): void {
		$this->init_counter();

		// Assign numbers on new orders only — never on edit/update.
		add_action( 'woocommerce_new_order', [ $this, 'assign_on_new_order' ], 10, 2 );
		add_action( 'woocommerce_checkout_order_created', [ $this, 'assign_order_number' ] );
		add_action( 'woocommerce_order_status_changed', [ $this, 'assign_on_draft_transition' ], 10, 3 );

		// Display the custom order number everywhere WC renders it.
		add_filter( 'woocommerce_order_number', [ $this, 'display_order_number' ], PHP_INT_MAX, 2 );

		// Admin search support (CPT-based stores).
		add_filter( 'woocommerce_shop_order_search_fields', [ $this, 'add_search_fields' ] );

		// Admin search support (HPOS / custom order tables). Only consulted on the
		// non-full-text code path — see search_by_order_number() for why that is
		// not enough on its own.
		add_filter( 'woocommerce_order_table_search_query_meta_keys', [ $this, 'add_search_fields' ] );

		// Offer "Order number" as an explicit choice in the admin search dropdown.
		add_filter( 'woocommerce_hpos_admin_search_filters', [ $this, 'add_search_filter_option' ] );
		add_filter( 'woocommerce_hpos_generate_where_for_search_filter', [ $this, 'search_filter_where' ], 10, 3 );

		// And make the default ("All") admin search find order numbers too.
		add_filter( 'woocommerce_order_list_table_prepare_items_query_args', [ $this, 'search_by_order_number' ] );
	}

	/**
	 * Ensure the counter option exists.
	 *
	 * Creates the counter seeded to `start_number - 1` so the first increment
	 * yields exactly `start_number`. If the counter already exists but is lower
	 * than `start_number`, the counter is advanced so users can "jump forward".
	 * The counter never moves backward.
	 *
	 * @since 1.0.0
	 */
	public function init_counter(): void {
		$start = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'start_number' );

		$current = get_option( self::COUNTER_OPTION, null );

		if ( null === $current ) {
			// First use — seed the counter.
			add_option( self::COUNTER_OPTION, (string) ( $start - 1 ), '', false );
			return;
		}

		// Allow jumping forward, never backward.
		if ( (int) $current < $start - 1 ) {
			update_option( self::COUNTER_OPTION, (string) ( $start - 1 ), false );
		}
	}

	/**
	 * Atomically increment the counter and return the new number.
	 *
	 * Uses `LAST_INSERT_ID(expr)` to read the incremented value back in a single
	 * InnoDB-safe statement — no application-level locks or retries needed.
	 *
	 * @since 1.0.0
	 *
	 * @return int The next sequential order number.
	 */
	private function next_number(): int {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = LAST_INSERT_ID(CAST(option_value AS UNSIGNED) + 1) WHERE option_name = %s",
				self::COUNTER_OPTION
			)
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$next = (int) $wpdb->get_var( 'SELECT LAST_INSERT_ID()' );

		// The atomic UPDATE above deliberately bypasses update_option() — that is
		// what makes it race-safe — but it also leaves WordPress's options cache
		// holding the pre-increment value. Anything reading the counter back with
		// get_option() then sees a stale number (the "Next order: X" preview on
		// the settings screen, for one), and on a site with a persistent object
		// cache the staleness survives across requests rather than ending with
		// this one. Drop the cached copy so the next read comes from the row.
		wp_cache_delete( self::COUNTER_OPTION, 'options' );
		wp_cache_delete( 'alloptions', 'options' );

		return $next;
	}

	/**
	 * Build the display string for a sequential number.
	 *
	 * Prefix/suffix support date tokens: {Y}, {y}, {m}, {d}.
	 *
	 * @since 1.0.0
	 *
	 * @param int $number Raw sequential number.
	 * @return string Formatted order number string.
	 */
	private function format_number( int $number ): string {
		$padding = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'padding' );
		$prefix  = (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'prefix' );
		$suffix  = (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'suffix' );

		$padded = $padding > 0 ? str_pad( (string) $number, $padding, '0', STR_PAD_LEFT ) : (string) $number;

		$replacements = [
			'{Y}' => current_time( 'Y' ),
			'{y}' => current_time( 'y' ),
			'{m}' => current_time( 'm' ),
			'{d}' => current_time( 'd' ),
		];

		$prefix = strtr( $prefix, $replacements );
		$suffix = strtr( $suffix, $replacements );

		return $prefix . $padded . $suffix;
	}

	/**
	 * Assign a sequential number to a WC_Order object (skips if already assigned).
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order object.
	 */
	public function assign_order_number( \WC_Order $order ): void {
		// Either key means the order already has a number: _order_number alone is
		// what every other order-number plugin writes (so migrated orders are left
		// alone), and _order_number_formatted alone can only be a half-finished
		// write of our own — renumbering that would burn a counter value and change
		// a number a customer may already have seen.
		if ( '' !== $order->get_meta( '_order_number' ) || '' !== $order->get_meta( '_order_number_formatted' ) ) {
			return;
		}

		$number    = $this->next_number();
		$formatted = $this->format_number( $number );

		// save_meta_data() writes each value with its own query and fires
		// added_order_meta between them, with no transaction around the pair — so a
		// fatal in another plugin's meta hook can land between these two writes.
		// The display value is written first because that is the half that has to
		// survive: an order left with only _order_number_formatted still shows the
		// right number, whereas one left with only the raw counter would display a
		// bare "1640" where the customer's invoice says "SILV01640".
		$order->update_meta_data( '_order_number_formatted', $formatted );
		$order->update_meta_data( '_order_number', $number );
		$order->save_meta_data();
	}

	/**
	 * Assign a sequential number when a new order is created.
	 *
	 * Skips draft orders (Blocks checkout) — those get numbered on the
	 * draft→real status transition instead.
	 *
	 * @since 1.0.0
	 *
	 * @param int       $order_id Order ID.
	 * @param \WC_Order $order    The order object.
	 */
	public function assign_on_new_order( int $order_id, \WC_Order $order ): void {
		if ( 'checkout-draft' === $order->get_status() ) {
			return;
		}
		$this->assign_order_number( $order );
	}

	/**
	 * Assign a sequential number when a draft order transitions to a real order.
	 *
	 * WooCommerce Blocks creates draft orders before checkout completes. This hook
	 * catches the checkout-draft → * transition and assigns a number if needed.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $order_id   Order ID.
	 * @param string $old_status Previous order status (without the wc- prefix).
	 * @param string $new_status New order status (without the wc- prefix).
	 */
	public function assign_on_draft_transition( int $order_id, string $old_status, string $new_status ): void {
		if ( 'checkout-draft' !== $old_status ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order instanceof \WC_Order ) {
			return;
		}

		$this->assign_order_number( $order );
	}

	/**
	 * Return the formatted order number for display.
	 *
	 * Hooked at PHP_INT_MAX priority so it runs after any other filters.
	 *
	 * @since 1.0.0
	 *
	 * @param string    $order_number Default order number string.
	 * @param \WC_Order $order        The order object.
	 * @return string Custom formatted number, or the default if not yet assigned.
	 */
	public function display_order_number( string $order_number, \WC_Order $order ): string {
		$formatted = $order->get_meta( '_order_number_formatted' );

		if ( '' !== $formatted ) {
			return (string) $formatted;
		}

		// Migration fallback. Every other order-number plugin writes the finished
		// string straight into _order_number, which is why assign_order_number()
		// skips orders that already have one. Without this, those historical orders
		// have no _order_number_formatted and fall back to the raw row id — so a
		// shop migrating in with 1,600 orders sees every one of them display as
		// "#26642" instead of the number printed on the customer's invoice.
		//
		// Reaching this line means the value almost certainly is not ours:
		// assign_order_number() writes both keys in one call and format_number()
		// never returns an empty string, so an order carrying a raw number without a
		// formatted one was numbered by something else — including plugins that
		// stored a plain number with no prefix, which is why numeric values are
		// accepted here too ("1640" is what those invoices say).
		//
		// Not a guarantee: the two writes are not atomic (see assign_order_number()),
		// so an interrupted save could in principle leave our own raw counter alone
		// here. Writing the formatted value first makes that the unlikely half to
		// survive, and showing a raw counter beats showing an internal row id either
		// way.
		$raw = $order->get_meta( '_order_number' );

		if ( is_scalar( $raw ) && '' !== (string) $raw ) {
			return (string) $raw;
		}

		return $order_number;
	}

	/**
	 * Add "Order number" to the admin search-filter dropdown.
	 *
	 * @since 1.0.3
	 *
	 * @param array<string, string> $options Existing filter options.
	 * @return array<string, string>
	 */
	public function add_search_filter_option( array $options ): array {
		$options['kramar_order_number'] = __( 'Order number', 'kramar' );

		return $options;
	}

	/**
	 * WHERE clause for the "Order number" search filter.
	 *
	 * @since 1.0.3
	 *
	 * @param string $where         Existing clause.
	 * @param string $search_term   The search term.
	 * @param string $search_filter Filter being generated.
	 * @return string
	 */
	public function search_filter_where( string $where, string $search_term, string $search_filter ): string {
		if ( 'kramar_order_number' !== $search_filter ) {
			return $where;
		}

		global $wpdb;

		$ids = $this->find_order_ids_by_number( (string) $search_term );

		if ( ! $ids ) {
			return '0=1';
		}

		$orders_table = esc_sql( $wpdb->prefix . 'wc_orders' );

		return "`{$orders_table}`.id IN (" . implode( ',', array_map( 'absint', $ids ) ) . ')';
	}

	/**
	 * Make the default admin order search find custom order numbers.
	 *
	 * WooCommerce only consults the meta keys registered through
	 * woocommerce_order_table_search_query_meta_keys on its non-full-text code
	 * path. Once the HPOS full-text index is enabled — increasingly common, and
	 * precisely on the busy shops that need search — that branch is skipped and
	 * the keys are silently ignored, so pasting an order number returns nothing.
	 * The failure is invisible on a small test site and only appears on the
	 * client's.
	 *
	 * Declaring a search filter (above) fixes the case where the operator picks
	 * "Order number" from the dropdown, but WooCommerce's "All" option expands to
	 * a hardcoded list of core filters that a plugin cannot join. So when the term
	 * matches a real order number, resolve it here and query those orders by id.
	 * Status, type and date constraints are untouched, so nothing hidden leaks in.
	 *
	 * @since 1.0.3
	 *
	 * @param array<string, mixed> $args Order query args.
	 * @return array<string, mixed>
	 */
	public function search_by_order_number( array $args ): array {
		// Respect an explicit filter choice. If the operator picked "Products" or
		// "Customers" from the dropdown, a term that also happens to look like an
		// order number must not silently override what they asked for.
		$filter = (string) ( $args['search_filter'] ?? '' );

		if ( '' !== $filter && 'all' !== $filter && 'kramar_order_number' !== $filter ) {
			return $args;
		}

		$term = trim( (string) ( $args['s'] ?? '' ) );

		// Every order number contains a digit. Requiring one keeps this from
		// hijacking a plain word search: a shop whose numbers are "SILV01640" also
		// has customers called "Silva", and taking over the search for "SILV" would
		// hide the customer matches that WooCommerce finds perfectly well.
		if ( '' === $term || ! preg_match( '/\d/', $term ) ) {
			return $args;
		}

		$ids = $this->find_order_ids_by_number( $term );

		if ( ! $ids ) {
			return $args; // Not an order number — leave the normal search alone.
		}

		// Dropping 's' below also drops WooCommerce's own "the search term is an
		// order id" behaviour, so a numeric term must keep matching that id too —
		// otherwise pasting a row id would stop finding its order as soon as some
		// other order happened to carry that figure as its number.
		if ( ctype_digit( $term ) ) {
			$ids[] = (int) $term;
			$ids   = array_values( array_unique( $ids ) );
		}

		// Respect an id restriction that is already present rather than widening it.
		if ( ! empty( $args['id'] ) ) {
			$ids = array_values( array_intersect( (array) $args['id'], $ids ) );

			if ( ! $ids ) {
				return $args;
			}
		}

		unset( $args['s'], $args['search_filter'] );
		$args['id'] = $ids;

		return $args;
	}

	/**
	 * Look up order ids whose stored order number matches a term (partial allowed).
	 *
	 * Reads the order meta table directly because this runs while the order query
	 * is being built. Both storage layouts are handled, and the result is capped so
	 * a broad term cannot build an unbounded IN () list.
	 *
	 * @since 1.0.3
	 *
	 * @param string $term Search term.
	 * @return int[] Matching order ids.
	 */
	private function find_order_ids_by_number( string $term ): array {
		global $wpdb;

		$term = trim( $term );

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

		$hpos = class_exists( '\\Automattic\\WooCommerce\\Utilities\\OrderUtil' )
			&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();

		$table  = $hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->postmeta;
		$column = $hpos ? 'order_id' : 'post_id';

		// One query, deliberately the widest match: a term can appear anywhere in a
		// number (staff paste fragments), and narrowing to exact-then-prefix tiers
		// would silently hide siblings — searching "1640" would match one order's
		// internal raw counter and drop every formatted number containing 1640.
		//
		// Ordering puts exact matches first and prefix matches next, so if a very
		// broad term does hit the cap it is the least relevant rows that fall off
		// rather than the order the operator was actually looking for.
		$exact  = $term;
		$prefix = $wpdb->esc_like( $term ) . '%';
		$any    = '%' . $wpdb->esc_like( $term ) . '%';

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table/column names come from $wpdb and a local ternary, never from input; every value is prepared.
		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT {$column} FROM {$table}
				 WHERE meta_key IN ( '_order_number_formatted', '_order_number' )
				 AND meta_value LIKE %s
				 GROUP BY {$column}
				 ORDER BY MAX( meta_value = %s ) DESC, MAX( meta_value LIKE %s ) DESC, {$column} DESC
				 LIMIT " . self::SEARCH_RESULT_LIMIT,
				$any,
				$exact,
				$prefix
			)
		);
		// phpcs:enable

		return array_values( array_filter( array_map( 'absint', (array) $ids ) ) );
	}

	/**
	 * The highest order number already issued on this shop, or 0 if none.
	 *
	 * Used to warn when the configured start number would re-issue a number that
	 * already exists — two orders answering to one number is an accounting problem,
	 * not a display one.
	 *
	 * @since 1.0.3
	 *
	 * @return int
	 */
	public function highest_existing_number(): int {
		global $wpdb;

		$hpos = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
			&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();

		$table  = $hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->postmeta;
		$column = $hpos ? 'order_id' : 'post_id';

		// Digits only: a formatted value like "SILV01640" is stored alongside the
		// raw counter, and CAST on the raw rows is what gives the real maximum.
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb, admin-only settings screen.
		$max = $wpdb->get_var(
			"SELECT MAX( CAST( meta_value AS UNSIGNED ) ) FROM {$table}
			 WHERE meta_key = '_order_number' AND meta_value REGEXP '^[0-9]+$'"
		);

		unset( $column );

		return (int) $max;
	}

	/**
	 * Add custom order number meta keys to the admin CPT order search fields.
	 *
	 * @since 1.0.0
	 *
	 * @param array<int, string> $fields Existing searchable meta keys.
	 * @return array<int, string> Extended list.
	 */
	public function add_search_fields( array $fields ): array {
		$fields[] = '_order_number_formatted';
		$fields[] = '_order_number';
		return $fields;
	}

	/**
	 * {@inheritDoc}
	 */
	public function render_extra_settings( string $option_name ): void {
		$slug = $this->get_slug();
		$cat  = $this->get_category();

		$start_number = (int) $this->plugin->get_setting( $cat, $slug, 'start_number' );
		$prefix       = (string) $this->plugin->get_setting( $cat, $slug, 'prefix' );
		$suffix       = (string) $this->plugin->get_setting( $cat, $slug, 'suffix' );
		$padding      = (int) $this->plugin->get_setting( $cat, $slug, 'padding' );

		// Build a preview based on current settings.
		$counter  = (int) get_option( self::COUNTER_OPTION, $start_number - 1 );
		$next_num = $counter + 1;
		$padded   = $padding > 0 ? str_pad( (string) $next_num, $padding, '0', STR_PAD_LEFT ) : (string) $next_num;

		// Use site time (not UTC) so the preview tokens match the number that is
		// actually issued via format_number(), which uses current_time(). See L2.
		$replacements = [
			'{Y}' => current_time( 'Y' ),
			'{y}' => current_time( 'y' ),
			'{m}' => current_time( 'm' ),
			'{d}' => current_time( 'd' ),
		];

		$preview_prefix = strtr( $prefix, $replacements );
		$preview_suffix = strtr( $suffix, $replacements );
		$preview        = $preview_prefix . $padded . $preview_suffix;
		// Warn when the shop has already issued a higher number than the counter is
		// about to hand out. Re-issuing one means two orders answer to the same
		// number in the client's books — an accounting problem, not a display one.
		// The default start (1000) is exactly the value that trips this on an
		// established shop.
		$highest  = $this->highest_existing_number();
		$next_out = max( $counter + 1, $start_number );
		?>
		<div class="kramar-module__extra">
			<div class="kramar-settings-stack">

				<?php if ( $highest > 0 && $next_out <= $highest ) : ?>
					<div class="notice notice-warning inline" style="margin:0 0 12px;">
						<p>
							<?php
							printf(
								/* translators: 1: the next number that would be issued, 2: the highest number already used. */
								esc_html__( 'This shop has already issued order number %2$s, but the next number to be given out is %1$s. Continuing would reuse numbers that are already on customer invoices. Set the start number above %2$s.', 'kramar' ),
								'<strong>' . esc_html( (string) $next_out ) . '</strong>',
								'<strong>' . esc_html( (string) $highest ) . '</strong>'
							);
							?>
						</p>
					</div>
				<?php endif; ?>

				<p>
					<label for="kramar-seq-start-number"><?php esc_html_e( 'Start number', 'kramar' ); ?></label>
					<input
						type="number"
						id="kramar-seq-start-number"
						name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][start_number]"
						value="<?php echo esc_attr( (string) $start_number ); ?>"
						min="1"
						step="1"
						style="width:120px;"
					/>
					<span class="description"><?php esc_html_e( 'The first order number issued. Raising this value jumps the counter forward; it never moves backward.', 'kramar' ); ?></span>
				</p>

				<p>
					<label for="kramar-seq-prefix"><?php esc_html_e( 'Prefix', 'kramar' ); ?></label>
					<input
						type="text"
						id="kramar-seq-prefix"
						name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][prefix]"
						value="<?php echo esc_attr( $prefix ); ?>"
						class="regular-text"
					/>
					<span class="description">
						<?php esc_html_e( 'Text before the number. Date tokens: {Y} (4-digit year), {y} (2-digit year), {m} (month), {d} (day). Example: INV-{Y}-', 'kramar' ); ?>
					</span>
				</p>

				<p>
					<label for="kramar-seq-suffix"><?php esc_html_e( 'Suffix', 'kramar' ); ?></label>
					<input
						type="text"
						id="kramar-seq-suffix"
						name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][suffix]"
						value="<?php echo esc_attr( $suffix ); ?>"
						class="regular-text"
					/>
					<span class="description">
						<?php esc_html_e( 'Text after the number. Date tokens: {Y}, {y}, {m}, {d}.', 'kramar' ); ?>
					</span>
				</p>

				<p>
					<label for="kramar-seq-padding"><?php esc_html_e( 'Zero-padding', 'kramar' ); ?></label>
					<input
						type="number"
						id="kramar-seq-padding"
						name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][padding]"
						value="<?php echo esc_attr( (string) $padding ); ?>"
						min="0"
						max="20"
						step="1"
						style="width:80px;"
					/>
					<span class="description"><?php esc_html_e( 'Pad the number with leading zeros to this minimum width. 0 = no padding.', 'kramar' ); ?></span>
				</p>

				<p>
					<strong><?php esc_html_e( 'Preview:', 'kramar' ); ?></strong>
					<?php
					printf(
						/* translators: %s: example formatted order number */
						esc_html__( 'Next order: %s', 'kramar' ),
						'<code>' . esc_html( $preview ) . '</code>'
					);
					?>
				</p>

			</div>
		</div>
		<?php
	}
}
