<?php
/**
 * Cookie Banner Module.
 *
 * Self-hosted cookie consent management with GDPR / CCPA / LGPD / APPI /
 * Loi 25 (Quebec) / KVKK / POPIA / DPDP support, Google Consent Mode v2,
 * IAB TCF v2.2 (auto-declared, non-certified), Global Privacy Control
 * acknowledgement, automatic blocking of ~50 common third-party scripts.
 *
 * @package BlaminhorEssentials
 */

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

require_once __DIR__ . '/includes/class-consent-manager.php';
require_once __DIR__ . '/includes/class-consent-log.php';
require_once __DIR__ . '/includes/class-regional-presets.php';
require_once __DIR__ . '/includes/class-geo-resolver.php';
require_once __DIR__ . '/includes/class-service-templates.php';
require_once __DIR__ . '/includes/class-script-blocker.php';
require_once __DIR__ . '/includes/class-banner-renderer.php';
require_once __DIR__ . '/includes/class-tcf-adapter.php';

/**
 * Orchestrator that wires every cookie-banner sub-component together.
 */
class Blaminhor_Essentials_Module_Cookies extends Blaminhor_Essentials_Module {

	const REST_NAMESPACE = 'blaminhor-essentials/cookies/v1';
	const CRON_CLEANUP   = 'blaminhor_essentials_cookies_log_cleanup';

	/**
	 * Module ID.
	 *
	 * @var string
	 */
	protected $id = 'cookies';

	/**
	 * Module display name.
	 *
	 * @var string
	 */
	protected $name = 'Cookie Banner';

	/**
	 * @var Blaminhor_Essentials_Cookies_Consent_Manager
	 */
	protected $consent_manager;

	/**
	 * @var Blaminhor_Essentials_Cookies_Consent_Log
	 */
	protected $consent_log;

	/**
	 * @var Blaminhor_Essentials_Cookies_Service_Templates
	 */
	protected $service_templates;

	/**
	 * @var Blaminhor_Essentials_Cookies_Geo_Resolver
	 */
	protected $geo_resolver;

	/**
	 * @var Blaminhor_Essentials_Cookies_Regional_Presets
	 */
	protected $regional_presets;

	/**
	 * @var Blaminhor_Essentials_Cookies_TCF_Adapter
	 */
	protected $tcf_adapter;

	/**
	 * @var Blaminhor_Essentials_Cookies_Script_Blocker
	 */
	protected $script_blocker;

	/**
	 * @var Blaminhor_Essentials_Cookies_Banner_Renderer
	 */
	protected $banner_renderer;

	/**
	 * Initialize labels and lazily construct sub-components.
	 */
	protected function init() {
		$this->name        = __( 'Cookie Banner', 'blaminhor-essentials' );
		$this->description = __( 'Show a consent banner and block tracking scripts until visitors agree.', 'blaminhor-essentials' );

		$this->consent_manager   = new Blaminhor_Essentials_Cookies_Consent_Manager( $this->get_setting( 'policy_version', '2026.05' ) );
		$this->consent_log       = new Blaminhor_Essentials_Cookies_Consent_Log();
		$this->service_templates = new Blaminhor_Essentials_Cookies_Service_Templates( __DIR__ . '/services/' );
		$this->geo_resolver      = new Blaminhor_Essentials_Cookies_Geo_Resolver();
		$this->regional_presets  = new Blaminhor_Essentials_Cookies_Regional_Presets();
		$this->tcf_adapter       = new Blaminhor_Essentials_Cookies_TCF_Adapter();
	}

	/**
	 * Default settings.
	 *
	 * @return array
	 */
	protected function get_default_settings() {
		return array(
			// General.
			'policy_version'      => '2026.05',
			'hide_for_admins'     => true,
			'layout'              => 'banner-bottom', // banner-bottom | banner-top | popup-center | box-bottom-right
			'symmetry_lock'       => true,
			'show_revoke_icon'    => true,
			// Per-visitor geolocation is off by default (back-to-essentials revision).
			// Most sites target a single market and pick their jurisdiction in the
			// General tab. Visitor-country detection (MaxMind) is opt-in from the
			// Advanced tab — it requires uploading a GeoLite2 database manually.
			'auto_geo'            => false,
			'force_preset'        => '', // empty = strictest default; otherwise preset key.
			'dark_mode'           => 'auto', // auto | light | dark
			// Visual.
			'color_bg'            => '#ffffff',
			'color_text'          => '#1f2329',
			'color_button_bg'     => '#1d4ed8',
			'color_button_text'   => '#ffffff',
			'color_button_border' => '#1d4ed8',
			'color_dark_bg'       => '#1f2329',
			'color_dark_text'     => '#f5f6f7',
			// Integrations.
			'gcm_enabled'         => true,
			'tcf_enabled'         => false,
			'wp_consent_api'      => true,
			'intercept_inline'    => true,
			'active_services'     => array(), // Empty means all-on.
			// Privacy policy URL (admin-provided).
			'privacy_policy_url'  => '',
		);
	}

	/**
	 * Register all module hooks (front + admin + REST + cron).
	 */
	protected function register_hooks() {
		add_action( 'admin_init', array( $this, 'handle_settings_submission' ) );
		add_action( 'admin_init', array( $this, 'maybe_install_schema' ) );

		// REST API.
		add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );

		// Cron cleanup of expired log entries.
		if ( ! wp_next_scheduled( self::CRON_CLEANUP ) ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_CLEANUP );
		}
		add_action( self::CRON_CLEANUP, array( $this->consent_log, 'cleanup_expired' ) );

		// Filters: feed the runtime config with the per-page service list.
		add_filter( 'blaminhor_cookies_runtime_services', array( $this, 'filter_runtime_services' ) );

		// Front-end only: assets + script interception.
		if ( ! is_admin() ) {
			$this->banner_renderer = new Blaminhor_Essentials_Cookies_Banner_Renderer(
				$this->consent_manager,
				$this->settings,
				BLAMINHOR_ESSENTIALS_PLUGIN_URL . 'modules/cookies/',
				BLAMINHOR_ESSENTIALS_VERSION
			);
			$this->banner_renderer->register_hooks();

			$this->script_blocker = new Blaminhor_Essentials_Cookies_Script_Blocker(
				$this->service_templates,
				$this->consent_manager,
				$this->settings
			);
			$this->script_blocker->register_hooks();

			// Acknowledge the GPC signal as a refusal of non-essential
			// categories — required for CCPA opt-out in 12+ US states.
			add_action( 'send_headers', array( $this, 'maybe_acknowledge_gpc' ) );
		}

		// TCF cron + handler.
		if ( ! empty( $this->settings['tcf_enabled'] ) ) {
			$this->tcf_adapter->schedule_vendor_list_refresh();
		}

		// Cross-module hooks (cache invalidation on consent change).
		add_action( 'blaminhor_cookies_consent_changed', array( $this, 'invalidate_cache_on_consent_change' ), 10, 2 );

		// Admin bar quick-preview entry (visible front + admin for admins only).
		add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_preview_item' ), 100 );
	}

	/**
	 * Add a "Preview cookie banner" entry to the admin bar for admins.
	 *
	 * @param WP_Admin_Bar $wp_admin_bar Admin bar instance.
	 */
	public function add_admin_bar_preview_item( $wp_admin_bar ) {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		// Hide the entry when we're already in preview mode (don't stack).
		if ( ! is_admin() && ! empty( $_GET['blaminhor_preview_banner'] ) ) {
			return;
		}

		if ( is_admin() ) {
			$target = home_url( '/' );
		} else {
			$scheme  = is_ssl() ? 'https' : 'http';
			$host    = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : wp_parse_url( home_url(), PHP_URL_HOST );
			$request = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
			// Strip any pre-existing preview/wpnonce params to avoid duplication.
			$request = remove_query_arg( array( 'blaminhor_preview_banner', '_wpnonce' ), $request );
			$target  = $scheme . '://' . $host . $request;
		}

		$url = wp_nonce_url(
			add_query_arg( 'blaminhor_preview_banner', '1', $target ),
			'blaminhor_essentials_cookies_preview'
		);

		$wp_admin_bar->add_node(
			array(
				'id'    => 'blaminhor-essentials-cookies-preview',
				'title' => esc_html__( 'Preview cookie banner', 'blaminhor-essentials' ),
				'href'  => $url,
				'meta'  => array(
					'title' => esc_attr__( 'Open this page with the cookie banner forced visible (admin preview, choices not saved).', 'blaminhor-essentials' ),
				),
			)
		);
	}

	/**
	 * Create the consent log table the first time the module is loaded.
	 */
	public function maybe_install_schema() {
		$this->consent_log->maybe_install_schema();
	}

	/**
	 * Enqueue admin assets (WordPress color picker for the colors tab).
	 *
	 * @param string $hook Current admin page hook.
	 */
	public function enqueue_assets( $hook ) {
		if ( false === strpos( $hook, 'blaminhor-essentials-cookies' ) ) {
			return;
		}
		wp_enqueue_style( 'wp-color-picker' );
		wp_enqueue_script( 'wp-color-picker' );
	}

	/**
	 * Hook: when consent changes, ask any active page cache to drop its copy.
	 *
	 * @param array  $choices New choices.
	 * @param string $source  banner|revoke|gpc|reconsent.
	 */
	public function invalidate_cache_on_consent_change( $choices, $source ) {
		// Cache module hook (no-op if not active).
		do_action( 'blaminhor_essentials_cache_purge_request' );
		unset( $choices, $source );
	}

	/**
	 * Set a header back acknowledging that we acted upon GPC.
	 */
	public function maybe_acknowledge_gpc() {
		if ( headers_sent() ) {
			return;
		}
		if ( $this->consent_manager->has_gpc_signal() ) {
			header( 'Sec-GPC-Acknowledged: 1' );
		}
	}

	/**
	 * Process the admin settings form.
	 *
	 * @param array $data $_POST data.
	 */
	protected function process_settings( $data ) {
		$data = wp_unslash( $data );

		$settings = array(
			'policy_version'      => isset( $data['policy_version'] ) ? sanitize_text_field( $data['policy_version'] ) : '2026.05',
			'hide_for_admins'     => isset( $data['hide_for_admins'] ),
			'layout'              => isset( $data['layout'] ) && in_array( $data['layout'], array( 'banner-bottom', 'banner-top', 'popup-center', 'box-bottom-right' ), true ) ? $data['layout'] : 'banner-bottom',
			'symmetry_lock'       => isset( $data['symmetry_lock'] ),
			'show_revoke_icon'    => isset( $data['show_revoke_icon'] ),
			'auto_geo'            => isset( $data['auto_geo'] ),
			'force_preset'        => isset( $data['force_preset'] ) ? sanitize_key( $data['force_preset'] ) : '',
			'dark_mode'           => isset( $data['dark_mode'] ) && in_array( $data['dark_mode'], array( 'auto', 'light', 'dark' ), true ) ? $data['dark_mode'] : 'auto',
			'color_bg'            => isset( $data['color_bg'] ) ? sanitize_hex_color( $data['color_bg'] ) : '#ffffff',
			'color_text'          => isset( $data['color_text'] ) ? sanitize_hex_color( $data['color_text'] ) : '#1f2329',
			'color_button_bg'     => isset( $data['color_button_bg'] ) ? sanitize_hex_color( $data['color_button_bg'] ) : '#1d4ed8',
			'color_button_text'   => isset( $data['color_button_text'] ) ? sanitize_hex_color( $data['color_button_text'] ) : '#ffffff',
			'color_button_border' => isset( $data['color_button_border'] ) ? sanitize_hex_color( $data['color_button_border'] ) : '#1d4ed8',
			'color_dark_bg'       => isset( $data['color_dark_bg'] ) ? sanitize_hex_color( $data['color_dark_bg'] ) : '#1f2329',
			'color_dark_text'     => isset( $data['color_dark_text'] ) ? sanitize_hex_color( $data['color_dark_text'] ) : '#f5f6f7',
			'gcm_enabled'         => isset( $data['gcm_enabled'] ),
			'tcf_enabled'         => isset( $data['tcf_enabled'] ),
			'wp_consent_api'      => isset( $data['wp_consent_api'] ),
			'intercept_inline'    => isset( $data['intercept_inline'] ),
			'privacy_policy_url'  => isset( $data['privacy_policy_url'] ) ? esc_url_raw( $data['privacy_policy_url'] ) : '',
			'active_services'     => isset( $data['active_services'] ) && is_array( $data['active_services'] ) ? array_map( 'sanitize_key', $data['active_services'] ) : array(),
		);

		// Bump policy version when admin requests forced re-consent.
		if ( isset( $data['force_reconsent'] ) ) {
			$settings['policy_version'] = gmdate( 'Y.m.d.His' );
			do_action( 'blaminhor_cookies_policy_bumped', $this->settings['policy_version'] ?? '', $settings['policy_version'] );
		}

		$this->save_settings( $settings );
		add_action(
			'admin_notices',
			function () {
				blaminhor_essentials_admin_notice( __( 'Settings saved.', 'blaminhor-essentials' ), 'success' );
			}
		);
	}

	/**
	 * Render the admin page (multi-tab).
	 */
	public function render_settings_page() {
		$this->handle_settings_submission();

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab switcher.
		$active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';

		$module          = $this;
		$consent_manager = $this->consent_manager;
		$consent_log     = $this->consent_log;
		$templates       = $this->service_templates;
		$presets         = $this->regional_presets;
		$geo_resolver    = $this->geo_resolver;
		$tcf_adapter     = $this->tcf_adapter;
		$settings        = $this->settings;

		require __DIR__ . '/views/settings-page.php';
	}

	/**
	 * REST endpoints.
	 */
	public function register_rest_routes() {
		register_rest_route(
			self::REST_NAMESPACE,
			'/context',
			array(
				'methods'             => 'GET',
				'permission_callback' => '__return_true',
				'callback'            => array( $this, 'rest_get_context' ),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/log',
			array(
				'methods'             => 'POST',
				'permission_callback' => '__return_true',
				'callback'            => array( $this, 'rest_post_log' ),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/export',
			array(
				'methods'             => 'GET',
				'permission_callback' => array( $this, 'rest_admin_permission' ),
				'callback'            => array( $this, 'rest_export_log' ),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/diagnostic/scan',
			array(
				'methods'             => 'GET',
				'permission_callback' => array( $this, 'rest_admin_permission' ),
				'callback'            => array( $this, 'rest_diagnostic_scan' ),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/diagnostic/block',
			array(
				'methods'             => 'POST',
				'permission_callback' => array( $this, 'rest_admin_permission' ),
				'callback'            => array( $this, 'rest_diagnostic_block' ),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/geoip/download',
			array(
				'methods'             => 'POST',
				'permission_callback' => array( $this, 'rest_admin_permission' ),
				'callback'            => array( $this, 'rest_geoip_download' ),
			)
		);
	}

	/**
	 * @return bool|WP_Error
	 */
	public function rest_admin_permission() {
		if ( current_user_can( 'manage_options' ) ) {
			return true;
		}
		return new WP_Error( 'rest_forbidden', __( 'Unauthorized', 'blaminhor-essentials' ), array( 'status' => 401 ) );
	}

	/**
	 * GET /context — resolves region + preset for the visitor.
	 *
	 * @return WP_REST_Response
	 */
	public function rest_get_context( $request ) {
		unset( $request );

		$country = $this->settings['auto_geo'] ? $this->geo_resolver->resolve_country() : null;
		$preset_key = '';

		if ( ! empty( $this->settings['force_preset'] ) ) {
			$preset = $this->regional_presets->get_presets();
			$preset = isset( $preset[ $this->settings['force_preset'] ] ) ? array_merge( array( 'key' => $this->settings['force_preset'] ), $preset[ $this->settings['force_preset'] ] ) : $this->regional_presets->resolve_preset( null );
			$preset_key = $preset['key'];
		} else {
			$preset     = $this->regional_presets->resolve_preset( $country );
			$preset_key = $preset['key'];
		}

		$response = array(
			'country'       => $country,
			'preset'        => $preset_key,
			'presetConfig'  => $preset,
			'lang'          => determine_locale(),
			'gpcActive'     => $this->consent_manager->has_gpc_signal(),
			'policyVersion' => $this->consent_manager->get_policy_version(),
			'tcf'           => array(
				'enabled'    => ! empty( $this->settings['tcf_enabled'] ),
				'descriptor' => ! empty( $this->settings['tcf_enabled'] ) ? $this->tcf_adapter->get_runtime_descriptor() : null,
			),
		);

		$res = rest_ensure_response( $response );
		$res->header( 'Cache-Control', 'public, max-age=3600' );
		return $res;
	}

	/**
	 * POST /log — append a consent record.
	 *
	 * @param WP_REST_Request $request Body.
	 * @return WP_REST_Response
	 */
	public function rest_post_log( $request ) {
		$choices  = (array) $request->get_param( 'choices' );
		$services = (array) $request->get_param( 'services' );
		$source   = sanitize_key( (string) $request->get_param( 'source' ) );
		$lang     = sanitize_text_field( (string) $request->get_param( 'lang' ) );
		$tc       = (string) $request->get_param( 'tc_string' );

		if ( ! in_array( $source, array( 'banner', 'revoke', 'gpc', 'reconsent' ), true ) ) {
			$source = 'banner';
		}

		$normalized = $this->consent_manager->normalize_choices( $choices );

		$payload = array(
			'created_at'     => gmdate( 'Y-m-d H:i:s' ),
			'visitor_hash'   => $this->consent_manager->get_visitor_hash(),
			'ua_hash'        => $this->consent_manager->get_ua_hash(),
			'choices'        => array(
				'categories' => $normalized,
				'services'   => array_map( 'absint', $services ),
			),
			'policy_version' => $this->consent_manager->get_policy_version(),
			'lang'           => '' !== $lang ? $lang : determine_locale(),
			'geo_country'    => $this->geo_resolver->resolve_country(),
			'source'         => $source,
			'tc_string'      => '' !== $tc ? $tc : null,
		);

		$id = $this->consent_log->insert( $payload );
		do_action( 'blaminhor_cookies_consent_changed', $normalized, $source );
		if ( $id ) {
			do_action( 'blaminhor_cookies_log_written', $id, $normalized );
		}

		// Write the canonical, HMAC-signed cookie so that on the next page
		// load the server-side fast-path recognizes the consent and skips
		// banner rendering (zero-byte branch).
		$encoded = $this->consent_manager->encode_payload(
			$normalized,
			isset( $payload['choices']['services'] ) ? $payload['choices']['services'] : array(),
			$payload['lang'],
			$source
		);
		if ( ! headers_sent() ) {
			setcookie(
				Blaminhor_Essentials_Cookies_Consent_Manager::COOKIE_NAME,
				$encoded,
				array(
					'expires'  => time() + Blaminhor_Essentials_Cookies_Consent_Manager::COOKIE_TTL,
					'path'     => defined( 'COOKIEPATH' ) && COOKIEPATH ? COOKIEPATH : '/',
					'domain'   => defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : '',
					'secure'   => is_ssl(),
					'httponly' => false,
					'samesite' => 'Lax',
				)
			);
		}

		return rest_ensure_response(
			array(
				'logged'        => (bool) $id,
				'id'            => $id,
				'policyVersion' => $this->consent_manager->get_policy_version(),
			)
		);
	}

	/**
	 * GET /export — stream CSV of the log (admins only).
	 *
	 * @param WP_REST_Request $request Query params.
	 */
	public function rest_export_log( $request ) {
		$from = sanitize_text_field( (string) $request->get_param( 'from' ) );
		$to   = sanitize_text_field( (string) $request->get_param( 'to' ) );
		$this->consent_log->stream_csv( $from ?: null, $to ?: null );
		exit;
	}

	/**
	 * GET /diagnostic/scan — return the service catalog + active list so the
	 * admin's browser can do the scan client-side.
	 *
	 * Why client-side?
	 *   - Avoids a PHP→PHP self-fetch (deadlocks on the PHP built-in dev
	 *     server, slow on shared hosting too).
	 *   - The browser sees the same HTML the visitor would (after CDN,
	 *     plugin output filters, etc.).
	 *   - We can fetch with credentials: 'omit' to simulate a fresh, anonymous
	 *     visitor — exactly the threat model the diagnostic is checking.
	 *
	 * @param WP_REST_Request $request Query params.
	 * @return WP_REST_Response
	 */
	public function rest_diagnostic_scan( $request ) {
		unset( $request );

		$active    = isset( $this->settings['active_services'] ) ? (array) $this->settings['active_services'] : array();
		$templates = $this->service_templates->all();

		// Strip data we don't need on the client to keep the payload small.
		$out = array();
		foreach ( $templates as $handle => $svc ) {
			$out[] = array(
				'handle'         => $handle,
				'name'           => $svc['name'],
				'provider'       => $svc['provider'],
				'category'       => $svc['category_default'],
				'scriptPatterns' => array_values( (array) ( $svc['script_patterns'] ?? array() ) ),
				'iframePatterns' => array_values( (array) ( $svc['iframe_patterns'] ?? array() ) ),
			);
		}

		return rest_ensure_response(
			array(
				'templates'        => $out,
				'activeServices'   => array_values( $active ),
				'allActiveImplicit' => empty( $active ), // empty = every service is implicitly active.
				'firstPartyHost'   => parse_url( home_url( '/' ), PHP_URL_HOST ),
				'homepageUrl'      => home_url( '/' ),
				'interceptInline'  => ! empty( $this->settings['intercept_inline'] ),
			)
		);
	}

	/**
	 * POST /diagnostic/block — enable or disable a service in the active list.
	 *
	 * Payload: { handle: 'google-analytics-4', action: 'enable'|'disable' }
	 *
	 * Sémantique des active_services :
	 *   - Liste vide  = TOUS les services connus sont implicitement actifs.
	 *   - Liste pleine = seuls les handles listés sont actifs.
	 *
	 * "enable" depuis un état vide est un no-op (déjà actif implicitement).
	 * "disable" depuis un état vide construit la liste complète moins le handle,
	 * pour pouvoir l'exclure proprement.
	 *
	 * @param WP_REST_Request $request Body params.
	 * @return WP_REST_Response
	 */
	public function rest_diagnostic_block( $request ) {
		$handle = sanitize_key( (string) $request->get_param( 'handle' ) );
		$action = sanitize_key( (string) $request->get_param( 'action' ) );

		if ( '' === $handle ) {
			return new WP_Error( 'invalid_handle', __( 'Missing service handle.', 'blaminhor-essentials' ), array( 'status' => 400 ) );
		}
		if ( ! in_array( $action, array( 'enable', 'disable' ), true ) ) {
			return new WP_Error( 'invalid_action', __( 'Action must be enable or disable.', 'blaminhor-essentials' ), array( 'status' => 400 ) );
		}

		$templates = $this->service_templates->all();
		if ( ! isset( $templates[ $handle ] ) ) {
			return new WP_Error( 'unknown_service', __( 'Service not found in the catalog.', 'blaminhor-essentials' ), array( 'status' => 404 ) );
		}

		$active = isset( $this->settings['active_services'] ) ? (array) $this->settings['active_services'] : array();

		if ( 'enable' === $action ) {
			if ( empty( $active ) ) {
				// All services already active implicitly — nothing to persist.
				return rest_ensure_response(
					array(
						'handle'   => $handle,
						'enabled'  => true,
						'implicit' => true,
					)
				);
			}
			if ( ! in_array( $handle, $active, true ) ) {
				$active[] = $handle;
			}
		} else { // disable
			if ( empty( $active ) ) {
				// Materialize the implicit list, minus the disabled handle.
				$active = array();
				foreach ( $templates as $h => $tpl ) {
					if ( $h !== $handle ) {
						$active[] = $h;
					}
				}
			} else {
				$active = array_values( array_diff( $active, array( $handle ) ) );
			}
		}

		$this->settings['active_services'] = $active;
		update_option( 'blaminhor_essentials_module_' . $this->id, $this->settings );

		return rest_ensure_response(
			array(
				'handle'   => $handle,
				'enabled'  => 'enable' === $action,
				'implicit' => false,
				'active'   => $active,
			)
		);
	}


	/**
	 * POST /geoip/download — fetch MaxMind GeoLite2 db (admin must supply
	 * an account ID + license key in settings, MaxMind's terms require it).
	 *
	 * @return WP_REST_Response
	 */
	public function rest_geoip_download( $request ) {
		unset( $request );
		// Skeleton: MaxMind requires a signed download; the admin is invited
		// to drop the file manually in uploads/blaminhor-essentials/geoip/.
		return rest_ensure_response(
			array(
				'message' => __( 'For licensing reasons, MaxMind GeoLite2-Country.mmdb must be downloaded manually from maxmind.com (free account required) and uploaded to uploads/blaminhor-essentials/geoip/. Server-side geolocation will activate automatically once the file is present.', 'blaminhor-essentials' ),
				'path'    => str_replace( ABSPATH, '', $this->geo_resolver->get_database_path() ),
				'present' => $this->geo_resolver->has_database(),
			)
		);
	}

	/**
	 * Feed the runtime config with the services to advertise to the visitor.
	 *
	 * @param array $services Filter input.
	 * @return array
	 */
	public function filter_runtime_services( $services ) {
		unset( $services );
		$out      = array();
		$active   = $this->settings['active_services'];
		$all      = $this->service_templates->all();
		$is_open  = empty( $active );
		foreach ( $all as $handle => $svc ) {
			if ( ! $is_open && ! in_array( $handle, $active, true ) ) {
				continue;
			}
			$out[] = array(
				'handle'   => $handle,
				'name'     => $svc['name'],
				'provider' => $svc['provider'],
				'category' => $svc['category_default'],
				'privacy'  => $svc['privacy_url'],
			);
		}
		return $out;
	}

	/**
	 * Public accessors (used in views and external code).
	 */
	public function get_consent_manager() {
		return $this->consent_manager;
	}
	public function get_consent_log() {
		return $this->consent_log;
	}
	public function get_service_templates() {
		return $this->service_templates;
	}
	public function get_geo_resolver() {
		return $this->geo_resolver;
	}
	public function get_regional_presets() {
		return $this->regional_presets;
	}
	public function get_tcf_adapter() {
		return $this->tcf_adapter;
	}
	public function get_settings() {
		return $this->settings;
	}
}
