<?php

class Eurostocks_Email_Verification {

	const STATUS_TRANSIENT_PREFIX = 'eurostocks_email_status_';
	const TOKEN_TRANSIENT_PREFIX  = 'eurostocks_email_token_';
	const ATTEMPT_TRANSIENT_PREFIX = 'eurostocks_email_attempts_';
	const MAX_ATTEMPTS_PER_DAY    = 3;
	const STATUS_NONE             = 'none';
	const STATUS_PENDING          = 'pending';
	const STATUS_VERIFIED         = 'verified';

	/**
	 * Generate a consistent hash for an IP address so we can safely use it in transient keys.
	 *
	 * @param string $ip
	 * @return string
	 */
	public static function hash_ip($ip) {
		return md5((string) $ip);
	}

	/**
	 * Retrieve the current verification status for a visitor.
	 *
	 * @param string $ip
	 * @return array
	 */
	public static function get_status($ip) {
		$hash   = self::hash_ip($ip);
		$status = get_transient(self::STATUS_TRANSIENT_PREFIX . $hash);

		if (false === $status || ! is_array($status)) {
			return array(
				'status' => self::STATUS_NONE,
			);
		}

		return $status;
	}

	/**
	 * Mark an IP as pending verification and remember the pending token.
	 *
	 * @param string $ip
	 * @param string $email
	 * @param string $token
	 * @param string $page_type
	 * @param string $return_url
	 * @return void
	 */
	public static function mark_pending($ip, $email, $token, $page_type, $return_url = '', $reason_key = '') {
		$hash = self::hash_ip($ip);
		$clean_reason = sanitize_key($reason_key);

		set_transient(
			self::STATUS_TRANSIENT_PREFIX . $hash,
			array(
				'status'     => self::STATUS_PENDING,
				'ip'         => $ip,
				'email'      => $email,
				'token'      => $token,
				'page_type'  => $page_type,
				'return_url' => esc_url_raw($return_url),
				'created_at' => time(),
				'reason_key' => $clean_reason,
			),
			DAY_IN_SECONDS
		);
	}

	/**
	 * Persist the token payload so we can validate it later.
	 *
	 * @param string $token
	 * @param string $ip
	 * @param string $email
	 * @param string $page_type
	 * @param string $return_url
	 * @return void
	 */
	public static function store_token($token, $ip, $email, $page_type, $return_url = '', $reason_key = '') {
		set_transient(
			self::TOKEN_TRANSIENT_PREFIX . $token,
			array(
				'ip'         => $ip,
				'ip_hash'    => self::hash_ip($ip),
				'email'      => $email,
				'page_type'  => $page_type,
				'return_url' => esc_url_raw($return_url),
				'reason_key' => sanitize_key($reason_key),
				'created_at' => time(),
			),
			12 * HOUR_IN_SECONDS
		);
	}

	/**
	 * Retrieve token payload and optionally remove it.
	 *
	 * @param string $token
	 * @param bool   $consume
	 * @return array|false
	 */
	public static function get_token_payload($token, $consume = false) {
		$payload = get_transient(self::TOKEN_TRANSIENT_PREFIX . $token);

		if (false !== $payload && $consume) {
			delete_transient(self::TOKEN_TRANSIENT_PREFIX . $token);
		}

		return $payload;
	}

	/**
	 * Mark an IP as verified for an extended period.
	 *
	 * @param string $ip
	 * @param string $email
	 * @return void
	 */
	public static function mark_verified($ip, $email) {
		$hash = self::hash_ip($ip);
		$current_status = self::get_status($ip);
		$reason_key = isset($current_status['reason_key']) ? sanitize_key($current_status['reason_key']) : '';

		set_transient(
			self::STATUS_TRANSIENT_PREFIX . $hash,
			array(
				'status'      => self::STATUS_VERIFIED,
				'ip'          => $ip,
				'email'       => $email,
				'verified_at' => time(),
				'reason_key'  => $reason_key,
			),
			30 * DAY_IN_SECONDS
		);

		self::clear_ip_block_flag($ip);
		self::reset_attempts($ip);
	}

	/**
	 * Return the number of verification attempts and the timestamp of the first attempt.
	 *
	 * @param string $ip
	 * @return array{count:int,first_attempt:int}
	 */
	public static function get_attempt_data($ip) {
		$ip = (string) $ip;
		if ('' === $ip) {
			return array(
				'count'         => 0,
				'first_attempt' => 0,
			);
		}

		$hash = self::hash_ip($ip);
		$data = get_transient(self::ATTEMPT_TRANSIENT_PREFIX . $hash);

		if (!is_array($data)) {
			return array(
				'count'         => 0,
				'first_attempt' => 0,
			);
		}

		return array(
			'count'         => isset($data['count']) ? (int) $data['count'] : 0,
			'first_attempt' => isset($data['first_attempt']) ? (int) $data['first_attempt'] : 0,
		);
	}

	/**
	 * Calculate remaining attempts for an IP within a 24 hour window.
	 *
	 * @param string $ip
	 * @param int    $limit
	 * @return int
	 */
	public static function attempts_remaining($ip, $limit = self::MAX_ATTEMPTS_PER_DAY) {
		$limit = max(0, (int) $limit);
		if (0 === $limit) {
			return $limit;
		}

		$data = self::get_attempt_data($ip);
		$now = time();

		if (0 === $data['first_attempt'] || ($now - $data['first_attempt']) >= DAY_IN_SECONDS) {
			return $limit;
		}

		return max(0, $limit - (int) $data['count']);
	}

	/**
	 * Check whether an IP can request another verification email.
	 *
	 * @param string $ip
	 * @param int    $limit
	 * @return bool
	 */
	public static function can_attempt($ip, $limit = self::MAX_ATTEMPTS_PER_DAY) {
		$limit = max(0, (int) $limit);
		if (0 === $limit) {
			return true;
		}

		return self::attempts_remaining($ip, $limit) > 0;
	}

	/**
	 * Increment the attempt counter for an IP, preserving the original 24h window.
	 *
	 * @param string $ip
	 * @param int    $limit
	 * @return array{count:int,first_attempt:int}
	 */
	public static function increment_attempts($ip, $limit = self::MAX_ATTEMPTS_PER_DAY) {
		$ip = (string) $ip;
		if ('' === $ip) {
			return array(
				'count'         => 0,
				'first_attempt' => 0,
			);
		}

		$limit = max(0, (int) $limit);
		$hash = self::hash_ip($ip);
		$data = self::get_attempt_data($ip);
		$now = time();

		if (0 === $data['first_attempt'] || ($now - $data['first_attempt']) >= DAY_IN_SECONDS) {
			$data['first_attempt'] = $now;
			$data['count'] = 1;
		} else {
			$data['count'] = (int) $data['count'] + 1;
		}
		$ttl = max(60, DAY_IN_SECONDS - ($now - $data['first_attempt']));

		set_transient(
			self::ATTEMPT_TRANSIENT_PREFIX . $hash,
			array(
				'count'         => (int) $data['count'],
				'first_attempt' => (int) $data['first_attempt'],
			),
			$ttl
		);

		return $data;
	}

	/**
	 * Reset the attempt counter for an IP.
	 *
	 * @param string $ip
	 * @return void
	 */
	public static function reset_attempts($ip) {
		$ip = (string) $ip;
		if ('' === $ip) {
			return;
		}

		delete_transient(self::ATTEMPT_TRANSIENT_PREFIX . self::hash_ip($ip));
	}

	/**
	 * Retrieve all verification status transient entries.
	 *
	 * @return array
	 */
	public static function get_all_status_entries() {
		$rows = self::fetch_transient_rows(self::STATUS_TRANSIENT_PREFIX);
		$entries = array();
		$prefix_length = strlen('_transient_' . self::STATUS_TRANSIENT_PREFIX);

		foreach ($rows as $row) {
			$data = maybe_unserialize($row['option_value']);
			if (!is_array($data)) {
				continue;
			}

			$hash = substr($row['option_name'], $prefix_length);
			if (false === $hash || '' === $hash) {
				continue;
			}

			$data['hash'] = sanitize_text_field($hash);
			$data['ip'] = isset($data['ip']) ? (string) $data['ip'] : '';
			$data['status'] = isset($data['status']) ? $data['status'] : self::STATUS_NONE;
			$data['created_at'] = isset($data['created_at']) ? (int) $data['created_at'] : 0;
			$data['verified_at'] = isset($data['verified_at']) ? (int) $data['verified_at'] : 0;
			$data['reason_key'] = isset($data['reason_key']) ? sanitize_key($data['reason_key']) : '';

			$entries[] = $data;
		}

		return $entries;
	}

	/**
	 * Delete a single verification status transient by hash.
	 *
	 * @param string $hash
	 * @return bool
	 */
	public static function delete_status_by_hash($hash) {
		$hash = sanitize_text_field($hash);
		if ('' === $hash) {
			return false;
		}

		return delete_transient(self::STATUS_TRANSIENT_PREFIX . $hash);
	}

	/**
	 * Delete pending verification tokens for a given IP.
	 *
	 * @param string $ip
	 * @return void
	 */
	public static function delete_tokens_for_ip($ip) {
		$ip = (string) $ip;
		if ('' === $ip) {
			return;
		}

		$rows = self::fetch_transient_rows(self::TOKEN_TRANSIENT_PREFIX);
		$prefix_length = strlen('_transient_' . self::TOKEN_TRANSIENT_PREFIX);

		foreach ($rows as $row) {
			$data = maybe_unserialize($row['option_value']);
			if (!is_array($data) || !isset($data['ip'])) {
				continue;
			}

			if ($data['ip'] !== $ip) {
				continue;
			}

			$token = substr($row['option_name'], $prefix_length);
			if (false === $token || '' === $token) {
				continue;
			}

			delete_transient(self::TOKEN_TRANSIENT_PREFIX . $token);
		}
	}

	/**
	 * Remove the blocked flag for a given IP in the request log.
	 *
	 * @param string $ip
	 * @return void
	 */
	public static function clear_ip_block_flag($ip) {
		$ip = trim((string) $ip);
		if ('' === $ip) {
			return;
		}

		global $wpdb;
		$table_name = $wpdb->prefix . 'eurostocks_ip';

		$wpdb->update(
			$table_name,
			array('blocked' => 0),
			array('remote_addr' => $ip, 'blocked' => 1),
			array('%d'),
			array('%s', '%d')
		);
	}

	/**
	 * Fetch transient rows for a given prefix directly from the options table.
	 *
	 * @param string $transient_prefix
	 * @return array
	 */
	protected static function fetch_transient_rows($transient_prefix) {
		global $wpdb;

		$table = $wpdb->options;
		$search_prefix = '_transient_' . $transient_prefix;
		$like = $wpdb->esc_like($search_prefix) . '%';

		$query = $wpdb->prepare(
			"SELECT option_name, option_value FROM {$table} WHERE option_name LIKE %s",
			$like
		);

		$results = $wpdb->get_results($query, ARRAY_A);

		return is_array($results) ? $results : array();
	}

	/**
	 * Basic disposable email detection using a configurable blocklist.
	 *
	 * @param string $email
	 * @return bool
	 */
	public static function is_disposable_email($email) {
		$parts = explode('@', strtolower($email));

		if (count($parts) !== 2) {
			return true;
		}

		$domain = trim(end($parts));

		if ('' === $domain) {
			return true;
		}

		$blocklist = self::get_blocklist();

		return in_array($domain, $blocklist, true);
	}

	/**
	 * Allow integrators to supply their own blocklist through a filter.
	 *
	 * @return array
	 */
	protected static function get_blocklist() {
		static $domains = null;

		if (null === $domains) {
			$default_file = plugin_dir_path(__FILE__) . 'disposable-email-domains.php';
			if (file_exists($default_file)) {
				$domains = include $default_file;
			}

			if (! is_array($domains)) {
				$domains = array();
			}
		}

		/**
		 * Filter the disposable email blocklist at runtime.
		 *
		 * @param array $domains
		 */
		return apply_filters('eurostocks_disposable_email_blocklist', $domains);
	}
}
