<?php
/**
 * Custom Fields — unique key generation.
 *
 * Group keys: group_{random}.
 * Field keys: {label_slug}; collisions receive numeric suffixes.
 *
 * @package CustomFields
 * @since 1.10.1
 */

namespace CustomFields;

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

/**
 * Generates group and field keys.
 */
final class KeyGenerator {

	/**
	 * Convert arbitrary text to a key-safe slug.
	 *
	 * @param string $text Source text.
	 */
	public static function slugFromText( string $text ): string {
		$slug = strtolower( trim( $text ) );
		$slug = preg_replace( '/[^a-z0-9_]+/', '_', $slug );
		$slug = trim( (string) $slug, '_' );

		if ( $slug !== '' && preg_match( '/^[0-9]/', $slug ) ) {
			$slug = 'f_' . $slug;
		}

		return substr( $slug, 0, Config::MAX_KEY_LENGTH );
	}

	/**
	 * Generate a unique group key (group_{random}).
	 *
	 * @param string               $title   Unused; kept for backward compatibility.
	 * @param array<string, mixed> $exclude Keys that must not be reused.
	 */
	public static function generateGroupKey( string $title = '', array $exclude = [] ): string {
		unset( $title );

		return self::prefixedRandomKey( 'group', '_', $exclude, 13 );
	}

	/**
	 * Generate a unique field key ({label_slug}, then {label_slug}_2 on collision).
	 *
	 * @param string               $label    Field label (or layout label).
	 * @param array<string, mixed> $exclude  Keys that must not be reused.
	 * @param string               $fallback Fallback slug when the label is empty.
	 */
	public static function generateFieldKey( string $label, array $exclude = [], string $fallback = 'field' ): string {
		$slug = self::slugFromText( $label );
		if ( $slug === '' ) {
			$slug = self::slugFromText( $fallback );
		}
		if ( $slug === '' ) {
			$slug = 'field';
		}

		return self::uniqueSlugKey( $slug, $exclude );
	}

	/**
	 * @param string               $prefix        Key prefix (group, novel_name, layout, …).
	 * @param string               $separator     Separator between prefix and random suffix.
	 * @param array<string, mixed> $exclude       Reserved keys.
	 * @param int                  $suffix_length Random suffix length.
	 */
	private static function prefixedRandomKey( string $prefix, string $separator, array $exclude, int $suffix_length = 13 ): string {
		$exclude_map = self::excludeMap( $exclude );

		for ( $attempt = 0; $attempt < 25; $attempt++ ) {
			$candidate = $prefix . $separator . self::randomSuffix( $suffix_length );
			$candidate = substr( $candidate, 0, Config::MAX_KEY_LENGTH );

			if ( $candidate !== '' && preg_match( '/^[a-z]/', $candidate ) && ! isset( $exclude_map[ $candidate ] ) ) {
				return $candidate;
			}
		}

		return substr( 'key_' . self::randomSuffix( 12 ), 0, Config::MAX_KEY_LENGTH );
	}

	/**
	 * Build a deterministic unique key from a slug.
	 *
	 * @param string               $slug    Base slug.
	 * @param array<string, mixed> $exclude Reserved keys.
	 */
	private static function uniqueSlugKey( string $slug, array $exclude ): string {
		$exclude_map = self::excludeMap( $exclude );
		$slug        = substr( $slug, 0, Config::MAX_KEY_LENGTH );

		if ( $slug !== '' && preg_match( '/^[a-z]/', $slug ) && ! isset( $exclude_map[ $slug ] ) ) {
			return $slug;
		}

		for ( $index = 2; $index < 1000; $index++ ) {
			$suffix    = '_' . $index;
			$base      = substr( $slug, 0, Config::MAX_KEY_LENGTH - strlen( $suffix ) );
			$candidate = $base . $suffix;

			if ( $candidate !== '' && preg_match( '/^[a-z]/', $candidate ) && ! isset( $exclude_map[ $candidate ] ) ) {
				return $candidate;
			}
		}

		return self::prefixedRandomKey( $slug !== '' ? $slug : 'field', '_', $exclude, 5 );
	}

	/**
	 * @param array<string, mixed> $exclude Reserved keys.
	 * @return array<string, bool>
	 */
	private static function excludeMap( array $exclude ): array {
		$exclude_map = [];
		foreach ( $exclude as $key => $value ) {
			if ( is_int( $key ) ) {
				$exclude_map[ (string) $value ] = true;
				continue;
			}
			$exclude_map[ (string) $key ] = true;
		}

		return $exclude_map;
	}

	/**
	 * @param int $length Suffix length.
	 */
	public static function randomSuffix( int $length = 6 ): string {
		$length = max( 4, min( 12, $length ) );
		$bytes  = random_bytes( (int) ceil( $length / 2 ) );

		return substr( bin2hex( $bytes ), 0, $length );
	}
}
