<?php
/**
 * CPT Builder storage.
 *
 * Persists all post type definitions in a single, non-autoloaded option to
 * avoid bloating WordPress' alloptions cache. Definitions are keyed by post
 * type key for O(1) lookups. Uses {@see NetworkScope} so network-activated
 * installs store shared definitions in site options.
 *
 * @package CptBuilder
 * @since 1.0.0
 */

namespace CptBuilder;

use WP_PowerSuite\Core\NetworkScope;

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

/**
 * Data access layer for stored CPT definitions.
 */
final class Storage {

	/**
	 * Option holding the keyed map of post type definitions.
	 */
	public const OPTION = 'wp_powersuite_cptb_post_types';

	/**
	 * Blog-level flag indicating rewrite rules need a deferred flush.
	 *
	 * Kept per-site even under network storage because rewrite rules are
	 * blog-scoped.
	 */
	public const FLUSH_FLAG = 'wp_powersuite_cptb_flush_needed';

	/**
	 * Request-level cache of the decoded option.
	 *
	 * @var array<string, array<string, mixed>>|null
	 */
	private ?array $cache = null;

	/**
	 * Retrieve all stored definitions keyed by post type key.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public function all(): array {
		if ( $this->cache !== null ) {
			return $this->cache;
		}

		$stored = NetworkScope::getPluginOption( self::OPTION, null );

		// Migrate blog-level data written before network-storage support.
		if ( ( ! is_array( $stored ) || $stored === [] ) && NetworkScope::usesNetworkStorage() ) {
			$legacy = get_option( self::OPTION, [] );
			if ( is_array( $legacy ) && $legacy !== [] ) {
				$stored = $legacy;
				NetworkScope::updatePluginOption( self::OPTION, $legacy );
				delete_option( self::OPTION );
			}
		}

		if ( ! is_array( $stored ) ) {
			$stored = [];
		}

		$clean = [];
		foreach ( $stored as $key => $definition ) {
			if ( ! is_string( $key ) || ! is_array( $definition ) ) {
				continue;
			}
			$clean[ $key ] = $definition;
		}

		$this->cache = $clean;

		return $this->cache;
	}

	/**
	 * Retrieve a single definition by post type key.
	 *
	 * @param string $key Post type key.
	 * @return array<string, mixed>|null
	 */
	public function get( string $key ): ?array {
		$all = $this->all();

		return $all[ $key ] ?? null;
	}

	/**
	 * Whether a definition exists for the given key.
	 *
	 * @param string $key Post type key.
	 */
	public function exists( string $key ): bool {
		return $this->get( $key ) !== null;
	}

	/**
	 * Create or update a single definition.
	 *
	 * @param string               $key        Post type key.
	 * @param array<string, mixed> $definition Sanitized definition.
	 */
	public function save( string $key, array $definition ): bool {
		if ( $key === '' ) {
			return false;
		}

		$all         = $this->all();
		$all[ $key ] = $definition;

		return $this->persist( $all );
	}

	/**
	 * Delete a single definition.
	 *
	 * @param string $key Post type key.
	 */
	public function delete( string $key ): bool {
		$all = $this->all();
		if ( ! isset( $all[ $key ] ) ) {
			return false;
		}

		unset( $all[ $key ] );

		return $this->persist( $all );
	}

	/**
	 * Persist the full map back to storage (non-autoloaded).
	 *
	 * @param array<string, array<string, mixed>> $all Full definition map.
	 */
	private function persist( array $all ): bool {
		$this->cache = $all;

		return NetworkScope::updatePluginOption( self::OPTION, $all );
	}

	/**
	 * Flag that rewrite rules must be flushed (per blog; all blogs when networked).
	 */
	public function flagFlush(): void {
		if ( NetworkScope::usesNetworkStorage() && is_multisite() && function_exists( 'get_sites' ) ) {
			$site_ids = get_sites(
				[
					'fields' => 'ids',
					'number' => 0,
				]
			);
			foreach ( $site_ids as $site_id ) {
				switch_to_blog( (int) $site_id );
				update_option( self::FLUSH_FLAG, '1', false );
				restore_current_blog();
			}

			return;
		}

		update_option( self::FLUSH_FLAG, '1', false );
	}

	/**
	 * Whether a rewrite flush is pending for the current blog.
	 */
	public function needsFlush(): bool {
		return get_option( self::FLUSH_FLAG, '' ) === '1';
	}

	/**
	 * Clear the pending rewrite flush flag for the current blog.
	 */
	public function clearFlushFlag(): void {
		delete_option( self::FLUSH_FLAG );
	}
}
