<?php
/**
 * Plugin Name: LMN Site Monitor
 * Plugin URI:  https://lumina.fmkr.net/site-monitor
 * Description: Monitor websites from the WordPress admin: HTTP/HTTPS pings, SSL expiry, and email alerts.
 * Version:     0.8.1
 * Author:      Francisco Moreno
 * Author URI:  https://fcomoreno.net
 * License:     GPL-2.0-or-later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: lmn-site-monitor
 * Domain Path: /languages
 *
 * @package LMN_Site_Monitor
 * @author  Francisco Moreno
 * @license GPL-2.0-or-later
 * @since   0.8.1 New languages: Arabic, German, Basque, Italian, Japanese, Korean, Dutch, Portuguese, Turkish, Ukrainian, and Simplified Chinese;
 *          Automatic retry after downtime to avoid false positives;
 * 			Domain expiry notifications;
 *          i18n maintained; PHPCS/WPCS compliant.
 */

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

class LMNSIMO_Site_Monitor {

	private $option_sites       = 'lmnsimo_sites';
	private $option_settings    = 'lmnsimo_settings';
	private $option_last_alerts = 'lmnsimo_last_alerts';
	private $option_order       = 'lmnsimo_order'; // persisted card order (array of IDs).

	const CRON_HOOK = 'lmnsimo_check_sites_cron';
	const MENU_SLUG = 'lmn-site-monitor';
	const VERSION   = '0.8.1';

	public function __construct() {
		// Admin UI.
		add_action( 'admin_menu', array( $this, 'admin_menu' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_assets' ) );

		// Form handlers (admin-post).
		add_action( 'admin_post_lmnsimo_add_site',          array( $this, 'handle_add_site' ) );
		add_action( 'admin_post_lmnsimo_delete_site',       array( $this, 'handle_delete_site' ) );
		add_action( 'admin_post_lmnsimo_force_check',       array( $this, 'handle_force_check' ) );
		add_action( 'admin_post_lmnsimo_save_settings',     array( $this, 'handle_save_settings' ) );
		add_action( 'admin_post_lmnsimo_check_all',         array( $this, 'handle_check_all' ) );
		add_action( 'admin_post_lmnsimo_save_widget_prefs', array( $this, 'handle_save_widget_prefs' ) );
		add_action( 'admin_post_lmnsimo_export_csv',        array( $this, 'handle_export_csv' ) );
		add_action( 'admin_post_lmnsimo_import_csv',        array( $this, 'handle_import_csv' ) );
		add_action( 'admin_post_lmnsimo_bulk_action',       array( $this, 'handle_bulk_action' ) );

		// AJAX handlers (logged-in, manage_options checked inside).
		add_action( 'wp_ajax_lmnsimo_save_order',  array( $this, 'ajax_save_order' ) );
		add_action( 'wp_ajax_lmnsimo_toggle_pin',  array( $this, 'ajax_toggle_pin' ) );
		add_action( 'wp_ajax_lmnsimo_whois',       array( $this, 'ajax_whois' ) );

		// Settings & Cron.
		add_action( 'admin_init', array( $this, 'maybe_boot_defaults' ) );
		add_filter( 'cron_schedules', array( $this, 'add_cron_intervals' ) );
		add_action( self::CRON_HOOK, array( $this, 'check_all_sites_cron' ) );
		add_action( 'phpmailer_init', array( $this, 'apply_smtp' ) );
		add_action( 'wp_mail_succeeded', array( $this, 'handle_wp_mail_succeeded' ) );
		add_action( 'wp_mail_failed',    array( $this, 'handle_wp_mail_failed' ) );
		add_action( 'admin_notices',     array( $this, 'admin_notice_mail_failed' ) );

		register_activation_hook( __FILE__, array( $this, 'activate' ) );
		register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );

		add_action( 'wp_dashboard_setup', array( $this, 'dashboard_widget' ) );
		add_action( 'plugins_loaded', array( $this, 'maybe_load_textdomain' ), 0 );
	}

	/* ============================================================
	   ACTIVATION / DEFAULTS
	   ============================================================ */

	public function activate() {
		$this->maybe_boot_defaults();
		$settings = get_option( $this->option_settings, array() );
		$interval = isset( $settings['interval'] ) ? $settings['interval'] : 'fivemin';
		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_event( time() + 60, $interval, self::CRON_HOOK );
		}
	}

	public function deactivate() {
		wp_clear_scheduled_hook( self::CRON_HOOK );
	}

	public function maybe_boot_defaults() {
		$defaults = array(
			'admin_email'               => get_option( 'admin_email' ),
			'interval'                  => 'fivemin',
			'days_before_expiry'        => 5,
			'days_before_domain_expiry' => 30,
			'throttle_hours'            => 12,
			'retry_count'               => 2,
			'default_theme'             => 'system',
			'use_local_font'            => 0,
			'allow_insecure_pings'      => 0,
		);
		$cur = get_option( $this->option_settings );
		if ( ! $cur ) {
			add_option( $this->option_settings, $defaults );
		} else {
			update_option( $this->option_settings, array_merge( $defaults, (array) $cur ) );
		}
	}

	/* ============================================================
	   ASSETS
	   ============================================================ */

	public function admin_assets( $hook ) {
		$is_our_page = ( 'toplevel_page_' . self::MENU_SLUG === $hook );
		if ( ! $is_our_page && 'index.php' !== $hook ) {
			return;
		}

		$ver = self::VERSION;

		// Main stylesheet.
		wp_enqueue_style(
			'lmnsimo-theme',
			plugins_url( 'assets/admin-theme.css', __FILE__ ),
			array(),
			$ver
		);

		// JetBrains Mono — bundled in plugin/assets/fonts/, no external CDN (GDPR safe).
		wp_enqueue_style(
			'lmnsimo-jetbrains-mono',
			plugins_url( 'assets/fonts/jetbrains-mono/jetbrains-mono.css', __FILE__ ),
			array(),
			$ver
		);

		$settings = get_option( $this->option_settings, array() );

		wp_enqueue_script(
			'lmnsimo-js',
			plugins_url( 'assets/admin-theme.js', __FILE__ ),
			array(),
			$ver,
			true
		);

		// Pass config + nonces to JS.
		wp_add_inline_script(
			'lmnsimo-js',
			'window.LM_MONITOR_SETTINGS = ' . wp_json_encode(
				array(
					'defaultTheme'    => isset( $settings['default_theme'] ) ? $settings['default_theme'] : 'system',
					'useLocalFont'    => (int) ( isset( $settings['use_local_font'] ) ? $settings['use_local_font'] : 0 ),
					'fontsBaseUrl'    => plugins_url( 'assets/fonts/', __FILE__ ),
					'ajaxUrl'         => admin_url( 'admin-ajax.php' ),
					'nonceSaveOrder'  => wp_create_nonce( 'lmnsimo_save_order' ),
					'nonceTogglePin'  => wp_create_nonce( 'lmnsimo_toggle_pin' ),
					'nonceWhois'      => wp_create_nonce( 'lmnsimo_whois' ),
					'i18n'            => array(
						'pinned'     => __( 'Pinned', 'lmn-site-monitor' ),
						'pin'        => __( 'Pin', 'lmn-site-monitor' ),
						'unpin'      => __( 'Unpin', 'lmn-site-monitor' ),
						'loading'    => __( 'Loading…', 'lmn-site-monitor' ),
						'error'      => __( 'Error retrieving WHOIS data.', 'lmn-site-monitor' ),
						'whoisTitle' => __( 'WHOIS / DNS', 'lmn-site-monitor' ),
						'close'      => __( 'Close', 'lmn-site-monitor' ),
						'noData'     => __( 'No data available.', 'lmn-site-monitor' ),
					),
				)
			) . ';',
			'before'
		);

		wp_add_inline_style( 'lmnsimo-theme', $this->get_inline_css() );
	}

	/**
	 * Supplemental inline CSS (small additions that don't need their own file).
	 *
	 * @return string
	 */
	private function get_inline_css() {
		return '
/* === Tabs === */
.lm-tabs{display:flex;gap:6px;margin:12px 0 8px;border-bottom:1px solid #ccd0d4;flex-wrap:wrap}
.lm-tabs a{padding:6px 10px;text-decoration:none;color:var(--wp-admin-theme-color,#2271b1);border:1px solid transparent;border-bottom:none;border-radius:4px 4px 0 0}
.lm-tabs a.is-active{background:var(--wp-admin-theme-color,#2271b1);color:#fff;border-color:var(--wp-admin-theme-color,#2271b1)}
.lm-tabpanel{display:none}
.lm-tabpanel.is-visible{display:block}

/* === Responsive grid === */
.lm-grid{display:grid;grid-template-columns:1fr;gap:16px}
@media(min-width:1024px){.lm-grid{grid-template-columns:1fr 320px}}

/* === Badges === */
.lm-badge{display:inline-block;padding:.2em .5em;border-radius:999px;font-weight:600}
.lm-badge.ok{background:#e7f7ed;color:#0a6d2a}
.lm-badge.down{background:#ff0000;color:#fff}
.lm-days.warn{font-weight:700;color:#ff3c00}

/* === Accordion === */
details.lm-accordion{border:1px solid #ccd0d4;border-radius:6px;padding:.5rem .75rem;background:#fff}
details.lm-accordion>summary{cursor:pointer;outline:none}
details.lm-accordion .lm-acc-body{margin-top:.5rem}
.lm-scope.lm-dark details.lm-accordion{border:1px solid #193a5b;background:#000}

/* === Lumina banner === */
.lm-banner-lumina{display:none}
@media(min-width:1025px){
  .lm-grid{position:relative}
  .lm-banner-lumina{display:block;position:absolute;right:30px;top:10px;width:260px;z-index:5}
  .lm-banner-lumina img{display:block;width:100%;height:auto;border-radius:12px;box-shadow:var(--lm-shadow,0 8px 24px rgba(0,0,0,.1))}
}

/* === Changelog === */
.lm-changelog-body{padding:16px 20px;max-height:60vh;overflow:auto;background:var(--lm-surface,#fff);border-radius:8px}
.lm-changelog-list{margin:.4em 0 .8em 1.2em}
.lm-changelog-list li{margin:.2em 0}
.lm-changelog-hr{border:0;height:1px;background:var(--lm-border,#e5e7eb);margin:.8em 0}
.lm-changelog-body code{padding:.1em .3em;border-radius:4px;background:rgba(0,0,0,.06);font-family:Consolas,monospace}

/* === Dashboard widget === */
.lm-kpis{display:flex;gap:8px;margin-bottom:8px}
.lm-kpis .k{flex:1 1 0;background:var(--lm-surface);border:1px solid var(--lm-border);border-radius:10px;padding:8px;text-align:center;color:var(--lm-text);box-shadow:var(--lm-shadow)}
.lm-mini{width:100%;border-collapse:collapse}
.lm-mini td{padding:6px 8px;border-bottom:1px solid var(--lm-border);color:var(--lm-text)}
.lm-mini .dot{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:6px}
.lm-mini .dot.ok{background:#16a34a}
.lm-mini .dot.down{background:#dc2626}
.lm-mini .right{text-align:right;color:var(--lm-muted)}
.lm-widget-pref{display:flex;gap:6px;align-items:center;justify-content:flex-end;margin:6px 0}
.lm-widget-pref label{font-size:12px;color:var(--lm-muted)}
.lm-widget-pref select{min-width:74px}
.lm-widget-pref .button{padding:2px 8px;line-height:1.6;height:auto}

/* === WHOIS modal === */
#lm-whois-modal{display:none;position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.55);align-items:center;justify-content:center}
#lm-whois-modal.is-open{display:flex}
.lm-whois-dialog{background:var(--lm-surface,#fff);border:1px solid var(--lm-border,#e5e7eb);border-radius:16px;padding:24px 28px;max-width:540px;width:95%;box-shadow:0 20px 60px rgba(0,0,0,.25);position:relative;max-height:80vh;overflow-y:auto}
.lm-whois-dialog h3{margin:0 0 16px;font-size:15px;font-weight:700;color:var(--lm-text,#111)}
.lm-whois-table{width:100%;border-collapse:collapse;font-size:13px}
.lm-whois-table td{padding:7px 10px;border-bottom:1px solid var(--lm-border,#e5e7eb);color:var(--lm-text,#111);vertical-align:top}
.lm-whois-table td:first-child{font-weight:600;color:var(--lm-muted,#6b7280);width:150px;white-space:nowrap}
.lm-whois-close{position:absolute;top:14px;right:16px;background:none;border:none;cursor:pointer;font-size:20px;color:var(--lm-muted,#6b7280);line-height:1;padding:4px}
.lm-scope.lm-dark .lm-whois-dialog{background:#0f141b;border-color:#202733}
.lm-whois-raw{margin-top:12px;padding:10px 12px;background:rgba(0,0,0,.04);border-radius:8px;font-family:"JetBrains Mono",monospace;font-size:11px;line-height:1.5;max-height:180px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;color:var(--lm-muted,#6b7280)}
.lm-scope.lm-dark .lm-whois-raw{background:rgba(255,255,255,.04)}

/* === Drag handle === */
.lm-drag-handle{cursor:grab;color:var(--lm-muted,#6b7280);opacity:.45;transition:opacity .15s;touch-action:none;padding:4px 2px;border-radius:4px;display:inline-flex;align-items:center;flex-shrink:0}
.lm-drag-handle:hover{opacity:1;background:rgba(0,0,0,.05)}
.lm-drag-handle:active{cursor:grabbing}
.lm-card-item.lm-dragging{opacity:.35;border:2px dashed var(--lm-accent,#3b82f6)!important;box-shadow:none!important}
.lm-card-item.lm-drag-over{outline:2px solid var(--lm-accent,#3b82f6);outline-offset:3px}
.is-pinned .lm-drag-handle{cursor:not-allowed;opacity:.2;pointer-events:none}

/* === Pin button & badge === */
.lm-pin-btn{background:none;border:none;cursor:pointer;padding:4px 5px;border-radius:6px;color:var(--lm-muted,#6b7280);transition:color .15s,background .15s;font-size:13px;line-height:1;display:inline-flex;align-items:center;gap:3px;flex-shrink:0}
.lm-pin-btn:hover{background:rgba(0,0,0,.06);color:var(--lm-text,#111)}
.lm-pin-btn.is-pinned{color:#f59e0b}
.lm-pin-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}
.lm-card-item.is-pinned{border-left:3px solid #f59e0b!important}

/* === Help tooltip === */
.lm-help{position:relative;display:inline-flex;align-items:center;vertical-align:middle;margin-left:5px}
.lm-help-icon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--lm-muted,#6b7280);color:#fff;font-size:10px;font-weight:700;line-height:1;cursor:default;flex-shrink:0;font-style:normal}
.lm-help-tip{display:none;position:absolute;left:50%;bottom:calc(100% + 7px);transform:translateX(-50%);background:#1e293b;color:#f1f5f9;font-size:12px;line-height:1.5;font-weight:400;padding:8px 11px;border-radius:8px;width:260px;white-space:normal;box-shadow:0 4px 16px rgba(0,0,0,.25);z-index:9999;pointer-events:none}
.lm-help-tip::after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-top-color:#1e293b}
.lm-help:hover .lm-help-tip,.lm-help:focus-within .lm-help-tip{display:block}
.lm-scope.lm-dark .lm-help-icon{background:#4b5563}
.lm-scope.lm-dark .lm-help-tip{background:#0f172a;color:#e2e8f0}
.lm-scope.lm-dark .lm-help-tip::after{border-top-color:#0f172a}
';
	}

	/* ============================================================
	   CRON INTERVALS
	   ============================================================ */

	public function add_cron_intervals( $schedules ) {
		$schedules['fivemin']    = array( 'interval' => 300,   'display' => __( 'Every 5 Minutes', 'lmn-site-monitor' ) );
		$schedules['fifteenmin'] = array( 'interval' => 900,   'display' => __( 'Every 15 Minutes', 'lmn-site-monitor' ) );
		if ( ! isset( $schedules['hourly'] ) ) {
			$schedules['hourly']  = array( 'interval' => 3600,  'display' => __( 'Hourly', 'lmn-site-monitor' ) );
		}
		if ( ! isset( $schedules['daily'] ) ) {
			$schedules['daily']   = array( 'interval' => 86400, 'display' => __( 'Daily', 'lmn-site-monitor' ) );
		}
		return $schedules;
	}

	/* ============================================================
	   ADMIN MENU
	   ============================================================ */

	public function admin_menu() {
		add_menu_page(
			__( 'Site Monitor', 'lmn-site-monitor' ),
			__( 'Site Monitor', 'lmn-site-monitor' ),
			'manage_options',
			self::MENU_SLUG,
			array( $this, 'admin_page' ),
			'dashicons-networking',
			60
		);
	}

	/* ============================================================
	   SORTED SITES HELPER
	   ============================================================ */

	/**
	 * Returns $sites sorted: pinned first (in stored order), then unpinned (in stored order).
	 * Pinned cards are excluded from drag-and-drop and bulk sort operations.
	 *
	 * @param array $sites Raw sites option.
	 * @return array
	 */
	private function get_sorted_sites( array $sites ) {
		$order = get_option( $this->option_order, array() );

		$pinned   = array();
		$unpinned = array();

		foreach ( $sites as $id => $s ) {
			if ( ! empty( $s['pinned'] ) ) {
				$pinned[ $id ] = $s;
			} else {
				$unpinned[ $id ] = $s;
			}
		}

		if ( ! empty( $order ) ) {
			$order_flip = array_flip( $order );

			$sort_fn = function ( $arr ) use ( $order_flip ) {
				uksort(
					$arr,
					function ( $a, $b ) use ( $order_flip ) {
						$ia = isset( $order_flip[ $a ] ) ? $order_flip[ $a ] : PHP_INT_MAX;
						$ib = isset( $order_flip[ $b ] ) ? $order_flip[ $b ] : PHP_INT_MAX;
						return $ia - $ib;
					}
				);
				return $arr;
			};

			$pinned   = $sort_fn( $pinned );
			$unpinned = $sort_fn( $unpinned );
		}

		return $pinned + $unpinned;
	}

	/* ============================================================
	   HEADER TOOLBAR
	   ============================================================ */

	private function header_toolbar() {
		$intro_title = esc_html__( 'LMN Site Monitor is a status monitor for websites.', 'lmn-site-monitor' );
		$intro_desc  = esc_html__( 'Monitor multiple sites from the WordPress admin: periodic HTTP/HTTPS pings, SSL certificate expiry checks, email alerts, a Dashboard widget, and CSV export/import.', 'lmn-site-monitor' );
		/* translators: Shown as helper tip in a bulleted list. */
		$tip1 = esc_html__( 'Add the URLs to monitor using the “Add URL” field. You can back up your list by exporting to CSV.', 'lmn-site-monitor' );
		/* translators: Shown as helper tip in a bulleted list. */
		$tip2 = esc_html__( 'You can import your CSV file into a new installation of the plugin.', 'lmn-site-monitor' );
		/* translators: Shown as helper tip in a bulleted list. */
		$tip3 = esc_html__( 'Remember to configure a valid SMTP if your WordPress does not already have one.', 'lmn-site-monitor' );
		/* translators: Shown as helper tip in a bulleted list. */
		$tip4 = esc_html__( 'In the plugin settings you can add an external SMTP service to send notifications.', 'lmn-site-monitor' );

		$tt_light  = esc_attr__( 'Light theme', 'lmn-site-monitor' );
		$tt_dark   = esc_attr__( 'Dark theme', 'lmn-site-monitor' );
		$tt_system = esc_attr__( 'Follow system', 'lmn-site-monitor' );

		echo '
	<div class="lm-toolbar">
		<div class="lm-description postbox lm-card">
			' . esc_html( $intro_title ) . ' ' . esc_html( $intro_desc ) . '
			<ul>
				<li>' . esc_html( $tip1 ) . '</li>
				<li>' . esc_html( $tip2 ) . '</li>
				<li>' . esc_html( $tip3 ) . '</li>
				<li>' . esc_html( $tip4 ) . '</li>
			</ul>
		</div>
		<div class="lm-theme-toggle">
			<button type="button" class="button lm-theme-btn" data-mode="light"  title="' . esc_attr( $tt_light ) . '">&#9728;&#65039;</button>
			<button type="button" class="button lm-theme-btn" data-mode="dark"   title="' . esc_attr( $tt_dark ) . '">&#127769;</button>
			<button type="button" class="button lm-theme-btn" data-mode="system" title="' . esc_attr( $tt_system ) . '">&#128421;&#65039;</button>
		</div>
	</div>';
	}

	/* ============================================================
	   ADMIN PAGE
	   ============================================================ */

	public function admin_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}

		$sites    = get_option( $this->option_sites, array() );
		$settings = get_option( $this->option_settings, array() );

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- benign admin notice.
		$msg = isset( $_GET['msg'] ) ? sanitize_text_field( wp_unslash( $_GET['msg'] ) ) : '';

		$interval_map = array(
			'fivemin'    => __( 'Every 5 Minutes', 'lmn-site-monitor' ),
			'fifteenmin' => __( 'Every 15 Minutes', 'lmn-site-monitor' ),
			'hourly'     => __( 'Hourly', 'lmn-site-monitor' ),
			'daily'      => __( 'Daily', 'lmn-site-monitor' ),
		);

		$current_interval = isset( $settings['interval'] ) ? $settings['interval'] : 'fivemin';
		$interval_lbl     = isset( $interval_map[ $current_interval ] ) ? $interval_map[ $current_interval ] : $current_interval;
		$next_run_ts      = wp_next_scheduled( self::CRON_HOOK );
		/* translators: Date/time format for scheduled cron run. */
		$next_run = $next_run_ts ? date_i18n( 'Y-m-d H:i:s', $next_run_ts ) : '—';
		$cron_off = ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON )
			? __( 'Yes', 'lmn-site-monitor' )
			: __( 'No', 'lmn-site-monitor' );

		$ok               = 0;
		$down             = 0;
		$exp_soon         = 0;
		$days_warn        = (int) ( isset( $settings['days_before_expiry'] ) ? $settings['days_before_expiry'] : 5 );
		$days_warn_domain = (int) ( isset( $settings['days_before_domain_expiry'] ) ? $settings['days_before_domain_expiry'] : 30 );

		foreach ( $sites as $s ) {
			$status = isset( $s['last_status'] ) ? $s['last_status'] : '';
			if ( 'down' === $status ) {
				++$down;
			} else {
				++$ok;
			}
			if ( ! empty( $s['ssl_expires'] ) ) {
				$days_left = (int) ceil( ( $s['ssl_expires'] - time() ) / 86400 );
				if ( $days_left <= $days_warn ) {
					++$exp_soon;
				}
			}
			if ( ! empty( $s['domain_expires'] ) ) {
				$domain_left = (int) ceil( ( $s['domain_expires'] - time() ) / 86400 );
				if ( $domain_left <= $days_warn_domain ) {
					++$exp_soon;
				}
			}
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view state only.
		$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'sites';

		$sorted_sites = $this->get_sorted_sites( $sites );
		?>
<div class="wrap lm-scope lmn-myheader">
	<h1>LMN Site Monitor <span class="lmn-version-badge">v.&nbsp;<?php echo esc_html( self::VERSION ); ?></span></h1>
	<?php $this->header_toolbar(); ?>
	<?php if ( $msg ) : ?>
	<div class="notice notice-success is-dismissible">
		<p><?php echo esc_html( $msg ); ?></p>
	</div>
	<?php endif; ?>

	<nav class="lm-tabs" aria-label="<?php echo esc_attr__( 'Sections', 'lmn-site-monitor' ); ?>">
		<a href="<?php echo esc_url( add_query_arg( 'tab', 'sites' ) ); ?>" class="<?php echo ( 'sites' === $tab ) ? 'is-active' : ''; ?>">
			<?php esc_html_e( 'Sites', 'lmn-site-monitor' ); ?>
		</a>
		<a href="<?php echo esc_url( add_query_arg( 'tab', 'config' ) ); ?>" class="<?php echo ( 'config' === $tab ) ? 'is-active' : ''; ?>">
			<?php esc_html_e( 'Settings', 'lmn-site-monitor' ); ?>
		</a>
		<a href="<?php echo esc_url( add_query_arg( 'tab', 'changelog' ) ); ?>" class="<?php echo ( 'changelog' === $tab ) ? 'is-active' : ''; ?>">
			<?php esc_html_e( 'Changelog', 'lmn-site-monitor' ); ?>
		</a>
	</nav>

	<!-- ========================================================
	     PANEL: SITES
	     ======================================================== -->
	<section class="lm-tabpanel <?php echo ( 'sites' === $tab ) ? 'is-visible' : ''; ?>" id="tab-sites">
		<div class="lm-grid">

			<aside class="lm-banner-lumina" aria-label="<?php echo esc_attr__( 'Lúmina – Accessibility checker', 'lmn-site-monitor' ); ?>">
				<a href="https://lumina.fmkr.net/" target="_blank" rel="noopener noreferrer">
					<img src="<?php echo esc_url( plugins_url( 'assets/banner-lumina.webp', __FILE__ ) ); ?>"
						alt="<?php echo esc_attr__( 'Check your site for accessibility – WCAG 2.1/2.2 (Lúmina)', 'lmn-site-monitor' ); ?>"
						title="<?php echo esc_attr__( 'Check your site for accessibility – WCAG 2.1/2.2 (Lúmina)', 'lmn-site-monitor' ); ?>"
						loading="lazy" decoding="async" />
				</a>
			</aside>

			<div class="metabox-holder lm-main">
				<div class="postbox lm-card">
					<h2 class="hndle"><span><?php esc_html_e( 'Monitored sites', 'lmn-site-monitor' ); ?></span></h2>
					<div class="inside">

						<!-- Add URL -->
						<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="lm-row">
							<?php wp_nonce_field( 'lmnsimo_add_site', 'lmnsimo_add_site_nonce' ); ?>
							<input type="hidden" name="action" value="lmnsimo_add_site">
							<p class="lm-grow">
								<label for="lm_url"><?php esc_html_e( 'Add URL', 'lmn-site-monitor' ); ?></label>
								<input id="lm_url" name="url" type="url" class="regular-text" placeholder="https://example.com" required>
							</p>
							<?php submit_button( __( 'Add', 'lmn-site-monitor' ), 'primary', 'lm_add_submit', false, array( 'id' => 'lm-add' ) ); ?>
						</form>

						<div class="lm-cards-wrap">

							<!-- Bulk bar -->
							<form id="lm-bulk-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="lm-bulkbar">
								<?php wp_nonce_field( 'lmnsimo_bulk_action', 'lmnsimo_bulk_nonce' ); ?>
								<input type="hidden" name="action" value="lmnsimo_bulk_action">
								<select name="bulk_action" aria-label="<?php echo esc_attr__( 'Bulk actions', 'lmn-site-monitor' ); ?>">
									<option value=""><?php esc_html_e( 'Bulk actions', 'lmn-site-monitor' ); ?></option>
									<option value="delete"><?php esc_html_e( 'Delete selected', 'lmn-site-monitor' ); ?></option>
									<option value="sort_alpha"><?php esc_html_e( 'Sort A → Z', 'lmn-site-monitor' ); ?></option>
									<option value="sort_date"><?php esc_html_e( 'Sort by date added', 'lmn-site-monitor' ); ?></option>
								</select>
								<?php submit_button( __( 'Apply', 'lmn-site-monitor' ), 'secondary', 'lm_bulk_apply', false, array( 'id' => 'lm-bulk-apply' ) ); ?>
							</form>

							<!-- Sortable cards -->
							<div class="lm-cards" id="lm-cards-sortable" role="list">

								<?php if ( empty( $sorted_sites ) ) : ?>
								<div class="lm-card-item lm-empty" role="listitem">
									<p><?php esc_html_e( 'No sites configured.', 'lmn-site-monitor' ); ?></p>
								</div>
								<?php else : ?>

								<?php foreach ( $sorted_sites as $id => $s ) :
									$checked   = isset( $s['last_checked'] ) ? date_i18n( 'Y-m-d H:i:s', $s['last_checked'] ) : '—';
									$status    = isset( $s['last_status'] ) ? $s['last_status'] : '';
									$http      = isset( $s['http_code'] ) ? $s['http_code'] : '—';
									$sslD      = isset( $s['ssl_expires'] ) ? (int) ceil( ( $s['ssl_expires'] - time() ) / 86400 ) : null;
									$sslF      = isset( $s['ssl_expires'] ) ? date_i18n( 'Y-m-d H:i', $s['ssl_expires'] ) : '—';
									$is_pinned = ! empty( $s['pinned'] );

									$badge = '—';
									if ( null !== $sslD ) {
										$days_label = sprintf(
											/* translators: %d: number of days until SSL certificate expiry */
											_n( '%d day', '%d days', $sslD, 'lmn-site-monitor' ),
											$sslD
										);
										$severity = ( $sslD <= $days_warn ? 'down' : ( $sslD <= ( $days_warn * 3 ) ? 'warn' : '' ) );
										$badge    = '<span class="lm-days ' . esc_attr( $severity ) . '">' . esc_html( $days_label ) . '</span>';
									}
								?>
								<article class="lm-card-item<?php echo $is_pinned ? ' is-pinned' : ''; ?>"
										role="listitem"
										data-id="<?php echo esc_attr( $id ); ?>"
										data-status="<?php echo esc_attr( $status ? $status : 'unknown' ); ?>"
										data-pinned="<?php echo $is_pinned ? '1' : '0'; ?>">

									<header class="lm-card-item__head">
										<!-- Drag handle (disabled for pinned) -->
										<span class="lm-drag-handle" aria-label="<?php echo esc_attr__( 'Drag to reorder', 'lmn-site-monitor' ); ?>" role="img" aria-hidden="true">
											<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
												<circle cx="3.5" cy="3" r="1.5"/><circle cx="8.5" cy="3" r="1.5"/>
												<circle cx="3.5" cy="8" r="1.5"/><circle cx="8.5" cy="8" r="1.5"/>
												<circle cx="3.5" cy="13" r="1.5"/><circle cx="8.5" cy="13" r="1.5"/>
											</svg>
										</span>

										<!-- Checkbox -->
										<label class="lm-card-check" aria-label="<?php echo esc_attr__( 'Select', 'lmn-site-monitor' ); ?>">
											<input class="lm-bulk-cb" type="checkbox" name="ids[]" form="lm-bulk-form" value="<?php echo esc_attr( $id ); ?>">
										</label>

										<!-- URL -->
										<div class="lm-card-url">
											<a href="<?php echo esc_url( $s['url'] ); ?>" target="_blank" rel="noreferrer"><?php echo esc_html( $s['url'] ); ?></a>
										</div>

										<!-- Status badge -->
										<div class="lm-card-state">
											<?php if ( 'down' === $status ) : ?>
											<span class="lm-badge down"><?php esc_html_e( 'Down', 'lmn-site-monitor' ); ?></span>
											<?php elseif ( 'ok' === $status ) : ?>
											<span class="lm-badge ok">OK</span>
											<?php else : ?>
											—
											<?php endif; ?>
										</div>

										<!-- Pin button -->
										<button type="button"
											class="lm-pin-btn<?php echo $is_pinned ? ' is-pinned' : ''; ?>"
											data-id="<?php echo esc_attr( $id ); ?>"
											aria-pressed="<?php echo $is_pinned ? 'true' : 'false'; ?>"
											title="<?php echo $is_pinned ? esc_attr__( 'Unpin', 'lmn-site-monitor' ) : esc_attr__( 'Pin to top', 'lmn-site-monitor' ); ?>">
											<?php if ( $is_pinned ) : ?>
											<svg width="13" height="13" viewBox="0 0 24 24" fill="#f59e0b" aria-hidden="true"><path d="M16 3H8v8L4 15h7v6h2v-6h7l-4-4V3z"/></svg>
											<span class="lm-pin-label"><?php esc_html_e( 'Pinned', 'lmn-site-monitor' ); ?></span>
											<?php else : ?>
											<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M16 3H8v8L4 15h7v6h2v-6h7l-4-4V3z"/></svg>
											<?php endif; ?>
										</button>
									</header>

									<?php
									$domainD   = isset( $s['domain_expires'] ) ? (int) ceil( ( $s['domain_expires'] - time() ) / 86400 ) : null;
									$domainF   = isset( $s['domain_expires'] ) ? date_i18n( 'Y-m-d', $s['domain_expires'] ) : '—';
									$dom_badge = '—';
									if ( null !== $domainD ) {
										$dom_label    = sprintf(
											/* translators: %d: number of days until domain expiry */
											_n( '%d day', '%d days', $domainD, 'lmn-site-monitor' ),
											$domainD
										);
										$dom_severity = ( $domainD <= $days_warn_domain ? 'down' : ( $domainD <= ( $days_warn_domain * 2 ) ? 'warn' : '' ) );
										$dom_badge    = '<span class="lm-days ' . esc_attr( $dom_severity ) . '">' . esc_html( $dom_label ) . '</span>';
									}
									?>
									<dl class="lm-card-meta">
										<div>
											<dt><?php esc_html_e( 'Last check', 'lmn-site-monitor' ); ?></dt>
											<dd><?php echo esc_html( $checked ); ?></dd>
										</div>
										<div>
											<dt>HTTP</dt>
											<dd><?php echo esc_html( (string) $http ); ?></dd>
										</div>
										<div>
											<dt><?php esc_html_e( 'SSL expires', 'lmn-site-monitor' ); ?></dt>
											<dd><?php echo esc_html( $sslF ); ?></dd>
										</div>
										<div>
											<dt><?php esc_html_e( 'SSL days', 'lmn-site-monitor' ); ?></dt>
											<dd><?php echo wp_kses_post( $badge ); ?></dd>
										</div>
										<div>
											<dt><?php esc_html_e( 'Domain expires', 'lmn-site-monitor' ); ?></dt>
											<dd><?php echo esc_html( $domainF ); ?></dd>
										</div>
										<div>
											<dt><?php esc_html_e( 'Domain days', 'lmn-site-monitor' ); ?></dt>
											<dd><?php echo wp_kses_post( $dom_badge ); ?></dd>
										</div>
									</dl>

									<footer class="lm-card-actions">
										<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="lm-inline">
											<?php wp_nonce_field( 'lm_force_check_' . $id, 'lmnsimo_force_nonce_' . $id ); ?>
											<input type="hidden" name="action" value="lmnsimo_force_check">
											<input type="hidden" name="site_id" value="<?php echo esc_attr( $id ); ?>">
											<?php submit_button( __( 'Force', 'lmn-site-monitor' ), '', 'lm_force_' . $id, false ); ?>
										</form>
										<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="lm-inline"
											onsubmit="return confirm('<?php echo esc_js( __( 'Delete site?', 'lmn-site-monitor' ) ); ?>');">
											<?php wp_nonce_field( 'lm_delete_site_' . $id, 'lmnsimo_delete_nonce_' . $id ); ?>
											<input type="hidden" name="action" value="lmnsimo_delete_site">
											<input type="hidden" name="site_id" value="<?php echo esc_attr( $id ); ?>">
											<?php submit_button( __( 'Delete', 'lmn-site-monitor' ), 'secondary', 'lm_delete_' . $id, false ); ?>
										</form>
										<button type="button" class="button lm-whois-btn"
											data-url="<?php echo esc_attr( $s['url'] ); ?>"
											aria-label="<?php echo esc_attr__( 'WHOIS / DNS lookup', 'lmn-site-monitor' ); ?>">
											WHOIS
										</button>
									</footer>
								</article>
								<?php endforeach; ?>
								<?php endif; ?>

							</div><!-- .lm-cards -->
						</div><!-- .lm-cards-wrap -->

						<!-- Check all -->
						<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-top:12px;">
							<?php wp_nonce_field( 'lmnsimo_check_all', 'lmnsimo_check_all_nonce_a' ); ?>
							<input type="hidden" name="action" value="lmnsimo_check_all">
							<?php submit_button( __( 'Check all now', 'lmn-site-monitor' ), 'primary', 'lm_check_all_submit_top', false, array( 'id' => 'lm-check-all-top' ) ); ?>
						</form>

						<!-- Import / Export -->
						<div class="lm-inline-actions" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;">
							<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
								<?php wp_nonce_field( 'lmnsimo_export_csv', 'lmnsimo_export_nonce' ); ?>
								<input type="hidden" name="action" value="lmnsimo_export_csv">
								<?php submit_button( __( 'Export CSV', 'lmn-site-monitor' ), 'secondary', 'lm_export_submit', false ); ?>
							</form>
							<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
								<?php wp_nonce_field( 'lmnsimo_import_csv', 'lmnsimo_import_nonce' ); ?>
								<input type="hidden" name="action" value="lmnsimo_import_csv">
								<input type="file" name="csv_file" accept=".csv" required>
								<?php submit_button( __( 'Import CSV', 'lmn-site-monitor' ), 'secondary', 'lm_import_submit', false ); ?>
							</form>
						</div>

					</div><!-- .inside -->
				</div><!-- .postbox -->
			</div><!-- .metabox-holder -->
		</div><!-- .lm-grid -->
	</section>

	<!-- ========================================================
	     PANEL: SETTINGS
	     ======================================================== -->
	<section class="lm-tabpanel <?php echo ( 'config' === $tab ) ? 'is-visible' : ''; ?>" id="tab-config">
		<div class="metabox-holder lm-main" style="max-width:900px">
			<div class="postbox lm-card">
				<h2 class="hndle"><span><?php esc_html_e( 'Settings', 'lmn-site-monitor' ); ?></span></h2>
				<div class="inside">
					<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
						<?php wp_nonce_field( 'lmnsimo_save_settings', 'lmnsimo_settings_nonce' ); ?>
						<input type="hidden" name="action" value="lmnsimo_save_settings">
						<table class="form-table">
							<tr>
								<th><label for="lm_mail"><?php esc_html_e( 'Alert email', 'lmn-site-monitor' ); ?></label></th>
								<td><input id="lm_mail" name="admin_email" type="email" value="<?php echo esc_attr( isset( $settings['admin_email'] ) ? $settings['admin_email'] : get_option( 'admin_email' ) ); ?>" class="regular-text"></td>
							</tr>
							<tr>
								<th><label for="lm_interval"><?php esc_html_e( 'Cron interval', 'lmn-site-monitor' ); ?></label></th>
								<td>
									<select id="lm_interval" name="interval">
										<option value="fivemin"    <?php selected( 'fivemin',    isset( $settings['interval'] ) ? $settings['interval'] : '' ); ?>><?php esc_html_e( 'Every 5 Minutes', 'lmn-site-monitor' ); ?></option>
										<option value="fifteenmin" <?php selected( 'fifteenmin', isset( $settings['interval'] ) ? $settings['interval'] : '' ); ?>><?php esc_html_e( 'Every 15 Minutes', 'lmn-site-monitor' ); ?></option>
										<option value="hourly"     <?php selected( 'hourly',     isset( $settings['interval'] ) ? $settings['interval'] : '' ); ?>><?php esc_html_e( 'Hourly', 'lmn-site-monitor' ); ?></option>
										<option value="daily"      <?php selected( 'daily',      isset( $settings['interval'] ) ? $settings['interval'] : '' ); ?>><?php esc_html_e( 'Daily', 'lmn-site-monitor' ); ?></option>
									</select>
								</td>
							</tr>
							<tr>
								<th><label for="lm_days"><?php esc_html_e( 'SSL expiry warning (days)', 'lmn-site-monitor' ); ?></label></th>
								<td><input id="lm_days" name="days_before_expiry" type="number" min="1" value="<?php echo esc_attr( isset( $settings['days_before_expiry'] ) ? $settings['days_before_expiry'] : 15 ); ?>"></td>
							</tr>
							<tr>
								<th><label for="lm_domain_days"><?php esc_html_e( 'Domain expiry warning (days)', 'lmn-site-monitor' ); ?></label></th>
								<td>
									<input id="lm_domain_days" name="days_before_domain_expiry" type="number" min="1" value="<?php echo esc_attr( isset( $settings['days_before_domain_expiry'] ) ? $settings['days_before_domain_expiry'] : 30 ); ?>">
									<span class="description"><?php esc_html_e( 'Default: 30. Domain expiry is checked via WHOIS once per day.', 'lmn-site-monitor' ); ?></span>
								</td>
							</tr>
							<tr>
								<th><label for="lm_thr"><?php esc_html_e( 'Alert resend (hours)', 'lmn-site-monitor' ); ?></label></th>
								<td><input id="lm_thr" name="throttle_hours" type="number" min="1" value="<?php echo esc_attr( isset( $settings['throttle_hours'] ) ? $settings['throttle_hours'] : 3 ); ?>"></td>
							</tr>
							<tr>
								<th>
									<label for="lm_retry"><?php esc_html_e( 'Retries on failure', 'lmn-site-monitor' ); ?></label>
									<span class="lm-help" aria-hidden="true">
										<i class="lm-help-icon">?</i>
										<span class="lm-help-tip"><?php esc_html_e( 'When a site appears to be down, the plugin will wait 10 seconds and re-check it up to this many times before sending an alert. This prevents false positives caused by brief network glitches. Set to 0 to alert immediately on the first failure.', 'lmn-site-monitor' ); ?></span>
									</span>
								</th>
								<td>
									<input id="lm_retry" name="retry_count" type="number" min="0" max="5" value="<?php echo esc_attr( isset( $settings['retry_count'] ) ? $settings['retry_count'] : 2 ); ?>">
									<span class="description"><?php esc_html_e( '0 = disabled · max 5 · 10 s between each retry', 'lmn-site-monitor' ); ?></span>
								</td>
							</tr>
							<tr>
								<th><label for="lm_theme"><?php esc_html_e( 'Default theme', 'lmn-site-monitor' ); ?></label></th>
								<td>
									<select id="lm_theme" name="default_theme">
										<option value="system" <?php selected( 'system', isset( $settings['default_theme'] ) ? $settings['default_theme'] : 'system' ); ?>><?php esc_html_e( 'System', 'lmn-site-monitor' ); ?></option>
										<option value="light"  <?php selected( 'light',  isset( $settings['default_theme'] ) ? $settings['default_theme'] : 'system' ); ?>><?php esc_html_e( 'Light', 'lmn-site-monitor' ); ?></option>
										<option value="dark"   <?php selected( 'dark',   isset( $settings['default_theme'] ) ? $settings['default_theme'] : 'system' ); ?>><?php esc_html_e( 'Dark', 'lmn-site-monitor' ); ?></option>
									</select>
								</td>
							</tr>
							<tr>
								<th><label for="lm_allow_insec"><?php esc_html_e( 'Allow pings without SSL verification', 'lmn-site-monitor' ); ?></label></th>
								<td>
									<label>
										<input id="lm_allow_insec" type="checkbox" name="allow_insecure_pings" value="1" <?php checked( ! empty( $settings['allow_insecure_pings'] ) ); ?>>
										<span class="description"><?php esc_html_e( 'For diagnostics only. By default, pings verify SSL.', 'lmn-site-monitor' ); ?></span>
									</label>
								</td>
							</tr>
						</table>

						<details class="lm-accordion">
							<summary><strong><?php esc_html_e( 'SMTP (optional)', 'lmn-site-monitor' ); ?></strong></summary>
							<div class="lm-acc-body">
								<table class="form-table">
									<tr>
										<th><?php esc_html_e( 'Enable SMTP', 'lmn-site-monitor' ); ?></th>
										<td><label><input type="checkbox" name="smtp_enabled" value="1" <?php checked( ! empty( $settings['smtp_enabled'] ) ); ?>><?php esc_html_e( 'Use SMTP for LMN Site Monitor emails', 'lmn-site-monitor' ); ?></label></td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'Server (host)', 'lmn-site-monitor' ); ?></th>
										<td><input type="text" name="smtp_host" value="<?php echo esc_attr( isset( $settings['smtp_host'] ) ? $settings['smtp_host'] : '' ); ?>" class="regular-text"></td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'Port', 'lmn-site-monitor' ); ?></th>
										<td>
											<input type="number" name="smtp_port" value="<?php echo esc_attr( (int) ( isset( $settings['smtp_port'] ) ? $settings['smtp_port'] : 587 ) ); ?>" class="small-text">
											<span class="description"><?php esc_html_e( '587 (TLS) or 465 (SSL), depending on your provider.', 'lmn-site-monitor' ); ?></span>
										</td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'Encryption', 'lmn-site-monitor' ); ?></th>
										<td>
											<select name="smtp_secure">
												<option value=""    <?php selected( ( isset( $settings['smtp_secure'] ) ? $settings['smtp_secure'] : '' ) === '' ); ?>><?php esc_html_e( 'None', 'lmn-site-monitor' ); ?></option>
												<option value="tls" <?php selected( ( isset( $settings['smtp_secure'] ) ? $settings['smtp_secure'] : '' ) === 'tls' ); ?>><?php esc_html_e( 'TLS', 'lmn-site-monitor' ); ?></option>
												<option value="ssl" <?php selected( ( isset( $settings['smtp_secure'] ) ? $settings['smtp_secure'] : '' ) === 'ssl' ); ?>><?php esc_html_e( 'SSL', 'lmn-site-monitor' ); ?></option>
											</select>
										</td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'Authentication', 'lmn-site-monitor' ); ?></th>
										<td><label><input type="checkbox" name="smtp_auth" value="1" <?php checked( ! empty( $settings['smtp_auth'] ) ); ?>><?php esc_html_e( 'Requires username and password (recommended)', 'lmn-site-monitor' ); ?></label></td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'Username', 'lmn-site-monitor' ); ?></th>
										<td><input type="text" name="smtp_username" value="<?php echo esc_attr( isset( $settings['smtp_username'] ) ? $settings['smtp_username'] : '' ); ?>" class="regular-text"></td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'Password', 'lmn-site-monitor' ); ?></th>
										<td>
											<input type="password" name="smtp_password" value="" placeholder="••••••" class="regular-text">
											<br><span class="description"><?php esc_html_e( 'Leave blank to keep the stored password.', 'lmn-site-monitor' ); ?></span>
										</td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'From email', 'lmn-site-monitor' ); ?></th>
										<td><input type="email" name="smtp_from_email" value="<?php echo esc_attr( isset( $settings['smtp_from_email'] ) ? $settings['smtp_from_email'] : '' ); ?>" class="regular-text"></td>
									</tr>
									<tr>
										<th><?php esc_html_e( 'From name', 'lmn-site-monitor' ); ?></th>
										<td><input type="text" name="smtp_from_name" value="<?php echo esc_attr( isset( $settings['smtp_from_name'] ) ? $settings['smtp_from_name'] : 'LMN Site Monitor' ); ?>" class="regular-text"></td>
									</tr>
								</table>
							</div>
						</details>
						<?php submit_button( __( 'Save settings', 'lmn-site-monitor' ), 'primary', 'lmn_settings_submit', false, array( 'id' => 'lm-settings-save' ) ); ?>
					</form>
				</div>
			</div>

			<div class="postbox lm-card">
				<h2 class="hndle"><span><?php esc_html_e( 'About LMN Site Monitor', 'lmn-site-monitor' ); ?></span></h2>
				<div class="inside">
					<table class="form-table">
						<tr>
							<th><?php esc_html_e( 'Monitoring:', 'lmn-site-monitor' ); ?></th>
							<td>
								<?php esc_html_e( 'Sites:', 'lmn-site-monitor' ); ?> <strong><?php echo (int) count( $sites ); ?></strong> &middot;
								<?php esc_html_e( 'OK:', 'lmn-site-monitor' ); ?> <strong><?php echo (int) $ok; ?></strong> &middot;
								<?php esc_html_e( 'Down:', 'lmn-site-monitor' ); ?> <strong><?php echo (int) $down; ?></strong> &middot;
								<?php esc_html_e( 'SSL expiring soon:', 'lmn-site-monitor' ); ?> <strong><?php echo (int) $exp_soon; ?></strong>
							</td>
						</tr>
						<tr>
							<th><?php esc_html_e( 'Cron interval:', 'lmn-site-monitor' ); ?></th>
							<td><?php echo esc_html( $interval_lbl ); ?></td>
						</tr>
						<tr>
							<th><?php esc_html_e( 'Next run:', 'lmn-site-monitor' ); ?></th>
							<td><?php echo esc_html( $next_run ); ?></td>
						</tr>
						<tr>
							<th><?php esc_html_e( 'WP-Cron disabled:', 'lmn-site-monitor' ); ?></th>
							<td><?php echo esc_html( $cron_off ); ?></td>
						</tr>
						<tr>
							<th><?php esc_html_e( 'Version:', 'lmn-site-monitor' ); ?></th>
							<td><?php echo esc_html( self::VERSION ); ?></td>
						</tr>
						<tr>
							<th><?php esc_html_e( 'Developed by:', 'lmn-site-monitor' ); ?></th>
							<td><?php esc_html_e( 'Francisco Moreno Sánchez-Aguililla', 'lmn-site-monitor' ); ?></td>
						</tr>
						<tr>
							<th><?php esc_html_e( 'Author website:', 'lmn-site-monitor' ); ?></th>
							<td><a href="https://www.fcomoreno.net" target="_blank" rel="noreferrer">www.fcomoreno.net</a></td>
						</tr>
						<tr>
							<th><?php esc_html_e( 'Donations:', 'lmn-site-monitor' ); ?></th>
							<td>
								<a href="<?php echo esc_url( 'https://www.paypal.com/donate/?business=FTTYSUMRSPWKN&no_recurring=0&currency_code=EUR&amount=5.00' ); ?>"
									title="<?php echo esc_attr__( 'Would you like to make a donation?', 'lmn-site-monitor' ); ?>"
									target="_blank" rel="noreferrer">
									<img src="<?php echo esc_url( plugins_url( 'assets/donate.svg', __FILE__ ) ); ?>"
										alt="<?php echo esc_attr__( 'Make a donation', 'lmn-site-monitor' ); ?>"
										style="height:35px">
								</a>
							</td>
						</tr>
					</table>
					<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-top:8px;">
						<?php wp_nonce_field( 'lmnsimo_check_all', 'lmnsimo_check_all_nonce_b' ); ?>
						<input type="hidden" name="action" value="lmnsimo_check_all">
						<?php submit_button( __( 'Check all now', 'lmn-site-monitor' ), 'secondary', 'lm_check_all_submit_bottom', false, array( 'id' => 'lm-check-all-bottom' ) ); ?>
					</form>
				</div>
			</div>
		</div>
	</section>

	<!-- ========================================================
	     PANEL: CHANGELOG
	     ======================================================== -->
	<section class="lm-tabpanel <?php echo ( 'changelog' === $tab ) ? 'is-visible' : ''; ?>" id="tab-changelog">
		<div class="lm-card">
			<div class="lm-card__head">
				<h2 class="hndle" style="margin:0;"><?php esc_html_e( 'Changelog', 'lmn-site-monitor' ); ?></h2>
			</div>
			<div class="lm-card__body lm-changelog-body">
				<?php $this->render_changelog(); ?>
			</div>
		</div>
	</section>
</div><!-- .wrap -->

<!-- WHOIS modal (outside .wrap so z-index works fine) -->
<div id="lm-whois-modal" role="dialog" aria-modal="true" aria-labelledby="lm-whois-title">
	<div class="lm-whois-dialog">
		<button type="button" class="lm-whois-close" id="lm-whois-close" aria-label="<?php echo esc_attr__( 'Close', 'lmn-site-monitor' ); ?>">&#10005;</button>
		<h3 id="lm-whois-title"><?php esc_html_e( 'WHOIS / DNS', 'lmn-site-monitor' ); ?></h3>
		<div id="lm-whois-content"></div>
	</div>
</div>
		<?php
	}

	/* ============================================================
	   CHANGELOG RENDERER
	   ============================================================ */

	private function render_changelog() {
		$path = plugin_dir_path( __FILE__ ) . 'changelog.txt';
		if ( ! file_exists( $path ) ) {
			echo '<p>' . esc_html__( 'The changelog.txt file was not found or is empty.', 'lmn-site-monitor' ) . '</p>';
			return;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file.
		$raw = file_get_contents( $path );
		if ( false === $raw || '' === trim( (string) $raw ) ) {
			echo '<p>' . esc_html__( 'The changelog.txt file was not found or is empty.', 'lmn-site-monitor' ) . '</p>';
			return;
		}
		$text    = str_replace( array( "\r\n", "\r" ), "\n", $raw );
		$text    = esc_html( $text );
		$text    = preg_replace( '/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $text );
		$text    = preg_replace( '/`([^`]+)`/s', '<code>$1</code>', $text );
		$lines   = explode( "\n", $text );
		$html    = '';
		$in_list = false;

		foreach ( $lines as $line ) {
			$trim = ltrim( $line );
			if ( preg_match( '/^#{3}\s*(.+)$/', $trim, $mm ) ) {
				if ( $in_list ) { $html .= '</ul>'; $in_list = false; }
				$html .= '<h4 style="margin:1em 0 .5em">' . $mm[1] . '</h4>';
				continue;
			} elseif ( preg_match( '/^#{2}\s*(.+)$/', $trim, $mm ) ) {
				if ( $in_list ) { $html .= '</ul>'; $in_list = false; }
				$html .= '<h3 style="margin:1.2em 0 .6em">' . $mm[1] . '</h3>';
				continue;
			} elseif ( preg_match( '/^#\s*(.+)$/', $trim, $mm ) ) {
				if ( $in_list ) { $html .= '</ul>'; $in_list = false; }
				$html .= '<h2 style="margin:1.2em 0 .6em">' . $mm[1] . '</h2>';
				continue;
			}
			if ( preg_match( '/^-\s+(.+)$/', $trim, $mm ) ) {
				if ( ! $in_list ) { $html .= '<ul class="lm-changelog-list">'; $in_list = true; }
				$item = preg_replace( '/^(NEW|FIX|IMPROVE|CHANGE|SECURITY):\s*/i', '<strong>$1:</strong> ', $mm[1] );
				$html .= '<li>' . $item . '</li>';
				continue;
			} else {
				if ( $in_list ) { $html .= '</ul>'; $in_list = false; }
			}
			if ( preg_match( '/^(=|-){3,}\s*$/', $trim ) ) {
				$html .= '<hr class="lm-changelog-hr">';
				continue;
			}
			if ( '' === $trim ) {
				$html .= '<p style="margin:.6em 0"></p>';
				continue;
			}
			$html .= '<p>' . $trim . '</p>';
		}
		if ( $in_list ) { $html .= '</ul>'; }
		echo wp_kses_post( $html );
	}

	/* ============================================================
	   FORM HANDLERS
	   ============================================================ */

	public function handle_add_site() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		check_admin_referer( 'lmnsimo_add_site', 'lmnsimo_add_site_nonce' );

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized next line.
		$raw_url = isset( $_POST['url'] ) ? wp_unslash( $_POST['url'] ) : '';
		$url     = '' !== $raw_url ? esc_url_raw( trim( (string) $raw_url ) ) : '';

		if ( ! $url ) {
			$msg = rawurlencode( __( 'Invalid URL', 'lmn-site-monitor' ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}

		$sites = get_option( $this->option_sites, array() );
		$id    = md5( (string) $url );

		if ( ! isset( $sites[ $id ] ) ) {
			$sites[ $id ] = array(
				'id'         => $id,
				'url'        => $url,
				'added_time' => time(),
			);
		} else {
			$sites[ $id ] = array_merge( array( 'id' => $id, 'url' => $url ), $sites[ $id ] );
		}
		update_option( $this->option_sites, $sites );

		$msg = rawurlencode( __( 'Site added', 'lmn-site-monitor' ) );
		wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
		exit;
	}

	public function handle_delete_site() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		$id = sanitize_text_field( wp_unslash( isset( $_POST['site_id'] ) ? $_POST['site_id'] : '' ) );
		check_admin_referer( 'lm_delete_site_' . $id, 'lmnsimo_delete_nonce_' . $id );

		$sites = get_option( $this->option_sites, array() );
		if ( isset( $sites[ $id ] ) ) {
			unset( $sites[ $id ] );
			update_option( $this->option_sites, $sites );
		}
		$order = array_values( array_diff( get_option( $this->option_order, array() ), array( $id ) ) );
		update_option( $this->option_order, $order );

		$msg = rawurlencode( __( 'Site deleted', 'lmn-site-monitor' ) );
		wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
		exit;
	}

	public function handle_force_check() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		$id = sanitize_text_field( wp_unslash( isset( $_POST['site_id'] ) ? $_POST['site_id'] : '' ) );
		check_admin_referer( 'lm_force_check_' . $id, 'lmnsimo_force_nonce_' . $id );

		$sites = get_option( $this->option_sites, array() );
		if ( isset( $sites[ $id ] ) ) {
			$this->check_site( $id, $sites[ $id ], true );
		}
		$msg = rawurlencode( __( 'Checked', 'lmn-site-monitor' ) );
		wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
		exit;
	}

	public function handle_check_all() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		if ( isset( $_POST['lmnsimo_check_all_nonce_a'] ) ) {
			check_admin_referer( 'lmnsimo_check_all', 'lmnsimo_check_all_nonce_a' );
		} elseif ( isset( $_POST['lmnsimo_check_all_nonce_b'] ) ) {
			check_admin_referer( 'lmnsimo_check_all', 'lmnsimo_check_all_nonce_b' );
		} else {
			check_admin_referer( 'lmnsimo_check_all' );
		}
		$this->check_all_sites_cron();
		$msg = rawurlencode( __( 'Check started', 'lmn-site-monitor' ) );
		wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
		exit;
	}

	public function handle_save_settings() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		check_admin_referer( 'lmnsimo_save_settings', 'lmnsimo_settings_nonce' );

		$allowed_intervals = array( 'fivemin', 'fifteenmin', 'hourly', 'daily' );
		$old               = get_option( $this->option_settings, array() );

		$post_interval = isset( $_POST['interval'] ) ? sanitize_key( wp_unslash( $_POST['interval'] ) ) : ( isset( $old['interval'] ) ? $old['interval'] : 'hourly' );
		$interval      = in_array( $post_interval, $allowed_intervals, true ) ? $post_interval : 'hourly';

		$post_theme = isset( $_POST['default_theme'] ) ? sanitize_key( wp_unslash( $_POST['default_theme'] ) ) : 'system';
		$new_theme  = in_array( $post_theme, array( 'system', 'light', 'dark' ), true ) ? $post_theme : 'system';

		$post_smtp_secure = isset( $_POST['smtp_secure'] ) ? sanitize_key( wp_unslash( $_POST['smtp_secure'] ) ) : '';
		$new_smtp_secure  = in_array( $post_smtp_secure, array( '', 'ssl', 'tls' ), true ) ? $post_smtp_secure : '';

		$new_password = isset( $_POST['smtp_password'] )
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- password may contain any chars.
			? (string) wp_unslash( $_POST['smtp_password'] )
			: '';
		if ( '' === $new_password ) {
			$new_password = isset( $old['smtp_password'] ) ? $old['smtp_password'] : '';
		}

		$new = array(
			'admin_email'               => isset( $_POST['admin_email'] ) ? sanitize_email( wp_unslash( $_POST['admin_email'] ) ) : ( isset( $old['admin_email'] ) ? $old['admin_email'] : get_option( 'admin_email' ) ),
			'interval'                  => $interval,
			'days_before_expiry'        => max( 1, absint( isset( $_POST['days_before_expiry'] ) ? wp_unslash( $_POST['days_before_expiry'] ) : ( isset( $old['days_before_expiry'] ) ? $old['days_before_expiry'] : 15 ) ) ),
			'days_before_domain_expiry' => max( 1, absint( isset( $_POST['days_before_domain_expiry'] ) ? wp_unslash( $_POST['days_before_domain_expiry'] ) : ( isset( $old['days_before_domain_expiry'] ) ? $old['days_before_domain_expiry'] : 30 ) ) ),
			'throttle_hours'            => max( 1, absint( isset( $_POST['throttle_hours'] ) ? wp_unslash( $_POST['throttle_hours'] ) : ( isset( $old['throttle_hours'] ) ? $old['throttle_hours'] : 3 ) ) ),
			'retry_count'          => min( 5, absint( isset( $_POST['retry_count'] ) ? wp_unslash( $_POST['retry_count'] ) : ( isset( $old['retry_count'] ) ? $old['retry_count'] : 2 ) ) ),
			'default_theme'        => $new_theme,
			'use_local_font'       => ! empty( $_POST['use_local_font'] ) ? 1 : 0,
			'allow_insecure_pings' => ! empty( $_POST['allow_insecure_pings'] ) ? 1 : 0,
			'smtp_enabled'         => ! empty( $_POST['smtp_enabled'] ) ? 1 : 0,
			'smtp_host'            => isset( $_POST['smtp_host'] ) ? sanitize_text_field( wp_unslash( $_POST['smtp_host'] ) ) : ( isset( $old['smtp_host'] ) ? $old['smtp_host'] : '' ),
			'smtp_port'            => isset( $_POST['smtp_port'] ) ? absint( wp_unslash( $_POST['smtp_port'] ) ) : ( isset( $old['smtp_port'] ) ? $old['smtp_port'] : 587 ),
			'smtp_secure'          => $new_smtp_secure,
			'smtp_auth'            => ! empty( $_POST['smtp_auth'] ) ? 1 : 0,
			'smtp_username'        => isset( $_POST['smtp_username'] ) ? sanitize_text_field( wp_unslash( $_POST['smtp_username'] ) ) : ( isset( $old['smtp_username'] ) ? $old['smtp_username'] : '' ),
			'smtp_password'        => $new_password,
			'smtp_from_email'      => isset( $_POST['smtp_from_email'] ) ? sanitize_email( wp_unslash( $_POST['smtp_from_email'] ) ) : ( isset( $old['smtp_from_email'] ) ? $old['smtp_from_email'] : '' ),
			'smtp_from_name'       => isset( $_POST['smtp_from_name'] ) ? sanitize_text_field( wp_unslash( $_POST['smtp_from_name'] ) ) : ( isset( $old['smtp_from_name'] ) ? $old['smtp_from_name'] : 'LMN Site Monitor' ),
		);

		update_option( $this->option_settings, $new );

		if ( ( isset( $old['interval'] ) ? $old['interval'] : 'hourly' ) !== $new['interval'] ) {
			wp_clear_scheduled_hook( self::CRON_HOOK );
			$schedules = function_exists( 'wp_get_schedules' ) ? wp_get_schedules() : array();
			$sched     = isset( $schedules[ $new['interval'] ] ) ? $new['interval'] : 'hourly';
			wp_schedule_event( time() + 60, $sched, self::CRON_HOOK );
		}

		$msg = rawurlencode( __( 'Settings saved', 'lmn-site-monitor' ) );
		wp_safe_redirect( add_query_arg( array( 'page' => self::MENU_SLUG, 'tab' => 'config', 'msg' => $msg ), admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handle_save_widget_prefs() {
		if ( ! current_user_can( 'read' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		check_admin_referer( 'lmnsimo_save_widget_prefs', 'lmnsimo_widget_nonce' );
		$limit = isset( $_POST['lmnsimo_widget_limit'] ) ? absint( wp_unslash( $_POST['lmnsimo_widget_limit'] ) ) : 5;
		$limit = max( 1, min( 100, $limit ) );
		update_user_meta( get_current_user_id(), 'lmnsimo_widget_limit', $limit );
		wp_safe_redirect( admin_url( 'index.php?lm_widget_saved=1' ) );
		exit;
	}

	public function handle_export_csv() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		check_admin_referer( 'lmnsimo_export_csv', 'lmnsimo_export_nonce' );

		$sites    = get_option( $this->option_sites, array() );
		$sorted   = $this->get_sorted_sites( is_array( $sites ) ? $sites : array() );
		$filename = 'site-monitor-export-' . date_i18n( 'Ymd-His' ) . '.csv';

		nocache_headers();
		header( 'Content-Type: text/csv; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );

		$out = fopen( 'php://output', 'w' );
		fprintf( $out, "\xEF\xBB\xBF" );
		fputcsv( $out, array(
			/* translators: CSV column header */ __( 'URL', 'lmn-site-monitor' ),
			/* translators: CSV column header */ __( 'Last check', 'lmn-site-monitor' ),
			/* translators: CSV column header */ __( 'Status', 'lmn-site-monitor' ),
			/* translators: CSV column header */ __( 'HTTP code', 'lmn-site-monitor' ),
			/* translators: CSV column header */ __( 'SSL expires', 'lmn-site-monitor' ),
			/* translators: CSV column header */ __( 'SSL days remaining', 'lmn-site-monitor' ),
			/* translators: CSV column header */ __( 'Pinned', 'lmn-site-monitor' ),
		) );

		foreach ( $sorted as $s ) {
			$url     = isset( $s['url'] ) ? $s['url'] : '';
			$checked = ! empty( $s['last_checked'] ) ? date_i18n( 'Y-m-d H:i:s', (int) $s['last_checked'] ) : '';
			$status  = isset( $s['last_status'] ) ? $s['last_status'] : '';
			$statusL = ( 'down' === $status ? __( 'Down', 'lmn-site-monitor' ) : ( 'ok' === $status ? 'OK' : '' ) );
			$http    = isset( $s['http_code'] ) ? (string) $s['http_code'] : '';
			$sslF    = ! empty( $s['ssl_expires'] ) ? date_i18n( 'Y-m-d H:i', (int) $s['ssl_expires'] ) : '';
			$sslD    = isset( $s['ssl_expires'] ) ? (int) ceil( ( (int) $s['ssl_expires'] - time() ) / 86400 ) : '';
			$pinned  = ! empty( $s['pinned'] ) ? '1' : '0';
			fputcsv( $out, array( $url, $checked, $statusL, $http, $sslF, $sslD, $pinned ) );
		}
		exit;
	}

	public function handle_import_csv() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		check_admin_referer( 'lmnsimo_import_csv', 'lmnsimo_import_nonce' );

		if ( empty( $_FILES['csv_file'] ) || empty( $_FILES['csv_file']['tmp_name'] ) ) {
			$msg = rawurlencode( __( 'No CSV was uploaded', 'lmn-site-monitor' ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- tmp_name verified below.
		$tmp = $_FILES['csv_file']['tmp_name'];
		if ( ! empty( $_FILES['csv_file']['error'] ) && UPLOAD_ERR_OK !== (int) $_FILES['csv_file']['error'] ) {
			/* translators: %d: PHP upload error code */
			$msg = rawurlencode( sprintf( __( 'CSV upload error (%d)', 'lmn-site-monitor' ), (int) $_FILES['csv_file']['error'] ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}
		if ( ! is_uploaded_file( $tmp ) && ! file_exists( $tmp ) ) {
			$msg = rawurlencode( __( 'Could not read CSV', 'lmn-site-monitor' ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local temp file.
		$content = file_get_contents( $tmp );
		if ( false === $content || '' === $content ) {
			$msg = rawurlencode( __( 'Could not read CSV', 'lmn-site-monitor' ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}
		if ( substr( $content, 0, 3 ) === "\xEF\xBB\xBF" ) {
			$content = substr( $content, 3 );
		}
		$content = str_replace( array( "\r\n", "\r" ), "\n", $content );
		$lines   = explode( "\n", $content );

		$sample     = '';
		foreach ( $lines as $l ) { if ( '' !== trim( $l ) ) { $sample = $l; break; } }
		$best_delim = ',';
		$best_count = -1;
		foreach ( array( ',', ';', "\t" ) as $d ) {
			$c = substr_count( (string) $sample, $d );
			if ( $c > $best_count ) { $best_count = $c; $best_delim = $d; }
		}

		$sites = get_option( $this->option_sites, array() );
		if ( ! is_array( $sites ) ) { $sites = array(); }
		$added = 0;
		$seen  = array();

		foreach ( $lines as $csv_line ) {
			$csv_line = trim( (string) $csv_line );
			if ( '' === $csv_line ) { continue; }
			$row = str_getcsv( $csv_line, $best_delim );
			if ( empty( $row ) || array( null ) === $row ) { continue; }
			$url = isset( $row[0] ) ? trim( (string) $row[0] ) : '';
			if ( '' === $url || 'url' === strtolower( $url ) ) { continue; }
			$url = esc_url_raw( $url );
			if ( ! $url || isset( $seen[ $url ] ) ) { continue; }
			$seen[ $url ] = true;
			$id = md5( (string) $url );
			if ( ! isset( $sites[ $id ] ) ) {
				$sites[ $id ] = array(
					'id'         => $id,
					'url'        => $url,
					'added_time' => time(),
					'pinned'     => ( isset( $row[6] ) && '1' === trim( $row[6] ) ) ? 1 : 0,
				);
				++$added;
			}
		}
		if ( $added > 0 ) { update_option( $this->option_sites, $sites ); }

		/* translators: %d: number of imported sites */
		$msg = rawurlencode( sprintf( __( 'Imported %d', 'lmn-site-monitor' ), (int) $added ) );
		wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
		exit;
	}

	public function handle_bulk_action() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to access this page.', 'lmn-site-monitor' ) );
		}
		check_admin_referer( 'lmnsimo_bulk_action', 'lmnsimo_bulk_nonce' );

		$action = sanitize_text_field( wp_unslash( isset( $_POST['bulk_action'] ) ? $_POST['bulk_action'] : '' ) );
		$ids    = ( isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) )
			? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['ids'] ) )
			: array();

		$sites = get_option( $this->option_sites, array() );

		if ( 'delete' === $action && ! empty( $ids ) ) {
			$removed = 0;
			foreach ( $ids as $id ) {
				if ( isset( $sites[ $id ] ) ) { unset( $sites[ $id ] ); ++$removed; }
			}
			if ( $removed > 0 ) {
				update_option( $this->option_sites, $sites );
				$order = array_values( array_diff( get_option( $this->option_order, array() ), $ids ) );
				update_option( $this->option_order, $order );
			}
			/* translators: %d: number of deleted sites */
			$msg = rawurlencode( sprintf( __( 'Deleted %d', 'lmn-site-monitor' ), (int) $removed ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}

		// Sort A→Z (unpinned only).
		if ( 'sort_alpha' === $action ) {
			$pinned_ids   = array();
			$unpinned_ids = array();
			foreach ( $sites as $id => $s ) {
				if ( ! empty( $s['pinned'] ) ) { $pinned_ids[] = $id; } else { $unpinned_ids[] = $id; }
			}
			usort( $unpinned_ids, function ( $a, $b ) use ( $sites ) {
				return strcmp( isset( $sites[ $a ]['url'] ) ? $sites[ $a ]['url'] : '', isset( $sites[ $b ]['url'] ) ? $sites[ $b ]['url'] : '' );
			} );
			update_option( $this->option_order, array_merge( $pinned_ids, $unpinned_ids ) );
			$msg = rawurlencode( __( 'Sorted A→Z', 'lmn-site-monitor' ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}

		// Sort by date added (unpinned only).
		if ( 'sort_date' === $action ) {
			$pinned_ids   = array();
			$unpinned_ids = array();
			foreach ( $sites as $id => $s ) {
				if ( ! empty( $s['pinned'] ) ) { $pinned_ids[] = $id; } else { $unpinned_ids[] = $id; }
			}
			usort( $unpinned_ids, function ( $a, $b ) use ( $sites ) {
				$ta = isset( $sites[ $a ]['added_time'] ) ? (int) $sites[ $a ]['added_time'] : 0;
				$tb = isset( $sites[ $b ]['added_time'] ) ? (int) $sites[ $b ]['added_time'] : 0;
				return $ta - $tb;
			} );
			update_option( $this->option_order, array_merge( $pinned_ids, $unpinned_ids ) );
			$msg = rawurlencode( __( 'Sorted by date', 'lmn-site-monitor' ) );
			wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&msg=' . $msg ) );
			exit;
		}

		wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG ) );
		exit;
	}

	/* ============================================================
	   AJAX HANDLERS
	   ============================================================ */

	/**
	 * AJAX: persist drag-and-drop order.
	 * $_POST: { nonce, order[] }
	 */
	public function ajax_save_order() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'lmn-site-monitor' ) ), 403 );
		}
		check_ajax_referer( 'lmnsimo_save_order', 'nonce' );

		$raw_order = isset( $_POST['order'] ) && is_array( $_POST['order'] )
			? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['order'] ) )
			: array();
		if ( ! is_array( $raw_order ) ) {
			wp_send_json_error( array( 'message' => 'Invalid data.' ), 400 );
		}

		$sites     = get_option( $this->option_sites, array() );
		$valid_ids = array_keys( $sites );
		$order     = array();

		foreach ( $raw_order as $raw_id ) {
			$id = sanitize_text_field( (string) $raw_id );
			if ( in_array( $id, $valid_ids, true ) ) {
				$order[] = $id;
			}
		}
		// Append any missing IDs.
		foreach ( $valid_ids as $vid ) {
			if ( ! in_array( $vid, $order, true ) ) { $order[] = $vid; }
		}

		update_option( $this->option_order, $order );
		wp_send_json_success( array( 'saved' => true ) );
	}

	/**
	 * AJAX: toggle pin.
	 * $_POST: { nonce, id, pinned }
	 */
	public function ajax_toggle_pin() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'lmn-site-monitor' ) ), 403 );
		}
		check_ajax_referer( 'lmnsimo_toggle_pin', 'nonce' );

		$id     = sanitize_text_field( wp_unslash( isset( $_POST['id'] ) ? $_POST['id'] : '' ) );
		$pinned = isset( $_POST['pinned'] ) ? absint( wp_unslash( $_POST['pinned'] ) ) : 0;

		$sites = get_option( $this->option_sites, array() );
		if ( ! isset( $sites[ $id ] ) ) {
			wp_send_json_error( array( 'message' => 'Site not found.' ), 404 );
		}
		$sites[ $id ]['pinned'] = $pinned ? 1 : 0;
		update_option( $this->option_sites, $sites );
		wp_send_json_success( array( 'id' => $id, 'pinned' => (bool) $pinned ) );
	}

	/**
	 * AJAX: WHOIS / DNS lookup — native PHP only, no external APIs.
	 * $_POST: { nonce, url }
	 */
	public function ajax_whois() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'lmn-site-monitor' ) ), 403 );
		}
		check_ajax_referer( 'lmnsimo_whois', 'nonce' );

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized next line.
		$raw_url = isset( $_POST['url'] ) ? wp_unslash( $_POST['url'] ) : '';
		$url     = esc_url_raw( trim( (string) $raw_url ) );

		if ( ! $url ) {
			wp_send_json_error( array( 'message' => __( 'Invalid URL.', 'lmn-site-monitor' ) ), 400 );
		}

		$host = wp_parse_url( $url, PHP_URL_HOST );
		if ( ! $host ) {
			wp_send_json_error( array( 'message' => __( 'Cannot parse host.', 'lmn-site-monitor' ) ), 400 );
		}
		$host      = strtolower( (string) $host );
		$root_host = preg_replace( '/^www\./', '', $host );

		$data = array(
			'host'          => $host,
			'root'          => $root_host,
			'ip'            => array(),
			'ipv6'          => array(),
			'ns'            => array(),
			'mx'            => array(),
			'ttl'           => null,
			'ssl_expiry'    => null,
			'ssl_days'      => null,
			'domain_expiry' => null,
			'domain_days'   => null,
			'provider'      => '—',
			'whois_raw'     => '',
		);

		// A records + TTL.
		$a_rec = dns_get_record( $host, DNS_A );
		if ( is_array( $a_rec ) ) {
			foreach ( $a_rec as $r ) {
				if ( isset( $r['ip'] ) ) { $data['ip'][] = $r['ip']; }
				if ( null === $data['ttl'] && isset( $r['ttl'] ) ) { $data['ttl'] = (int) $r['ttl']; }
			}
		}

		// AAAA records.
		$aaaa_rec = dns_get_record( $host, DNS_AAAA );
		if ( is_array( $aaaa_rec ) ) {
			foreach ( $aaaa_rec as $r ) {
				if ( isset( $r['ipv6'] ) ) { $data['ipv6'][] = $r['ipv6']; }
			}
		}

		// NS records.
		$ns_rec = dns_get_record( $root_host, DNS_NS );
		if ( is_array( $ns_rec ) ) {
			foreach ( $ns_rec as $r ) {
				if ( isset( $r['target'] ) ) { $data['ns'][] = $r['target']; }
			}
		}

		// MX records.
		$mx_rec = dns_get_record( $root_host, DNS_MX );
		if ( is_array( $mx_rec ) ) {
			foreach ( $mx_rec as $r ) {
				if ( isset( $r['target'] ) ) {
					$data['mx'][] = $r['target'] . ( isset( $r['pri'] ) ? ' (pri ' . $r['pri'] . ')' : '' );
				}
			}
		}

		// SSL expiry.
		if ( 'https' === wp_parse_url( $url, PHP_URL_SCHEME ) ) {
			$ssl_ts = $this->get_ssl_expiry_timestamp( $host );
			if ( false !== $ssl_ts ) {
				$data['ssl_expiry'] = date_i18n( 'Y-m-d H:i', $ssl_ts );
				$data['ssl_days']   = (int) ceil( ( $ssl_ts - time() ) / 86400 );
			}
		}

		// WHOIS via TCP port 43.
		$data['whois_raw'] = $this->query_whois_tcp( $root_host );

		// Domain expiry from WHOIS.
		if ( $data['whois_raw'] ) {
			$domain_ts = $this->parse_whois_expiry( $data['whois_raw'] );
			if ( false !== $domain_ts ) {
				$data['domain_expiry'] = date_i18n( 'Y-m-d', $domain_ts );
				$data['domain_days']   = (int) ceil( ( $domain_ts - time() ) / 86400 );
			}
		}

		// Provider guess.
		$data['provider'] = $this->guess_provider_from_ns( $data['ns'] );

		// Dedupe.
		$data['ip']   = array_values( array_unique( $data['ip'] ) );
		$data['ipv6'] = array_values( array_unique( $data['ipv6'] ) );
		$data['ns']   = array_values( array_unique( $data['ns'] ) );
		$data['mx']   = array_values( array_unique( $data['mx'] ) );

		wp_send_json_success( $data );
	}

	/**
	 * Parse WHOIS raw response to extract domain expiry date as Unix timestamp.
	 * Best-effort: returns false if the date cannot be found or parsed.
	 *
	 * @param string $raw Raw WHOIS response text.
	 * @return int|false Unix timestamp or false.
	 */
	private function parse_whois_expiry( $raw ) {
		if ( ! $raw ) { return false; }
		$pattern = '/(?:registry expiry date|registrar registration expiration date|expir(?:y|ation) date|paid-till|expire[sd]?|valid-date|domain expir(?:y|ation)|renewal date)\s*:\s*([^\r\n]+)/i';
		if ( ! preg_match( $pattern, $raw, $m ) ) { return false; }
		$date_str = trim( $m[1] );
		if ( '' === $date_str ) { return false; }
		// phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date -- strtotime for WHOIS date parsing.
		$ts = strtotime( $date_str );
		return ( false !== $ts && $ts > 0 ) ? $ts : false;
	}

	/**
	 * Query WHOIS server via TCP port 43. Best-effort; returns '' on failure.
	 *
	 * @param string $domain Root domain.
	 * @return string
	 */
	private function query_whois_tcp( $domain ) {
		$tld        = ltrim( (string) strrchr( $domain, '.' ), '.' );
		$server_map = array(
			'com'  => 'whois.verisign-grs.com',
			'net'  => 'whois.verisign-grs.com',
			'org'  => 'whois.pir.org',
			'io'   => 'whois.nic.io',
			'es'   => 'whois.nic.es',
			'co'   => 'whois.nic.co',
			'uk'   => 'whois.nic.uk',
			'de'   => 'whois.denic.de',
			'fr'   => 'whois.nic.fr',
			'eu'   => 'whois.eu',
			'cat'  => 'whois.nic.cat',
			'info' => 'whois.afilias.net',
			'biz'  => 'whois.neulevel.biz',
			'app'  => 'whois.nic.google',
			'dev'  => 'whois.nic.google',
		);
		$server = isset( $server_map[ $tld ] ) ? $server_map[ $tld ] : 'whois.iana.org';

		$errno  = 0;
		$errstr = '';
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort network call.
		$sock = @stream_socket_client( 'tcp://' . $server . ':43', $errno, $errstr, 8, STREAM_CLIENT_CONNECT );
		if ( ! $sock ) {
			return '';
		}
		stream_set_timeout( $sock, 8 );
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- TCP socket, no WP_Filesystem equivalent.
		fwrite( $sock, $domain . "\r\n" );

		$response = '';
		while ( ! feof( $sock ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- TCP socket, no WP_Filesystem equivalent.
			$chunk = fread( $sock, 4096 );
			if ( false === $chunk ) { break; }
			$response .= $chunk;
			$meta      = stream_get_meta_data( $sock );
			if ( $meta['timed_out'] ) { break; }
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- socket handle.
		fclose( $sock );
		return $response;
	}

	/**
	 * Guess hosting/DNS provider from nameserver names.
	 *
	 * @param array $ns Array of NS hostnames.
	 * @return string
	 */
	private function guess_provider_from_ns( array $ns ) {
		if ( empty( $ns ) ) { return '—'; }
		$map = array(
			'cloudflare'   => 'Cloudflare',
			'awsdns'       => 'Amazon Web Services (Route 53)',
			'google'       => 'Google Cloud DNS',
			'azure-dns'    => 'Microsoft Azure DNS',
			'digitalocean' => 'DigitalOcean',
			'ovh'          => 'OVH',
			'siteground'   => 'SiteGround',
			'wpengine'     => 'WP Engine',
			'kinsta'       => 'Kinsta',
			'netlify'      => 'Netlify',
			'vercel'       => 'Vercel',
			'hetzner'      => 'Hetzner',
			'arsys'        => 'Arsys',
			'godaddy'      => 'GoDaddy',
			'namecheap'    => 'Namecheap',
		);
		$ns_str = strtolower( implode( ' ', $ns ) );
		foreach ( $map as $keyword => $label ) {
			if ( false !== strpos( $ns_str, $keyword ) ) { return $label; }
		}
		return $ns[0];
	}

	/* ============================================================
	   DASHBOARD WIDGET
	   ============================================================ */

	public function dashboard_widget() {
		wp_add_dashboard_widget(
			'lmnsimo_monitor_widget',
			esc_html__( 'Site Monitor Summary', 'lmn-site-monitor' ),
			array( $this, 'render_dashboard_widget' )
		);
	}

	public function render_dashboard_widget() {
		$sites       = get_option( $this->option_sites, array() );
		$settings    = get_option( $this->option_settings, array() );
		$limit       = (int) get_user_meta( get_current_user_id(), 'lmnsimo_widget_limit', true );
		if ( $limit <= 0 ) { $limit = 5; }

		$ok                 = 0;
		$down               = 0;
		$exp_soon           = 0;
		$rows               = array();
		$days_before        = (int) ( isset( $settings['days_before_expiry'] ) ? $settings['days_before_expiry'] : 5 );
		$days_before_domain = (int) ( isset( $settings['days_before_domain_expiry'] ) ? $settings['days_before_domain_expiry'] : 30 );

		foreach ( $sites as $s ) {
			$status = isset( $s['last_status'] ) ? $s['last_status'] : '';
			if ( 'down' === $status ) { ++$down; } else { ++$ok; }
			if ( ! empty( $s['ssl_expires'] ) ) {
				$d = (int) ceil( ( (int) $s['ssl_expires'] - time() ) / 86400 );
				if ( $d <= $days_before ) { ++$exp_soon; }
			}
			if ( ! empty( $s['domain_expires'] ) ) {
				$dd = (int) ceil( ( (int) $s['domain_expires'] - time() ) / 86400 );
				if ( $dd <= $days_before_domain ) { ++$exp_soon; }
			}
			$rows[] = array(
				'url'     => $s['url'],
				'status'  => $status,
				'checked' => isset( $s['last_checked'] ) ? date_i18n( 'Y-m-d H:i', (int) $s['last_checked'] ) : '—',
			);
		}
		usort( $rows, function ( $a, $b ) {
			if ( 'down' === $a['status'] && 'down' !== $b['status'] ) { return -1; }
			if ( 'down' === $b['status'] && 'down' !== $a['status'] ) { return 1; }
			return strcmp( $a['url'], $b['url'] );
		} );
		$rows = array_slice( $rows, 0, $limit );

		echo '<div class="lm-scope">';
		echo '<form class="lm-widget-pref" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		wp_nonce_field( 'lmnsimo_save_widget_prefs', 'lmnsimo_widget_nonce' );
		echo '<input type="hidden" name="action" value="lmnsimo_save_widget_prefs">';
		echo '<label for="lm_widget_limit">' . esc_html__( 'View', 'lmn-site-monitor' ) . '</label>';
		echo '<select id="lm_widget_limit" name="lm_widget_limit">';
		foreach ( array( 5, 10, 15, 20, 30, 50 ) as $opt ) {
			echo '<option value="' . esc_attr( $opt ) . '" ' . selected( $limit, $opt, false ) . '>' . esc_html( $opt ) . '</option>';
		}
		echo '</select><span style="font-size:12px;color:var(--lm-muted);margin-left:4px">' . esc_html__( 'sites', 'lmn-site-monitor' ) . '</span>';
		submit_button( __( 'Apply', 'lmn-site-monitor' ), 'secondary', 'lm_widget_apply', false );
		echo '</form>';
		echo '<div class="lm-kpis">
			<div class="k"><strong>OK</strong><div>' . (int) $ok . '</div></div>
			<div class="k"><strong>' . esc_html__( 'Down', 'lmn-site-monitor' ) . '</strong><div>' . (int) $down . '</div></div>
			<div class="k"><strong>' . esc_html__( 'SSL expiring', 'lmn-site-monitor' ) . '</strong><div>' . (int) $exp_soon . '</div></div>
		</div>';
		if ( ! empty( $rows ) ) {
			echo '<table class="lm-mini"><tbody>';
			foreach ( $rows as $r ) {
				$dot = ( 'down' === $r['status'] ) ? 'down' : 'ok';
				echo '<tr>
					<td><span class="dot ' . esc_attr( $dot ) . '"></span></td>
					<td><a href="' . esc_url( $r['url'] ) . '" target="_blank" rel="noreferrer">' . esc_html( $r['url'] ) . '</a></td>
					<td class="right">' . esc_html( $r['checked'] ) . '</td>
				</tr>';
			}
			echo '</tbody></table>';
		} else {
			echo '<p>' . esc_html__( 'No sites configured.', 'lmn-site-monitor' ) . '</p>';
		}
		echo '<p style="margin-top:8px"><a class="button button-primary" href="' . esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG ) ) . '">' . esc_html__( 'Open Site Monitor', 'lmn-site-monitor' ) . '</a></p>';
		echo '</div>';
	}

	/* ============================================================
	   CORE CHECKS
	   ============================================================ */

	public function check_all_sites_cron() {
		$sites = get_option( $this->option_sites, array() );
		foreach ( $sites as $id => $s ) {
			$this->check_site( $id, $s );
		}
	}

	/**
	 * Perform a single HTTP ping and populate $result with status/code/error.
	 *
	 * @param string $url       URL to ping.
	 * @param bool   $sslverify Whether to verify SSL.
	 * @param array  $result    Result array to populate (passed by reference).
	 */
	private function ping_url( $url, $sslverify, array &$result ) {
		$response = wp_remote_head( $url, array( 'timeout' => 15, 'redirection' => 5, 'sslverify' => $sslverify ) );
		if ( is_wp_error( $response ) || ! wp_remote_retrieve_response_code( $response ) ) {
			$response = wp_remote_get( $url, array(
				'timeout'     => 15,
				'redirection' => 5,
				'sslverify'   => $sslverify,
				'headers'     => array( 'Accept' => 'text/html,application/xhtml+xml,*/*;q=0.8' ),
			) );
		}

		if ( is_wp_error( $response ) ) {
			$result['last_status'] = 'down';
			$result['http_code']   = 0;
			$result['error_msg']   = $response->get_error_message();
		} else {
			$code                   = (int) wp_remote_retrieve_response_code( $response );
			$msg_txt                = (string) wp_remote_retrieve_response_message( $response );
			$result['http_code']    = $code;
			$result['status_label'] = trim( $code . ' ' . $msg_txt );
			if ( $code >= 400 || $code < 100 ) {
				$result['last_status'] = 'down';
				$body = wp_remote_retrieve_body( $response );
				if ( $body ) {
					$snippet             = wp_strip_all_tags( mb_substr( (string) $body, 0, 500 ) );
					$result['error_msg'] = ( isset( $result['status_label'] ) ? $result['status_label'] : '' );
					if ( '' !== $snippet ) { $result['error_msg'] .= ' — ' . $snippet; }
				}
			} else {
				$result['last_status'] = 'ok';
				unset( $result['error_msg'] );
			}
		}
	}

	public function check_site( $id, $site, $force_email = false ) {
		$url    = rtrim( (string) ( isset( $site['url'] ) ? $site['url'] : '' ), '/' );
		$result = array(
			'id'            => $id,
			'url'           => $url,
			'last_checked'  => time(),
			'last_status'   => 'ok',
			'http_code'     => null,
			'ssl_expires'   => null,
			'ssl_days_left' => null,
		);

		$settings  = get_option( $this->option_settings, array() );
		$sslverify = empty( $settings['allow_insecure_pings'] );

		$this->ping_url( $url, $sslverify, $result );

		// Retry on failure to avoid false positives.
		if ( 'down' === $result['last_status'] ) {
			$max_retries = max( 0, (int) ( isset( $settings['retry_count'] ) ? $settings['retry_count'] : 2 ) );
			for ( $attempt = 0; $attempt < $max_retries; $attempt++ ) {
				sleep( 10 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_sleep -- intentional brief wait between retries.
				$result['last_status'] = 'ok'; // reset before retry.
				$this->ping_url( $url, $sslverify, $result );
				if ( 'ok' === $result['last_status'] ) {
					break; // recovered — no false positive.
				}
			}
		}

		$parsed = wp_parse_url( $url );
		if ( 'https' === ( isset( $parsed['scheme'] ) ? $parsed['scheme'] : '' ) && ! empty( $parsed['host'] ) ) {
			$cert_ts = $this->get_ssl_expiry_timestamp( $parsed['host'] );
			if ( false !== $cert_ts ) {
				$result['ssl_expires']   = (int) $cert_ts;
				$result['ssl_days_left'] = (int) ceil( ( (int) $cert_ts - time() ) / 86400 );
			}
		}

		// Domain expiry via WHOIS — max once per 24 h to avoid rate-limiting.
		$last_domain_check = isset( $site['domain_last_checked'] ) ? (int) $site['domain_last_checked'] : 0;
		if ( time() - $last_domain_check >= 86400 ) {
			$dhost = ! empty( $parsed['host'] ) ? strtolower( (string) $parsed['host'] ) : '';
			$droot = $dhost ? preg_replace( '/^www\./', '', $dhost ) : '';
			if ( $droot ) {
				$whois_raw = $this->query_whois_tcp( $droot );
				$domain_ts = $this->parse_whois_expiry( $whois_raw );
				if ( false !== $domain_ts ) {
					$result['domain_expires']   = (int) $domain_ts;
					$result['domain_days_left'] = (int) ceil( ( (int) $domain_ts - time() ) / 86400 );
				} elseif ( ! empty( $site['domain_expires'] ) ) {
					$result['domain_expires']   = $site['domain_expires'];
					$result['domain_days_left'] = $site['domain_days_left'];
				}
				$result['domain_last_checked'] = time();
			}
		} elseif ( ! empty( $site['domain_expires'] ) ) {
			$result['domain_expires']      = $site['domain_expires'];
			$result['domain_days_left']    = $site['domain_days_left'];
			$result['domain_last_checked'] = $last_domain_check;
		}

		$prev_status = isset( $site['last_status'] ) ? $site['last_status'] : 'unknown';
		if ( 'down' === ( isset( $result['last_status'] ) ? $result['last_status'] : '' ) && 'down' !== $prev_status ) {
			$result['downtime_start'] = time();
		} elseif ( 'ok' === ( isset( $result['last_status'] ) ? $result['last_status'] : '' ) && 'down' === $prev_status ) {
			if ( ! empty( $site['downtime_start'] ) ) { $result['downtime_start'] = $site['downtime_start']; }
		}

		$sites        = get_option( $this->option_sites, array() );
		$sites[ $id ] = array_merge( $site, $result );
		update_option( $this->option_sites, $sites );
		$this->maybe_send_alert( $id, $sites[ $id ], $force_email );
	}

	private function get_ssl_expiry_timestamp( $host ) {
		if ( empty( $host ) ) { return false; }
		$context = stream_context_create( array(
			'ssl' => array(
				'capture_peer_cert' => true,
				'verify_peer'       => false,
				'verify_peer_name'  => false,
				'SNI_enabled'       => true,
				'peer_name'         => $host,
				'crypto_method'     => defined( 'STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT' )
					? STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT
					: STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
			),
		) );
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort SSL read.
		$client = @stream_socket_client( "ssl://{$host}:443", $errno, $errstr, 15, STREAM_CLIENT_CONNECT, $context );
		if ( ! $client ) { return false; }
		$params = stream_context_get_params( $client );
		if ( empty( $params['options']['ssl']['peer_certificate'] ) ) { return false; }
		$certinfo = openssl_x509_parse( $params['options']['ssl']['peer_certificate'] );
		if ( ! $certinfo || empty( $certinfo['validTo_time_t'] ) ) { return false; }
		return (int) $certinfo['validTo_time_t'];
	}

	/* ============================================================
	   EMAILS
	   ============================================================ */

	private function email_wrapper( $title, $subtitle, $pills, $list_html, array $rows, $extra, $btn_url, $btn_label, $footer ) {
		$rows_html = '';
		foreach ( $rows as $label => $value ) {
			$rows_html .= '<tr>
				<td style="padding:10px 8px;border-bottom:1px solid #e5e7eb;color:#475569;font:600 14px/1.2 system-ui;">' . $label . '</td>
				<td style="padding:10px 8px;border-bottom:1px solid #e5e7eb;color:#0f172a;font:400 14px/1.4 system-ui;">' . $value . '</td>
			</tr>';
		}
		return '<!doctype html><html><head><meta charset="utf-8"><meta name="color-scheme" content="light dark">
<style>@media(prefers-color-scheme:dark){
.mw{background:#0f141b!important;color:#e5e7eb!important}
.mc{background:#101720!important;border-color:#1f2937!important}
.mh{color:#fff!important}.mt{color:#cbd5e1!important}
.mb{background:#0e141d!important}
td{color:#e5e7eb!important;border-color:#233143!important}
.mbtn{background:#1f2937!important;color:#e5e7eb!important;border-color:#374151!important}
.mf{color:#94a3b8!important}}</style></head>
<body class="mw" style="margin:0;padding:0;background:#f1f5f9;font:400 16px/1.6 system-ui,Arial,sans-serif;">
<div style="height:24px"></div>
<div class="mc" style="max-width:680px;margin:0 auto;background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden">
<div style="padding:20px 20px 0"><h1 class="mh" style="margin:0 0 8px;font:800 20px/1.2 system-ui">' . $title . '</h1>
<p class="mt" style="margin:0 0 16px;color:#475569">' . $subtitle . '</p></div>
<div class="mb" style="background:#f8fafc;padding:16px 20px">
<div style="margin-bottom:10px">' . $pills . '</div>
<ul style="margin:0;padding-left:18px;font:400 14px/1.6 system-ui">' . $list_html . '</ul>
<table style="width:100%;border-collapse:collapse;margin-top:10px">' . $rows_html . '</table>' . $extra . '</div>
<div style="padding:14px 20px 20px"><a class="mbtn" href="' . $btn_url . '" style="display:inline-block;padding:10px 14px;border:1px solid #e5e7eb;border-radius:10px;background:#f8fafc;color:#0f172a;font:700 14px/1.2 system-ui;text-decoration:none">' . $btn_label . '</a></div>
<div class="mf" style="padding:14px 20px 20px;color:#64748b;font:400 12px/1.6 system-ui">' . $footer . '</div>
</div><div style="height:24px"></div></body></html>';
	}

	private function build_alert_email( array $siteData, array $alerts ) {
		$host      = wp_parse_url( isset( $siteData['url'] ) ? $siteData['url'] : '', PHP_URL_HOST ) ?: ( isset( $siteData['url'] ) ? $siteData['url'] : '' );
		$hasDown   = array_reduce( $alerts, function ( $c, $a ) { return (bool) ( $c || 'down' === ( isset( $a['type'] ) ? $a['type'] : '' ) ); }, false );
		$hasSSL    = array_reduce( $alerts, function ( $c, $a ) { return (bool) ( $c || 'ssl_expiring' === ( isset( $a['type'] ) ? $a['type'] : '' ) ); }, false );
		$hasDomain = array_reduce( $alerts, function ( $c, $a ) { return (bool) ( $c || 'domain_expiring' === ( isset( $a['type'] ) ? $a['type'] : '' ) ); }, false );

		if ( $hasDown ) {
			/* translators: %s: hostname */
			$subject = sprintf( __( '⛔ Site down: %s', 'lmn-site-monitor' ), $host );
		} elseif ( $hasSSL ) {
			/* translators: %s: hostname */
			$subject = sprintf( __( '⚠️ SSL expiring soon: %s', 'lmn-site-monitor' ), $host );
		} elseif ( $hasDomain ) {
			/* translators: %s: hostname */
			$subject = sprintf( __( '⚠️ Domain expiring soon: %s', 'lmn-site-monitor' ), $host );
		} else {
			/* translators: %s: hostname */
			$subject = sprintf( __( '🔔 Site notice: %s', 'lmn-site-monitor' ), $host );
		}

		$checked    = isset( $siteData['last_checked'] ) ? gmdate( 'Y-m-d H:i:s', (int) $siteData['last_checked'] ) : '—';
		$http       = isset( $siteData['http_code'] ) ? $siteData['http_code'] : '—';
		$err        = isset( $siteData['error_msg'] ) ? (string) $siteData['error_msg'] : '';
		$sslDate    = ! empty( $siteData['ssl_expires'] ) ? gmdate( 'Y-m-d H:i', (int) $siteData['ssl_expires'] ) : '—';
		$sslDays    = isset( $siteData['ssl_days_left'] ) ? $siteData['ssl_days_left'] : null;
		$domainDate = ! empty( $siteData['domain_expires'] ) ? gmdate( 'Y-m-d', (int) $siteData['domain_expires'] ) : '—';
		$domainDays = isset( $siteData['domain_days_left'] ) ? $siteData['domain_days_left'] : null;

		$settings            = get_option( $this->option_settings, array() );
		$admin_to            = isset( $settings['admin_email'] ) ? $settings['admin_email'] : get_option( 'admin_email' );
		$days_before         = (int) ( isset( $settings['days_before_expiry'] ) ? $settings['days_before_expiry'] : 5 );
		$days_before_domain  = (int) ( isset( $settings['days_before_domain_expiry'] ) ? $settings['days_before_domain_expiry'] : 30 );
		$throttle_hours      = (int) ( isset( $settings['throttle_hours'] ) ? $settings['throttle_hours'] : 12 );

		$adminUrl = esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG ) );
		$siteUrl  = esc_url( isset( $siteData['url'] ) ? $siteData['url'] : '' );

		$pills = $hasDown
			? '<span style="display:inline-block;padding:3px 10px;border-radius:999px;background:#dc2626;color:#fff;font:700 12px/1 system-ui;">' . esc_html__( 'DOWN', 'lmn-site-monitor' ) . '</span>'
			: ( $hasSSL ? '<span style="display:inline-block;padding:3px 10px;border-radius:999px;background:#d97706;color:#fff;font:700 12px/1 system-ui;">' . esc_html__( 'SSL', 'lmn-site-monitor' ) . '</span>' : '' );

		if ( $hasDomain ) {
			$pills .= ' <span style="display:inline-block;padding:3px 10px;border-radius:999px;background:#7c3aed;color:#fff;font:700 12px/1 system-ui;">' . esc_html__( 'DOMAIN', 'lmn-site-monitor' ) . '</span>';
		}
		if ( null !== $sslDays && '' !== $sslDays ) {
			/* translators: %d: number of days remaining */
			$pills .= ' <span style="display:inline-block;padding:3px 8px;border-radius:999px;background:#e2e8f0;color:#111827;font:700 12px/1 system-ui;">' . sprintf( esc_html__( '%d days left', 'lmn-site-monitor' ), (int) $sslDays ) . '</span>';
		}
		if ( $hasDomain && null !== $domainDays ) {
			/* translators: %d: number of days remaining */
			$pills .= ' <span style="display:inline-block;padding:3px 8px;border-radius:999px;background:#ede9fe;color:#4c1d95;font:700 12px/1 system-ui;">' . sprintf( esc_html__( 'Domain: %d days', 'lmn-site-monitor' ), (int) $domainDays ) . '</span>';
		}

		$li = '';
		foreach ( $alerts as $a ) {
			if ( 'down' === ( isset( $a['type'] ) ? $a['type'] : '' ) ) {
				$li .= '<li style="margin:6px 0">' . esc_html__( 'The site is unreachable.', 'lmn-site-monitor' ) . '</li>';
			} elseif ( 'ssl_expiring' === ( isset( $a['type'] ) ? $a['type'] : '' ) ) {
				$li .= null !== $sslDays
					/* translators: 1: days, 2: date */
					? '<li style="margin:6px 0">' . sprintf( esc_html__( 'SSL will expire in %1$d days (on %2$s).', 'lmn-site-monitor' ), (int) $sslDays, esc_html( $sslDate ) ) . '</li>'
					/* translators: %s: date */
					: '<li style="margin:6px 0">' . sprintf( esc_html__( 'SSL expires on %s.', 'lmn-site-monitor' ), esc_html( $sslDate ) ) . '</li>';
			} elseif ( 'domain_expiring' === ( isset( $a['type'] ) ? $a['type'] : '' ) ) {
				$li .= null !== $domainDays
					/* translators: 1: days, 2: date */
					? '<li style="margin:6px 0">' . sprintf( esc_html__( 'Domain will expire in %1$d days (on %2$s).', 'lmn-site-monitor' ), (int) $domainDays, esc_html( $domainDate ) ) . '</li>'
					/* translators: %s: date */
					: '<li style="margin:6px 0">' . sprintf( esc_html__( 'Domain expires on %s.', 'lmn-site-monitor' ), esc_html( $domainDate ) ) . '</li>';
			}
		}
		if ( '' === $li ) {
			$li = '<li style="margin:6px 0">' . esc_html__( 'There are notices for this site.', 'lmn-site-monitor' ) . '</li>';
		}

		if ( $hasDown ) {
			/* translators: %s: host */
			$wrapper_title = sprintf( esc_html__( '⛔ Site down · %s', 'lmn-site-monitor' ), esc_html( $host ) );
		} elseif ( $hasSSL ) {
			/* translators: %s: host */
			$wrapper_title = sprintf( esc_html__( '⚠️ SSL expiring soon · %s', 'lmn-site-monitor' ), esc_html( $host ) );
		} elseif ( $hasDomain ) {
			/* translators: %s: host */
			$wrapper_title = sprintf( esc_html__( '⚠️ Domain expiring soon · %s', 'lmn-site-monitor' ), esc_html( $host ) );
		} else {
			/* translators: %s: host */
			$wrapper_title = sprintf( esc_html__( '🔔 Site notice · %s', 'lmn-site-monitor' ), esc_html( $host ) );
		}

		$detail_rows = array(
			esc_html__( 'Site', 'lmn-site-monitor' )              => '<a href="' . $siteUrl . '" rel="noreferrer">' . esc_html( $host ) . '</a>',
			esc_html__( 'Last check (UTC)', 'lmn-site-monitor' )  => esc_html( $checked ),
			esc_html__( 'HTTP status', 'lmn-site-monitor' )       => is_numeric( $http ) ? (int) $http : esc_html( (string) $http ),
			esc_html__( 'SSL expires (UTC)', 'lmn-site-monitor' ) => esc_html( $sslDate ),
		);
		if ( '—' !== $domainDate ) {
			$detail_rows[ esc_html__( 'Domain expires', 'lmn-site-monitor' ) ] = esc_html( $domainDate );
		}

		$html = $this->email_wrapper(
			$wrapper_title,
			esc_html__( 'We detected the following issues in the last check.', 'lmn-site-monitor' ),
			$pills, $li,
			$detail_rows,
			$err ? '<pre style="margin:12px 0 0;padding:12px;background:#0f172a;color:#fff;border-radius:8px;overflow:auto"><code style="font:500 12px/1.4 monospace">' . esc_html( $err ) . '</code></pre>' : '',
			$adminUrl,
			esc_html__( 'Open Site Monitor', 'lmn-site-monitor' ),
			sprintf(
				/* translators: 1: email address, 2: throttle hours, 3: SSL warning days, 4: domain warning days */
				esc_html__( 'This email was sent to %1$s. SSL threshold: %3$s days · Domain threshold: %4$s days · Resend every %2$sh.', 'lmn-site-monitor' ),
				esc_html( $admin_to ), intval( $throttle_hours ), intval( $days_before ), intval( $days_before_domain )
			)
		);
		return array( $subject, $html );
	}

	public function build_recovered_email( array $siteData ) {
		$host    = wp_parse_url( isset( $siteData['url'] ) ? $siteData['url'] : '', PHP_URL_HOST ) ?: ( isset( $siteData['url'] ) ? $siteData['url'] : '' );
		/* translators: %s: host */
		$subject = sprintf( __( '✅ Site recovered: %s', 'lmn-site-monitor' ), $host );

		$checked      = gmdate( 'Y-m-d H:i:s', isset( $siteData['last_checked'] ) ? $siteData['last_checked'] : time() );
		$http         = isset( $siteData['http_code'] ) ? $siteData['http_code'] : '—';
		$sslDate      = ! empty( $siteData['ssl_expires'] ) ? gmdate( 'Y-m-d H:i', $siteData['ssl_expires'] ) : '—';
		$sslDays      = isset( $siteData['ssl_days_left'] ) ? $siteData['ssl_days_left'] : null;
		$downtimeFrom = ! empty( $siteData['downtime_start'] ) ? gmdate( 'Y-m-d H:i', (int) $siteData['downtime_start'] ) : '—';

		$settings       = get_option( $this->option_settings, array() );
		$admin_to       = isset( $settings['admin_email'] ) ? $settings['admin_email'] : get_option( 'admin_email' );
		$days_before    = intval( isset( $settings['days_before_expiry'] ) ? $settings['days_before_expiry'] : 5 );
		$throttle_hours = intval( isset( $settings['throttle_hours'] ) ? $settings['throttle_hours'] : 12 );

		$adminUrl = esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG ) );
		$siteUrl  = esc_url( isset( $siteData['url'] ) ? $siteData['url'] : '' );

		$okBadge  = '<span style="display:inline-block;padding:3px 10px;border-radius:6px;background:#16a34a;color:#fff;font:700 12px/1 system-ui">OK</span>';
		$daysPill = null !== $sslDays
			? '<span style="display:inline-block;padding:3px 8px;border-radius:999px;background:#e2e8f0;color:#0f172a;font:600 12px/1 system-ui">' . intval( $sslDays ) . ' ' . esc_html__( 'days', 'lmn-site-monitor' ) . '</span>'
			: '—';

		$li = '<li style="margin:6px 0">' . esc_html__( 'The site is back online.', 'lmn-site-monitor' ) . '</li>';
		if ( '—' !== $downtimeFrom ) {
			/* translators: %s: datetime UTC */
			$li .= '<li style="margin:6px 0">' . sprintf( esc_html__( 'It was down since: %s (UTC).', 'lmn-site-monitor' ), esc_html( $downtimeFrom ) ) . '</li>';
		}

		$html = $this->email_wrapper(
			esc_html__( 'Site recovered ·', 'lmn-site-monitor' ) . ' ' . esc_html( $host ),
			esc_html__( 'The site is online again. No issues were detected in the last check.', 'lmn-site-monitor' ),
			$okBadge, $li,
			array(
				esc_html__( 'Status', 'lmn-site-monitor' )      => $okBadge,
				'HTTP'                                           => esc_html( (string) $http ),
				esc_html__( 'Last check', 'lmn-site-monitor' )  => esc_html( $checked ),
				esc_html__( 'SSL expires', 'lmn-site-monitor' ) => esc_html( $sslDate ),
				esc_html__( 'SSL days', 'lmn-site-monitor' )    => $daysPill,
				'URL'                                           => '<a href="' . $siteUrl . '">' . $siteUrl . '</a>',
			),
			'', $adminUrl,
			esc_html__( 'Open Site Monitor', 'lmn-site-monitor' ),
			sprintf(
				/* translators: 1: email, 2: throttle hours, 3: ssl days */
				esc_html__( 'This email was sent to %1$s. SSL threshold: %3$s days · Resend every %2$sh.', 'lmn-site-monitor' ),
				esc_html( $admin_to ), intval( $throttle_hours ), intval( $days_before )
			)
		);
		return array( $subject, $html );
	}

	private function maybe_send_alert( $id, $siteData, $force_email = false ) {
		$settings            = get_option( $this->option_settings, array() );
		$admin_email         = isset( $settings['admin_email'] ) ? $settings['admin_email'] : get_option( 'admin_email' );
		$days_before         = intval( isset( $settings['days_before_expiry'] ) ? $settings['days_before_expiry'] : 5 );
		$days_before_domain  = intval( isset( $settings['days_before_domain_expiry'] ) ? $settings['days_before_domain_expiry'] : 30 );
		$throttle_seconds    = max( 3600, intval( isset( $settings['throttle_hours'] ) ? $settings['throttle_hours'] : 12 ) * 3600 );
		$last_alerts         = get_option( $this->option_last_alerts, array() );
		$alerts              = array();

		if ( 'down' === ( isset( $siteData['last_status'] ) ? $siteData['last_status'] : '' ) ) {
			$alerts[] = array(
				'type' => 'down',
				/* translators: 1: URL, 2: HTTP code */
				'message' => sprintf( __( 'Site down: %1$s (HTTP: %2$s).', 'lmn-site-monitor' ),
					(string) ( isset( $siteData['url'] ) ? $siteData['url'] : '' ),
					(string) ( isset( $siteData['http_code'] ) ? $siteData['http_code'] : '0' ) ),
			);
		}
		if ( ! empty( $siteData['ssl_expires'] ) ) {
			$days = ceil( ( $siteData['ssl_expires'] - time() ) / 86400 );
			if ( $days <= $days_before ) {
				$alerts[] = array(
					'type' => 'ssl_expiring',
					/* translators: 1: days, 2: URL, 3: date UTC */
					'message' => sprintf( __( 'SSL certificate expires in %1$d day(s) for %2$s (date: %3$s UTC).', 'lmn-site-monitor' ),
						intval( $days ),
						(string) ( isset( $siteData['url'] ) ? $siteData['url'] : '' ),
						gmdate( 'Y-m-d H:i:s', intval( $siteData['ssl_expires'] ) ) ),
				);
			}
		}
		if ( ! empty( $siteData['domain_expires'] ) ) {
			$days = ceil( ( $siteData['domain_expires'] - time() ) / 86400 );
			if ( $days <= $days_before_domain ) {
				$alerts[] = array(
					'type'    => 'domain_expiring',
					/* translators: 1: days, 2: URL, 3: date */
					'message' => sprintf( __( 'Domain expires in %1$d day(s) for %2$s (date: %3$s).', 'lmn-site-monitor' ),
						intval( $days ),
						(string) ( isset( $siteData['url'] ) ? $siteData['url'] : '' ),
						gmdate( 'Y-m-d', intval( $siteData['domain_expires'] ) ) ),
				);
			}
		}

		if ( empty( $alerts ) ) {
			if ( isset( $last_alerts[ $id ]['state'] ) && 'ok' !== $last_alerts[ $id ]['state'] ) {
				list( $subject, $html ) = $this->build_recovered_email( $siteData );
				wp_mail( $admin_email, '[Monitor] ' . $subject, $html, array( 'Content-Type: text/html; charset=UTF-8' ) );
				$last_alerts[ $id ] = array( 'state' => 'ok', 'sent' => 0, 'last_sent' => time() );
				update_option( $this->option_last_alerts, $last_alerts );
			}
			return;
		}

		$send = false;
		if ( $force_email ) { $send = true; }
		if ( ! isset( $last_alerts[ $id ] ) ) {
			$send = true;
			$last_alerts[ $id ] = array( 'state' => 'problem', 'sent' => 0, 'last_sent' => 0 );
		} elseif ( 'ok' === ( isset( $last_alerts[ $id ]['state'] ) ? $last_alerts[ $id ]['state'] : '' ) ) {
			$send = true;
		} elseif ( time() - intval( isset( $last_alerts[ $id ]['last_sent'] ) ? $last_alerts[ $id ]['last_sent'] : 0 ) > $throttle_seconds ) {
			$send = true;
		}

		if ( $send ) {
			list( $subject, $html ) = $this->build_alert_email( $siteData, $alerts );
			wp_mail( $admin_email, '[Monitor] ' . $subject, $html, array( 'Content-Type: text/html; charset=UTF-8' ) );
			$last_alerts[ $id ] = array(
				'state'     => 'problem',
				'sent'      => intval( isset( $last_alerts[ $id ]['sent'] ) ? $last_alerts[ $id ]['sent'] : 0 ) + 1,
				'last_sent' => time(),
			);
			update_option( $this->option_last_alerts, $last_alerts );
		}
	}

	/* ============================================================
	   SMTP
	   ============================================================ */

	public function apply_smtp( $phpmailer ) {
		$settings = get_option( $this->option_settings, array() );
		if ( empty( $settings['smtp_enabled'] ) ) { return; }

		$host       = trim( (string) ( isset( $settings['smtp_host'] ) ? $settings['smtp_host'] : '' ) );
		$port       = (int) ( isset( $settings['smtp_port'] ) ? $settings['smtp_port'] : 587 );
		$secure     = (string) ( isset( $settings['smtp_secure'] ) ? $settings['smtp_secure'] : '' );
		$auth_flag  = ! empty( $settings['smtp_auth'] );
		$username   = trim( (string) ( isset( $settings['smtp_username'] ) ? $settings['smtp_username'] : '' ) );
		$password   = (string) ( isset( $settings['smtp_password'] ) ? $settings['smtp_password'] : '' );
		$from_email = trim( (string) ( isset( $settings['smtp_from_email'] ) ? $settings['smtp_from_email'] : '' ) );
		$from_name  = (string) ( isset( $settings['smtp_from_name'] ) ? $settings['smtp_from_name'] : 'LMN Site Monitor' );

		$phpmailer->isSMTP();
		$phpmailer->Host = $host;
		$phpmailer->Port = $port ? $port : 587;
		if ( ! $auth_flag && $username && $password ) { $auth_flag = true; }
		$phpmailer->SMTPAuth = $auth_flag;

		$is_gmail = ( false !== stripos( $host, 'smtp.gmail.com' ) || false !== stripos( $host, 'googlemail.com' ) );
		if ( $is_gmail ) {
			$phpmailer->SMTPSecure = 'tls'; $phpmailer->Port = 587;
			$phpmailer->SMTPAuth = true; $phpmailer->SMTPAutoTLS = true;
		} else {
			$phpmailer->SMTPSecure = $secure ? $secure : ''; $phpmailer->SMTPAutoTLS = true;
		}
		if ( $phpmailer->SMTPAuth ) {
			if ( $username ) { $phpmailer->Username = $username; }
			if ( $password ) { $phpmailer->Password = $password; }
		}
		$visible_from = $from_email ? $from_email : $username;
		if ( $is_gmail ) {
			$eff = $username ? $username : $visible_from;
			if ( $eff ) { $phpmailer->setFrom( $eff, $from_name, false ); $phpmailer->Sender = $eff; }
			if ( $from_email && $username && 0 !== strcasecmp( $from_email, $username ) ) {
				$phpmailer->clearReplyTos(); $phpmailer->addReplyTo( $from_email, $from_name );
			}
		} elseif ( $visible_from ) {
			$phpmailer->setFrom( $visible_from, $from_name, false ); $phpmailer->Sender = $visible_from;
		}
		$phpmailer->SMTPOptions = array( 'ssl' => array( 'verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => false ) );
		$phpmailer->Timeout   = 20;
		$phpmailer->SMTPDebug = 0;
	}

	public function handle_wp_mail_failed( $wp_error ) {
		$error_msg = ( $wp_error instanceof WP_Error ? $wp_error->get_error_message() : (string) $wp_error );
		$msg = sprintf(
			/* translators: %s: WP error message */
			__( 'LMN Site Monitor: email sending failed. %s', 'lmn-site-monitor' ),
			$error_msg
		);
		set_transient( 'lmnsimo_smtp_error', $msg, 180 );
		set_transient( 'lmnsimo_mail_recent_failure', $error_msg, 60 );
	}

	public function handle_wp_mail_succeeded( $mail_data ) {
		delete_transient( 'lmnsimo_smtp_error' );
	}

	public function admin_notice_mail_failed() {
		if ( ! current_user_can( 'manage_options' ) ) { return; }
		$msg = get_transient( 'lmnsimo_mail_recent_failure' );
		if ( ! $msg ) { return; }
		$url = admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=config' );
		echo '<div class="notice notice-error"><p><strong>LMN Site Monitor:</strong> '
			. esc_html__( 'Email sending failed.', 'lmn-site-monitor' ) . ' '
			. esc_html( $msg ) . ' &nbsp;<a href="' . esc_url( $url ) . '">'
			. esc_html__( 'Configure SMTP', 'lmn-site-monitor' ) . '</a>.</p></div>';
	}

	/* ============================================================
	   i18n
	   ============================================================ */

	public function maybe_load_textdomain() {
		$domain = 'lmn-site-monitor';
		if ( is_textdomain_loaded( $domain ) ) { return; }
		$locale        = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
		$mofile_global = WP_LANG_DIR . '/plugins/' . $domain . '-' . $locale . '.mo';
		if ( file_exists( $mofile_global ) ) { load_textdomain( $domain, $mofile_global ); return; }
		$mofile_local = plugin_dir_path( __FILE__ ) . 'languages/' . $domain . '-' . $locale . '.mo';
		if ( file_exists( $mofile_local ) ) { load_textdomain( $domain, $mofile_local ); }
	}
}

new LMNSIMO_Site_Monitor();

/* ============================================================
   PLUGIN ACTION LINKS
   ============================================================ */
add_filter(
	'plugin_action_links_' . plugin_basename( __FILE__ ),
	function ( $links ) {
		$settings_link = '<a href="admin.php?page=lmn-site-monitor">' . esc_html__( 'Settings', 'lmn-site-monitor' ) . '</a>';
		$donate_url    = 'https://www.paypal.com/donate/?business=FTTYSUMRSPWKN&no_recurring=0&currency_code=EUR&amount=5.00';
		$donate_link   = '<a href="' . esc_url( $donate_url ) . '" target="_blank" rel="noopener noreferrer" style="color:#0073aa;font-weight:600;">'
			. esc_html__( 'Make a donation', 'lmn-site-monitor' ) . '</a>';
		array_unshift( $links, $settings_link, $donate_link );
		return $links;
	}
);

