<?php
/**
 * Surface state storage - DOIG_Post_Meta's interface for surfaces (2.7.0 M1).
 *
 * The capture pipeline stores per-item state (status, cached_url, job_id,
 * error_code...) through DOIG_Post_Meta's get/set-by-short-key interface.
 * This shim exposes the SAME interface for synthetic surface IDs
 * (DOIG_Surfaces) and routes each surface type to WordPress-native
 * storage that scales with its cardinality:
 *
 *     tax:{taxonomy}:{term_id}  -> term meta   (hundreds of terms scale here)
 *     author:{user_id}          -> user meta
 *     home / archive:{pt}       -> one option array (a handful of rows)
 *
 * The meta key names are identical to the postmeta ones (_doig_status,
 * _doig_cached_url, ...) so log lines, greps and mental models carry
 * straight over.
 *
 * NOT in M1: the atomic claim_job() equivalent (callback-vs-poll dedup).
 * That lands with the M2 pipeline work, where the callback and polling
 * paths learn to route surface IDs here - term/user meta can reuse the
 * postmeta claim's compare-and-clear SQL shape; the option-backed types
 * will use the same approach against the options row.
 *
 * @package Dynamic_OG_Image_Generator
 */

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

class DOIG_Surface_Meta {

	/**
	 * Option holding state for option-backed surfaces (home, archives):
	 * array[ surface_key ][ meta_key ] = value.
	 */
	const STATE_OPTION = 'doig_surface_state';

	/**
	 * Gets a surface meta value by short key name.
	 *
	 * @param int    $surface_id Synthetic surface ID.
	 * @param string $key        Short key (e.g. 'status', 'cached_url').
	 * @return mixed '' when unset (matching get_post_meta single-value
	 *               semantics so callers cannot tell the two apart).
	 */
	public static function get( $surface_id, $key ) {
		$surface_key = DOIG_Surfaces::key_for_id( $surface_id );
		if ( '' === $surface_key ) {
			return '';
		}
		$meta_key = self::meta_key( $key );
		$parts    = explode( ':', $surface_key );

		switch ( $parts[0] ) {
			case 'tax':
				return get_term_meta( (int) $parts[2], $meta_key, true );

			case 'author':
				return get_user_meta( (int) $parts[1], $meta_key, true );

			default:
				$state = (array) get_option( self::STATE_OPTION, array() );
				return $state[ $surface_key ][ $meta_key ] ?? '';
		}
	}

	/**
	 * Sets a surface meta value by short key name.
	 *
	 * @param int    $surface_id
	 * @param string $key
	 * @param mixed  $value
	 */
	public static function set( $surface_id, $key, $value ) {
		$surface_key = DOIG_Surfaces::key_for_id( $surface_id );
		if ( '' === $surface_key ) {
			return;
		}
		$meta_key = self::meta_key( $key );
		$parts    = explode( ':', $surface_key );

		switch ( $parts[0] ) {
			case 'tax':
				update_term_meta( (int) $parts[2], $meta_key, $value );
				return;

			case 'author':
				update_user_meta( (int) $parts[1], $meta_key, $value );
				return;

			default:
				$state = (array) get_option( self::STATE_OPTION, array() );
				$state[ $surface_key ][ $meta_key ] = $value;
				update_option( self::STATE_OPTION, $state, false );
		}
	}

	/**
	 * Deletes one surface meta value.
	 *
	 * @param int    $surface_id
	 * @param string $key
	 */
	public static function delete( $surface_id, $key ) {
		$surface_key = DOIG_Surfaces::key_for_id( $surface_id );
		if ( '' === $surface_key ) {
			return;
		}
		$meta_key = self::meta_key( $key );
		$parts    = explode( ':', $surface_key );

		switch ( $parts[0] ) {
			case 'tax':
				delete_term_meta( (int) $parts[2], $meta_key );
				return;

			case 'author':
				delete_user_meta( (int) $parts[1], $meta_key );
				return;

			default:
				$state = (array) get_option( self::STATE_OPTION, array() );
				unset( $state[ $surface_key ][ $meta_key ] );
				if ( isset( $state[ $surface_key ] ) && array() === $state[ $surface_key ] ) {
					unset( $state[ $surface_key ] );
				}
				update_option( self::STATE_OPTION, $state, false );
		}
	}

	/**
	 * Claim an in-flight surface job - the surface counterpart of
	 * DOIG_Post_Meta::claim_job()'s callback-vs-poll dedup.
	 *
	 * Deliberately BEST-EFFORT (read, compare, clear) rather than
	 * SQL-atomic. The postmeta claim went atomic because losing the race
	 * created duplicate Media Library attachments (the v1.2.6 incident) -
	 * a visible, accumulating wrong outcome. Surfaces have no Media
	 * Library integration: double-processing a surface job re-downloads
	 * the image and overwrites the SAME cache file ({surface_id}.webp),
	 * which is idempotent. The worst cost of the tiny race window is one
	 * redundant download and a duplicate log line, and the window only
	 * exists at all when the callback and the 15-minute poller resolve
	 * the same 10-minute-late job in the same instant. If surfaces ever
	 * gain a non-idempotent success side effect, revisit this with the
	 * postmeta claim's compare-and-clear SQL against the backing store.
	 *
	 * @param int    $surface_id
	 * @param string $job_id
	 * @return bool True when this caller cleared the matching job.
	 */
	public static function claim_job( $surface_id, $job_id ) {
		$job_id = (string) $job_id;
		if ( '' === $job_id ) {
			return false;
		}
		if ( (string) self::get( $surface_id, 'job_id' ) !== $job_id ) {
			return false;
		}
		self::set( $surface_id, 'job_id', '' );
		return true;
	}

	/**
	 * Short key -> full meta key, mirroring DOIG_Post_Meta's convention
	 * (its KEYS map is a naming shim over the same '_doig_' prefix).
	 *
	 * @param string $key
	 * @return string
	 */
	private static function meta_key( $key ) {
		return '_doig_' . $key;
	}
}
