<?php
/**
 * Age Verification Extension — server-side enforcement.
 *
 * Gated on the 'age-verification' extension toggle. The storefront PDP already
 * renders a CLIENT-side age gate (templates/product/single.php); this class adds
 * the SERVER-side guard that gate lacked. Restricted items cannot be added to the
 * cart or checked out unless the visitor has verified an age >= the product's
 * effective requirement.
 *
 * Verification signal: the `tc_age_verified` cookie. The PDP gate's JS sets it to
 * the verified age (e.g. "21") on a successful confirmation; this guard reads that
 * age server-side and compares it to each restricted item's requirement.
 *
 * Chosen over an httponly server-set cookie per the S238 scope decision: this
 * closes the trivial bypass (direct cart-endpoint hits / deleting the overlay)
 * without a verify round-trip. A determined user could still forge the cookie;
 * httponly hardening is a documented future upgrade, not a Part-1 requirement.
 *
 * Effective age requirement for a product (Part 1 = product-level):
 *   age_restricted ? (age_limit > 0 ? age_limit : default_age_limit) : 0
 * Category-level restriction (Part 2) and the optional site-wide gate (Part 3)
 * extend this class; the guards below are their single enforcement point.
 *
 * Settings (tc_settings section = 'age_verification'): default_age_limit,
 * verification_type, warning_message, deny_action, remember_days. Unlike
 * reCAPTCHA there are NO external keys, so is_active() is just the toggle.
 *
 * @package TitanCart\Extensions
 */

namespace TitanCart\Extensions;

use TitanCart\REST\Extensions\ExtensionsController;
use TitanCart\Services\SettingsService;

defined( 'ABSPATH' ) || exit;

class AgeVerificationExtension {

	/** Cookie carrying the verified age as an integer string, e.g. "21". */
	const COOKIE = 'tc_age_verified';

	/** @var array<int,array{restricted:bool,limit:int}> Per-request product requirement cache. */
	private static $product_cache = [];

	/**
	 * Hooks registered here, in priority order:
	 *   - wp_footer @1: site-wide gate render (skipped when not enabled, or when
	 *     the visitor has already verified to >= site_age_limit). Fires on every
	 *     storefront page; doesn't fire on REST/AJAX/cron/admin/login by virtue
	 *     of where WP dispatches wp_footer. No role-based skip — the gate is
	 *     intentionally pre-login, so logging in requires verifying first.
	 *
	 * The cart/checkout server guards (Part 1) are NOT hooked — they're invoked
	 * directly by the REST controllers at their real dispatch points (#192).
	 */
	public static function init(): void {
		if ( ! self::is_active() ) {
			return;
		}
		if ( self::site_wide_enabled() ) {
			add_action( 'wp_footer', [ self::class, 'render_site_wide_gate' ], 1 );
		}
	}

	/** Enabled when the registry toggle is on. No external keys required. */
	public static function is_active(): bool {
		return ExtensionsController::is_active( 'age-verification' );
	}

	/* ══════════════════════════════════════════════════════════════════════
	 * SETTINGS
	 * ══════════════════════════════════════════════════════════════════════ */

	public static function default_age_limit(): int {
		return max( 0, SettingsService::int( 'age_verification', 'default_age_limit', 18 ) );
	}

	/** Whether the site-wide gate is configured on. */
	public static function site_wide_enabled(): bool {
		return SettingsService::bool( 'age_verification', 'site_wide_enabled', false );
	}

	/** Site-wide minimum age; falls back to the global default when 0. */
	public static function site_age_limit(): int {
		$age = max( 0, SettingsService::int( 'age_verification', 'site_age_limit', 0 ) );
		return $age > 0 ? $age : self::default_age_limit();
	}

	/* ══════════════════════════════════════════════════════════════════════
	 * PRODUCT REQUIREMENT
	 * ══════════════════════════════════════════════════════════════════════ */

	/**
	 * Effective minimum age required to purchase a product. 0 = no restriction.
	 * Resolves to MAX("strictest age wins") of:
	 *   - the product's own age_limit (if product.age_restricted)
	 *   - any age-restricted category the product belongs to
	 * Global default fallback applies to each side independently when its limit
	 * is left at 0. Per-request cache via $product_cache keeps repeated guards
	 * on the same product to a single DB round-trip (cart iteration in
	 * guard_items() hits this method N times per request).
	 */
	public static function product_age_requirement( int $product_id ): int {
		if ( $product_id <= 0 ) {
			return 0;
		}
		if ( isset( self::$product_cache[ $product_id ] ) ) {
			$c = self::$product_cache[ $product_id ];
			return $c['restricted'] ? $c['limit'] : 0;
		}

		$db      = titancart_db();
		$default = self::default_age_limit();

		// Product-side requirement.
		$row = $db->getRow(
			"SELECT age_restricted, age_limit FROM {$db->table('products')} WHERE id = ?i",
			$product_id
		);
		$product_restricted = $row ? ( (int) $row['age_restricted'] === 1 ) : false;
		$product_limit      = $row ? (int) $row['age_limit'] : 0;
		if ( $product_restricted && $product_limit <= 0 ) {
			$product_limit = $default;
		}
		$product_age = $product_restricted ? $product_limit : 0;

		// Category-side requirement — JOIN product_categories → categories, take
		// the highest age_limit (with default fallback) among any restricted
		// category the product is linked to. Soft-deleted categories ignored.
		$cat_age = (int) $db->getField(
			"SELECT COALESCE(MAX(IF(c.age_limit > 0, c.age_limit, ?i)), 0)
			 FROM {$db->table('product_categories')} pc
			 INNER JOIN {$db->table('categories')} c ON c.id = pc.category_id
			 WHERE pc.product_id = ?i
			   AND c.age_restricted = 1
			   AND c.deleted_at IS NULL",
			$default,
			$product_id
		);

		$effective  = max( $product_age, $cat_age );
		$restricted = $effective > 0;
		self::$product_cache[ $product_id ] = [ 'restricted' => $restricted, 'limit' => $effective ];
		return $effective;
	}

	/** True when the product is age-restricted at all. */
	public static function is_restricted( int $product_id ): bool {
		return self::product_age_requirement( $product_id ) > 0;
	}

	/* ══════════════════════════════════════════════════════════════════════
	 * VERIFICATION SIGNAL
	 * ══════════════════════════════════════════════════════════════════════ */

	/**
	 * The age the current visitor has verified, read from the cookie. 0 = none.
	 * The legacy "1" value (pre-S238 boolean cookie) is treated as "verified at
	 * the global default age" so already-verified visitors aren't re-prompted by
	 * the upgrade — while a higher-age product still correctly re-gates them.
	 */
	public static function verified_age(): int {
		if ( empty( $_COOKIE[ self::COOKIE ] ) ) {
			return 0;
		}
		$raw = sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE ] ) );
		if ( $raw === '1' ) {
			return self::default_age_limit();
		}
		return max( 0, (int) $raw );
	}

	/** Whether the visitor has verified an age >= the required minimum. */
	public static function meets( int $required_age ): bool {
		if ( $required_age <= 0 ) {
			return true;
		}
		return self::verified_age() >= $required_age;
	}

	/* ══════════════════════════════════════════════════════════════════════
	 * GUARDS — called by the REST controllers
	 * ══════════════════════════════════════════════════════════════════════ */

	/**
	 * Guard a single add-to-cart. Returns a customer-facing error message to
	 * block, or null to allow. Always allows when the extension is inactive or
	 * the product is unrestricted.
	 */
	public static function guard_add_to_cart( int $product_id ): ?string {
		if ( ! self::is_active() ) {
			return null;
		}
		$required = self::product_age_requirement( $product_id );
		if ( $required <= 0 || self::meets( $required ) ) {
			return null;
		}
		return self::deny_message( $required );
	}

	/**
	 * Guard a list of cart line items at checkout (defense in depth — catches
	 * items added before the extension was enabled, or via non-guarded paths).
	 * Each item is an array carrying at least 'product_id'. Returns an error for
	 * the highest unmet requirement, or null to allow.
	 *
	 * @param array<int,array<string,mixed>> $items
	 */
	public static function guard_items( array $items ): ?string {
		if ( ! self::is_active() ) {
			return null;
		}
		$highest = 0;
		foreach ( $items as $item ) {
			$pid = (int) ( $item['product_id'] ?? 0 );
			if ( $pid <= 0 ) {
				continue;
			}
			$required = self::product_age_requirement( $pid );
			if ( $required > 0 && ! self::meets( $required ) && $required > $highest ) {
				$highest = $required;
			}
		}
		return $highest > 0 ? self::deny_message( $highest ) : null;
	}

	/** Build the customer-facing block message for a given required age. */
	private static function deny_message( int $required_age ): string {
		$tpl = SettingsService::get(
			'age_verification',
			'warning_message',
			'This product is age-restricted. You must be at least {age} years old to view or purchase this item.'
		);
		return str_replace( '{age}', (string) $required_age, $tpl );
	}

	/* ══════════════════════════════════════════════════════════════════════
	 * SITE-WIDE GATE — render hook (Part 3)
	 * ══════════════════════════════════════════════════════════════════════ */

	/**
	 * wp_footer render: site-wide age gate modal. Skips when the visitor's
	 * verified age already meets the site-wide threshold. Uses the same cookie
	 * (`tc_age_verified={age}`) as the PDP/category gates so a single accept
	 * composes across all three. A separate DOM id (`tc-age-gate--site`) keeps
	 * it distinct from the PDP gate on pages where both might render — the
	 * PDP modal hides itself when meets() returns true, so the typical flow
	 * is: site-wide accept → cookie set → page reveals; if the product needs
	 * a higher age the PDP modal then asks for the step-up.
	 *
	 * Reuses the existing .tc-age-gate__* CSS classes (site-wide loads
	 * titancart.css). Verification method/deny action/remember days come from
	 * the same settings the PDP gate reads.
	 */
	public static function render_site_wide_gate(): void {
		if ( ! self::is_active() || ! self::site_wide_enabled() ) {
			return;
		}
		$age = self::site_age_limit();
		if ( $age <= 0 ) {
			return;
		}
		if ( self::meets( $age ) ) {
			return;
		}

		$verify_type = SettingsService::get( 'age_verification', 'verification_type', 'confirm' );
		$deny_action = SettingsService::get( 'age_verification', 'deny_action', 'redirect' );
		$remember    = max( 0, SettingsService::int( 'age_verification', 'remember_days', 30 ) );
		$message     = SettingsService::get(
			'age_verification',
			'site_message',
			'This site contains age-restricted content. You must be at least {age} years old to enter.'
		);
		$message     = str_replace( '{age}', (string) $age, $message );

		// Render the modal. Stylesheet (.tc-age-gate__*) is shared with the PDP
		// gate; this overlay is given a slightly higher z-index so it sits on
		// top if both happen to render on the same page (fresh visit to a
		// stricter PDP). Distinct id so JS in single.php doesn't pick it up.
		?>
		<div class="tc-age-gate" id="tc-age-gate--site" style="z-index:100000">
			<div class="tc-age-gate__card">
				<div class="tc-age-gate__icon"><i class="bi bi-shield-exclamation"></i></div>
				<h3 class="tc-age-gate__title"><?php esc_html_e( 'Age Verification Required', 'titancart-lite' ); ?></h3>
				<p class="tc-age-gate__message"><?php echo esc_html( $message ); ?></p>
				<?php if ( $verify_type === 'confirm' ) : ?>
				<div class="tc-age-gate__actions">
					<button type="button" class="tc-age-gate__btn tc-age-gate__btn--yes" id="tc-age-site-confirm">
						<?php
						/* translators: %d: minimum age required to view the site. */
						echo esc_html( sprintf( __( 'I am %d+ years old', 'titancart-lite' ), $age ) );
						?>
					</button>
					<button type="button" class="tc-age-gate__btn tc-age-gate__btn--no" id="tc-age-site-deny">
						<?php
						/* translators: %d: minimum age required to view the site. */
						echo esc_html( sprintf( __( 'I am under %d', 'titancart-lite' ), $age ) );
						?>
					</button>
				</div>
				<?php elseif ( $verify_type === 'birthday' ) : ?>
				<div class="tc-age-gate__birthday">
					<label class="tc-age-gate__label"><?php esc_html_e( 'Enter your date of birth:', 'titancart-lite' ); ?></label>
					<input type="date" id="tc-age-site-dob" class="tc-age-gate__input"
					       max="<?php echo esc_attr( gmdate( 'Y-m-d' ) ); ?>">
					<button type="button" class="tc-age-gate__btn tc-age-gate__btn--yes" id="tc-age-site-dob-submit"><?php esc_html_e( 'Verify', 'titancart-lite' ); ?></button>
				</div>
				<?php elseif ( $verify_type === 'checkbox' ) : ?>
				<div class="tc-age-gate__checkbox">
					<label class="tc-age-gate__check-label">
						<input type="checkbox" id="tc-age-site-check">
						<?php
						/* translators: %d: minimum age required to view the site. */
						echo esc_html( sprintf( __( 'I confirm I am %d years of age or older', 'titancart-lite' ), $age ) );
						?>
					</label>
					<button type="button" class="tc-age-gate__btn tc-age-gate__btn--yes" id="tc-age-site-check-submit" disabled><?php esc_html_e( 'Continue', 'titancart-lite' ); ?></button>
				</div>
				<?php endif; ?>
			</div>
		</div>
		<?php
		// WP.org R2: NOWDOC + strtr() slots, attached via wp_add_inline_script on a
		// footer stub handle (renders on wp_footer@1; footer scripts print at 20, so
		// the handle still prints).
		$tc_sg_limit = (int) $age;
		$tc_sg_deny  = wp_json_encode( $deny_action );
		$tc_sg_rem   = (int) $remember;
		$tc_sg_home  = wp_json_encode( home_url( '/' ) );
		$tc_sg_js    = <<<'JS'
		(function(){
			var gate = document.getElementById('tc-age-gate--site');
			if (!gate) return;
			var AGE_LIMIT = %%AGE_LIMIT%%;
			var DENY_ACTION = %%DENY_ACTION%%;
			var REMEMBER = %%REMEMBER%%;
			var HOME = %%HOME%%;

			function accept() {
				var c = 'tc_age_verified=' + AGE_LIMIT + ';path=/;SameSite=Lax';
				if (REMEMBER > 0) {
					var d = new Date(); d.setTime(d.getTime() + REMEMBER*86400000);
					c += ';expires=' + d.toUTCString();
				}
				document.cookie = c;
				gate.style.opacity = '0';
				setTimeout(function(){ gate.style.display = 'none'; }, 250);
			}
			function deny() {
				if (DENY_ACTION === 'message') {
					gate.querySelector('.tc-age-gate__card').innerHTML = '<div class="tc-age-gate__icon"><i class="bi bi-x-circle" style="color:#dc2626"></i></div><h3 class="tc-age-gate__title">Access Denied</h3><p class="tc-age-gate__message">Sorry, you must be at least ' + AGE_LIMIT + ' years old to enter this site.</p>';
				} else {
					// 'redirect' and 'hide' both map to redirect for the site-wide gate
					// (you can't "hide the whole site" — leaving is the equivalent).
					window.location.href = HOME;
				}
			}

			var yes = document.getElementById('tc-age-site-confirm');
			var no  = document.getElementById('tc-age-site-deny');
			if (yes) yes.addEventListener('click', accept);
			if (no)  no.addEventListener('click', deny);

			var dobBtn = document.getElementById('tc-age-site-dob-submit');
			if (dobBtn) dobBtn.addEventListener('click', function() {
				var dob = document.getElementById('tc-age-site-dob').value;
				if (!dob) return;
				var bd = new Date(dob);
				var now = new Date();
				var age = now.getFullYear() - bd.getFullYear();
				var m = now.getMonth() - bd.getMonth();
				if (m < 0 || (m === 0 && now.getDate() < bd.getDate())) age--;
				if (age >= AGE_LIMIT) accept(); else deny();
			});

			var chk = document.getElementById('tc-age-site-check');
			var chkBtn = document.getElementById('tc-age-site-check-submit');
			if (chk && chkBtn) {
				chk.addEventListener('change', function(){ chkBtn.disabled = !chk.checked; });
				chkBtn.addEventListener('click', function(){ if (chk.checked) accept(); });
			}
		})();
JS;
		$tc_sg_js = strtr( $tc_sg_js, [
			'%%AGE_LIMIT%%'   => (string) $tc_sg_limit,
			'%%DENY_ACTION%%' => $tc_sg_deny,
			'%%REMEMBER%%'    => (string) $tc_sg_rem,
			'%%HOME%%'        => $tc_sg_home,
		] );
		wp_register_script( 'titancart-agegate-inline', false, [], TITANCART_VERSION, true );
		wp_enqueue_script( 'titancart-agegate-inline' );
		wp_add_inline_script( 'titancart-agegate-inline', $tc_sg_js );
	}
}
