<?php
/**
 * Drop-in license client for Algorisys freemium plugins (single plugin + Pro unlock).
 *
 * Copy this file into the plugin (e.g. algo-forms/includes/) and bootstrap:
 *
 *   $license = new Algo_Freemium_License( array(
 *     'product'         => 'algo-forms-pro',
 *     'prefix'          => 'AFPRO',
 *     'option'          => 'algo_forms_license',
 *     'cron'            => 'algo_forms_license_recheck',
 *     'textdomain'      => 'algo-forms',
 *     'api_url_const'   => 'ALGO_FORMS_LICENSE_API_URL',
 *     'allow_dev_const' => 'ALGO_FORMS_ALLOW_DEV_UNLOCK',
 *     'publisher_hosts' => array( 'algorisys.com', 'www.algorisys.com' ),
 *   ) );
 *
 * Do not use this for Algo Media Desk Pro (that stays a separate Pro plugin).
 *
 * Keep in sync with the copy inside each freemium plugin.
 *
 * @package Algorisys
 */

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

if ( class_exists( 'Algo_Freemium_License', false ) ) {
	return;
}

class Algo_Freemium_License {

	/** @var array<string, mixed> */
	private $config;

	/**
	 * @param array<string, mixed> $config Client config.
	 */
	public function __construct( array $config ) {
		$this->config = array_merge(
			array(
				'product'         => '',
				'prefix'          => 'ALGO',
				'option'          => 'algo_freemium_license',
				'cron'            => 'algo_freemium_license_recheck',
				'textdomain'      => 'default',
				'api_url_const'   => '',
				'allow_dev_const' => '',
				'publisher_hosts' => array( 'algorisys.com', 'www.algorisys.com' ),
				'filter_api'      => 'algo_freemium_license_api_url',
				'filter_dev'      => 'algo_freemium_allow_local_unlock',
			),
			$config
		);
	}

	/**
	 * @return array<string, mixed>
	 */
	public function defaults() {
		return array(
			'active'          => false,
			'mode'            => '', // license | local
			'license_key'     => '',
			'license_key_raw' => '',
			'activated_at'    => 0,
			'last_check'      => 0,
			'grace_until'     => 0,
			'status_message'  => '',
		);
	}

	/**
	 * @return array<string, mixed>
	 */
	public function get() {
		$stored = get_option( $this->option_key(), array() );
		if ( ! is_array( $stored ) ) {
			$stored = array();
		}
		return array_merge( $this->defaults(), $stored );
	}

	/**
	 * License API base URL (no trailing slash).
	 *
	 * @return string
	 */
	public function api_base() {
		$const = (string) $this->config['api_url_const'];
		if ( $const !== '' && defined( $const ) && constant( $const ) ) {
			return untrailingslashit( (string) constant( $const ) );
		}

		$host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
		$host = is_string( $host ) ? strtolower( $host ) : '';

		if (
			$host === 'localhost'
			|| $host === '127.0.0.1'
			|| substr( $host, -6 ) === '.local'
			|| substr( $host, -5 ) === '.test'
		) {
			$local = untrailingslashit( rest_url( 'algo-license/v1' ) );
			return (string) apply_filters( $this->config['filter_api'], $local, $this->product() );
		}

		return (string) apply_filters(
			$this->config['filter_api'],
			'https://algorisys.com/wp-json/algo-license/v1',
			$this->product()
		);
	}

	/**
	 * @return bool
	 */
	public function is_active() {
		$data = $this->get();
		if ( empty( $data['active'] ) ) {
			return false;
		}

		if ( ( $data['mode'] ?? '' ) === 'local' && ! $this->allow_local_unlock() ) {
			return false;
		}

		if ( ( $data['mode'] ?? '' ) === 'license' ) {
			$grace = (int) ( $data['grace_until'] ?? 0 );
			if ( $grace > 0 && time() > $grace ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Local / publisher sites may unlock without a sold key.
	 *
	 * @return bool
	 */
	public function allow_local_unlock() {
		$const = (string) $this->config['allow_dev_const'];
		if ( $const !== '' && defined( $const ) ) {
			return (bool) constant( $const );
		}

		$env = function_exists( 'wp_get_environment_type' )
			? wp_get_environment_type()
			: 'production';

		$allowed = in_array( $env, array( 'local', 'development' ), true );

		$host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
		$host = is_string( $host ) ? strtolower( $host ) : '';
		$host = preg_replace( '/:\d+$/', '', $host );

		if (
			$host === 'localhost'
			|| $host === '127.0.0.1'
			|| substr( $host, -6 ) === '.local'
			|| substr( $host, -5 ) === '.test'
		) {
			$allowed = true;
		}

		$publishers = isset( $this->config['publisher_hosts'] ) && is_array( $this->config['publisher_hosts'] )
			? $this->config['publisher_hosts']
			: array();
		foreach ( $publishers as $pub ) {
			$pub = strtolower( (string) $pub );
			if ( $pub !== '' && $host === $pub ) {
				$allowed = true;
				break;
			}
		}

		return (bool) apply_filters( $this->config['filter_dev'], $allowed, $this->product() );
	}

	/**
	 * Activate with a license key via the Algorisys license server.
	 *
	 * @param string $key Raw key.
	 * @return true|WP_Error
	 */
	public function activate_license( $key ) {
		$td  = $this->textdomain();
		$key = strtoupper( trim( (string) $key ) );
		$key = preg_replace( '/\s+/', '', $key );

		if ( $key === '' ) {
			return new WP_Error( 'algo_empty_key', __( 'Enter a license key.', $td ) );
		}

		$prefix = preg_quote( $this->prefix(), '/' );
		if ( ! preg_match( '/^' . $prefix . '-[A-Z0-9]{4}(?:-[A-Z0-9]{4}){3}$/', $key ) ) {
			return new WP_Error(
				'algo_invalid_format',
				sprintf(
					/* translators: %s: key prefix */
					__( 'That does not look like a valid license key (%s-XXXX-XXXX-XXXX-XXXX).', $td ),
					$this->prefix()
				)
			);
		}

		$response = $this->remote(
			'activate',
			array(
				'key'      => $key,
				'site_url' => home_url( '/' ),
				'product'  => $this->product(),
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$this->persist(
			array(
				'active'          => true,
				'mode'            => 'license',
				'license_key'     => $this->mask_key( $key ),
				'license_key_raw' => $key,
				'activated_at'    => time(),
				'last_check'      => time(),
				'grace_until'     => 0,
				'status_message'  => (string) ( $response['message'] ?? __( 'License activated. Pro features are unlocked.', $td ) ),
			)
		);

		$this->schedule_recheck();

		return true;
	}

	/**
	 * Unlock Pro on local / publisher sites without a sold key.
	 *
	 * @return true|WP_Error
	 */
	public function activate_local() {
		$td = $this->textdomain();
		if ( ! $this->allow_local_unlock() ) {
			return new WP_Error(
				'algo_local_blocked',
				__( 'Enter a license key purchased from algorisys.com. Local unlock is only available on development or Algorisys sites.', $td )
			);
		}

		$this->persist(
			array(
				'active'          => true,
				'mode'            => 'local',
				'license_key'     => '',
				'license_key_raw' => '',
				'activated_at'    => time(),
				'last_check'      => time(),
				'grace_until'     => 0,
				'status_message'  => __( 'Pro unlocked on this site (development / publisher).', $td ),
			)
		);

		return true;
	}

	/**
	 * @return true|WP_Error
	 */
	public function deactivate() {
		$data = $this->get();
		$raw  = (string) ( $data['license_key_raw'] ?? '' );

		if ( $raw !== '' && ( $data['mode'] ?? '' ) === 'license' ) {
			$this->remote(
				'deactivate',
				array(
					'key'      => $raw,
					'site_url' => home_url( '/' ),
					'product'  => $this->product(),
				)
			);
		}

		$this->persist( $this->defaults() );
		wp_clear_scheduled_hook( $this->cron_hook() );

		return true;
	}

	/**
	 * Periodic remote re-check for license mode.
	 *
	 * @return void
	 */
	public function recheck() {
		$td   = $this->textdomain();
		$data = $this->get();
		if ( empty( $data['active'] ) || ( $data['mode'] ?? '' ) !== 'license' ) {
			return;
		}

		$raw = (string) ( $data['license_key_raw'] ?? '' );
		if ( $raw === '' ) {
			$this->persist(
				array_merge(
					$data,
					array(
						'active'         => false,
						'status_message' => __( 'License key missing — Pro locked. Activate again.', $td ),
					)
				)
			);
			return;
		}

		$response = $this->remote(
			'check',
			array(
				'key'      => $raw,
				'site_url' => home_url( '/' ),
				'product'  => $this->product(),
			)
		);

		if ( is_wp_error( $response ) ) {
			$code = $response->get_error_code();
			if ( in_array( $code, array( 'algo_transport', 'http_request_failed' ), true ) || strpos( (string) $code, 'transport' ) !== false ) {
				$grace = (int) ( $data['grace_until'] ?? 0 );
				if ( $grace <= 0 ) {
					$grace = time() + ( 3 * DAY_IN_SECONDS );
				}
				$this->persist(
					array_merge(
						$data,
						array(
							'last_check'     => time(),
							'grace_until'    => $grace,
							'status_message' => __( 'Could not reach the license server. Pro stays active during a short grace period.', $td ),
						)
					)
				);
				return;
			}

			$this->persist(
				array_merge(
					$this->defaults(),
					array(
						'license_key'    => $data['license_key'] ?? '',
						'status_message' => $response->get_error_message(),
					)
				)
			);
			return;
		}

		$this->persist(
			array_merge(
				$data,
				array(
					'active'         => true,
					'last_check'     => time(),
					'grace_until'    => 0,
					'status_message' => (string) ( $response['message'] ?? __( 'License valid.', $td ) ),
				)
			)
		);
	}

	/**
	 * @param string               $endpoint Endpoint path.
	 * @param array<string,string> $body     JSON body.
	 * @return array<string,mixed>|WP_Error
	 */
	public function request( $endpoint, array $body ) {
		return $this->remote( $endpoint, $body );
	}

	/**
	 * Human status line for the admin UI.
	 *
	 * @return string
	 */
	public function status_label() {
		$td = $this->textdomain();
		if ( ! $this->is_active() ) {
			$data = $this->get();
			if ( ! empty( $data['active'] ) && ( $data['mode'] ?? '' ) === 'local' && ! $this->allow_local_unlock() ) {
				return __( 'Inactive — local unlock ignored outside development / publisher sites', $td );
			}
			if ( ! empty( $data['active'] ) && ( $data['mode'] ?? '' ) === 'license' && (int) ( $data['grace_until'] ?? 0 ) > 0 && time() > (int) $data['grace_until'] ) {
				return __( 'Inactive — license check failed (grace period ended)', $td );
			}
			$msg = (string) ( $data['status_message'] ?? '' );
			return $msg !== '' ? $msg : __( 'Inactive — Pro features locked', $td );
		}

		$data = $this->get();
		if ( ( $data['mode'] ?? '' ) === 'local' ) {
			return __( 'Active (development / publisher unlock)', $td );
		}

		return __( 'Active (licensed)', $td );
	}

	/**
	 * @return void
	 */
	public function schedule_recheck() {
		if ( ! wp_next_scheduled( $this->cron_hook() ) ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', $this->cron_hook() );
		}
	}

	/**
	 * @return string
	 */
	public function product() {
		return sanitize_key( (string) $this->config['product'] );
	}

	/**
	 * @return string
	 */
	public function prefix() {
		$prefix = strtoupper( preg_replace( '/[^A-Z0-9]/', '', (string) $this->config['prefix'] ) );
		return $prefix !== '' ? $prefix : 'ALGO';
	}

	/**
	 * @return string
	 */
	public function option_key() {
		return (string) $this->config['option'];
	}

	/**
	 * @return string
	 */
	public function cron_hook() {
		return (string) $this->config['cron'];
	}

	/**
	 * @return string
	 */
	private function textdomain() {
		$td = (string) $this->config['textdomain'];
		return $td !== '' ? $td : 'default';
	}

	/**
	 * @param string               $endpoint activate|deactivate|check
	 * @param array<string,string> $body     JSON body.
	 * @return array<string,mixed>|WP_Error
	 */
	private function remote( $endpoint, array $body ) {
		$td  = $this->textdomain();
		$url = $this->api_base() . '/' . ltrim( (string) $endpoint, '/' );

		$response = wp_remote_post(
			$url,
			array(
				'timeout' => 20,
				'headers' => array(
					'Content-Type' => 'application/json',
					'Accept'       => 'application/json',
				),
				'body'    => wp_json_encode( $body ),
			)
		);

		if ( is_wp_error( $response ) ) {
			return new WP_Error(
				'algo_transport',
				sprintf(
					/* translators: %s: error message */
					__( 'Could not reach the license server: %s', $td ),
					$response->get_error_message()
				)
			);
		}

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

		if ( $code < 200 || $code >= 300 ) {
			$message = '';
			if ( is_array( $data ) ) {
				$message = (string) ( $data['message'] ?? '' );
			}
			if ( $message === '' ) {
				$message = sprintf(
					/* translators: %d: HTTP status */
					__( 'License server rejected the request (HTTP %d).', $td ),
					$code
				);
			}
			return new WP_Error( 'algo_http', $message );
		}

		if ( ! is_array( $data ) || empty( $data['ok'] ) ) {
			return new WP_Error(
				'algo_http',
				__( 'Unexpected response from the license server.', $td )
			);
		}

		return $data;
	}

	/**
	 * @param array<string, mixed> $data State.
	 * @return void
	 */
	private function persist( array $data ) {
		update_option( $this->option_key(), array_merge( $this->defaults(), $data ), false );
	}

	/**
	 * @param string $key Key.
	 * @return string
	 */
	private function mask_key( $key ) {
		$len = strlen( $key );
		if ( $len <= 8 ) {
			return str_repeat( '•', $len );
		}
		return substr( $key, 0, 4 ) . str_repeat( '•', max( 4, $len - 8 ) ) . substr( $key, -4 );
	}
}
