<?php
declare(strict_types=1);

namespace DMRS\MBS\Api\License;

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

/**
 * File Path: includes/api/license/class-mbs-license-credentials.php
 *
 * License Credentials Manager
 *
 * Handles secure storage and retrieval of all license-related wp_options.
 *
 * @package MyBookShowroom
 * @since 1.0.0
 */

use WP_Error;

/** Class MBS_License_Credentials */
final class MBS_License_Credentials {

	// -------------------------------------------------------------------------
	// store() / retrieve() / clear()
	// -------------------------------------------------------------------------

	/**
	 * Store license credentials received from the API server.
	 *
	 * Writes every canonical option key that the rest of the plugin reads.
	 *
	 * Expected keys in $credentials (from activation/ping API response):
	 *   license_key    — raw license key string
	 *   api_secret     — per-activation HMAC secret
	 *   nonce          — current nonce
	 *   tier           — 'free' | 'pro' | 'pub'
	 *   status         — 'active' | 'inactive' | etc.
	 *   sites_used     — (int) current number of active sites on this license
	 *   sites_allowed  — (int) maximum sites for this tier
	 *
	 * @param array $credentials Credentials array from the API response.
	 */
	public function store( array $credentials ): bool {
		$license_key   = $credentials['license_key']   ?? '';
		$api_secret    = $credentials['api_secret']    ?? '';
		$nonce         = $credentials['nonce']         ?? '';
		$tier          = sanitize_text_field( $credentials['tier'] ?? 'free' );
		$sites_used    = isset( $credentials['sites_used'] )    ? (int) $credentials['sites_used']    : -1;
		$sites_allowed = isset( $credentials['sites_allowed'] ) ? (int) $credentials['sites_allowed'] : -1;

		// ── Canonical option keys (read by the entire plugin) ─────────────────
		$tier = in_array( $tier, array( 'free', 'pro', 'pub' ), true ) ? $tier : 'free'; // V7-A-004
		update_option( 'mbs_license_registered', true );
		update_option( 'mbs_license_tier',       $tier );
		// SEC-1: Bind tier to api_secret via HMAC to detect DB-edit escalation on keyed installs.
		$_hmac_secret = \MBS_Encryption::decrypt( get_option( 'mbs_api_secret_encrypted', '' ) ) ?: ''; // R1-authorized: license credential machinery -- value not logged.
		$_hmac        = self::sign_tier( $tier, $_hmac_secret );
		if ( '' !== $_hmac ) {
			update_option( 'mbs_license_tier_hmac', $_hmac, false );
		}
		// SV-12: mbs_license_last_ping records the time of the most recent SUCCESSFUL
		// ping (activation counts as the first ping). mbs_is_pro() compares this value
		// against time() using MBS_PING_INTERVAL + MBS_PING_GRACE to enforce freshness.
		// Resetting it here on activation prevents an immediate stale-ping lockout.
		update_option( 'mbs_license_last_ping', time() );


		if ( ! empty( $license_key ) ) {
			update_option( 'mbs_license_key_encrypted', \MBS_Encryption::encrypt( $license_key ), false );
		}
		if ( ! empty( $api_secret ) ) {
			update_option( 'mbs_api_secret_encrypted', \MBS_Encryption::encrypt( $api_secret ), false );
		}
		if ( ! empty( $nonce ) ) {
			update_option( 'mbs_api_nonce', sanitize_text_field( $nonce ), false );
		}

		// Phase 3a: record the activation timestamp so the UI can display
		// "{tier} since {date}". Reset on re-activation of the same key —
		// semantically "most recent activation on this domain".
		update_option( 'mbs_license_activated_at', time(), false );

		// PLG-055: Store site-usage counters returned by the server so the
		// License tab can display "x of 3 sites active" for PUB licenses.
		if ( $sites_used >= 0 ) {
			update_option( 'mbs_license_sites_used', $sites_used, false );
		}
		if ( $sites_allowed >= 0 ) {
			update_option( 'mbs_license_sites_allowed', $sites_allowed, false );
		}

		return true;
	}

	/**
	 * Retrieve license credentials for display (decrypted).
	 *
	 * Reads from the canonical option keys so this always reflects the true
	 * stored state, even for licenses activated before the parent/child refactor.
	 *
	 * @return array|null Array with 'license_key', 'tier', 'status', or null if none.
	 */
	public function retrieve(): ?array {
		$registered = (bool) get_option( 'mbs_license_registered', false );
		if ( ! $registered ) {
			return null;
		}

		$key_encrypted = get_option( 'mbs_license_key_encrypted', '' );
		$license_key   = $key_encrypted ? ( \MBS_Encryption::decrypt( $key_encrypted ) ?: '' ) : ''; // R1-authorized: license credential machinery -- value not logged.
		$tier          = mbs_get_license_tier_label();
		$last_ping     = (int) get_option( 'mbs_license_last_ping', 0 );

		// Derive a human-readable status from the stored state.
		$status = 'active';
		if ( ! $license_key ) {
			$status = 'inactive';
		} elseif ( in_array( $tier, array( 'pro', 'pub' ), true ) ) {
			$max_age = defined( 'MBS_PING_INTERVAL' ) ? MBS_PING_INTERVAL + MBS_PING_GRACE : 176400;
			if ( $last_ping > 0 && ( time() - $last_ping ) > $max_age ) {
				$status = 'expired';
			} else {
				$status = $tier; // 'pro' or 'pub'
			}
		}

		return array(
			'license_key' => $license_key,
			'tier'        => $tier,
			'status'      => $status,
			'last_ping'   => $last_ping,
		);
	}

	/**
	 * Clear all stored license credentials.
	 *
	 * Deletes every canonical option key so mbs_is_registered() returns false
	 * and the entire plugin correctly reflects the deactivated state.
	 *
	 */
	public function clear(): bool {
		// Canonical keys read by the rest of the plugin
		delete_option( 'mbs_license_registered' );
		delete_option( 'mbs_license_tier' );
		delete_option( 'mbs_license_last_ping' );
		delete_option( 'mbs_license_activated_at' );
		delete_option( 'mbs_license_key_encrypted' );
		delete_option( 'mbs_api_secret_encrypted' );
		delete_option( 'mbs_api_nonce' );

		// PLG-055: site-usage counters
		delete_option( 'mbs_license_sites_used' );
		delete_option( 'mbs_license_sites_allowed' );
		delete_option( 'mbs_license_tier_hmac' ); // SEC-1: Remove stale tier HMAC on deactivation.


		// Tamper / integrity cache
		delete_transient( 'mbs_file_hashes_cache' );

		// B-008: Manifest cache and version cleared on deactivation so a fresh
		// activation with a different key re-fetches the manifest rather than
		// treating the old activation's cached version as still valid.
		delete_transient( 'mbs_manifest_cache' );
		delete_option( 'mbs_manifest_version' );

		return true;
	}

	/**
	 * Check if a valid license is currently stored.
	 *
	 * Reads the canonical 'mbs_license_registered' option — the same key that
	 * mbs_is_registered() reads — so both always agree.
	 *
	 */
	public function is_registered(): bool {
		return (bool) get_option( 'mbs_license_registered', false );
	}

	// -------------------------------------------------------------------------
	// Nonce rotation (called by ping handler after a successful validate)
	// -------------------------------------------------------------------------

	/**
	 * Update the stored nonce, last-ping timestamp, and site-usage counters
	 * after a successful ping.
	 *
	 * @param string $new_nonce     Nonce returned by the server.
	 * @param string $tier          Tier returned by the server.
	 * @param int    $sites_used    Current active-site count (-1 = not returned, skip).
	 * @param int    $sites_allowed Max sites for this license (-1 = not returned, skip).
	 */
	public function update_after_ping( string $new_nonce, string $tier, int $sites_used = -1, int $sites_allowed = -1 ): void {
		if ( ! empty( $new_nonce ) ) {
			update_option( 'mbs_api_nonce', sanitize_text_field( $new_nonce ), false );
		}

		// Phase 3a: backfill activated_at for legacy installs where the
		// option was never written. Use the prior last_ping value (set on
		// the original activation as the "first ping") as the best
		// approximation before we overwrite it with the fresh time() below.
		if ( ! get_option( 'mbs_license_activated_at', 0 ) ) {
			$prior_ping = (int) get_option( 'mbs_license_last_ping', 0 );
			update_option( 'mbs_license_activated_at', $prior_ping > 0 ? $prior_ping : time(), false );
		}

		update_option( 'mbs_license_last_ping', time() );
		$clean_tier = sanitize_text_field( $tier );
		$clean_tier = in_array( $clean_tier, array( 'free', 'pro', 'pub' ), true ) ? $clean_tier : 'free'; // V7-A-004
		update_option( 'mbs_license_tier',           $clean_tier );
		// SEC-1: Bind tier to api_secret via HMAC to detect DB-edit escalation on keyed installs.
		$_hmac_secret = \MBS_Encryption::decrypt( get_option( 'mbs_api_secret_encrypted', '' ) ) ?: ''; // R1-authorized: license credential machinery -- value not logged.
		$_hmac        = self::sign_tier( $clean_tier, $_hmac_secret );
		if ( '' !== $_hmac ) {
			update_option( 'mbs_license_tier_hmac', $_hmac, false );
		}


		// PLG-055: refresh site-usage counters if the server returned them.
		if ( $sites_used >= 0 ) {
			update_option( 'mbs_license_sites_used', $sites_used, false );
		}
		if ( $sites_allowed >= 0 ) {
			update_option( 'mbs_license_sites_allowed', $sites_allowed, false );
		}
	}
	// -------------------------------------------------------------------------
	// SEC-1 — Tier HMAC helpers
	// -------------------------------------------------------------------------
	// -------------------------------------------------------------------------
	// SEC-1 — Tier HMAC helpers
	// -------------------------------------------------------------------------

	/**
	 * Compute HMAC binding tier to api_secret — prevents plain-text DB-edit escalation on keyed installs.
	 *
	 * @param string $tier       Tier string ('free'|'pro'|'pub').
	 * @param string $api_secret Decrypted API secret. Empty on keyless installs.
	 * @return string HMAC-SHA256 hex string, or '' when api_secret is not available.
	 */
	public static function sign_tier( string $tier, string $api_secret ): string {
		if ( '' === $api_secret ) {
			return ''; // Keyless FREE installs cannot HMAC-bind their tier.
		}
		return hash_hmac( 'sha256', 'mbs_tier:' . $tier, $api_secret );
	}

	/**
	 * Verify the stored tier HMAC matches the current tier on keyed installs.
	 * Returns true unconditionally on keyless installs (no secret = no verification possible).
	 * Returns false when stored_hmac is '' on a keyed install (F-03 fail-closed);
	 * the HMAC is re-minted only from a verified ping/activation response.
	 *
	 * @param string $tier        Current mbs_license_tier value.
	 * @param string $stored_hmac Stored mbs_license_tier_hmac value.
	 * @param string $api_secret  Decrypted API secret.
	 * @return bool
	 */
	public static function verify_tier( string $tier, string $stored_hmac, string $api_secret ): bool {
		if ( '' === $api_secret ) {
			return true; // Keyless installs: no secret, no HMAC verification.
		}
		if ( '' === $stored_hmac ) {
			// F-03: A keyed install with a missing tier HMAC must FAIL CLOSED.
			// Never self-heal/mint the HMAC from the bare stored tier value here —
			// deleting the option was a one-DB-write escalation. The tier HMAC is
			// (re)minted ONLY from a VERIFIED ping/activation response
			// (store() / update_after_ping()), never from an unverified option.
			// Returning false demotes the caller to free.
			return false;
		}
		return hash_equals( self::sign_tier( $tier, $api_secret ), $stored_hmac );
	}
}
