<?php
/**
 * Temporary Storage for Index Building
 *
 * Handles filesystem-based temporary storage with transient fallback
 * for intermediate index data during batch processing.
 * Supports multi-file storage for sharded index builds.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.5.0
 */

namespace AIPT\Api\StoreIndex;

/**
 * TempStorage class
 *
 * Provides persistent temporary storage for index build state.
 * Primary strategy: filesystem (JSON file in uploads directory)
 * Fallback strategy: WordPress transient
 *
 * For sharded builds, supports:
 * - Manifest file: build metadata and progress
 * - Per-shard files: incremental shard data during build
 */
class TempStorage {

	/**
	 * Base transient key for fallback storage.
	 * Actual key includes job_id for isolation between concurrent builds.
	 */
	private const TRANSIENT_KEY_PREFIX = 'aipt_index_build_';

	/**
	 * Maximum size for transient fallback storage (1MB).
	 *
	 * When filesystem storage is unavailable and transient fallback is used,
	 * this limit prevents wp_options table bloat (autoload=yes issue).
	 * 1MB accommodates ~2,000-3,000 products with typical metadata.
	 * Larger catalogs MUST have writable filesystem temp directory.
	 *
	 * @since 2.5.0
	 */
	private const MAX_TRANSIENT_SIZE = 1048576;

	/**
	 * Storage strategy: 'filesystem' or 'transient'.
	 *
	 * @var string
	 */
	private $strategy;

	/**
	 * Path to the temporary file (only used with filesystem strategy).
	 *
	 * @var string
	 */
	private $file_path;

	/**
	 * Job ID for this build.
	 *
	 * @var int
	 */
	private $job_id;

	/**
	 * Temp directory path.
	 *
	 * @var string
	 */
	private $temp_dir;

	/**
	 * Constructor.
	 *
	 * Detects filesystem writability and selects appropriate storage strategy.
	 *
	 * @param int $job_id Unique job identifier for this build.
	 */
	public function __construct( int $job_id ) {
		$this->job_id  = $job_id;
		$upload_dir    = wp_upload_dir();
		$this->temp_dir = $upload_dir['basedir'] . '/ai-product-tools/temp';

		if ( $this->ensureDirectory( $this->temp_dir ) ) {
			$this->strategy  = 'filesystem';
			$this->file_path = $this->temp_dir . '/build-' . $job_id . '.json';
		} else {
			$this->strategy  = 'transient';
			$this->file_path = '';
			$this->temp_dir  = '';

			error_log(
				sprintf(
					'[AIPT] TempStorage: Falling back to transient storage. Directory not writable: %s',
					$upload_dir['basedir'] . '/ai-product-tools/temp'
				)
			);
		}
	}

	/**
	 * Get the job-specific transient key.
	 *
	 * Uses job_id to ensure isolation between concurrent builds.
	 * Note: Transient operations are not atomic (get/modify/set race window exists),
	 * but job-specific keys prevent cross-job data corruption.
	 *
	 * @return string Transient key for this job.
	 */
	private function getTransientKey(): string {
		return self::TRANSIENT_KEY_PREFIX . $this->job_id;
	}

	/**
	 * Write index data to temporary storage.
	 *
	 * @param array $data Index data array.
	 * @return bool True on success, false on failure.
	 */
	public function write( array $data ): bool {
		if ( $this->strategy === 'filesystem' ) {
			$json = wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
			if ( $json === false ) {
				return false;
			}

			$result = file_put_contents( $this->file_path, $json );
			return $result !== false;
		}

		$json = wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
		if ( $json === false ) {
			return false;
		}

		if ( strlen( $json ) > self::MAX_TRANSIENT_SIZE ) {

			error_log(
				sprintf(
					'[AIPT] TempStorage: Data size (%d bytes) exceeds transient limit (%d bytes). ' .
					'Build cannot continue with transient storage. Please ensure filesystem temp directory is writable.',
					strlen( $json ),
					self::MAX_TRANSIENT_SIZE
				)
			);
			return false;
		}

		return set_transient( $this->getTransientKey(), $data, HOUR_IN_SECONDS );
	}

	/**
	 * Read index data from temporary storage.
	 *
	 * @return array|null Index data array or null if not found.
	 */
	public function read(): ?array {
		if ( $this->strategy === 'filesystem' ) {
			if ( ! file_exists( $this->file_path ) ) {
				return null;
			}

			$json = file_get_contents( $this->file_path );
			if ( $json === false ) {
				return null;
			}

			$data = json_decode( $json, true );
			return is_array( $data ) ? $data : null;
		}

		$data = get_transient( $this->getTransientKey() );
		return is_array( $data ) ? $data : null;
	}

	/**
	 * Clean up temporary storage.
	 *
	 * Removes all temp files (main, manifest, shards) and transient.
	 *
	 * @return void
	 */
	public function cleanup(): void {
		if ( $this->strategy === 'filesystem' ) {

			if ( file_exists( $this->file_path ) ) {

				@unlink( $this->file_path );
			}

			$manifest_path = $this->getManifestPath();
			if ( file_exists( $manifest_path ) ) {

				@unlink( $manifest_path );
			}

			$pattern = $this->temp_dir . '/build-' . $this->job_id . '-shard-*.json';
			$files   = glob( $pattern );
			if ( $files ) {
				foreach ( $files as $file ) {

					@unlink( $file );
				}
			}
		}

		delete_transient( $this->getTransientKey() );
	}

	/**
	 * Get the current storage strategy.
	 *
	 * @return string 'filesystem' or 'transient'.
	 */
	public function getStrategy(): string {
		return $this->strategy;
	}

	/**
	 * Get the transient key prefix used for fallback storage.
	 *
	 * Note: Actual transient key is job-specific (prefix + job_id).
	 * Use instance method getTransientKey() to get the full key.
	 *
	 * @return string Transient key prefix.
	 */
	public static function getTransientKeyPrefix(): string {
		return self::TRANSIENT_KEY_PREFIX;
	}

	/**
	 * Write build manifest for sharded builds.
	 *
	 * Manifest contains build metadata: current shard ID, category distribution,
	 * completed shard IDs, etc. Does NOT contain product data.
	 *
	 * @since 2.5.0
	 * @param array $manifest Manifest data.
	 * @return bool True on success.
	 */
	public function writeManifest( array $manifest ): bool {
		if ( $this->strategy !== 'filesystem' ) {

			$data = get_transient( $this->getTransientKey() );
			if ( ! is_array( $data ) ) {
				$data = array();
			}
			$data['_manifest'] = $manifest;
			return set_transient( $this->getTransientKey(), $data, HOUR_IN_SECONDS );
		}

		$manifest_path = $this->getManifestPath();
		$json          = wp_json_encode( $manifest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );

		if ( $json === false ) {
			return false;
		}

		$result = file_put_contents( $manifest_path, $json );
		return $result !== false;
	}

	/**
	 * Read build manifest for sharded builds.
	 *
	 * @since 2.5.0
	 * @return array|null Manifest data or null if not found.
	 */
	public function readManifest(): ?array {
		if ( $this->strategy !== 'filesystem' ) {

			$data = get_transient( $this->getTransientKey() );
			if ( is_array( $data ) && isset( $data['_manifest'] ) ) {
				return $data['_manifest'];
			}
			return null;
		}

		$manifest_path = $this->getManifestPath();
		if ( ! file_exists( $manifest_path ) ) {
			return null;
		}

		$json = file_get_contents( $manifest_path );
		if ( $json === false ) {
			return null;
		}

		$data = json_decode( $json, true );
		return is_array( $data ) ? $data : null;
	}

	/**
	 * Write a shard file during build.
	 *
	 * Used to persist completed shards incrementally, reducing memory usage.
	 *
	 * @since 2.5.0
	 * @param int   $shard_id Shard ID.
	 * @param array $data     Shard data (products, categories).
	 * @return bool True on success.
	 */
	public function writeShard( int $shard_id, array $data ): bool {
		if ( $this->strategy !== 'filesystem' ) {

			$existing = get_transient( $this->getTransientKey() );
			if ( ! is_array( $existing ) ) {
				$existing = array();
			}
			$existing[ '_shard_' . $shard_id ] = $data;

			$json = wp_json_encode( $existing, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
			if ( $json !== false && strlen( $json ) > self::MAX_TRANSIENT_SIZE ) {

				error_log(
					'[AIPT] TempStorage: Shard data exceeds transient limit. ' .
					'Please ensure filesystem temp directory is writable for large catalogs.'
				);
				return false;
			}

			return set_transient( $this->getTransientKey(), $existing, HOUR_IN_SECONDS );
		}

		$shard_path = $this->getShardPath( $shard_id );
		$json       = wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );

		if ( $json === false ) {
			return false;
		}

		$result = file_put_contents( $shard_path, $json );
		return $result !== false;
	}

	/**
	 * Read a shard file during build.
	 *
	 * @since 2.5.0
	 * @param int $shard_id Shard ID.
	 * @return array|null Shard data or null if not found.
	 */
	public function readShard( int $shard_id ): ?array {
		if ( $this->strategy !== 'filesystem' ) {

			$data = get_transient( $this->getTransientKey() );
			if ( is_array( $data ) && isset( $data[ '_shard_' . $shard_id ] ) ) {
				return $data[ '_shard_' . $shard_id ];
			}
			return null;
		}

		$shard_path = $this->getShardPath( $shard_id );
		if ( ! file_exists( $shard_path ) ) {
			return null;
		}

		$json = file_get_contents( $shard_path );
		if ( $json === false ) {
			return null;
		}

		$data = json_decode( $json, true );
		return is_array( $data ) ? $data : null;
	}

	/**
	 * Get all shard IDs that have been written.
	 *
	 * @since 2.5.0
	 * @return int[] Array of shard IDs.
	 */
	public function getShardIds(): array {
		if ( $this->strategy !== 'filesystem' ) {

			$data = get_transient( $this->getTransientKey() );
			if ( ! is_array( $data ) ) {
				return array();
			}
			$ids = array();
			foreach ( array_keys( $data ) as $key ) {
				if ( strpos( $key, '_shard_' ) === 0 ) {
					$ids[] = (int) substr( $key, 7 );
				}
			}
			return $ids;
		}

		$pattern = $this->temp_dir . '/build-' . $this->job_id . '-shard-*.json';
		$files   = glob( $pattern );

		if ( ! $files ) {
			return array();
		}

		$ids = array();
		foreach ( $files as $file ) {
			$basename = basename( $file, '.json' );
			if ( preg_match( '/shard-(\d+)$/', $basename, $matches ) ) {
				$ids[] = (int) $matches[1];
			}
		}

		sort( $ids, SORT_NUMERIC );
		return $ids;
	}

	/**
	 * Check if filesystem strategy is available.
	 *
	 * @since 2.5.0
	 * @return bool True if filesystem storage is available.
	 */
	public function isFilesystemAvailable(): bool {
		return $this->strategy === 'filesystem';
	}

	/**
	 * Get manifest file path.
	 *
	 * @since 2.5.0
	 * @return string Manifest file path.
	 */
	private function getManifestPath(): string {
		return $this->temp_dir . '/build-' . $this->job_id . '-manifest.json';
	}

	/**
	 * Get shard file path.
	 *
	 * @since 2.5.0
	 * @param int $shard_id Shard ID.
	 * @return string Shard file path.
	 */
	private function getShardPath( int $shard_id ): string {
		return $this->temp_dir . '/build-' . $this->job_id . '-shard-' . $shard_id . '.json';
	}

	/**
	 * Ensure the temporary directory exists with proper permissions.
	 *
	 * Creates directory if needed and adds security files.
	 *
	 * @param string $dir Directory path.
	 * @return bool True if directory is writable.
	 */
	private function ensureDirectory( string $dir ): bool {

		if ( is_dir( $dir ) && \wp_is_writable( $dir ) ) {
			return true;
		}

		if ( ! wp_mkdir_p( $dir ) ) {
			return false;
		}

		$index_file = $dir . '/index.php';
		if ( ! file_exists( $index_file ) ) {

			file_put_contents( $index_file, '<?php exit;' );
		}

		$htaccess_file = $dir . '/.htaccess';
		if ( ! file_exists( $htaccess_file ) ) {

			file_put_contents( $htaccess_file, 'deny from all' );
		}

		return \wp_is_writable( $dir );
	}
}
