<?php
/**
 * Privacy Hardening module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Compliance;

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

/**
 * Privacy Hardening module.
 *
 * @since 1.0.0
 */
final class Privacy_Hardening extends \Kramar\Module {

	/**
	 * {@inheritDoc}
	 */
	public function get_slug(): string {
		return 'privacy-hardening';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_category(): string {
		return 'compliance';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_name(): string {
		return __( 'Privacy Hardening', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Harden WooCommerce\'s privacy: IP anonymization, retention warnings, security notices, and PII cleanup schedules.', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_default_settings(): array {
		return [
			'enabled'              => false,
			// IP anonymization (default OFF — explicit enable required)
			'anonymize_order_ips'  => false,
			'anonymize_review_ips' => false,
			// Warnings (default ON — read-only, never touches data)
			'retention_warnings'   => true,
			'security_warnings'    => true,
			// Cleanup schedules (default OFF — explicit enable required)
			'cleanup_geolocation'  => false,
			'cleanup_sessions'     => false,
			'cleanup_analytics'    => false,
			'analytics_retention'  => 730,
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		return [
			'enabled'              => ! empty( $input['enabled'] ),
			'anonymize_order_ips'  => ! empty( $input['anonymize_order_ips'] ),
			'anonymize_review_ips' => ! empty( $input['anonymize_review_ips'] ),
			'retention_warnings'   => ! empty( $input['retention_warnings'] ),
			'security_warnings'    => ! empty( $input['security_warnings'] ),
			'cleanup_geolocation'  => ! empty( $input['cleanup_geolocation'] ),
			'cleanup_sessions'     => ! empty( $input['cleanup_sessions'] ),
			'cleanup_analytics'    => ! empty( $input['cleanup_analytics'] ),
			'analytics_retention'  => max( 30, absint( $input['analytics_retention'] ?? 730 ) ),
		];
	}

	/**
	 * {@inheritDoc}
	 */
	protected function run(): void {
		$this->run_ip_anonymization();
		$this->run_cleanup_schedules();
	}

	/**
	 * Register IP anonymization hooks.
	 *
	 * @since 1.0.0
	 */
	private function run_ip_anonymization(): void {
		if ( $this->get_setting( 'anonymize_order_ips' ) ) {
			add_action( 'woocommerce_checkout_order_created', [ $this, 'anonymize_order_ip' ] );
		}

		if ( $this->get_setting( 'anonymize_review_ips' ) ) {
			add_filter( 'preprocess_comment', [ $this, 'anonymize_comment_ip' ] );
		}
	}

	/**
	 * Render the active retention/security warnings inline on Kramar's own module
	 * settings panel. Deliberately NOT hooked to admin_notices — these warnings
	 * live only on the plugin's own page, never spread across WP/WooCommerce
	 * admin screens.
	 *
	 * @since 1.0.0
	 */
	private function render_active_warnings(): void {
		$warnings = [];
		if ( $this->get_setting( 'retention_warnings' ) ) {
			$warnings = array_merge( $warnings, $this->get_retention_warnings() );
		}
		if ( $this->get_setting( 'security_warnings' ) ) {
			$warnings = array_merge( $warnings, $this->get_security_warnings() );
		}

		if ( empty( $warnings ) ) {
			return;
		}

		echo '<div class="kramar-inline-warnings notice notice-warning inline" style="margin:8px 0;"><p><strong>';
		esc_html_e( 'Privacy checks', 'kramar' );
		echo '</strong></p><ul style="list-style:disc;margin:0 0 0 18px;">';
		foreach ( $warnings as $warning ) {
			printf( '<li>%s</li>', esc_html( $warning ) );
		}
		echo '</ul></div>';
	}

	/**
	 * Register and unregister cron cleanup events.
	 *
	 * @since 1.0.0
	 */
	private function run_cleanup_schedules(): void {
		if ( $this->get_setting( 'cleanup_geolocation' ) ) {
			if ( ! wp_next_scheduled( 'kramar_cleanup_geolocation' ) ) {
				wp_schedule_event( time(), 'daily', 'kramar_cleanup_geolocation' );
			}
		} else {
			wp_unschedule_hook( 'kramar_cleanup_geolocation' );
		}
		add_action( 'kramar_cleanup_geolocation', [ $this, 'cleanup_geolocation_transients' ] );

		if ( $this->get_setting( 'cleanup_sessions' ) ) {
			if ( ! wp_next_scheduled( 'kramar_cleanup_sessions' ) ) {
				wp_schedule_event( time(), 'daily', 'kramar_cleanup_sessions' );
			}
		} else {
			wp_unschedule_hook( 'kramar_cleanup_sessions' );
		}
		add_action( 'kramar_cleanup_sessions', [ $this, 'cleanup_expired_sessions' ] );

		if ( $this->get_setting( 'cleanup_analytics' ) ) {
			if ( ! wp_next_scheduled( 'kramar_cleanup_analytics' ) ) {
				wp_schedule_event( time(), 'daily', 'kramar_cleanup_analytics' );
			}
		} else {
			wp_unschedule_hook( 'kramar_cleanup_analytics' );
		}
		add_action( 'kramar_cleanup_analytics', [ $this, 'cleanup_analytics_pii' ] );
	}

	/**
	 * Anonymize the IP address on a new order.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order object.
	 */
	public function anonymize_order_ip( \WC_Order $order ): void {
		$ip = $order->get_customer_ip_address();
		if ( empty( $ip ) ) {
			return;
		}
		$order->set_customer_ip_address( $this->truncate_ip( $ip ) );
		$order->save();
	}

	/**
	 * Anonymize the IP address on a new comment/review.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $data Comment data.
	 * @return array<string, mixed> Modified comment data.
	 */
	public function anonymize_comment_ip( array $data ): array {
		if ( ! empty( $data['comment_author_IP'] ) ) {
			$data['comment_author_IP'] = $this->truncate_ip( $data['comment_author_IP'] );
		}
		return $data;
	}

	/**
	 * Show admin notices when WooCommerce retention settings are unconfigured.
	 *
	 * @since 1.0.0
	 */
	/**
	 * @return array<int, string>
	 */
	public function get_retention_warnings(): array {
		$options = [
			'woocommerce_trash_pending_orders'       => 'pending orders',
			'woocommerce_trash_failed_orders'        => 'failed orders',
			'woocommerce_trash_cancelled_orders'     => 'cancelled orders',
			'woocommerce_anonymize_completed_orders' => 'completed orders',
		];

		$blank = [];
		foreach ( $options as $option => $label ) {
			if ( empty( get_option( $option ) ) ) {
				$blank[] = $label;
			}
		}

		if ( empty( $blank ) ) {
			return [];
		}

		return [
			sprintf(
				/* translators: %s is a comma-separated list of order types (e.g. "pending orders, failed orders") */
				__( 'WooCommerce retains data for %s indefinitely. Consider setting retention periods.', 'kramar' ),
				implode( ', ', $blank )
			),
		];
	}

	/**
	 * @return array<int, string>
	 */
	public function get_security_warnings(): array {
		$warnings = [];

		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			$warnings[] = __( 'Debug mode is on. WooCommerce logs may write customer PII to disk.', 'kramar' );
		}

		if ( ! is_ssl() ) {
			$warnings[] = __( 'Your shop is not using HTTPS. Customer data is transmitted unencrypted.', 'kramar' );
		}

		if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) {
			$warnings[] = __( 'Geolocation is set to use an external API. Customer IPs are sent to a third-party service.', 'kramar' );
		}

		return $warnings;
	}

	/**
	 * Delete geoip transients from the options table.
	 *
	 * @since 1.0.0
	 */
	public function cleanup_geolocation_transients(): void {
		global $wpdb;

		// Collect first, then delete through the options API. A bare DELETE
		// removes the rows but leaves the object cache holding them, so on any
		// site with a persistent cache — most production sites — get_transient()
		// kept returning values that no longer exist.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$names = $wpdb->get_col(
			"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_geoip_%' OR option_name LIKE '_transient_timeout_geoip_%' OR option_name LIKE '_transient_external_ip_address_%' OR option_name LIKE '_transient_timeout_external_ip_address_%'"
		);

		foreach ( (array) $names as $name ) {
			delete_option( (string) $name );
		}
	}

	/**
	 * Delete expired WooCommerce sessions.
	 *
	 * @since 1.0.0
	 */
	public function cleanup_expired_sessions(): void {
		global $wpdb;
		$table = $wpdb->prefix . 'woocommerce_sessions';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- trusted internal table name; values are prepared
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE session_expiry < %d", time() ) );
		}
	}

	/**
	 * Anonymize old PII entries in the WooCommerce analytics customer lookup table.
	 *
	 * @since 1.0.0
	 */
	public function cleanup_analytics_pii(): void {
		global $wpdb;
		$table = $wpdb->prefix . 'wc_customer_lookup';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
			return;
		}

		$days   = max( 30, (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'analytics_retention' ) );
		$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- trusted internal table name ($wpdb->prefix + fixed string); values are prepared; one-off privacy-cleanup write.
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table} SET first_name = 'Anonymous', last_name = '', email = CONCAT('anon-', customer_id, '@example.invalid'), username = '', city = '', postcode = '' WHERE date_registered < %s AND first_name != 'Anonymous'",
				$cutoff
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
	}

	/**
	 * {@inheritDoc}
	 */
	public function render_extra_settings( string $option_name ): void {
		$slug = $this->get_slug();
		$cat  = $this->get_category();

		$anonymize_order_ips  = $this->get_setting( 'anonymize_order_ips' );
		$anonymize_review_ips = $this->get_setting( 'anonymize_review_ips' );
		$retention_warnings   = $this->get_setting( 'retention_warnings' );
		$security_warnings    = $this->get_setting( 'security_warnings' );
		$cleanup_geolocation  = $this->get_setting( 'cleanup_geolocation' );
		$cleanup_sessions     = $this->get_setting( 'cleanup_sessions' );
		$cleanup_analytics    = $this->get_setting( 'cleanup_analytics' );
		$analytics_retention  = (int) $this->plugin->get_setting( $cat, $slug, 'analytics_retention' );
		?>
		<div class="kramar-module__extra">
			<div class="kramar-settings-stack">

				<p><strong><?php esc_html_e( 'IP Anonymization', 'kramar' ); ?></strong></p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][anonymize_order_ips]"
							value="1"
							<?php checked( $anonymize_order_ips ); ?>
						/>
						<?php esc_html_e( 'Anonymize order IP addresses', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Truncates the customer IP to /24 (IPv4) or /48 (IPv6) on new orders at checkout.', 'kramar' ); ?></span>
				</p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][anonymize_review_ips]"
							value="1"
							<?php checked( $anonymize_review_ips ); ?>
						/>
						<?php esc_html_e( 'Anonymize review IP addresses', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Truncates the commenter IP on new product reviews before it is stored.', 'kramar' ); ?></span>
				</p>

				<p><strong><?php esc_html_e( 'Warnings', 'kramar' ); ?></strong></p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][retention_warnings]"
							value="1"
							<?php checked( $retention_warnings ); ?>
						/>
						<?php esc_html_e( 'Warn when retention periods are not configured', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Listed below on this Kramar page when order retention settings are blank. Read-only — never modifies data, never shown elsewhere in wp-admin.', 'kramar' ); ?></span>
				</p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][security_warnings]"
							value="1"
							<?php checked( $security_warnings ); ?>
						/>
						<?php esc_html_e( 'Warn about security configuration issues', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Listed below on this Kramar page for debug mode, missing HTTPS, or external geolocation API use. Read-only — never modifies data, never shown elsewhere in wp-admin.', 'kramar' ); ?></span>
				</p>

				<?php $this->render_active_warnings(); ?>

				<p><strong><?php esc_html_e( 'Cleanup Schedules', 'kramar' ); ?></strong></p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][cleanup_geolocation]"
							value="1"
							<?php checked( $cleanup_geolocation ); ?>
						/>
						<?php esc_html_e( 'Daily: delete geolocation transients', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Removes cached IP geolocation lookups from the options table daily.', 'kramar' ); ?></span>
				</p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][cleanup_sessions]"
							value="1"
							<?php checked( $cleanup_sessions ); ?>
						/>
						<?php esc_html_e( 'Daily: purge expired WooCommerce sessions', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Deletes rows from woocommerce_sessions where the session has expired.', 'kramar' ); ?></span>
				</p>

				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][cleanup_analytics]"
							value="1"
							<?php checked( $cleanup_analytics ); ?>
						/>
						<?php esc_html_e( 'Daily: anonymize old WooCommerce analytics PII', 'kramar' ); ?>
					</label>
					<span class="description" style="color:#d63638;"><strong><?php esc_html_e( 'Anonymizes customer names, emails, and addresses in WooCommerce analytics after the retention period. Irreversible.', 'kramar' ); ?></strong></span>
				</p>

				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Analytics retention (days):', 'kramar' ); ?>
						<input
							type="number"
							name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][analytics_retention]"
							value="<?php echo esc_attr( (string) $analytics_retention ); ?>"
							min="30"
							step="1"
							style="width:80px;"
						/>
					</label>
					<span class="description"><?php esc_html_e( 'Minimum 30 days. Only used when analytics cleanup is enabled.', 'kramar' ); ?></span>
				</p>

			</div>
		</div>
		<?php
	}

	/**
	 * Read a boolean setting for this module.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Setting key.
	 * @return bool Setting value.
	 */
	private function get_setting( string $key ): bool {
		return (bool) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), $key );
	}

	/**
	 * Truncate an IP address for anonymization.
	 *
	 * IPv4: zeroes the last octet (/24). IPv6: zeroes the last 80 bits (/48).
	 *
	 * @since 1.0.0
	 *
	 * @param string $ip IP address.
	 * @return string Truncated IP address.
	 */
	private function truncate_ip( string $ip ): string {
		if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
			// Mask the last 80 bits, keeping the /48 prefix — the same thing the
			// old string-slicing intended. It split on ":" and re-appended "::",
			// which only produced a valid address for a fully expanded one:
			// "2001:db8::1" came out as "2001:db8:::" and "::1" as "::1::",
			// neither of which is a real IP, and both were written straight onto
			// the order. Working on the packed bytes can't produce that.
			$packed = @inet_pton( $ip );
			if ( false !== $packed && 16 === strlen( $packed ) ) {
				$masked = substr( $packed, 0, 6 ) . str_repeat( "\0", 10 );
				$result = @inet_ntop( $masked );
				if ( is_string( $result ) ) {
					return $result;
				}
			}

			return '::';
		}

		// Same validate-then-transform the IPv6 branch above already does. This
		// branch mutated whatever it was handed, so anything that was not an
		// address came back with ".0" stuck on the end and was stored as the
		// customer's IP — 'not-an-ip' became 'not-an-ip.0', 'fe80::1%eth0' became
		// 'fe80::1%eth0.0'. WC_Geolocation normally supplies clean addresses, but
		// an unusual proxy header does not have to.
		if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			return '';
		}

		$parts    = explode( '.', $ip );
		$parts[3] = '0';
		return implode( '.', $parts );
	}
}
