<?php

namespace Ugcify\License;

defined( 'ABSPATH' ) || exit;

/**
 * Issues and verifies HMAC-SHA256 signed license tokens.
 *
 * Token layout (all values are base64-encoded JSON):
 *
 *   outer  →  { "p": "<base64 payload>", "sig": "<hmac-sha256 hex>" }
 *   payload → { domain, plan, key_hash, issued_at, token_expires_at, license_expires }
 *
 * Security properties
 * ─────────────────────────────────────────────────────────────────────────────
 * Tamper resistance:  HMAC-SHA256 signature covers the entire payload.
 *                     Changing any field breaks the signature.
 *
 * Domain binding:     Token embeds the site's home_url() domain.
 *                     Copying the DB to another domain fails the domain check.
 *
 * 24-hour TTL:        token_expires_at = issued_at + TOKEN_TTL (86 400 s).
 *                     After that the token is invalid regardless of DB state.
 *
 * Site-specific key:  HMAC secret = hmac( HARDCODED_SALT, AUTH_KEY ).
 *                     AUTH_KEY lives in wp-config.php, so copying the plugin
 *                     files to another server does not transfer the secret.
 *
 * @since 1.1.0
 * @package Ugcify\License
 */
class TokenVerifier {

	/**
	 * Token time-to-live in seconds (24 hours).
	 */
	const TOKEN_TTL = 86400;

	/**
	 * WP option key where the serialised token is stored.
	 */
	const OPTION_KEY = 'ugcify_license_token';

	/**
	 * Hardcoded component of the HMAC secret.
	 * Must NOT be changed after release — doing so invalidates all tokens.
	 */
	private const SALT = 'ugcify_cmmrc_tok_v1_rx2025_7f3k';

	// =========================================================================
	// Public API
	// =========================================================================

	/**
	 * Issue a new signed token and persist it in wp_options.
	 *
	 * Must be called every time a license is successfully activated or renewed.
	 *
	 * @param string $plan            Canonical plan slug ('growth'|'pro'|'business').
	 * @param string $license_key     Raw (unmasked) license key.
	 * @param string $license_expires License subscription end date (Y-m-d or Unix ts).
	 *                                Pass empty string for lifetime / never-expires.
	 */
	public static function issue_token( string $plan, string $license_key, string $license_expires = '' ): void {
		$payload = array(
			'domain'            => self::get_current_domain(),
			'plan'              => $plan,
			// Store a hash of the key, not the key itself.
			'key_hash'          => hash( 'sha256', $license_key ),
			'issued_at'         => time(),
			'token_expires_at'  => time() + self::TOKEN_TTL,
			'license_expires'   => $license_expires,
		);

		$payload_b64 = base64_encode( wp_json_encode( $payload ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		$signature   = hash_hmac( 'sha256', $payload_b64, self::derive_secret() );

		$token = base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
			wp_json_encode(
				array(
					'p'   => $payload_b64,
					'sig' => $signature,
				)
			)
		);

		// Store without autoloading — only read during feature gate checks.
		update_option( self::OPTION_KEY, $token, false );
	}

	/**
	 * Verify the stored token and return its decoded data.
	 *
	 * Checks performed in order:
	 *   1. Token exists.
	 *   2. Outer JSON is well-formed.
	 *   3. HMAC signature matches (hash_equals — prevents timing attacks).
	 *   4. Inner payload is well-formed.
	 *   5. Domain matches home_url().
	 *   6. Token has not exceeded its 24-hour TTL.
	 *   7. License subscription end date has not passed (if set).
	 *
	 * @return array {
	 *     @type bool   $valid            Whether all checks passed.
	 *     @type string $plan             Canonical plan slug (empty on failure).
	 *     @type string $reason           Short failure key (empty on success).
	 *     @type int    $issued_at        Unix timestamp when token was issued.
	 *     @type int    $token_expires_at Unix timestamp when token expires.
	 * }
	 */
	public static function verify(): array {
		$token = get_option( self::OPTION_KEY, '' );

		if ( empty( $token ) ) {
			return self::fail( 'no_token' );
		}

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		$outer = json_decode( base64_decode( $token ), true );
		if ( ! is_array( $outer ) || empty( $outer['p'] ) || empty( $outer['sig'] ) ) {
			return self::fail( 'malformed' );
		}

		// ── 1. Signature integrity check ─────────────────────────────────────
		$expected = hash_hmac( 'sha256', $outer['p'], self::derive_secret() );
		if ( ! hash_equals( $expected, $outer['sig'] ) ) {
			return self::fail( 'tampered' );
		}

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		$payload = json_decode( base64_decode( $outer['p'] ), true );
		if ( ! is_array( $payload ) ) {
			return self::fail( 'malformed_payload' );
		}

		// ── 2. Domain binding ────────────────────────────────────────────────
		if ( isset( $payload['domain'] ) && $payload['domain'] !== self::get_current_domain() ) {
			return self::fail( 'domain_mismatch' );
		}

		// ── 3. Token TTL — hard expiry, no grace period ──────────────────────
		if ( isset( $payload['token_expires_at'] ) && (int) $payload['token_expires_at'] < time() ) {
			return self::fail( 'token_expired' );
		}

		// ── 4. License subscription end date ────────────────────────────────
		if ( ! empty( $payload['license_expires'] ) ) {
			$exp_ts = is_numeric( $payload['license_expires'] )
				? (int) $payload['license_expires']
				: (int) strtotime( (string) $payload['license_expires'] );

			if ( $exp_ts > 0 && $exp_ts < time() ) {
				return self::fail( 'license_expired' );
			}
		}

		return array(
			'valid'            => true,
			'plan'             => (string) ( $payload['plan'] ?? '' ),
			'reason'           => '',
			'issued_at'        => (int) ( $payload['issued_at'] ?? 0 ),
			'token_expires_at' => (int) ( $payload['token_expires_at'] ?? 0 ),
		);
	}

	/**
	 * Delete the stored token, revoking license access immediately.
	 */
	public static function revoke(): void {
		delete_option( self::OPTION_KEY );
	}

	/**
	 * Get the token expiry timestamp (0 if no valid token).
	 *
	 * @return int
	 */
	public static function get_token_expires_at(): int {
		$result = self::verify();
		return $result['valid'] ? (int) $result['token_expires_at'] : 0;
	}

	// =========================================================================
	// Internal helpers
	// =========================================================================

	/**
	 * Derive the HMAC secret from two components:
	 *   - SALT: hardcoded in the plugin source (part of the build).
	 *   - AUTH_KEY: the unique cryptographic key from wp-config.php.
	 *
	 * This means even if the plugin files are copied elsewhere the HMAC secret
	 * changes, so tokens issued on site A cannot be transferred to site B.
	 *
	 * @return string 64-char hexadecimal string.
	 */
	private static function derive_secret(): string {
		$auth_key = defined( 'AUTH_KEY' ) ? (string) AUTH_KEY : 'ugcify-fallback-auth-key-2025';
		return hash_hmac( 'sha256', self::SALT, $auth_key );
	}

	/**
	 * Return the normalised domain for the current site (no protocol prefix).
	 *
	 * @return string
	 */
	private static function get_current_domain(): string {
		if ( function_exists( 'home_url' ) ) {
			$url = home_url();
		} elseif ( defined( 'UGCIFY_INSTANCE' ) ) {
			return (string) UGCIFY_INSTANCE;
		} else {
			return '';
		}

		$protocol = ( ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			|| ( ! empty( $_SERVER['SERVER_PORT'] ) && 443 === (int) $_SERVER['SERVER_PORT'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			? 'https://'
			: 'http://';

		return str_replace( $protocol, '', $url );
	}

	/**
	 * Build a failed-verification return array.
	 *
	 * @param string $reason Short descriptor for the failure cause.
	 * @return array
	 */
	private static function fail( string $reason ): array {
		return array(
			'valid'            => false,
			'plan'             => '',
			'reason'           => $reason,
			'issued_at'        => 0,
			'token_expires_at' => 0,
		);
	}
}
