<?php
/**
 * Opaque error code mapper for StoreIndex REST responses.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.6.1
 */

namespace AIPT\Api\StoreIndex;

/**
 * ErrorCode
 *
 * Centralises the mapping between INTERNAL exceptions (which may carry DB
 * hosts, file paths, secrets, or other privileged context) and the OPAQUE
 * client-facing error response. Raw exception text is sent only to
 * error_log; REST consumers see a stable code + a translatable user message.
 *
 * Usage:
 *
 *     try {
 *         ...
 *     } catch ( \Throwable $e ) {
 *         return ErrorCode::toRestResponse(
 *             $e,
 *             ErrorCode::AIPT_INDEX_BUILD_FAILED,
 *             500
 *         );
 *     }
 *
 * Or, when persisting the user-visible message to the status JSON without
 * routing through a fake exception:
 *
 *     $message = ErrorCode::messageFor( ErrorCode::AIPT_INDEX_BUILD_FAILED );
 *     $settings->updateStatus( array( 'error_code' => $code, 'error' => $message ) );
 */
final class ErrorCode {

	public const AIPT_INDEX_BUILD_FAILED             = 'aipt_index_build_failed';
	public const AIPT_INDEX_LOCK_HELD                = 'aipt_index_lock_held';
	public const AIPT_INDEX_OPERATION_IN_PROGRESS    = 'aipt_index_operation_in_progress';
	public const AIPT_INDEX_SETTINGS_PERSIST_FAILED  = 'aipt_index_settings_persist_failed';
	public const AIPT_INDEX_MAP_VERSION_MISMATCH     = 'aipt_index_map_version_mismatch';
	public const AIPT_INDEX_CORRUPT                  = 'aipt_index_corrupt';
	public const AIPT_INDEX_GENERIC                  = 'aipt_index_generic';

	/**
	 * Build a sanitised WP_REST_Response for a thrown exception.
	 *
	 * The raw exception message is logged via error_log() for support /
	 * post-mortem use but is NEVER included in the REST body. The response
	 * carries an opaque code (validated against the known map; unknown codes
	 * fall back to {@see self::AIPT_INDEX_GENERIC}) and a translatable user
	 * message that tells the operator what to try next.
	 *
	 * @param \Throwable $exception Underlying exception. Logged, not surfaced.
	 * @param string     $code      Intended opaque error code.
	 * @param int        $status    HTTP status code (e.g. 409, 500).
	 * @return \WP_REST_Response
	 */
	public static function toRestResponse( \Throwable $exception, string $code, int $status ): \WP_REST_Response {
		$safe_code = self::normaliseCode( $code );

		\error_log(
			sprintf(
				'[AIPT][StoreIndex][%s] %s in %s:%d',
				$safe_code,
				$exception->getMessage(),
				$exception->getFile(),
				$exception->getLine()
			)
		);

		return new \WP_REST_Response(
			array(
				'code'    => $safe_code,
				'message' => self::messageFor( $safe_code ),
				'data'    => array( 'status' => $status ),
			),
			$status
		);
	}

	/**
	 * Return the translatable user-facing message for the given opaque code.
	 *
	 * Unknown codes fall back to the {@see self::AIPT_INDEX_GENERIC} message
	 * so callers cannot manufacture arbitrary strings into a 200 response by
	 * passing a code that the map doesn't recognise.
	 *
	 * @param string $code Opaque error code.
	 * @return string Translated user-facing message.
	 */
	public static function messageFor( string $code ): string {
		$safe_code = self::normaliseCode( $code );
		$map       = self::messageMap();

		return $map[ $safe_code ] ?? $map[ self::AIPT_INDEX_GENERIC ];
	}

	/**
	 * Validate the requested code against the known map; fall back to
	 * AIPT_INDEX_GENERIC for unknown codes.
	 *
	 * @param string $code Requested code.
	 * @return string Validated code (always one of the known constants).
	 */
	private static function normaliseCode( string $code ): string {
		$map = self::messageMap();
		return isset( $map[ $code ] ) ? $code : self::AIPT_INDEX_GENERIC;
	}

	/**
	 * Code => translated user message map.
	 *
	 * Kept private + materialised on each call so the strings are translated
	 * in the current request's locale (avoids stale translations across
	 * locale switches within a single PHP process).
	 *
	 * @return array<string, string>
	 */
	private static function messageMap(): array {
		return array(
			self::AIPT_INDEX_BUILD_FAILED            => \__( 'The index build failed. Please retry; if the problem persists, reset the index and rebuild.', 'ai-product-tools' ),
			self::AIPT_INDEX_LOCK_HELD               => \__( 'The index build is currently locked. Please wait a moment and try again.', 'ai-product-tools' ),
			self::AIPT_INDEX_OPERATION_IN_PROGRESS   => \__( 'Another index operation is already in progress. Please wait for it to finish before starting a new one.', 'ai-product-tools' ),
			self::AIPT_INDEX_SETTINGS_PERSIST_FAILED => \__( 'The index settings could not be saved. Please refresh the page and try again.', 'ai-product-tools' ),
			self::AIPT_INDEX_MAP_VERSION_MISMATCH    => \__( 'The on-disk index format is incompatible with this version of the plugin. Please reset the index and rebuild.', 'ai-product-tools' ),
			self::AIPT_INDEX_CORRUPT                 => \__( 'The on-disk index is corrupted or unreadable. Please reset the index and rebuild.', 'ai-product-tools' ),
			self::AIPT_INDEX_GENERIC                 => \__( 'An unexpected error occurred while processing the request. Please try again, and contact support if the problem persists.', 'ai-product-tools' ),
		);
	}
}
