<?php
/**
 * Livada SEO — License Manager (client side)
 *
 * Validates the license key against the livada.fr license server.
 * The key is stored encrypted (AES-256-CBC) in its own option so it
 * is never accidentally exported or logged.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_License {

	/**
	 * Base URL of the license server.
	 *
	 * Routed through a Cloudflare Worker proxy that forwards to livada.fr's
	 * REST API. The Worker bypasses Hostinger HCDN bot detection that blocks
	 * server-to-server requests from non-Hostinger origins (OVH, LWS, etc.).
	 *
	 * Custom domain license.livada.fr (DNS via Cloudflare → Worker) is the
	 * pro-grade endpoint; the workers.dev fallback stays available in case
	 * the custom domain is mis-configured.
	 *
	 * Cloudflare Free tier: 100k requests/day — far above licence-check needs.
	 */
	const SERVER_URL = 'https://livada.io/api/license/';

	/** WP option that stores the encrypted license key. */
	const OPTION_KEY = 'livada_seo_license_key';

	/** WP option that caches the last validation result. */
	const OPTION_CACHE = 'livada_seo_license_status';

	/**
	 * Re-validate every 1 h.
	 * Short TTL = faster propagation when admin disables a key server-side.
	 * Trade-off: more requests to the licence server, but Cloudflare Worker
	 * easily handles thousands per minute on free tier.
	 */
	const CACHE_TTL = 1 * HOUR_IN_SECONDS;

	// ── Bootstrap ──────────────────────────────────────────────────────────────

	public static function init(): void {
		add_action( 'wp_ajax_livada_seo_activate_license',   [ self::class, 'ajax_activate'   ] );
		add_action( 'wp_ajax_livada_seo_deactivate_license', [ self::class, 'ajax_deactivate' ] );
		add_action( 'wp_ajax_livada_seo_check_license',      [ self::class, 'ajax_check'      ] );
	}

	// ── Key storage ────────────────────────────────────────────────────────────

	public static function store_key( string $key ): void {
		update_option( self::OPTION_KEY, self::encrypt( $key ), false );
	}

	public static function get_key(): string {
		$raw = (string) get_option( self::OPTION_KEY, '' );
		return '' !== $raw ? self::decrypt( $raw ) : '';
	}

	public static function remove_key(): void {
		delete_option( self::OPTION_KEY );
		delete_option( self::OPTION_CACHE );
	}

	// ── Validation ─────────────────────────────────────────────────────────────

	/** Returns true when a valid, non-expired licence is on record. */
	public static function is_valid(): bool {
		$status = self::get_status();
		return isset( $status['valid'] ) && true === $status['valid'];
	}

	/**
	 * Returns the cached status array, refreshing from the server when the
	 * cache is older than CACHE_TTL or when $force is true.
	 *
	 * @return array{valid:bool, message:string, plan?:string, email?:string, expires_at?:string, checked_at:int}
	 */
	public static function get_status( bool $force = false ): array {
		if ( ! $force ) {
			$cached = get_option( self::OPTION_CACHE, null );
			if ( is_array( $cached ) && isset( $cached['checked_at'] ) ) {
				if ( time() - (int) $cached['checked_at'] < self::CACHE_TTL ) {
					return $cached;
				}
			}
		}

		$key = self::get_key();
		if ( '' === $key ) {
			return [ 'valid' => false, 'message' => 'no_key', 'checked_at' => time() ];
		}

		return self::remote_validate( $key );
	}

	// ── Activate / Deactivate ──────────────────────────────────────────────────

	/**
	 * Activates a licence key for this site.
	 * Stores the key and caches the server response.
	 *
	 * @return array{valid:bool, message:string, ...}
	 */
	public static function activate( string $key ): array {
		$response = wp_remote_post(
			self::SERVER_URL . 'activate',
			[
				'timeout' => 15,
				'body'    => [
					'license_key' => sanitize_text_field( $key ),
					'site_url'    => home_url( '/' ),
				],
			]
		);

		$status               = self::parse_response( $response );
		$status['checked_at'] = time();

		if ( $status['valid'] ) {
			self::store_key( sanitize_text_field( $key ) );
		}
		update_option( self::OPTION_CACHE, $status, false );
		return $status;
	}

	/** Deactivates this site's licence and clears local storage. */
	public static function deactivate(): array {
		$key = self::get_key();
		if ( '' !== $key ) {
			wp_remote_post(
				self::SERVER_URL . 'deactivate',
				[
					'timeout' => 10,
					'body'    => [
						'license_key' => $key,
						'site_url'    => home_url( '/' ),
					],
				]
			);
		}
		self::remove_key();
		return [ 'valid' => false, 'message' => 'deactivated', 'checked_at' => time() ];
	}

	// ── AJAX handlers ──────────────────────────────────────────────────────────

	public static function ajax_activate(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'Unauthorized.' ] );
		}

		$key = isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '';
		if ( '' === $key ) {
			wp_send_json_error( [ 'message' => 'Empty license key.' ] );
		}

		$result = self::activate( $key );
		if ( $result['valid'] ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	}

	public static function ajax_deactivate(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'Unauthorized.' ] );
		}
		wp_send_json_success( self::deactivate() );
	}

	public static function ajax_check(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => 'Unauthorized.' ] );
		}
		$status = self::get_status( true ); // force re-check
		if ( $status['valid'] ) {
			wp_send_json_success( $status );
		} else {
			wp_send_json_error( $status );
		}
	}

	// ── Download URL helper (used by Updater) ─────────────────────────────────

	/**
	 * Builds the authenticated download URL by appending the licence key and
	 * site URL as query params.  Returns '' when no valid licence is present
	 * (WP treats an empty package as "no update available").
	 */
	public static function get_download_url( string $base_url ): string {
		if ( '' === $base_url ) {
			return '';
		}
		$key = self::get_key();
		if ( '' === $key || ! self::is_valid() ) {
			return ''; // licence missing or invalid → block download
		}

		return add_query_arg(
			[
				'license_key' => rawurlencode( $key ),
				'site_url'    => rawurlencode( home_url( '/' ) ),
			],
			$base_url
		);
	}

	// ── Remote call + response parser ─────────────────────────────────────────

	private static function remote_validate( string $key ): array {
		$response = wp_remote_post(
			self::SERVER_URL . 'validate',
			[
				'timeout' => 10,
				'body'    => [
					'license_key' => $key,
					'site_url'    => home_url( '/' ),
				],
			]
		);

		$status               = self::parse_response( $response );
		$status['checked_at'] = time();
		update_option( self::OPTION_CACHE, $status, false );
		return $status;
	}

	/**
	 * @param \WP_Error|array $response
	 * @return array{valid:bool, message:string, plan?:string, email?:string, expires_at?:string}
	 */
	private static function parse_response( $response ): array {
		if ( is_wp_error( $response ) ) {
			return [ 'valid' => false, 'message' => 'connection_error' ];
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( ! is_array( $data ) ) {
			return [ 'valid' => false, 'message' => 'invalid_response' ];
		}

		if ( 200 === $code && ! empty( $data['valid'] ) ) {
			return [
				'valid'      => true,
				'plan'       => (string) ( $data['plan']       ?? 'pro' ),
				'email'      => (string) ( $data['email']      ?? '' ),
				'expires_at' => (string) ( $data['expires_at'] ?? '' ),
				'message'    => 'active',
			];
		}

		return [
			'valid'   => false,
			'message' => (string) ( $data['message'] ?? 'invalid_license' ),
		];
	}

	// ── AES-256-CBC encryption ─────────────────────────────────────────────────

	private static function get_enc_key(): string {
		$salt = defined( 'AUTH_SALT' ) ? AUTH_SALT : 'livada-seo-license-fallback-salt';
		return substr( hash( 'sha256', $salt . 'livada_license_enc_v1', true ), 0, 32 );
	}

	public static function encrypt( string $plain ): string {
		if ( '' === $plain ) {
			return '';
		}
		$key = self::get_enc_key();
		$iv  = random_bytes( 16 );
		$enc = openssl_encrypt( $plain, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
		if ( false === $enc ) {
			return '';
		}
		return base64_encode( $iv . $enc );
	}

	public static function decrypt( string $encoded ): string {
		if ( '' === $encoded ) {
			return '';
		}
		$raw = base64_decode( $encoded, true );
		if ( false === $raw || strlen( $raw ) <= 16 ) {
			return '';
		}
		$key = self::get_enc_key();
		$iv  = substr( $raw, 0, 16 );
		$enc = substr( $raw, 16 );
		$dec = openssl_decrypt( $enc, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
		return false === $dec ? '' : $dec;
	}
}
