<?php
/**
 * Admin dashboard and settings for DataStream.
 *
 * @package DataStream
 */

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

/**
 * Class DataStream_Admin
 */
class DataStream_Admin {

	/**
	 * Top-level menu slug.
	 */
	private const MENU_SLUG = 'datastream';

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'admin_menu', array( $this, 'register_menu' ) );
		add_action( 'admin_init', array( $this, 'register_settings' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
		add_action( 'update_option_datastream_cron_enabled', array( $this, 'on_cron_enabled_changed' ), 10, 2 );
		add_action( 'admin_post_datastream_save_stream', array( $this, 'handle_save_stream' ) );
		add_action( 'admin_post_datastream_delete_stream', array( $this, 'handle_delete_stream' ) );
		add_action( 'admin_notices', array( $this, 'maybe_failing_notice' ) );
		add_filter( 'site_status_tests', array( $this, 'register_site_health_test' ) );
	}

	/**
	 * One quiet, dismissible notice when a stream has been failing repeatedly.
	 * Dismissal lasts a day; never more than one notice at a time.
	 */
	public function maybe_failing_notice() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		if ( isset( $_GET['ds_dismiss_failing'] ) && check_admin_referer( 'ds_dismiss_failing' ) ) {
			set_transient( 'datastream_failing_dismissed_' . get_current_user_id(), 1, DAY_IN_SECONDS );
		}

		if ( get_transient( 'datastream_failing_dismissed_' . get_current_user_id() ) ) {
			return;
		}

		$failing = array();

		foreach ( DataStream_Streams::get_streams() as $stream ) {
			if ( absint( $stream['failure_count'] ) >= 3 && (int) $stream['backoff_until'] > time() ) {
				$failing[] = $stream['label'] ?: $stream['id'];
			}
		}

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

		$dismiss_url = wp_nonce_url( add_query_arg( 'ds_dismiss_failing', '1' ), 'ds_dismiss_failing' );

		printf(
			'<div class="notice notice-warning"><p>%s <a href="%s">%s</a> | <a href="%s">%s</a></p></div>',
			esc_html( sprintf(
				/* translators: %s: comma-separated stream names */
				__( 'DataStream: these streams keep failing and are paused between retries: %s.', 'datastream-api-sync' ),
				implode( ', ', $failing )
			) ),
			esc_url( admin_url( 'admin.php?page=datastream-settings&tab=streams' ) ),
			esc_html__( 'Review streams', 'datastream-api-sync' ),
			esc_url( $dismiss_url ),
			esc_html__( 'Dismiss for today', 'datastream-api-sync' )
		);
	}

	/**
	 * Site Health: one direct test summarizing stream health.
	 *
	 * @param array<string, mixed> $tests Registered tests.
	 * @return array<string, mixed>
	 */
	public function register_site_health_test( $tests ) {
		$tests['direct']['datastream_streams'] = array(
			'label' => __( 'DataStream streams are syncing', 'datastream-api-sync' ),
			'test'  => static function () {
				$failing = array();

				foreach ( DataStream_Streams::get_streams() as $stream ) {
					if ( ! empty( $stream['enabled'] ) && (int) $stream['backoff_until'] > time() ) {
						$failing[] = $stream['label'] ?: $stream['id'];
					}
				}

				$result = array(
					'label'       => __( 'DataStream streams are syncing', 'datastream-api-sync' ),
					'status'      => 'good',
					'badge'       => array(
						'label' => __( 'DataStream', 'datastream-api-sync' ),
						'color' => 'blue',
					),
					'description' => '<p>' . esc_html__( 'All enabled streams synced successfully on their last run.', 'datastream-api-sync' ) . '</p>',
					'actions'     => '',
					'test'        => 'datastream_streams',
				);

				if ( ! empty( $failing ) ) {
					$result['status']      = 'recommended';
					$result['label']       = __( 'Some DataStream streams are failing', 'datastream-api-sync' );
					$result['description'] = '<p>' . esc_html( sprintf(
						/* translators: %s: comma-separated stream names */
						__( 'These streams are paused between retries after repeated failures: %s. Their APIs may be down, rate limiting, or misconfigured.', 'datastream-api-sync' ),
						implode( ', ', $failing )
					) ) . '</p>';
					$result['actions']     = '<a href="' . esc_url( admin_url( 'admin.php?page=datastream-settings&tab=streams' ) ) . '">' . esc_html__( 'Review streams', 'datastream-api-sync' ) . '</a>';
				}

				return $result;
			},
		);

		return $tests;
	}

	/**
	 * Persist a stream from the Streams form.
	 */
	public function handle_save_stream() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to manage streams.', 'datastream-api-sync' ) );
		}

		check_admin_referer( 'datastream_save_stream' );

		$raw = isset( $_POST['stream'] ) && is_array( $_POST['stream'] ) ? wp_unslash( $_POST['stream'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- sanitized field-by-field in save_stream().

		// "Clear" checkboxes translate to the registry's clear sentinel, so
		// the UI never exposes that convention.
		foreach ( array( 'api_bearer_token', 'api_basic_pass', 'oauth_client_secret', 'oauth_refresh_token' ) as $secret ) {
			if ( ! empty( $raw[ $secret . '_clear' ] ) ) {
				$raw[ $secret ] = '-';
			}
			unset( $raw[ $secret . '_clear' ] );
		}

		$saved = DataStream_Streams::save_stream( $raw );

		if ( is_wp_error( $saved ) ) {
			$redirect = add_query_arg(
				array(
					'page'     => 'datastream-settings',
					'tab'      => 'streams',
					'ds_error' => rawurlencode( $saved->get_error_message() ),
				),
				admin_url( 'admin.php' )
			);
		} else {
			$redirect = add_query_arg(
				array(
					'page'     => 'datastream-settings',
					'tab'      => 'streams',
					'stream'   => $saved['id'],
					'ds_saved' => '1',
				),
				admin_url( 'admin.php' )
			);
		}

		wp_safe_redirect( $redirect );
		exit;
	}

	/**
	 * Delete a stream (records are kept).
	 */
	public function handle_delete_stream() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to manage streams.', 'datastream-api-sync' ) );
		}

		$id = isset( $_GET['stream'] ) ? sanitize_key( wp_unslash( $_GET['stream'] ) ) : '';

		check_admin_referer( 'datastream_delete_stream_' . $id );

		DataStream_Streams::delete_stream( $id );

		wp_safe_redirect(
			add_query_arg(
				array(
					'page'       => 'datastream-settings',
					'tab'        => 'streams',
					'ds_deleted' => '1',
				),
				admin_url( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Reschedule or clear cron when enabled toggle changes.
	 *
	 * @param mixed $old_value Previous value.
	 * @param mixed $new_value New value.
	 */
	public function on_cron_enabled_changed( $old_value, $new_value ) {
		if ( $new_value ) {
			DataStream_Engine::reschedule_sync( (string) get_option( 'datastream_cron_schedule', 'hourly' ) );
		} else {
			DataStream_Engine::unschedule_sync();
		}
	}

	/**
	 * Register admin menu pages.
	 */
	public function register_menu() {
		add_menu_page(
			__( 'DataStream', 'datastream-api-sync' ),
			__( 'DataStream', 'datastream-api-sync' ),
			'manage_options',
			self::MENU_SLUG,
			array( $this, 'render_dashboard_page' ),
			'dashicons-database-import',
			58
		);

		add_submenu_page(
			self::MENU_SLUG,
			__( 'Dashboard', 'datastream-api-sync' ),
			__( 'Dashboard', 'datastream-api-sync' ),
			'manage_options',
			self::MENU_SLUG,
			array( $this, 'render_dashboard_page' )
		);

		add_submenu_page(
			self::MENU_SLUG,
			__( 'Settings', 'datastream-api-sync' ),
			__( 'Settings', 'datastream-api-sync' ),
			'manage_options',
			self::MENU_SLUG . '-settings',
			array( $this, 'render_settings_page' )
		);
	}

	/**
	 * Register all plugin settings.
	 */
	public function register_settings() {
		$group = 'datastream_settings';

		$fields = array(
			'datastream_api_url'              => array( 'sanitize_callback' => array( 'DataStream_Config', 'sanitize_api_url' ) ),
			'datastream_api_method'           => array( 'sanitize_callback' => array( 'DataStream_Config', 'sanitize_api_method' ) ),
			'datastream_api_timeout'          => array( 'sanitize_callback' => array( 'DataStream_Config', 'sanitize_timeout' ) ),
			'datastream_api_headers'          => array( 'sanitize_callback' => array( 'DataStream_Config', 'sanitize_headers_json' ) ),
			'datastream_api_bearer_token'     => array( 'sanitize_callback' => array( $this, 'sanitize_bearer_token' ) ),
			'datastream_api_ssl_verify'       => array( 'sanitize_callback' => array( $this, 'sanitize_checkbox' ) ),
			'datastream_json_items_path'      => array( 'sanitize_callback' => 'sanitize_text_field' ),
			// Field names must keep their case — APIs use camelCase keys, and
			// sanitize_key() would silently break the lookup.
			'datastream_id_field'             => array( 'sanitize_callback' => array( $this, 'sanitize_field_name' ) ),
			'datastream_content_field'        => array( 'sanitize_callback' => array( $this, 'sanitize_field_name' ) ),
			'datastream_stream_name'          => array( 'sanitize_callback' => 'sanitize_key' ),
			'datastream_cron_schedule'        => array( 'sanitize_callback' => array( $this, 'sanitize_schedule' ) ),
			'datastream_cron_enabled'         => array( 'sanitize_callback' => array( $this, 'sanitize_checkbox' ) ),
			'datastream_max_records_per_sync' => array( 'sanitize_callback' => function ( $v ) {
				return DataStream_Config::sanitize_bounded_int( $v, 1, 5000, 500 );
			} ),
			'datastream_max_payload_bytes'    => array( 'sanitize_callback' => function ( $v ) {
				return DataStream_Config::sanitize_bounded_int( $v, 1024, 10485760, 1048576 );
			} ),
			'datastream_max_response_bytes'   => array( 'sanitize_callback' => function ( $v ) {
				return DataStream_Config::sanitize_bounded_int( $v, 65536, 52428800, 5242880 );
			} ),
			'datastream_max_external_id_len'  => array( 'sanitize_callback' => function ( $v ) {
				return DataStream_Config::sanitize_bounded_int( $v, 10, 100, 100 );
			} ),
			'datastream_stale_record_days'    => array( 'sanitize_callback' => function ( $v ) {
				return DataStream_Config::sanitize_bounded_int( $v, 0, 3650, 0 );
			} ),
			'datastream_cache_ttl'            => array( 'sanitize_callback' => function ( $v ) {
				return DataStream_Config::sanitize_bounded_int( $v, 60, 86400, HOUR_IN_SECONDS );
			} ),
			'datastream_enable_logging'       => array( 'sanitize_callback' => array( $this, 'sanitize_checkbox' ) ),
			'datastream_log_level'            => array( 'sanitize_callback' => array( $this, 'sanitize_log_level' ) ),
			'datastream_sync_lock_timeout'    => array( 'sanitize_callback' => function ( $v ) {
				return DataStream_Config::sanitize_bounded_int( $v, 60, 3600, 300 );
			} ),
			'datastream_delete_on_uninstall'  => array( 'sanitize_callback' => array( $this, 'sanitize_checkbox' ) ),
		);

		foreach ( $fields as $option => $args ) {
			register_setting( $group, $option, $args );
		}

		$this->register_settings_sections();
	}

	/**
	 * Register settings sections and fields.
	 */
	private function register_settings_sections() {
		$page = 'datastream_settings';

		// Connection and mapping moved into per-stream configuration (the
		// Streams tab) in 1.4.0; only site-wide HTTP settings stay here.
		add_settings_section( 'ds_sync', __( 'Sync & Data Retention', 'datastream-api-sync' ), array( $this, 'section_sync' ), $page );
		add_settings_field( 'datastream_api_timeout', __( 'Request Timeout (seconds)', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_sync', array( 'option' => 'datastream_api_timeout', 'min' => 5, 'max' => 120 ) );
		add_settings_field( 'datastream_api_ssl_verify', __( 'Verify SSL Certificate', 'datastream-api-sync' ), array( $this, 'field_checkbox' ), $page, 'ds_sync', array( 'option' => 'datastream_api_ssl_verify', 'label' => __( 'Validate SSL certificates on remote requests (disable only for local/dev testing).', 'datastream-api-sync' ) ) );
		add_settings_field( 'datastream_cron_enabled', __( 'Enable Scheduled Sync', 'datastream-api-sync' ), array( $this, 'field_checkbox' ), $page, 'ds_sync', array( 'option' => 'datastream_cron_enabled', 'label' => __( 'Run background ingestion on the configured interval.', 'datastream-api-sync' ) ) );
		add_settings_field( 'datastream_cron_schedule', __( 'Sync Interval', 'datastream-api-sync' ), array( $this, 'field_schedule' ), $page, 'ds_sync' );
		add_settings_field( 'datastream_max_records_per_sync', __( 'Max Records Per Sync', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_sync', array( 'option' => 'datastream_max_records_per_sync', 'min' => 1, 'max' => 5000, 'description' => __( 'Caps ingestion per run to prevent timeouts on massive datasets.', 'datastream-api-sync' ) ) );
		add_settings_field( 'datastream_stale_record_days', __( 'Auto-Purge Stale Records (days)', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_sync', array( 'option' => 'datastream_stale_record_days', 'min' => 0, 'max' => 3650, 'description' => __( '0 = disabled. Deletes records older than N days after each successful sync.', 'datastream-api-sync' ) ) );
		add_settings_field( 'datastream_sync_lock_timeout', __( 'Sync Lock Timeout (seconds)', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_sync', array( 'option' => 'datastream_sync_lock_timeout', 'min' => 60, 'max' => 3600, 'description' => __( 'Prevents overlapping syncs. Lock auto-expires after this duration.', 'datastream-api-sync' ) ) );
		add_settings_field( 'datastream_delete_on_uninstall', __( 'Delete Data on Uninstall', 'datastream-api-sync' ), array( $this, 'field_checkbox' ), $page, 'ds_sync', array( 'option' => 'datastream_delete_on_uninstall', 'label' => __( 'Remove all synced records and settings when the plugin is deleted. Off by default so uninstalling never destroys data unexpectedly.', 'datastream-api-sync' ) ) );

		add_settings_section( 'ds_limits', __( 'Safety Limits', 'datastream-api-sync' ), array( $this, 'section_limits' ), $page );
		add_settings_field( 'datastream_max_response_bytes', __( 'Max API Response Size (bytes)', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_limits', array( 'option' => 'datastream_max_response_bytes', 'min' => 65536, 'max' => 52428800, 'description' => __( 'HTTP responses are cut off at this size so an oversized API cannot exhaust memory. Default 5 MB.', 'datastream-api-sync' ) ) );
		add_settings_field( 'datastream_max_payload_bytes', __( 'Max Payload Size (bytes)', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_limits', array( 'option' => 'datastream_max_payload_bytes', 'min' => 1024, 'max' => 10485760 ) );
		add_settings_field( 'datastream_max_external_id_len', __( 'Max External ID Length', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_limits', array( 'option' => 'datastream_max_external_id_len', 'min' => 10, 'max' => 100 ) );

		add_settings_section( 'ds_cache', __( 'Caching', 'datastream-api-sync' ), array( $this, 'section_cache' ), $page );
		add_settings_field( 'datastream_cache_ttl', __( 'Cache TTL (seconds)', 'datastream-api-sync' ), array( $this, 'field_number' ), $page, 'ds_cache', array( 'option' => 'datastream_cache_ttl', 'min' => 60, 'max' => 86400, 'description' => __( 'How long frontend/provider transients are cached.', 'datastream-api-sync' ) ) );

		add_settings_section( 'ds_logging', __( 'Security & Logging', 'datastream-api-sync' ), array( $this, 'section_logging' ), $page );
		add_settings_field( 'datastream_enable_logging', __( 'Enable Logging', 'datastream-api-sync' ), array( $this, 'field_checkbox' ), $page, 'ds_logging', array( 'option' => 'datastream_enable_logging', 'label' => __( 'Write ingestion events to the PHP error log. Credentials are never logged.', 'datastream-api-sync' ) ) );
		add_settings_field( 'datastream_log_level', __( 'Log Level', 'datastream-api-sync' ), array( $this, 'field_log_level' ), $page, 'ds_logging' );
	}

	/**
	 * Sanitize cron schedule and reschedule.
	 *
	 * @param string $value Schedule slug.
	 * @return string
	 */
	public function sanitize_schedule( $value ) {
		$value   = DataStream_Config::validate_schedule( $value );
		$current = get_option( 'datastream_cron_schedule', 'hourly' );

		if ( $current !== $value && DataStream_Config::get( 'cron_enabled' ) ) {
			DataStream_Engine::reschedule_sync( $value );
		}

		return $value;
	}

	/**
	 * Sanitize an API field name: trim it, keep its case.
	 *
	 * @param string $value Raw field name.
	 * @return string
	 */
	public function sanitize_field_name( $value ) {
		return trim( sanitize_text_field( (string) $value ) );
	}

	/**
	 * Sanitize checkbox — unchecked boxes submit hidden 0.
	 *
	 * @param mixed $value Submitted value.
	 * @return bool
	 */
	public function sanitize_checkbox( $value ) {
		return ! empty( $value );
	}

	/**
	 * Sanitize log level.
	 *
	 * @param string $value Level slug.
	 * @return string
	 */
	public function sanitize_log_level( $value ) {
		$value = sanitize_key( $value );
		return in_array( $value, array( 'error', 'info', 'debug' ), true ) ? $value : 'error';
	}

	/**
	 * Preserve bearer token when field left blank.
	 *
	 * @param string $value Submitted token.
	 * @return string
	 */
	public function sanitize_bearer_token( $value ) {
		$value = sanitize_text_field( $value );

		if ( '' === $value || '********' === $value ) {
			return (string) get_option( 'datastream_api_bearer_token', '' );
		}

		return $value;
	}

	/**
	 * Enqueue admin assets.
	 *
	 * @param string $hook Current admin page hook.
	 */
	public function enqueue_assets( $hook ) {
		if ( false === strpos( $hook, self::MENU_SLUG ) ) {
			return;
		}

		wp_enqueue_style(
			'datastream-admin',
			plugins_url( 'assets/css/admin-dashboard.css', DATASTREAM_PLUGIN_FILE ),
			array(),
			DATASTREAM_VERSION
		);

		wp_enqueue_style(
			'datastream-wizard',
			plugins_url( 'assets/css/datastream-wizard.css', DATASTREAM_PLUGIN_FILE ),
			array(),
			DATASTREAM_VERSION
		);

		wp_enqueue_script( 'wp-api-fetch' );

		if ( 'toplevel_page_' . self::MENU_SLUG === $hook ) {
			wp_enqueue_script(
				'datastream-client',
				plugins_url( 'assets/js/datastream-client.js', DATASTREAM_PLUGIN_FILE ),
				array( 'wp-api-fetch' ),
				DATASTREAM_VERSION,
				true
			);
			wp_localize_script( 'datastream-client', 'datastreamAdmin', $this->get_js_labels() );
		}

		if ( 'datastream_page_' . self::MENU_SLUG . '-settings' === $hook ) {
			wp_enqueue_script(
				'datastream-settings',
				plugins_url( 'assets/js/datastream-settings.js', DATASTREAM_PLUGIN_FILE ),
				array( 'wp-api-fetch' ),
				DATASTREAM_VERSION,
				true
			);
			wp_localize_script( 'datastream-settings', 'datastreamSettings', $this->get_settings_js_config() );

			wp_enqueue_script(
				'datastream-streams',
				plugins_url( 'assets/js/datastream-streams.js', DATASTREAM_PLUGIN_FILE ),
				array( 'wp-api-fetch' ),
				DATASTREAM_VERSION,
				true
			);

			wp_enqueue_script(
				'datastream-wizard',
				plugins_url( 'assets/js/datastream-wizard.js', DATASTREAM_PLUGIN_FILE ),
				array( 'wp-api-fetch' ),
				DATASTREAM_VERSION,
				true
			);

			wp_enqueue_script(
				'datastream-admin-form',
				plugins_url( 'assets/js/datastream-admin-form.js', DATASTREAM_PLUGIN_FILE ),
				array(),
				DATASTREAM_VERSION,
				true
			);
		}
	}

	/**
	 * Dashboard JS labels.
	 *
	 * @return array<string, mixed>
	 */
	private function get_js_labels() {
		return array(
			'settingsUrl' => admin_url( 'admin.php?page=datastream-settings&tab=streams' ),
			'labels' => array(
				'justNow'      => __( 'just now', 'datastream-api-sync' ),
				/* translators: prefixed by a number, e.g. "5 min ago" */
				'minAgo'       => __( ' min ago', 'datastream-api-sync' ),
				/* translators: prefixed by a number, e.g. "3 h ago" */
				'hourAgo'      => __( ' h ago', 'datastream-api-sync' ),
				/* translators: prefixed by a number, e.g. "2 d ago" */
				'dayAgo'       => __( ' d ago', 'datastream-api-sync' ),
				'updated'      => __( 'updated', 'datastream-api-sync' ),
				'paused'       => __( 'Paused — not syncing.', 'datastream-api-sync' ),
				'failing'      => __( 'The last sync failed.', 'datastream-api-sync' ),
				'retryAt'      => __( 'Retrying at', 'datastream-api-sync' ),
				'browse'       => __( 'Browse records', 'datastream-api-sync' ),
				'browseAll'    => __( 'Show all records', 'datastream-api-sync' ),
				'edit'         => __( 'Edit', 'datastream-api-sync' ),
				'syncAll'      => __( 'Sync all now', 'datastream-api-sync' ),
				'working'      => __( 'Working…', 'datastream-api-sync' ),
				'heroTitle'    => __( 'Connect your first API', 'datastream-api-sync' ),
				'heroBody'     => __( 'Paste an address, and your data appears on your site.', 'datastream-api-sync' ),
				'heroCta'      => __( 'Open the Connect Wizard', 'datastream-api-sync' ),
				'recordsHeading' => __( 'Records', 'datastream-api-sync' ),
				'stream'       => __( 'Stream', 'datastream-api-sync' ),
				'updatedCol'   => __( 'Updated', 'datastream-api-sync' ),
				'payload'      => __( 'Record', 'datastream-api-sync' ),
				'loading'      => __( 'Loading records…', 'datastream-api-sync' ),
				'empty'        => __( 'No records yet. Configure an API endpoint in Settings, then run a sync.', 'datastream-api-sync' ),
				'error'        => __( 'Could not load records. Check your API URL and error log.', 'datastream-api-sync' ),
				'syncSuccess'  => __( 'Sync completed successfully.', 'datastream-api-sync' ),
				'syncFailed'   => __( 'Sync failed. See error log for details.', 'datastream-api-sync' ),
				'records'      => __( 'Records', 'datastream-api-sync' ),
				'lastSync'     => __( 'Last Sync', 'datastream-api-sync' ),
				'nextSync'     => __( 'Next Scheduled Sync', 'datastream-api-sync' ),
				'refresh'      => __( 'Refresh', 'datastream-api-sync' ),
				'syncNow'      => __( 'Sync Now', 'datastream-api-sync' ),
				'viewPayload'  => __( 'View Payload', 'datastream-api-sync' ),
				'hidePayload'  => __( 'Hide Payload', 'datastream-api-sync' ),
				'never'        => __( 'Never', 'datastream-api-sync' ),
				'notScheduled' => __( 'Not scheduled', 'datastream-api-sync' ),
				'streams'      => __( 'Streams', 'datastream-api-sync' ),
				'auditLogs'    => __( 'Audit Logs', 'datastream-api-sync' ),
				'telemetry'    => __( 'Sensor Telemetry', 'datastream-api-sync' ),
				'generic'      => __( 'Generic', 'datastream-api-sync' ),
				'healthy'      => __( 'System Healthy', 'datastream-api-sync' ),
				'warnings'     => __( 'Warnings', 'datastream-api-sync' ),
				'cronDisabled' => __( 'Cron Disabled', 'datastream-api-sync' ),
			),
		);
	}

	/**
	 * Settings page JS config.
	 *
	 * @return array<string, mixed>
	 */
	private function get_settings_js_config() {
		$health = DataStream_Health::get_report();

		return array(
			'health'   => $health,
			'labels'   => array(
				'testing'       => __( 'Testing connection…', 'datastream-api-sync' ),
				'testBtn'       => __( 'Test Connection', 'datastream-api-sync' ),
				'syncNow'       => __( 'Run Sync Now', 'datastream-api-sync' ),
				'repairCron'    => __( 'Repair Cron Schedule', 'datastream-api-sync' ),
				'repairSchema'  => __( 'Repair Database Schema', 'datastream-api-sync' ),
				'flushCache'    => __( 'Flush All Caches', 'datastream-api-sync' ),
				'clearLock'     => __( 'Clear Sync Lock', 'datastream-api-sync' ),
				'purgeStale'    => __( 'Purge Stale Records', 'datastream-api-sync' ),
				'purgeAll'      => __( 'Purge All Data', 'datastream-api-sync' ),
				'confirmPurge'  => __( 'Type DELETE to confirm purging all ingested data.', 'datastream-api-sync' ),
				'working'       => __( 'Working…', 'datastream-api-sync' ),
				'done'          => __( 'Done.', 'datastream-api-sync' ),
				'failed'        => __( 'Action failed.', 'datastream-api-sync' ),
			),
			'restBase' => esc_url_raw( rest_url( 'datastream/v1' ) ),
		);
	}

	/**
	 * Render dashboard page.
	 */
	public function render_dashboard_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		include DATASTREAM_PLUGIN_DIR . 'admin/views/dashboard.php';
	}

	/**
	 * Render settings page.
	 */
	public function render_settings_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		include DATASTREAM_PLUGIN_DIR . 'admin/views/settings.php';
	}

	/* -- Section callbacks -- */

	public function section_connection() {
		echo '<p>' . esc_html__( 'Configure how DataStream connects to your external API.', 'datastream-api-sync' ) . '</p>';
		if ( DataStream_Config::reject_unsafe_urls() ) {
			echo '<p class="description">' . esc_html__( 'Requests to private and local network addresses are blocked by default. Define DATASTREAM_ALLOW_PRIVATE_URLS as true in wp-config.php to allow intranet APIs.', 'datastream-api-sync' ) . '</p>';
		}
	}

	public function section_mapping() {
		echo '<p>' . esc_html__( 'Tell DataStream where the records live in the API response and which fields identify them. Field names are case-sensitive.', 'datastream-api-sync' ) . '</p>';
	}

	public function section_sync() {
		echo '<p>' . esc_html__( 'Control the background sync schedule and data lifecycle.', 'datastream-api-sync' ) . '</p>';
	}

	public function section_limits() {
		echo '<p>' . esc_html__( 'Hard limits protect the database from malformed or oversized payloads.', 'datastream-api-sync' ) . '</p>';
	}

	public function section_cache() {
		echo '<p>' . esc_html__( 'Transient caching keeps frontend reads fast without hitting the database.', 'datastream-api-sync' ) . '</p>';
	}

	public function section_logging() {
		echo '<p>' . esc_html__( 'Diagnostic logging writes to PHP error_log only — never to the admin UI.', 'datastream-api-sync' ) . '</p>';
	}

	/* -- Field callbacks -- */

	public function field_api_url() {
		$value = get_option( 'datastream_api_url', '' );
		printf(
			'<input type="url" class="regular-text" name="datastream_api_url" value="%s" placeholder="https://api.example.com/v1/data" />',
			esc_attr( $value )
		);
		echo '<p class="description">' . esc_html__( 'Leave empty to disable remote fetch (useful for manual-only workflows).', 'datastream-api-sync' ) . '</p>';
		echo '<p><button type="button" class="button" id="datastream-test-connection">' . esc_html__( 'Test Connection', 'datastream-api-sync' ) . '</button> <span id="datastream-test-result" aria-live="polite"></span></p>';
	}

	public function field_api_method() {
		$value = get_option( 'datastream_api_method', 'GET' );
		echo '<select name="datastream_api_method">';
		foreach ( array( 'GET' => 'GET', 'POST' => 'POST' ) as $slug => $label ) {
			printf( '<option value="%s" %s>%s</option>', esc_attr( $slug ), selected( $value, $slug, false ), esc_html( $label ) );
		}
		echo '</select>';
	}

	public function field_bearer_token() {
		if ( defined( 'DATASTREAM_API_BEARER_TOKEN' ) && '' !== (string) DATASTREAM_API_BEARER_TOKEN ) {
			echo '<p><code>DATASTREAM_API_BEARER_TOKEN</code> ' . esc_html__( 'is defined in wp-config.php and overrides this field. The token stays out of the database.', 'datastream-api-sync' ) . '</p>';
			return;
		}

		$has_token = '' !== (string) get_option( 'datastream_api_bearer_token', '' );
		$display   = $has_token ? '********' : '';
		printf(
			'<input type="password" class="regular-text" name="datastream_api_bearer_token" value="%s" autocomplete="new-password" placeholder="%s" />',
			esc_attr( $display ),
			esc_attr__( 'Optional bearer token', 'datastream-api-sync' )
		);
		echo '<p class="description">' . esc_html__( 'Leave unchanged to keep the existing token. Sent as Authorization: Bearer header. Stored in the WordPress options table; to keep it out of the database, define DATASTREAM_API_BEARER_TOKEN in wp-config.php instead.', 'datastream-api-sync' ) . '</p>';
	}

	public function field_headers() {
		$value = get_option( 'datastream_api_headers', '' );
		printf(
			'<textarea name="datastream_api_headers" rows="4" cols="50" class="large-text code" placeholder="%s">%s</textarea>',
			esc_attr( '{"X-Api-Key":"your-key"}' ),
			esc_textarea( $value )
		);
	}

	public function field_schedule() {
		$value = get_option( 'datastream_cron_schedule', 'hourly' );
		echo '<select name="datastream_cron_schedule">';
		foreach ( DataStream_Config::cron_schedules() as $slug => $label ) {
			printf( '<option value="%s" %s>%s</option>', esc_attr( $slug ), selected( $value, $slug, false ), esc_html( $label ) );
		}
		echo '</select>';
	}

	public function field_log_level() {
		$value = get_option( 'datastream_log_level', 'error' );
		$levels = array(
			'error' => __( 'Errors only', 'datastream-api-sync' ),
			'info'  => __( 'Info + Errors', 'datastream-api-sync' ),
			'debug' => __( 'Debug (verbose)', 'datastream-api-sync' ),
		);
		echo '<select name="datastream_log_level">';
		foreach ( $levels as $slug => $label ) {
			printf( '<option value="%s" %s>%s</option>', esc_attr( $slug ), selected( $value, $slug, false ), esc_html( $label ) );
		}
		echo '</select>';
	}

	/**
	 * Generic text field renderer.
	 *
	 * @param array<string, string> $args Field args.
	 */
	public function field_text( $args ) {
		$option = $args['option'];
		$value  = get_option( $option, '' );
		printf(
			'<input type="text" class="regular-text" name="%1$s" value="%2$s" placeholder="%3$s" />',
			esc_attr( $option ),
			esc_attr( $value ),
			esc_attr( $args['placeholder'] ?? '' )
		);
		if ( ! empty( $args['description'] ) ) {
			printf( '<p class="description">%s</p>', esc_html( $args['description'] ) );
		}
	}

	/**
	 * Generic number field renderer.
	 *
	 * @param array<string, mixed> $args Field args.
	 */
	public function field_number( $args ) {
		$option = $args['option'];
		$value  = get_option( $option, 0 );
		printf(
			'<input type="number" class="small-text" name="%1$s" value="%2$d" min="%3$d" max="%4$d" />',
			esc_attr( $option ),
			(int) $value,
			(int) ( $args['min'] ?? 0 ),
			(int) ( $args['max'] ?? 99999 )
		);
		if ( ! empty( $args['description'] ) ) {
			printf( '<p class="description">%s</p>', esc_html( $args['description'] ) );
		}
	}

	/**
	 * Generic checkbox field renderer.
	 *
	 * @param array<string, string> $args Field args.
	 */
	public function field_checkbox( $args ) {
		$option = $args['option'];
		$value  = (bool) get_option( $option, false );
		printf( '<input type="hidden" name="%s" value="0" />', esc_attr( $option ) );
		printf(
			'<label><input type="checkbox" name="%1$s" value="1" %2$s /> %3$s</label>',
			esc_attr( $option ),
			checked( $value, true, false ),
			esc_html( $args['label'] ?? '' )
		);
	}
}
