<?php
/*
Plugin Name: SSP Debug
Plugin URI: https://www.stupidsimpleplugins.com/ssp-debugging
Description: Logs PHP errors with selectable levels, uncaught exceptions, shutdown fatals, size limits, filters, and a download option; includes an admin page for viewing, clearing, and managing logs. Includes a modern, image-based showcase panel for other products.
Version: 1.0.0
Requires at least: 6.0
Requires PHP: 7.4
Author: Stupid Simple Plugins
Author URI: https://www.stupidsimpleplugins.com
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: ssp-debugging
Domain Path: /languages
*/

defined( 'ABSPATH' ) || exit;

/* ─────────── helpers: Filesystem ─────────── */
if ( ! function_exists( 'sspd_get_fs' ) ) {
	function sspd_get_fs() {
		global $wp_filesystem;
		if ( ! $wp_filesystem ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}
		return $wp_filesystem;
	}
}

/* ─────────── shared TZ mapping helpers ─────────── */
if ( ! function_exists('sspd_map_tz_key_to_iana') ) {
	function sspd_map_tz_key_to_iana( $key ) {
		$k = (string)$key;

		switch ( strtoupper($k) ) {
			case 'ET':    return 'America/New_York';
			case 'CT':    return 'America/Chicago';
			case 'MT':    return 'America/Denver';
			case 'MT_AZ': return 'America/Phoenix';
			case 'PT':    return 'America/Los_Angeles';
			case 'AKT':   return 'America/Anchorage';
			case 'HST':   return 'Pacific/Honolulu';
			case 'AT':    return 'America/Puerto_Rico';
			case 'UTC':   return 'UTC';
		}

		switch ( $k ) {
			case 'International Date Line West': return 'Etc/GMT+12';
			case 'Samoa Standard Time': return 'Pacific/Apia';
			case 'Niue Time': return 'Pacific/Niue';
			case 'Hawaii–Aleutian Standard Time': return 'America/Adak';
			case 'Cook Islands Time': return 'Pacific/Rarotonga';
			case 'Tahiti Time': return 'Pacific/Tahiti';
			case 'Marquesas Time': return 'Pacific/Marquesas';
			case 'Alaska Standard Time': return 'America/Anchorage';
			case 'Gambier Time': return 'Pacific/Gambier';
			case 'Pacific Time': return 'America/Los_Angeles';
			case 'Mountain Time': return 'America/Denver';
			case 'Central Time': return 'America/Chicago';
			case 'Galápagos Time': return 'Pacific/Galapagos';
			case 'Eastern Time': return 'America/New_York';
			case 'Colombia Time': return 'America/Bogota';
			case 'Peru Time': return 'America/Lima';
			case 'Cuba Standard Time': return 'America/Havana';
			case 'Atlantic Time': return 'America/Halifax';
			case 'Bolivia Time': return 'America/La_Paz';
			case 'Venezuela Time': return 'America/Caracas';
			case 'Newfoundland Time': return 'America/St_Johns';
			case 'Argentina Time': return 'America/Argentina/Buenos_Aires';
			case 'Brasília Time': return 'America/Sao_Paulo';
			case 'Uruguay Time': return 'America/Montevideo';
			case 'French Guiana Time': return 'America/Cayenne';
			case 'South Georgia Time': return 'Atlantic/South_Georgia';
			case 'Fernando de Noronha Time': return 'America/Noronha';
			case 'Cape Verde Time': return 'Atlantic/Cape_Verde';
			case 'Azores Time': return 'Atlantic/Azores';
			case 'Greenwich Mean Time': return 'Etc/GMT';
			case 'Western European Time': return 'Europe/Lisbon';
			case 'Central European Time': return 'Europe/Berlin';
			case 'West Africa Time': return 'Africa/Lagos';
			case 'Eastern European Time': return 'Europe/Bucharest';
			case 'Central Africa Time': return 'Africa/Maputo';
			case 'South Africa Standard Time': return 'Africa/Johannesburg';
			case 'Israel Standard Time': return 'Asia/Jerusalem';
			case 'Arabia Standard Time': return 'Asia/Riyadh';
			case 'East Africa Time': return 'Africa/Nairobi';
			case 'Turkey Time': return 'Europe/Istanbul';
			case 'Iran Standard Time': return 'Asia/Tehran';
			case 'Gulf Standard Time': return 'Asia/Dubai';
			case 'Azerbaijan Time': return 'Asia/Baku';
			case 'Georgia Standard Time': return 'Asia/Tbilisi';
			case 'Armenia Time': return 'Asia/Yerevan';
			case 'Afghanistan Time': return 'Asia/Kabul';
			case 'Pakistan Standard Time': return 'Asia/Karachi';
			case 'Uzbekistan Time': return 'Asia/Tashkent';
			case 'Tajikistan Time': return 'Asia/Dushanbe';
			case 'Turkmenistan Time': return 'Asia/Ashgabat';
			case 'India Standard Time': return 'Asia/Kolkata';
			case 'Sri Lanka Standard Time': return 'Asia/Colombo';
			case 'Nepal Time': return 'Asia/Kathmandu';
			case 'Bangladesh Standard Time': return 'Asia/Dhaka';
			case 'Bhutan Time': return 'Asia/Thimphu';
			case 'Cocos Islands Time': return 'Indian/Cocos';
			case 'Myanmar Time': return 'Asia/Yangon';
			case 'Indochina Time': return 'Asia/Bangkok';
			case 'Western Indonesia Time': return 'Asia/Jakarta';
			case 'China Standard Time': return 'Asia/Shanghai';
			case 'Australian Western Standard Time': return 'Australia/Perth';
			case 'Singapore Standard Time': return 'Asia/Singapore';
			case 'Philippine Standard Time': return 'Asia/Manila';
			case 'Central Indonesia Time': return 'Asia/Makassar';
			case 'Brunei Darussalam Time': return 'Asia/Brunei';
			case 'Malaysia Time': return 'Asia/Kuala_Lumpur';
			case 'Australian Central Western Time': return 'Australia/Eucla';
			case 'Japan Standard Time': return 'Asia/Tokyo';
			case 'Korea Standard Time': return 'Asia/Seoul';
			case 'Eastern Indonesia Time': return 'Asia/Jayapura';
			case 'Australian Central Standard Time': return 'Australia/Darwin';
			case 'Australian Eastern Standard Time': return 'Australia/Sydney';
			case 'Papua New Guinea Time': return 'Pacific/Port_Moresby';
			case 'Chamorro Standard Time': return 'Pacific/Guam';
			case 'Lord Howe Standard Time': return 'Australia/Lord_Howe';
			case 'Solomon Islands Time': return 'Pacific/Guadalcanal';
			case 'New Caledonia Time': return 'Pacific/Noumea';
			case 'New Zealand Standard Time': return 'Pacific/Auckland';
			case 'Fiji Time': return 'Pacific/Fiji';
			case 'Marshall Islands Time': return 'Pacific/Majuro';
			case 'Tuvalu Time': return 'Pacific/Funafuti';
			case 'Nauru Time': return 'Pacific/Nauru';
			case 'Chatham Standard Time': return 'Pacific/Chatham';
			case 'Tonga Time': return 'Pacific/Tongatapu';
			case 'Phoenix Island Time': return 'Pacific/Kanton';
			case 'Line Islands Time': return 'Pacific/Kiritimati';
		}

		return null;
	}
}
if ( ! function_exists('sspd_get_timezone_from_option') ) {
	function sspd_get_timezone_from_option() {
		$opt = function_exists('get_option') ? get_option('sspd_tz', 'site') : 'UTC';
		try {
			if ( $opt === 'site' ) {
				if ( function_exists('wp_timezone') ) return wp_timezone();
				if ( function_exists('wp_timezone_string') ) return new DateTimeZone( wp_timezone_string() ?: 'UTC' );
				return new DateTimeZone('UTC');
			}
			$iana = sspd_map_tz_key_to_iana($opt);
			if ( $iana ) return new DateTimeZone($iana);
			if ( strtoupper($opt) === 'UTC' ) return new DateTimeZone('UTC');
			return new DateTimeZone($opt);
		} catch ( Exception $e ) {
			return new DateTimeZone('UTC');
		}
	}
}

/* ───── Emergency shutdown logger ───── */
if ( ! function_exists('sspd_emergency_shutdown') ) {
	function sspd_emergency_shutdown() {
		if ( ! defined('WP_CONTENT_DIR') ) return;
		$e = error_get_last();
		if ( ! $e ) return;

		$fatal = array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR );
		if ( in_array( $e['type'], $fatal, true ) ) {
			$upload_dir = wp_upload_dir();
			$log_dir    = trailingslashit( $upload_dir['basedir'] ) . 'ssp-debug/';
			if ( ! file_exists( $log_dir ) ) {
				wp_mkdir_p( $log_dir );
			}
			$log = $log_dir . 'ssp-debug.log';

			$fs  = sspd_get_fs();
			if ( $fs ) {
				$existing = $fs->exists( $log ) ? (string) $fs->get_contents( $log ) : '';
				$tz       = sspd_get_timezone_from_option();
				$line     = wp_date( 'Y-m-d H:i:s', time(), $tz ) . ' | EMERGENCY CRITICAL FATAL: ' . $e['message'] . ' in ' . $e['file'] . ':' . $e['line'] . "\n";
				$fs->put_contents( $log, $existing . $line );
			}
		}
	}
	register_shutdown_function('sspd_emergency_shutdown');
}

if ( ! class_exists( 'SSPD_Debug_Logger' ) ) :

class SSPD_Debug_Logger {

	const OPT_ENABLED     = 'sspd_enabled';
	const OPT_LEVEL       = 'sspd_level';
	const OPT_SIZE_LIMIT  = 'sspd_size_limit_mb';
	const OPT_FORMAT      = 'sspd_format';
	const OPT_SCOPES      = 'sspd_scopes';
	const OPT_IGNORE_LIST = 'sspd_ignore_list';
	const OPT_TZ          = 'sspd_tz';

	const LOG_FILE        = 'ssp-debug.log';

	const OLD_OPT_ENABLED = 'afpd_enabled';
	const OLD_OPT_LEVEL   = 'afpd_level';
	const OLD_LOG_FILE    = 'afp-debug.log';

	private static $ins;

	public static function instance() {
		return self::$ins ?: self::$ins = new self();
	}

	private function __construct() {
		// Define plugin constants once
		if ( ! defined( 'SSPDBG_PLUGIN_FILE' ) ) {
			define( 'SSPDBG_PLUGIN_FILE', __FILE__ );
			define( 'SSPDBG_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
			define( 'SSPDBG_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
		}

		// Centralize log directory under uploads/ssp-debug/
		$upload_dir      = wp_upload_dir();
		$sspdbg_log_dir  = trailingslashit( $upload_dir['basedir'] ) . 'ssp-debug/';
		if ( ! file_exists( $sspdbg_log_dir ) ) {
			wp_mkdir_p( $sspdbg_log_dir );
		}

		// Always point logs into uploads
		if ( ! defined( 'SSPD_LOG_FILE' ) ) define( 'SSPD_LOG_FILE', $sspdbg_log_dir . self::LOG_FILE );
		if ( ! defined( 'SSPD_AFPD_LOG_FILE' ) ) define( 'SSPD_AFPD_LOG_FILE', $sspdbg_log_dir . self::OLD_LOG_FILE );

		register_activation_hook( __FILE__, array( __CLASS__, 'activate' ) );
		register_uninstall_hook(  __FILE__, array( __CLASS__, 'uninstall' ) );

		add_action( 'admin_menu', array( $this, 'admin_menu' ) );
		add_action( 'admin_post_sspd_download', array( $this, 'handle_download' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );

		if ( $this->enabled() ) {
			add_action( 'plugins_loaded', array( $this, 'bootstrap_handlers' ), 1 );
		}

		add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'plugin_row_links' ) );
	}

	public static function activate() {
		if ( get_option( self::OPT_ENABLED, null ) === null ) add_option( self::OPT_ENABLED, 1 );
		if ( get_option( self::OPT_LEVEL, null ) === null ) add_option( self::OPT_LEVEL, 'all' );
		if ( get_option( self::OPT_SIZE_LIMIT, null ) === null ) add_option( self::OPT_SIZE_LIMIT, 5 );
		if ( get_option( self::OPT_FORMAT, null ) === null ) add_option( self::OPT_FORMAT, 'detailed' );
		if ( get_option( self::OPT_SCOPES, null ) === null ) add_option( self::OPT_SCOPES, array( 'mu-plugins','plugins','themes','core-other' ) );
		if ( get_option( self::OPT_IGNORE_LIST, null ) === null ) add_option( self::OPT_IGNORE_LIST, "" );
		if ( get_option( self::OPT_TZ, null ) === null ) add_option( self::OPT_TZ, 'site' );

		$fs = sspd_get_fs();

		$old_enabled = get_option( self::OLD_OPT_ENABLED, null );
		$old_level   = get_option( self::OLD_OPT_LEVEL,   null );
		if ( $old_enabled !== null && get_option( self::OPT_ENABLED, 1 ) === 1 ) update_option( self::OPT_ENABLED, (int) $old_enabled ? 1 : 0 );
		if ( $old_level   !== null && get_option( self::OPT_LEVEL, 'all' ) === 'all' ) update_option( self::OPT_LEVEL, sanitize_key( $old_level ) === 'fatal' ? 'fatal' : 'all' );

		// Ensure log files exist under uploads path and migrate old ones if present
		if ( $fs && ! $fs->exists( SSPD_LOG_FILE ) ) {
			if ( $fs->exists( SSPD_AFPD_LOG_FILE ) ) {
				$fs->move( SSPD_AFPD_LOG_FILE, SSPD_LOG_FILE, true );
			}
			if ( ! $fs->exists( SSPD_LOG_FILE ) ) {
				$fs->put_contents( SSPD_LOG_FILE, '' );
			}
		}
	}

	public static function uninstall() {
		delete_option( self::OPT_ENABLED );
		delete_option( self::OPT_LEVEL );
		delete_option( self::OPT_SIZE_LIMIT );
		delete_option( self::OPT_FORMAT );
		delete_option( self::OPT_SCOPES );
		delete_option( self::OPT_IGNORE_LIST );
		delete_option( self::OPT_TZ );

		$fs = sspd_get_fs();
		if ( $fs ) {
			if ( $fs->exists( SSPD_LOG_FILE ) ) $fs->delete( SSPD_LOG_FILE );
			if ( $fs->exists( SSPD_AFPD_LOG_FILE ) ) $fs->delete( SSPD_AFPD_LOG_FILE );
			// Attempt removing empty directory
			$upload_dir = wp_upload_dir();
			$dir        = trailingslashit( $upload_dir['basedir'] ) . 'ssp-debug/';
			if ( is_dir( $dir ) ) {
				// best-effort cleanup if empty
				$fs->rmdir( $dir, true );
			}
		}

		delete_option( self::OLD_OPT_ENABLED );
		delete_option( self::OLD_OPT_LEVEL );
	}

	/* ───────── helpers ───────── */

	private function enabled() { return (bool) get_option( self::OPT_ENABLED, 1 ); }
	private function level()   { $l = get_option( self::OPT_LEVEL, 'all' ); return $l === 'fatal' ? 'fatal' : 'all'; }
	private function format()  { $f = get_option( self::OPT_FORMAT, 'detailed' ); return $f === 'basic' ? 'basic' : 'detailed'; }
	private function size_limit_bytes() { $mb = (int) get_option( self::OPT_SIZE_LIMIT, 5 ); return $mb > 0 ? $mb * 1024 * 1024 : 0; }
	private function scopes()  { $v = get_option( self::OPT_SCOPES, array( 'mu-plugins','plugins','themes','core-other' ) ); return is_array($v)?$v:array(); }
	private function ignore_patterns() { $raw = (string) get_option( self::OPT_IGNORE_LIST, "" ); return array_filter( array_map( 'trim', preg_split( "/\r\n|\r|\n/", $raw ) ) ); }

	private function current_timezone() { return sspd_get_timezone_from_option(); }
	private function now_string() { return wp_date( 'Y-m-d H:i:s', time(), $this->current_timezone() ); }

	private function ctx() {
		$scheme = is_ssl() ? 'https' : 'http';
		$host   = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
		$uri    = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$url    = $host ? ($scheme . '://' . $host . $uri) : $uri;
		$ip     = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
		$ua_raw = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
		$user   = function_exists('wp_get_current_user') ? wp_get_current_user() : null;
		$uid    = ( $user && $user->ID ) ? (int) $user->ID : 0;
		return "URL={$url} | IP={$ip} | UA=" . substr( $ua_raw, 0, 300 ) . " | UID={$uid}";
	}

	private function classify_file( $file ) {
		$file = wp_normalize_path( (string) $file );
		$mu   = defined('WPMU_PLUGIN_DIR') ? wp_normalize_path( WPMU_PLUGIN_DIR ) : '';

		$pl = wp_normalize_path( plugin_dir_path( __FILE__ ) );
		$th = function_exists( 'get_theme_root' ) ? wp_normalize_path( get_theme_root() ) : '';
		$ab = wp_normalize_path( dirname( $pl, 2 ) );

		if ( $mu && strpos( $file, trailingslashit($mu) ) === 0 ) return 'mu-plugins';
		if ( $pl && strpos( $file, trailingslashit($pl) ) === 0 ) return 'plugins';
		if ( $th && strpos( $file, trailingslashit($th) ) === 0 ) return 'themes';
		if ( $ab && ( strpos( $file, trailingslashit($ab) . 'wp-includes/' ) === 0 || strpos( $file, trailingslashit($ab) . 'wp-admin/' ) === 0 ) ) return 'core-other';
		return 'core-other';
	}

	private function should_log_scope( $file ) {
		return in_array( $this->classify_file( $file ), $this->scopes(), true );
	}

	private function is_known_success_noise( $full_message ) {
		$hay = strtolower($full_message);
		if ( strpos($hay, 'ssp-license-gate') !== false ) return true;
		if ( strpos($hay, '"valid":true') !== false && strpos($hay, 'license_prefix') !== false ) return true;
		if ( strpos($hay, '200 ok') !== false || strpos($hay, 'success') !== false ) return true;
		return false;
	}

	private function should_ignore( $full_message ) {
		if ( $this->is_known_success_noise( $full_message ) ) return true;

		$pats = $this->ignore_patterns();
		if ( empty( $pats ) ) return false;
		foreach ( $pats as $p ) {
			if ( ( strlen($p) > 2 ) && ( ($p[0] === '/' && substr($p,-1) === '/') || ($p[0] === '#' && substr($p,-1) === '#') ) ) {
				if ( @preg_match( $p, $full_message ) === 1 ) return true;
			} else {
				if ( stripos( $full_message, $p ) !== false ) return true;
			}
		}
		return false;
	}

	private function log( $msg ) {
		if ( ! $this->enabled() ) return;
		if ( $this->should_ignore( $msg ) ) return;
		if ( $this->format() === 'detailed' ) $msg .= ' | ' . $this->ctx();

		$fs = sspd_get_fs();
		if ( $fs ) {
			// Ensure directory exists before each write
			$upload_dir = wp_upload_dir();
			$log_dir    = trailingslashit( $upload_dir['basedir'] ) . 'ssp-debug/';
			if ( ! file_exists( $log_dir ) ) {
				wp_mkdir_p( $log_dir );
			}

			$existing = $fs->exists( SSPD_LOG_FILE ) ? (string) $fs->get_contents( SSPD_LOG_FILE ) : '';
			$existing .= $this->now_string() . " | {$msg}\n";
			$fs->put_contents( SSPD_LOG_FILE, $existing );

			$limit = $this->size_limit_bytes();
			if ( $limit > 0 ) {
				$size = function_exists('wp_filesize') ? (int) wp_filesize( SSPD_LOG_FILE ) : strlen( $existing );
				if ( $size > $limit ) {
					$this->truncate_log_to_bytes( (int) floor( $limit * 0.7 ) );
				}
			}
		}
	}

	private function truncate_log_to_bytes( $keep_bytes ) {
		$fs = sspd_get_fs();
		$data = $fs && $fs->exists( SSPD_LOG_FILE ) ? (string) $fs->get_contents( SSPD_LOG_FILE ) : '';
		$len  = strlen( $data );
		if ( $len > $keep_bytes && $fs ) {
			$data = substr( $data, $len - $keep_bytes );
			$fs->put_contents( SSPD_LOG_FILE, $data );
		}
	}

	private function errno_label( $errno ) {
		switch ( $errno ) {
			case E_ERROR: return 'E_ERROR';
			case E_WARNING: return 'E_WARNING';
			case E_PARSE: return 'E_PARSE';
			case E_NOTICE: return 'E_NOTICE';
			case E_CORE_ERROR: return 'E_CORE_ERROR';
			case E_CORE_WARNING: return 'E_CORE_WARNING';
			case E_COMPILE_ERROR: return 'E_COMPILE_ERROR';
			case E_COMPILE_WARNING: return 'E_COMPILE_WARNING';
			case E_USER_ERROR: return 'E_USER_ERROR';
			case E_USER_WARNING: return 'E_USER_WARNING';
			case E_USER_NOTICE: return 'E_USER_NOTICE';
			case E_STRICT: return 'E_STRICT';
			case E_RECOVERABLE_ERROR: return 'E_RECOVERABLE_ERROR';
			case E_DEPRECATED: return 'E_DEPRECATED';
			case E_USER_DEPRECATED: return 'E_USER_DEPRECATED';
			default: return (string) $errno;
		}
	}

	/* ───────── handlers ───────── */

	public function bootstrap_handlers() {
		$mask_all   = E_ALL | ( defined( 'E_STRICT' ) ? E_STRICT : 0 );
		$mask_fatal = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR;

		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		set_error_handler( array( $this, 'handle_php_error' ), $this->level() === 'fatal' ? $mask_fatal : $mask_all );
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		set_exception_handler( array( $this, 'handle_exception' ) );
		register_shutdown_function( array( $this, 'handle_shutdown' ) );
	}

	public function handle_php_error( $errno, $errstr, $errfile, $errline ) {
		if ( ! $this->should_log_scope( $errfile ) ) return false;
		$msg = "PHP " . $this->errno_label($errno) . " ({$errno}): {$errstr} in {$errfile}:{$errline}";
		if ( $this->should_ignore( $msg ) ) return false;
		$this->log( $msg );
		return false;
	}

	public function handle_exception( $ex ) {
		$file = is_object($ex) && method_exists($ex,'getFile') ? $ex->getFile() : 'unknown';
		if ( ! $this->should_log_scope( $file ) ) return;
		$cls  = is_object( $ex ) ? get_class( $ex ) : 'Throwable';
		$msg  = is_object( $ex ) && method_exists( $ex, 'getMessage' ) ? $ex->getMessage() : 'Unknown';
		$line = is_object( $ex ) && method_exists( $ex, 'getLine' ) ? $ex->getLine() : 0;
		$out  = "UNCAUGHT {$cls}: {$msg} in {$file}:{$line}";
		if ( $this->should_ignore( $out ) ) return;
		$trace = is_object( $ex ) && method_exists( $ex, 'getTraceAsString' ) ? $ex->getTraceAsString() : '';
		if ( $trace ) $out .= " | TRACE: " . substr( $trace, 0, 4000 );
		$this->log( $out );
	}

	public function handle_shutdown() {
		$e = error_get_last();
		if ( ! $e ) return;
		if ( ! $this->should_log_scope( $e['file'] ) ) return;
		$fatal_types = array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR );
		if ( in_array( $e['type'], $fatal_types, true ) ) {
			$msg = "CRITICAL FATAL: {$e['message']} in {$e['file']}:{$e['line']}";
			if ( $this->should_ignore( $msg ) ) return;
			$this->log( $msg );
		}
	}

	/* ───────── admin ───────── */

	public function admin_menu() {
		add_menu_page(
			'SSP Debug',
			'SSP Debug',
			'manage_options',
			'ssp-debug',
			array( $this, 'render_admin' ),
			'dashicons-warning',
			62
		);
		add_submenu_page(
			'ssp-debug',
			'SSP Debug',
			'Dashboard',
			'manage_options',
			'ssp-debug',
			array( $this, 'render_admin' )
		);
	}

	public function admin_styles( $hook ) {
		if ( $hook !== 'toplevel_page_ssp-debug' ) return;

		wp_register_style( 'sspd-admin', false, array(), '1.0.0', 'all' );
		wp_enqueue_style( 'sspd-admin' );

		$css = "
.sspd-wrap{background:#fff;border:1px solid #ccd0d4;border-top:none;padding:16px;overflow:hidden;max-width:100%}
.sspd-hero{text-align:center;margin:2px 0 18px}
.sspd-headline{margin:0;font-size:22px;font-weight:800;letter-spacing:.2px}
.sspd-sub{margin:6px 0 0;color:#64748b;font-size:13px}
.sspd-grid{display:grid;gap:16px;justify-content:center;grid-template-columns:repeat(4,minmax(0,1fr));max-width:1280px;margin:0 auto}
@media (max-width:1099.98px){.sspd-grid{grid-template-columns:repeat(3,1fr)}}
@media (max-width:899.98px){.sspd-grid{grid-template-columns:repeat(2,1fr)}}
@media (max-width:599.98px){.sspd-grid{grid-template-columns:repeat(1,1fr)}}
.sspd-card-link{display:block;text-decoration:none;color:inherit;cursor:pointer;outline:none !important;box-shadow:none !important;-webkit-tap-highlight-color:transparent}
.sspd-card-link:focus,.sspd-card-link:focus-visible{outline:0 !important;box-shadow:none !important}
.sspd-card{display:flex;flex-direction:column;border:1px solid #e5e7eb;border-radius:18px;background:#ffffff;box-shadow:0 6px 14px rgba(20,40,120,.06);overflow:hidden;transition:transform .18s ease, box-shadow .18s ease;width:100%;height:100%}
.sspd-card-link:hover .sspd-card,.sspd-card-link:focus-visible .sspd-card{transform:translateY(-4px);box-shadow:0 12px 26px rgba(20,40,120,.16)}
.sspd-thumb-wrap{position:relative;padding:9px 9px 0 9px}
.sspd-thumb{width:100%;aspect-ratio:1/1;object-fit:contain;border-radius:12px;background:#f8fafc}
.sspd-ribbon{position:absolute;top:18px;left:-34px;transform:rotate(-45deg);background:#ef4444;color:#fff;font-weight:800;letter-spacing:.4px;text-transform:uppercase;font-size:11px;width:135px;text-align:center;padding:6px 0;border-radius:5px;box-shadow:0 4px 10px rgba(0,0,0,.15)}
.sspd-body{padding:10px 14px 14px;text-align:center}
.sspd-title{margin:6px 0 0;font-size:16px;line-height:1.35;min-height:44px;display:flex;align-items:center;justify-content:center;text-align:center}
.sspd-actions{margin-top:auto;display:flex;justify-content:center;padding:0 16px 16px}
.sspd-view{display:inline-block;padding:11px 20px;border-radius:999px;background:linear-gradient(90deg,#4caf50,#2196f3);color:#ffffff !important;font-weight:800;font-size:13px;text-decoration:none;box-shadow:0 6px 14px rgba(33,150,243,.25);user-select:none;min-width:108px;text-align:center}
";
		wp_add_inline_style( 'sspd-admin', $css );
	}

	public function plugin_row_links( $links ) {
		$url = admin_url( 'admin.php?page=ssp-debug' );
		$links[] = '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Open Debugging', 'ssp-debugging' ) . '</a>';
		return $links;
	}

	public function handle_download() {
		if ( ! current_user_can( 'manage_options' ) ) wp_die( esc_html__( 'Forbidden', 'ssp-debugging' ) );
		check_admin_referer( 'sspd_download' );
		$path = SSPD_LOG_FILE;

		$fs = sspd_get_fs();
		if ( ! $fs || ! $fs->exists( $path ) ) wp_die( esc_html__( 'Log file not readable.', 'ssp-debugging' ) );

		$contents = (string) $fs->get_contents( $path );

		header( 'Content-Type: text/plain' );
		header( 'Content-Disposition: attachment; filename="ssp-debug-' . gmdate('Ymd-His') . '.log"' );
		header( 'Content-Length: ' . strlen( $contents ) );
		echo $contents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		exit;
	}

	public function render_admin() {
		if ( ! current_user_can( 'manage_options' ) ) return;

		$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard';
		$img_base   = plugin_dir_url( __FILE__ ) . 'assets/images/';

		echo '<div class="wrap">';
		echo '<h1>' . esc_html__( 'Debugging', 'ssp-debugging' ) . '</h1>';
		echo '<h2 class="nav-tab-wrapper" style="margin-top:10px;">';
			echo '<a href="' . esc_url( admin_url( 'admin.php?page=ssp-debug&tab=dashboard' ) ) . '" class="nav-tab ' . ( $active_tab === 'dashboard' ? 'nav-tab-active' : '' ) . '">' . esc_html__( 'Dashboard', 'ssp-debugging' ) . '</a>';
			echo '<a href="' . esc_url( admin_url( 'admin.php?page=ssp-debug&tab=about' ) ) . '" class="nav-tab ' . ( $active_tab === 'about' ? 'nav-tab-active' : '' ) . '">' . esc_html__( 'Provided Free by StupidSimplePlugins.com', 'ssp-debugging' ) . '</a>';
		echo '</h2>';

		if ( $active_tab === 'about' ) {
			echo '<div class="sspd-wrap">';
				echo '<div class="sspd-hero">';
					echo '<h2 class="sspd-headline">' . esc_html__( 'Discover Powerful Plugins', 'ssp-debugging' ) . '</h2>';
					echo '<p class="sspd-sub">' . esc_html__( 'Hand-picked tools to level up WordPress & WooCommerce.', 'ssp-debugging' ) . '</p>';
				echo '</div>';

				$items = array(
					array( 'title' => 'Product Upsell for WooCommerce',     'url' => 'https://www.stupidsimpleplugins.com/product/wo-upsell/',                          'img' => 'upsell.png' ),
					array( 'title' => 'Product Bundles for WooCommerce',    'url' => 'https://www.stupidsimpleplugins.com/product/wo-product-bundles/',                  'img' => 'bundles.png' ),
					array( 'title' => 'Abandoned Checkout for WooCommerce', 'url' => 'https://www.stupidsimpleplugins.com/product/wo-abandoned-checkout/',               'img' => 'abandoned.png' ),
					array( 'title' => 'Ecommerce Countdown Bar',            'url' => 'https://www.stupidsimpleplugins.com/product/countdown-bar/',                        'img' => 'countdown.png' ),
					array( 'title' => 'Republish Old Posts',                'url' => 'https://www.stupidsimpleplugins.com/product/republish-old-posts/',                 'img' => 'republish-old-posts.png' ),
					array( 'title' => 'Plug and Play Geo Blocker',          'url' => 'https://www.stupidsimpleplugins.com/product/block-website-access-by-region-pro/', 'img' => 'geoblocker.png' ),
					array( 'title' => 'Local Website Backups',              'url' => 'https://www.stupidsimpleplugins.com/product/auto-website-backups/',            'img' => 'Local-wesbite-backups.png' ),
					array( 'title' => 'Website Speed Monitor',              'url' => 'https://www.stupidsimpleplugins.com/product/website-speed-monitor/',               'img' => 'speed-monitor.png' ),
				);

				echo '<div class="sspd-grid">';
				foreach ( $items as $it ) {
					echo '<a class="sspd-card-link" href="' . esc_url( $it['url'] ) . '" target="_blank" rel="noopener">';
						echo '<div class="sspd-card">';
							echo '<div class="sspd-thumb-wrap">';
								echo '<span class="sspd-ribbon">' . esc_html__( 'Sale', 'ssp-debugging' ) . '</span>';
								echo '<img class="sspd-thumb" src="' . esc_url( $img_base . $it['img'] ) . '" alt="' . esc_attr( $it['title'] ) . '">';
							echo '</div>';
							echo '<div class="sspd-body">';
								echo '<h3 class="sspd-title">' . esc_html( $it['title'] ) . '</h3>';
							echo '</div>';
							echo '<div class="sspd-actions"><span class="sspd-view">' . esc_html__( 'View', 'ssp-debugging' ) . '</span></div>';
						echo '</div>';
					echo '</a>';
				}
				echo '</div>';

			echo '</div>';
			echo '</div>';
			return;
		}

		if ( isset( $_POST['sspd_save'] ) && check_admin_referer( 'sspd_save' ) ) {
			update_option( self::OPT_ENABLED, isset( $_POST['sspd_enabled'] ) ? 1 : 0 );

			$level_raw = isset( $_POST['sspd_level'] ) ? sanitize_key( wp_unslash( $_POST['sspd_level'] ) ) : 'all';
			update_option( self::OPT_LEVEL, $level_raw === 'fatal' ? 'fatal' : 'all' );

			$size_mb = isset( $_POST['sspd_size_limit_mb'] )
				? max( 0, intval( wp_unslash( $_POST['sspd_size_limit_mb'] ) ) )
				: 5;
			update_option( self::OPT_SIZE_LIMIT, $size_mb );

			$format_raw = isset( $_POST['sspd_format'] ) ? sanitize_key( wp_unslash( $_POST['sspd_format'] ) ) : 'detailed';
			update_option( self::OPT_FORMAT, $format_raw === 'basic' ? 'basic' : 'detailed' );

			$scopes = array();
			foreach ( array('mu-plugins','plugins','themes','core-other') as $key ) {
				if ( isset( $_POST['sspd_scopes'][ $key ] ) ) $scopes[] = $key;
			}
			if ( empty( $scopes ) ) $scopes = array( 'mu-plugins','plugins','themes','core-other' );
			update_option( self::OPT_SCOPES, $scopes );

			$ignore_list = isset( $_POST['sspd_ignore_list'] )
				? sanitize_textarea_field( wp_unslash( $_POST['sspd_ignore_list'] ) )
				: '';
			update_option( self::OPT_IGNORE_LIST, $ignore_list );

			$tz_raw = isset( $_POST['sspd_tz'] ) ? sanitize_text_field( wp_unslash( $_POST['sspd_tz'] ) ) : 'site';
			if ( $tz_raw !== 'site' && strtoupper( $tz_raw ) !== 'UTC' ) {
				$iana = sspd_map_tz_key_to_iana( $tz_raw ) ?: $tz_raw;
				try { new DateTimeZone( $iana ); } catch ( Exception $e ) { $tz_raw = 'site'; }
			}
			update_option( self::OPT_TZ, $tz_raw );

			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'ssp-debugging' ) . '</p></div>';
		}

		if ( isset( $_POST['sspd_clear'] ) && check_admin_referer( 'sspd_clear' ) ) {
			$fs = sspd_get_fs();
			if ( $fs ) $fs->put_contents( SSPD_LOG_FILE, '' );
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Log cleared.', 'ssp-debugging' ) . '</p></div>';
		}

		$enabled     = $this->enabled();
		$level       = $this->level();
		$format      = $this->format();
		$size_mb     = (int) get_option( self::OPT_SIZE_LIMIT, 5 );
		$scopes      = $this->scopes();
		$ignore_list = (string) get_option( self::OPT_IGNORE_LIST, "" );
		$tz_sel_raw  = get_option( self::OPT_TZ, 'site' );

		$path  = SSPD_LOG_FILE;
		$fs    = sspd_get_fs();
		$data  = $fs ? (string) $fs->get_contents( $path ) : '';
		$size  = function_exists( 'wp_filesize' ) ? ( $fs ? wp_filesize( $path ) : 0 ) : strlen( $data );

		$lines = $data !== '' ? explode( "\n", $data ) : array();
		$lines = array_reverse( $lines );
		$log_display = implode( "\n", $lines );

		$tz_names = array(
			'International Date Line West','Samoa Standard Time','Niue Time','Hawaii–Aleutian Standard Time','Cook Islands Time','Tahiti Time','Marquesas Time','Alaska Standard Time','Gambier Time','Pacific Time','Mountain Time','Central Time','Galápagos Time','Eastern Time','Colombia Time','Peru Time','Cuba Standard Time','Atlantic Time','Bolivia Time','Venezuela Time','Newfoundland Time','Argentina Time','Brasília Time','Uruguay Time','French Guiana Time','South Georgia Time','Fernando de Noronha Time','Cape Verde Time','Azores Time','Greenwich Mean Time','Western European Time','Central European Time','West Africa Time','Eastern European Time','Central Africa Time','South Africa Standard Time','Israel Standard Time','Arabia Standard Time','East Africa Time','Turkey Time','Iran Standard Time','Gulf Standard Time','Azerbaijan Time','Georgia Standard Time','Armenia Time','Afghanistan Time','Pakistan Standard Time','Uzbekistan Time','Tajikistan Time','Turkmenistan Time','India Standard Time','Sri Lanka Standard Time','Nepal Time','Bangladesh Standard Time','Bhutan Time','Cocos Islands Time','Myanmar Time','Indochina Time','Western Indonesia Time','China Standard Time','Australian Western Standard Time','Singapore Standard Time','Philippine Standard Time','Central Indonesia Time','Brunei Darussalam Time','Malaysia Time','Australian Central Western Time','Japan Standard Time','Korea Standard Time','Eastern Indonesia Time','Australian Central Standard Time','Australian Eastern Standard Time','Papua New Guinea Time','Chamorro Standard Time','Lord Howe Standard Time','Solomon Islands Time','New Caledonia Time','New Zealand Standard Time','Fiji Time','Marshall Islands Time','Tuvalu Time','Nauru Time','Chatham Standard Time','Tonga Time','Phoenix Island Time','Line Islands Time',
		);

		?>
		<div class="wrap">
			<form method="post" action="" style="margin-bottom:1em;">
				<?php wp_nonce_field( 'sspd_save' ); ?>

				<p>
					<label style="margin-right:20px;">
						<input type="checkbox" name="sspd_enabled" value="1" <?php checked( $enabled ); ?> />
						<?php echo esc_html__( 'Enable logging', 'ssp-debugging' ); ?>
					</label>

					<label style="margin-right:20px;">
						<?php echo esc_html__( 'Level', 'ssp-debugging' ); ?>
						<select name="sspd_level">
							<option value="all"   <?php selected( $level, 'all' ); ?>><?php echo esc_html__( 'All', 'ssp-debugging' ); ?></option>
							<option value="fatal" <?php selected( $level, 'fatal' ); ?>><?php echo esc_html__( 'Fatal-only', 'ssp-debugging' ); ?></option>
						</select>
					</label>

					<label style="margin-right:20px;">
						<?php echo esc_html__( 'Log format', 'ssp-debugging' ); ?>
						<select name="sspd_format">
							<option value="basic"   <?php selected( $format, 'basic' ); ?>><?php echo esc_html__( 'Basic', 'ssp-debugging' ); ?></option>
							<option value="detailed"<?php selected( $format, 'detailed' ); ?>><?php echo esc_html__( 'Detailed (adds URL/IP/UA/User)', 'ssp-debugging' ); ?></option>
						</select>
					</label>

					<label style="margin-right:20px;">
						<?php echo esc_html__( 'Size limit (MB)', 'ssp-debugging' ); ?>
						<input type="number" min="0" step="1" name="sspd_size_limit_mb" value="<?php echo esc_attr( $size_mb ); ?>" style="width:80px;">
						<small>(0 = unlimited; <?php echo esc_html__( 'auto-truncates when exceeded', 'ssp-debugging' ); ?>)</small>
					</label>
				</p>

				<fieldset style="border:1px solid #ddd;padding:10px;margin:10px 0;">
					<legend><strong><?php echo esc_html__( 'Log sources to include', 'ssp-debugging' ); ?></strong></legend>
					<label style="margin-right:20px;">
						<input type="checkbox" name="sspd_scopes[mu-plugins]" value="1" <?php checked( in_array('mu-plugins',$scopes,true) ); ?> />
						<?php echo esc_html__( 'MU-Plugins', 'ssp-debugging' ); ?>
					</label>
					<label style="margin-right:20px;">
						<input type="checkbox" name="sspd_scopes[plugins]" value="1" <?php checked( in_array('plugins',$scopes,true) ); ?> />
						<?php echo esc_html__( 'Plugins', 'ssp-debugging' ); ?>
					</label>
					<label style="margin-right:20px;">
						<input type="checkbox" name="sspd_scopes[themes]" value="1" <?php checked( in_array('themes',$scopes,true) ); ?> />
						<?php echo esc_html__( 'Themes', 'ssp-debugging' ); ?>
					</label>
					<label style="margin-right:20px;">
						<input type="checkbox" name="sspd_scopes[core-other]" value="1" <?php checked( in_array('core-other',$scopes,true) ); ?> />
						<?php echo esc_html__( 'Core / Other', 'ssp-debugging' ); ?>
					</label>
				</fieldset>

				<p>
					<label style="display:block;margin-top:6px;"><strong><?php echo esc_html__( 'Ignore list', 'ssp-debugging' ); ?></strong> (<?php echo esc_html__( 'one per line; substring or regex like', 'ssp-debugging' ); ?> <code>/deprecated/i</code>)</label>
					<textarea name="sspd_ignore_list" rows="6" style="width:100%;font-family:monospace;"><?php echo esc_textarea( $ignore_list ); ?></textarea>
				</p>

				<p>
					<label style="display:block;margin-top:6px;"><strong><?php echo esc_html__( 'Timezone for log timestamps', 'ssp-debugging' ); ?></strong></label>
					<select name="sspd_tz" style="min-width:420px; max-width:100%;">
						<option value="site" <?php selected( $tz_sel_raw, 'site' ); ?>><?php echo esc_html__( 'Site default', 'ssp-debugging' ); ?></option>
						<option value="UTC"  <?php selected( $tz_sel_raw, 'UTC' ); ?>>UTC</option>
						<?php
						foreach ( $tz_names as $label ) {
							echo '<option value="' . esc_attr( $label ) . '"' . selected( $tz_sel_raw, $label, false ) . '>' . esc_html( $label ) . '</option>';
						}
						?>
					</select>
				</p>

				<p>
					<input type="submit" name="sspd_save" class="button button-primary" value="<?php echo esc_attr__( 'Save Settings', 'ssp-debugging' ); ?>">
				</p>
			</form>

			<form method="post" action="" style="margin-bottom:1em;display:inline-block;">
				<?php wp_nonce_field( 'sspd_clear' ); ?>
				<input type="submit" name="sspd_clear" class="button" value="<?php echo esc_attr__( 'Clear Log', 'ssp-debugging' ); ?>">
			</form>

			<a class="button" style="margin-left:6px;"
			   href="<?php echo esc_url( wp_nonce_url( admin_url('admin-post.php?action=sspd_download'), 'sspd_download' ) ); ?>">
				<?php echo esc_html__( 'Download Log', 'ssp-debugging' ); ?>
			</a>

			<p style="margin-top:10px;">
				<strong><?php echo esc_html__( 'Log file:', 'ssp-debugging' ); ?></strong>
				<?php echo esc_html( $path ); ?><?php if ( $size ) echo ' — ' . esc_html( size_format( (int) $size ) ); ?>
			</p>
			<p><?php echo esc_html__( 'Notes: Newest entries are shown first. Only errors/exceptions/fatals from this plugin’s handlers are written here (generic success/info writes via PHP error_log() are suppressed).', 'ssp-debugging' ); ?></p>

			<textarea readonly style="width:100%;height:600px;font-family:monospace;"><?php echo esc_textarea( $log_display ); ?></textarea>
		</div>
		<?php
		echo '</div>';
	}

}

SSPD_Debug_Logger::instance();

endif;
