<?php
/**
 * Schema version repository.
 *
 * Single responsibility: read and write the `affilun_db_schema_version`
 * option that tracks which numbered schema revision is currently
 * installed in the host WordPress site. The actual storage is delegated
 * to an {@see OptionStore}, so the repository remains unit-testable
 * without a running WordPress install.
 *
 * The option key, value semantics, and the notion of "version 0 means
 * no schema installed yet" are canonicalized in
 * `specifications/database-schema.md` §13 and reflected in the
 * {@see SchemaVersion} value object. This class is the only place in
 * the codebase that names the option key; every other layer should
 * depend on this repository rather than calling the option store
 * directly, so a future rename of the key is a one-file change.
 *
 * Lifecycle:
 *
 *   - On plugin activation / `plugins_loaded`, the migration runner
 *     asks this repository for the {@see self::currentVersion()} and
 *     compares it against the highest migration version known to the
 *     codebase.
 *
 *   - After each migration is applied, the runner calls
 *     {@see self::record()} with the migration's target version so
 *     that a mid-pipeline failure leaves the option pointing at the
 *     last successfully-applied migration (and the next boot resumes
 *     from there).
 *
 * @package Affilun\Infrastructure\Persistence
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence;

/**
 * Persistence adapter for the `affilun_db_schema_version` option.
 *
 * See the file-level docblock for the option's role in the migration
 * pipeline and the rationale for wrapping it in a dedicated repository.
 */
final class SchemaVersionRepository {

	/**
	 * Canonical option key under which the schema version is persisted.
	 *
	 * Named in `specifications/coding-standards.md` §7.3 and
	 * `specifications/database-schema.md` §13.1 as the single
	 * source of truth for the current schema revision.
	 */
	public const OPTION_KEY = 'affilun_db_schema_version';

	/**
	 * Underlying key/value store used to read and write the option.
	 *
	 * In production this is a {@see WpOptionStore} forwarded to the
	 * WordPress options API; in pure-unit tests it is an
	 * {@see InMemoryOptionStore} so the repository can be exercised
	 * without a real database.
	 */
	private OptionStore $options;

	/**
	 * Construct the repository.
	 *
	 * @param OptionStore $options Backing store for the option.
	 */
	public function __construct( OptionStore $options ) {
		$this->options = $options;
	}

	/**
	 * Option key this repository reads and writes.
	 *
	 * Exposed as a getter (rather than only as the class constant) so
	 * that diagnostics callers can report the key without having to
	 * import the class directly.
	 */
	public function optionKey(): string {
		return self::OPTION_KEY;
	}

	/**
	 * Return the currently-installed schema version.
	 *
	 * When the option is unset (fresh environment, or the plugin has
	 * never been activated on this site) this returns
	 * {@see SchemaVersion::zero()} so callers can treat missing rows
	 * as "no schema installed yet" without a special case.
	 *
	 * Any non-integer, non-numeric-string value stored under the key
	 * is a programming error and surfaces as an
	 * {@see \InvalidArgumentException} from
	 * {@see SchemaVersion::fromStoredValue()} rather than being
	 * silently coerced.
	 */
	public function currentVersion(): SchemaVersion {
		$stored = $this->options->get( self::OPTION_KEY, false );

		return SchemaVersion::fromStoredValue( $stored );
	}

	/**
	 * Report whether the option has ever been written on this site.
	 *
	 * Used by diagnostics to distinguish "fresh install, no migrations
	 * applied yet" from "migrations explicitly recorded at v0". Most
	 * migration-pipeline callers should prefer {@see self::currentVersion()}
	 * with the {@see SchemaVersion::isZero()} probe instead.
	 */
	public function isInitialized(): bool {
		return $this->options->has( self::OPTION_KEY );
	}

	/**
	 * Persist the given version as the current schema revision.
	 *
	 * Called by the migration runner after each migration has been
	 * successfully applied so that a mid-pipeline failure leaves the
	 * option pointing at the last completed step.
	 *
	 * Returns true when the stored value changed as a result of the
	 * call and false when the write was a no-op (the option already
	 * pointed at `$version`), matching the
	 * {@see OptionStore::update()} convention.
	 *
	 * @param SchemaVersion $version Version to persist.
	 */
	public function record( SchemaVersion $version ): bool {
		return $this->options->update( self::OPTION_KEY, $version->toInt() );
	}

	/**
	 * Remove the schema version option entirely.
	 *
	 * Intended for uninstall flows and for test setup that needs to
	 * reset the environment back to the "never activated" state. The
	 * return value reports whether a value existed prior to deletion,
	 * matching the {@see OptionStore::delete()} convention.
	 */
	public function forget(): bool {
		return $this->options->delete( self::OPTION_KEY );
	}
}
