<?php
/**
 * Error Monitor — catches critical errors and notifies admin + ParcelSelector support.
 *
 * Features:
 * 1. PHP error boundary — catches fatal errors in plugin code
 * 2. API healthcheck — monitors parcelselector.com availability
 * 3. Widget load monitor — tracks if checkout widget loads successfully
 * 4. Auto-email on critical errors — notifies office@parcelselector.pl
 * 5. Self-diagnostics — admin-visible healthcheck results
 *
 * @package ParcelSelector
 * @since   3.4.1
 */

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

class Parcselect_Error_Monitor {

	/** @var string Support email for critical error notifications. */
	const SUPPORT_EMAIL = 'office@parcelselector.pl';

	/** @var int Max emails per day (prevent flood). */
	const MAX_EMAILS_PER_DAY = 3;

	/**
	 * Initialize error monitoring.
	 */
	public static function init() {
		// Track widget load failures from JS (AJAX endpoint).
		add_action( 'wp_ajax_parcselect_report_error', array( __CLASS__, 'ajax_report_error' ) );
		// nopriv removed — frontend JS does not send a nonce, so exposing this to unauthenticated users is a CSRF vector (WP.org BLOCKER 6).

		// API healthcheck — runs on admin_init, max once per hour.
		add_action( 'admin_init', array( __CLASS__, 'maybe_run_healthcheck' ) );

		// Inject error boundary JS on checkout pages.
		add_action( 'wp_footer', array( __CLASS__, 'inject_error_boundary_js' ) );
	}

	/**
	 * Run API healthcheck (max once per hour).
	 */
	public static function maybe_run_healthcheck() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		$last = get_transient( 'parcselect_healthcheck_last' );
		if ( $last ) {
			return;
		}

		$results = self::run_healthcheck();
		set_transient( 'parcselect_healthcheck_last', $results, HOUR_IN_SECONDS );

		// If critical failure → send email (rate-limited).
		$critical = array_filter( $results, function( $r ) {
			return 'critical' === ( $r['severity'] ?? '' );
		} );
		if ( ! empty( $critical ) ) {
			self::send_error_email( 'Healthcheck failed', $critical );
		}
	}

	/**
	 * Run all diagnostic checks.
	 *
	 * @return array Array of check results.
	 */
	public static function run_healthcheck() {
		$checks = array();

		// 1. API connectivity.
		$api_url  = ( defined( 'PARCSELECT_API_BASE' ) ? PARCSELECT_API_BASE : 'https://parcelselector.com' ) . '/api/points/pl?carrier=inpost&limit=1';
		$response = wp_remote_get( $api_url, array( 'timeout' => 10, 'sslverify' => true ) );
		if ( is_wp_error( $response ) ) {
			$checks['api'] = array(
				'label'    => __( 'API Connection', 'parcel-selector' ),
				'status'   => 'fail',
				'severity' => 'critical',
				'message'  => $response->get_error_message(),
				'fix'      => __( 'Check server firewall — parcelselector.com must be reachable.', 'parcel-selector' ),
			);
		} else {
			$code = wp_remote_retrieve_response_code( $response );
			$checks['api'] = array(
				'label'   => __( 'API Connection', 'parcel-selector' ),
				'status'  => ( 200 === $code ) ? 'ok' : 'warn',
				'message' => 'HTTP ' . $code,
			);
		}

		// 2. WooCommerce active.
		$checks['woocommerce'] = array(
			'label'    => 'WooCommerce',
			'status'   => class_exists( 'WooCommerce' ) ? 'ok' : 'fail',
			'severity' => class_exists( 'WooCommerce' ) ? '' : 'critical',
			'message'  => class_exists( 'WooCommerce' ) ? WC()->version : __( 'Not installed', 'parcel-selector' ),
			'fix'      => __( 'Install and activate WooCommerce.', 'parcel-selector' ),
		);

		// 3. Shipping method registered.
		$method_active = false;
		if ( class_exists( 'WC_Shipping_Zones' ) ) {
			$zones = array_merge( WC_Shipping_Zones::get_zones(), array( array( 'zone_id' => 0 ) ) );
			foreach ( $zones as $zdata ) {
				$zone = new \WC_Shipping_Zone( $zdata['zone_id'] );
				foreach ( $zone->get_shipping_methods() as $m ) {
					if ( 'parcselect_inpost_paczkomat' === $m->id && 'yes' === $m->enabled ) {
						$method_active = true;
						break 2;
					}
				}
			}
		}
		$checks['shipping'] = array(
			'label'    => __( 'Shipping Method', 'parcel-selector' ),
			'status'   => $method_active ? 'ok' : 'warn',
			'severity' => $method_active ? '' : 'high',
			'message'  => $method_active ? __( 'Active in at least one zone', 'parcel-selector' ) : __( 'Not found in any shipping zone', 'parcel-selector' ),
			'fix'      => __( 'Go to WooCommerce → Settings → Shipping → Add "InPost Parcel Locker" method.', 'parcel-selector' ),
		);

		// 4. Label service connection (optional feature).
		$service_connected = (bool) get_option( 'parcselect_activated', false );
		$checks['label_service'] = array(
			'label'   => __( 'Label service', 'parcel-selector' ),
			'status'  => $service_connected ? 'ok' : 'warn',
			'message' => $service_connected ? __( 'Connected', 'parcel-selector' ) : __( 'Not connected', 'parcel-selector' ),
			'fix'     => __( 'Optional — set up in the Label tool tab to generate shipping labels.', 'parcel-selector' ),
		);

		// 5. Origin country set.
		$origin = function_exists( 'parcselect_get_shop_origin' ) ? parcselect_get_shop_origin() : '';
		$checks['origin'] = array(
			'label'   => __( 'Ship-from Country', 'parcel-selector' ),
			'status'  => ! empty( $origin ) ? 'ok' : 'warn',
			'message' => ! empty( $origin ) ? $origin : __( 'Not set', 'parcel-selector' ),
			'fix'     => __( 'Set in Maps tab → "I ship parcels from..."', 'parcel-selector' ),
		);

		// 6. JS assets loadable (file exists check).
		$bridge_file = PARCSELECT_PLUGIN_DIR . 'assets/js/ps-wc-bridge.js';
		$checks['js_bridge'] = array(
			'label'   => __( 'Checkout Widget JS', 'parcel-selector' ),
			'status'  => file_exists( $bridge_file ) ? 'ok' : 'fail',
			'severity' => file_exists( $bridge_file ) ? '' : 'critical',
			'message' => file_exists( $bridge_file ) ? 'ps-wc-bridge.js' : __( 'File missing!', 'parcel-selector' ),
			'fix'     => __( 'Reinstall the plugin — critical JS file is missing.', 'parcel-selector' ),
		);

		// 7. SSL (HTTPS) check.
		$checks['ssl'] = array(
			'label'   => 'HTTPS',
			'status'  => is_ssl() ? 'ok' : 'warn',
			'message' => is_ssl() ? __( 'Enabled', 'parcel-selector' ) : __( 'Not detected — crypto payments require HTTPS', 'parcel-selector' ),
		);

		return $checks;
	}

	/**
	 * Get last healthcheck results (cached).
	 *
	 * @return array|false
	 */
	public static function get_last_results() {
		return get_transient( 'parcselect_healthcheck_last' );
	}

	/**
	 * AJAX: Report error from frontend JS (widget load failure, map error, etc).
	 */
	public static function ajax_report_error() {
		check_ajax_referer( 'parcselect_error_report', 'nonce' );

		// Rate limit: max 5 reports per IP per hour.
		$ip   = sanitize_text_field( isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : 'unknown' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- REMOTE_ADDR is server-set.
		$lock = 'parcselect_err_' . md5( $ip );
		$cnt  = (int) get_transient( $lock );
		if ( $cnt >= 5 ) {
			wp_send_json_error( 'rate_limited' );
		}
		set_transient( $lock, $cnt + 1, HOUR_IN_SECONDS );

		$type    = sanitize_key( isset( $_POST['error_type'] ) ? $_POST['error_type'] : 'unknown' );
		$message = sanitize_text_field( wp_unslash( isset( $_POST['message'] ) ? $_POST['message'] : '' ) );
		$url     = esc_url_raw( wp_unslash( isset( $_POST['page_url'] ) ? $_POST['page_url'] : '' ) );
		$ua      = sanitize_text_field( isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- server header.

		$log_entry = sprintf(
			'[PS_ERROR] type=%s | msg=%s | url=%s | ip=%s | ua=%s',
			$type, mb_substr( $message, 0, 200 ), $url, $ip, mb_substr( $ua, 0, 100 )
		);

		if ( function_exists( 'parcselect_log' ) ) {
			parcselect_log( $log_entry );
		}
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log( $log_entry ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- only when WP_DEBUG enabled.
		}

		// Critical errors → email.
		$critical_types = array( 'widget_crash', 'api_timeout', 'js_fatal', 'checkout_blocked' );
		if ( in_array( $type, $critical_types, true ) ) {
			self::send_error_email( 'Frontend error: ' . $type, array(
				array( 'message' => $message, 'url' => $url, 'ip' => $ip ),
			) );
		}

		wp_send_json_success( 'logged' );
	}

	/**
	 * Send error notification email (rate-limited: max 3/day).
	 *
	 * @param string $subject Short description.
	 * @param array  $details Array of error details.
	 */
	public static function send_error_email( $subject, $details ) {
		$today_key = 'parcselect_err_emails_' . gmdate( 'Ymd' );
		$sent      = (int) get_transient( $today_key );
		if ( $sent >= self::MAX_EMAILS_PER_DAY ) {
			return;
		}
		set_transient( $today_key, $sent + 1, DAY_IN_SECONDS );

		$site_url  = home_url();
		$site_name = get_bloginfo( 'name' );
		$admin_email = get_option( 'admin_email' );
		$install_id  = get_option( 'parcselect_installation_id', 'unknown' );

		$body = "ParcelSelector Error Report\n";
		$body .= "==========================\n\n";
		$body .= "Site: {$site_name} ({$site_url})\n";
		$body .= "Installation: {$install_id}\n";
		$body .= "Admin: {$admin_email}\n";
		$body .= "Time: " . current_time( 'mysql' ) . "\n\n";
		$body .= "Subject: {$subject}\n\n";

		foreach ( $details as $d ) {
			if ( is_array( $d ) ) {
				foreach ( $d as $k => $v ) {
					$body .= "  {$k}: {$v}\n";
				}
				$body .= "\n";
			} else {
				$body .= "  {$d}\n";
			}
		}

		$body .= "\n--\nParcelSelector v" . ( defined( 'PARCSELECT_VERSION' ) ? PARCSELECT_VERSION : '?' );

		$headers = array( 'Content-Type: text/plain; charset=UTF-8' );

		// Always send to shop admin.
		wp_mail( $admin_email, '[ParcelSelector] ' . $subject, $body, $headers );

		// Send to PS support ONLY if admin opted in (Settings → Parcel Selector → Share diagnostics).
		if ( 'yes' === get_option( 'parcselect_share_diagnostics', 'no' ) ) {
			wp_mail( self::SUPPORT_EMAIL, '[PS Error] ' . $subject . ' — ' . $site_name, $body, $headers );
		}
	}

	/**
	 * Inject error boundary JS on checkout/cart pages.
	 * Catches widget load failures and reports them.
	 */
	public static function inject_error_boundary_js() {
		if ( ! function_exists( 'is_checkout' ) && ! function_exists( 'is_cart' ) ) {
			return;
		}
		if ( ! ( ( function_exists( 'is_checkout' ) && is_checkout() ) || ( function_exists( 'is_cart' ) && is_cart() ) ) ) {
			return;
		}

		$ajax_url    = esc_url( admin_url( 'admin-ajax.php' ) );
		$err_nonce   = esc_js( wp_create_nonce( 'parcselect_error_report' ) );
		$error_msg   = esc_js( __( 'Map loading issue detected. Please refresh the page (F5). If this persists, try a different browser or disable ad blockers.', 'parcel-selector' ) );

		// Build inline JS via string concatenation (no heredoc). Variables are pre-escaped:
		// $ajax_url through esc_url(), $err_nonce / $error_msg through esc_js().
		$js  = "(function(){";
		$js .= "var _psErrorSent=false;";
		$js .= "function psReportError(type,msg){";
		$js .=   "if(_psErrorSent)return;";
		$js .=   "_psErrorSent=true;";
		$js .=   "var xhr=new XMLHttpRequest();";
		$js .=   "xhr.open('POST','" . $ajax_url . "',true);";
		$js .=   "xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');";
		$js .=   "xhr.send('action=parcselect_report_error&nonce=" . $err_nonce . "&error_type='+encodeURIComponent(type)+'&message='+encodeURIComponent(msg)+'&page_url='+encodeURIComponent(location.href));";
		$js .= "}";
		$js .= "setTimeout(function(){";
		$js .=   "var hasInpost=document.querySelector('input[value*=\"parcselect_inpost_paczkomat\"]:checked,.wc-block-components-radio-control__input[value*=\"parcselect_inpost\"]:checked');";
		$js .=   "var hasUI=document.querySelector('.ps-selector-widget,.ps-selected-locker,.ps-open-map,.ps-blocks-widget-anchor');";
		$js .=   "if(hasInpost&&!hasUI){";
		$js .=     "psReportError('widget_missing','InPost shipping selected but widget not visible after 10s');";
		$js .=     "var target=document.querySelector('.woocommerce-shipping-totals,.wc-block-components-shipping-rates-control');";
		$js .=     "if(target){";
		$js .=       "var m=document.createElement('div');";
		$js .=       "m.style.cssText='padding:12px 16px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;margin:10px 0;font-size:13px;color:#991b1b;';";
		$js .=       "m.textContent='" . $error_msg . "';";
		$js .=       "target.parentNode.insertBefore(m,target.nextSibling);";
		$js .=     "}";
		$js .=   "}";
		$js .= "},20000);";
		$js .= "window.addEventListener('error',function(e){";
		$js .=   "if(e.filename&&(e.filename.indexOf('parcselect-wc-bridge')!==-1||e.filename.indexOf('anunaki')!==-1)){";
		$js .=     "psReportError('js_fatal',e.message+' at '+e.filename+':'+e.lineno);";
		$js .=   "}";
		$js .= "});";
		$js .= "})();";

		// Register a dummy handle to attach inline script to, output in footer.
		wp_register_script( 'parcselect-error-boundary', false, array(), PARCSELECT_VERSION, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
		wp_enqueue_script( 'parcselect-error-boundary' );
		wp_add_inline_script( 'parcselect-error-boundary', $js );
	}
}

Parcselect_Error_Monitor::init();
