<?php
/**
 * Dashboard view (2.4.0 M1.2): summary header, recent activity, quick
 * actions. Top-level menu landing page; sister to the Settings sub-page.
 *
 * View body is wrapped in a render function to keep locals out of the
 * includer's scope (Plugin Check NonPrefixedVariableFound rule).
 *
 * Called from DOIG_Admin::render_dashboard().
 *
 * @package dynamic-og-image-generator
 */

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

if ( ! function_exists( 'doig_render_dashboard_page' ) ) {

	function doig_render_dashboard_page() {

		// Boot-integrity guard - mirrors the one in settings-page.php so a
		// half-installed plugin shows a useful message instead of fatal-erroring
		// on a missing static method.
		$required_methods = array( 'render_page_header', 'render_admin_footer', 'docs_url', 'brand_url' );
		$missing          = array();
		foreach ( $required_methods as $method ) {
			if ( ! method_exists( 'DOIG_Plugin', $method ) ) {
				$missing[] = $method;
			}
		}
		if ( ! empty( $missing ) ) {
			echo '<div class="wrap"><h1>' . esc_html__( 'PlugUpp Dynamic OG Image Generator', 'dynamic-og-image-generator' ) . '</h1>';
			echo '<div class="notice notice-error"><p><strong>' . esc_html__( 'The plugin installation appears to be incomplete or stale.', 'dynamic-og-image-generator' ) . '</strong></p>';
			echo '<p><code>Missing: ' . esc_html( implode( ', ', $missing ) ) . '</code></p>';
			echo '</div></div>';
			return;
		}

		// The three status tables are WP_List_Table subclasses (2.5.2). Loaded
		// here rather than at plugin boot so WP_List_Table (a wp-admin include)
		// is only pulled in on the screen that renders them.
		require_once DOIG_PLUGIN_DIR . 'admin/class-dashboard-tables.php';

		$is_premium  = DOIG_License::is_premium();
		$settings_url = admin_url( 'admin.php?page=doig-settings' );

		// --- Counts: ready / pending / failed -----------------------------------
		//
		// The Dashboard summary uses three simple status counts and the
		// timestamp of the most recent successful capture. Single WP_Query per
		// status, fields=ids + posts_per_page=-1 then count() - fine for our
		// expected order of magnitude (dozens to low thousands of pages); if
		// performance becomes a concern at higher scale we can move to a
		// cached aggregate updated on capture state transitions.
		//
		// No `suppress_filters` parameter. The VIP coding standard
		// (WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters)
		// prohibits `suppress_filters => true` because it bypasses
		// posts_* filters that other plugins legitimately hook into.
		// The default (false) lets caching plugins and others adjust
		// the query normally; the Dashboard runs once per admin page
		// load, so the filter cost is acceptable. Removed from these
		// queries on 2026-06-23 in response to Plugin Check ERRORs;
		// do not re-add.
		$ready_ids = get_posts( array(
			'post_type'        => 'any',
			'post_status'      => 'any',
			'posts_per_page'   => -1,
			'fields'           => 'ids',
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- admin Dashboard, low frequency, intentional filter on _doig_status to count Ready captures; bounded by site post count.
			'meta_query'       => array(
				array(
					'key'     => '_doig_status',
					'value'   => 'ready',
					'compare' => '=',
				),
			),
		) );

		$pending_ids = get_posts( array(
			'post_type'        => 'any',
			'post_status'      => 'any',
			'posts_per_page'   => -1,
			'fields'           => 'ids',
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- admin Dashboard, low frequency, intentional filter on _doig_status to count Pending captures; bounded by in-flight job count which is small.
			'meta_query'       => array(
				array(
					'key'     => '_doig_status',
					'value'   => 'pending',
					'compare' => '=',
				),
			),
		) );

		$failed_ids = get_posts( array(
			'post_type'        => 'any',
			'post_status'      => 'any',
			'posts_per_page'   => -1,
			'fields'           => 'ids',
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- admin Dashboard, low frequency, intentional filter on _doig_status to count Failed + Quota-exceeded captures; bounded by site post count.
			'meta_query'       => array(
				'relation' => 'OR',
				array(
					'key'     => '_doig_status',
					'value'   => 'failed',
					'compare' => '=',
				),
				array(
					'key'     => '_doig_status',
					'value'   => 'quota_exceeded',
					'compare' => '=',
				),
			),
		) );

		// 2.7.0 M5: surfaces (homepage, archives, taxonomy and author
		// pages) live outside postmeta, so the queries above cannot see
		// them - merge their IDs in. The status tables and counts then
		// carry them like any other row; the table columns render surface
		// labels via the registry.
		$ready_ids   = array_merge( $ready_ids, DOIG_Surfaces::surface_ids_with_status( array( 'ready' ) ) );
		$pending_ids = array_merge( $pending_ids, DOIG_Surfaces::surface_ids_with_status( array( 'pending' ) ) );
		$failed_ids  = array_merge( $failed_ids, DOIG_Surfaces::surface_ids_with_status( array( 'failed', 'quota_exceeded' ) ) );

		$ready_count   = count( $ready_ids );
		$pending_count = count( $pending_ids );
		$failed_count  = count( $failed_ids );

		// --- Last captured timestamp -------------------------------------------
		$last_captured_label = __( 'Never', 'dynamic-og-image-generator' );
		if ( $ready_count > 0 ) {
			$latest = get_posts( array(
				'post_type'        => 'any',
				'post_status'      => 'any',
				'posts_per_page'   => 1,
				'fields'           => 'ids',
				'meta_key'         => '_doig_cached_at', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- intentional sort against the cached-at timestamp; dashboard is a low-frequency admin-only page so the slow_db_query notice is acceptable.
				'orderby'          => 'meta_value_num',
				'order'            => 'DESC',
			) );
			if ( ! empty( $latest ) ) {
				$ts = (int) get_post_meta( $latest[0], '_doig_cached_at', true );
				if ( $ts > 0 ) {
					$last_captured_label = sprintf(
						/* translators: %s: human-readable time-since, e.g. "5 minutes" */
						__( '%s ago', 'dynamic-og-image-generator' ),
						human_time_diff( $ts, time() )
					);
				}
			}
		}

		// --- Pending list (top 20 by scheduled_at DESC) ------------------------
		$pending_list_query = $pending_count > 0
			? new WP_Query( array(
				'post_type'        => 'any',
				'post_status'      => 'any',
				'posts_per_page'   => 20,
				'fields'           => 'ids',
				'no_found_rows'    => true,
				'meta_key'         => '_doig_scheduled_at', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- admin Dashboard, low frequency, deliberate sort by queue time.
				'orderby'          => 'meta_value_num',
				'order'            => 'DESC',
				'meta_query'       => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					array(
						'key'     => '_doig_status',
						'value'   => 'pending',
						'compare' => '=',
					),
				),
			) )
			: null;

		// --- Failed list (top 20 by post modified DESC) ------------------------
		$failed_list_query = $failed_count > 0
			? new WP_Query( array(
				'post_type'        => 'any',
				'post_status'      => 'any',
				'posts_per_page'   => 20,
				'fields'           => 'ids',
				'no_found_rows'    => true,
				'orderby'          => 'modified',
				'order'            => 'DESC',
				'meta_query'       => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					'relation' => 'OR',
					array(
						'key'     => '_doig_status',
						'value'   => 'failed',
						'compare' => '=',
					),
					array(
						'key'     => '_doig_status',
						'value'   => 'quota_exceeded',
						'compare' => '=',
					),
				),
			) )
			: null;

		// --- Recent activity (latest 20 ready captures) ------------------------
		$recent_query = new WP_Query( array(
			'post_type'        => 'any',
			'post_status'      => 'any',
			'posts_per_page'   => 20,
			'fields'           => 'ids',
			'no_found_rows'    => true,
			'meta_key'         => '_doig_cached_at', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			'orderby'          => 'meta_value_num',
			'order'            => 'DESC',
			'meta_query'       => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				array(
					'key'     => '_doig_status',
					'value'   => 'ready',
					'compare' => '=',
				),
			),
		) );

		?>
		<div class="wrap doig-wrap">

			<?php
			$doig_page_title = $is_premium
				? __( 'PlugUpp Dynamic OG Image Generator Premium', 'dynamic-og-image-generator' )
				: __( 'PlugUpp Dynamic OG Image Generator', 'dynamic-og-image-generator' );
			DOIG_Plugin::render_page_header( $doig_page_title, 'dashboard' );
			?>

			<?php
			// 2026-06-25: Upgrade banner. Lives on the Dashboard rather
			// than the Settings/Logs tabs per _standards/docs/CONVENTIONS.md
			// "Banner placement: the primary high-traffic screen, never
			// co-located with the Premium tab". The Dashboard is the
			// primary screen the operator opens (parent menu lands here),
			// so the banner reaches the high-context surface; the Premium
			// tab on Settings carries the consideration funnel.
			// SSAA 1.0.13 visual standard: bordered star icon + bold
			// headline + named-features sub-line + standalone
			// "Upgrade to Premium" button to the Freemius upgrade URL.
			// Suppressed entirely on premium installs (! $is_premium guard).
			$doig_upgrade_url = doig_fs()->get_upgrade_url();
			if ( ! $is_premium && '' !== $doig_upgrade_url ) :
			?>
			<div class="doig-upgrade-banner">
				<span class="doig-upgrade-banner__icon dashicons dashicons-star-filled" aria-hidden="true"></span>
				<div class="doig-upgrade-banner__content">
					<strong><?php esc_html_e( 'Do more with Premium', 'dynamic-og-image-generator' ); ?></strong>
					<span><?php esc_html_e( 'Custom post types and WooCommerce, per-page capture overrides, country routing, hide selectors, and Media Library integration.', 'dynamic-og-image-generator' ); ?></span>
				</div>
				<a class="button button-primary doig-upgrade-banner__button" href="<?php echo esc_url( $doig_upgrade_url ); ?>">
					<?php esc_html_e( 'Upgrade to Premium', 'dynamic-og-image-generator' ); ?>
				</a>
			</div>
			<?php endif; ?>

			<?php
			// Headline quota line + plan name. Populated by the same AJAX call
			// that pings /v1/health on the Settings page. Hidden when the
			// install is not connected - the setup banner on the Settings
			// page already tells the user what to do.
			if ( DOIG_License::is_connected() ) :
			?>
			<div id="doig-usage" class="doig-usage doig-usage--loading doig-dashboard-headline">
				<p><?php esc_html_e( 'Loading usage…', 'dynamic-og-image-generator' ); ?></p>
			</div>
			<?php else : ?>
			<div class="notice notice-warning inline">
				<p>
					<?php esc_html_e( 'Connect your PlugUpp licence on the Settings page to start capturing screenshots.', 'dynamic-og-image-generator' ); ?>
					<a href="<?php echo esc_url( $settings_url ); ?>"><?php esc_html_e( 'Open Settings →', 'dynamic-og-image-generator' ); ?></a>
				</p>
			</div>
			<?php endif; ?>

			<?php
			// --- Buried-diagnostics notices (2.6.2, P1 item 4) -----------------
			// Three conditions that previously lived only in the log file, or
			// nowhere at all. Read-only surfacing of existing state.

			// 1. Permanent failures: the Failed table below shows the detail,
			// but only to someone who scrolls. Say it up front.
			if ( $failed_count > 0 ) :
			?>
			<div class="notice notice-warning inline">
				<p>
					<?php
					printf(
						/* translators: %d: number of posts whose screenshot failed permanently. */
						esc_html( _n(
							'%d screenshot has failed permanently. The Failed table below lists the post and the reason - most failures include a troubleshooting link.',
							'%d screenshots have failed permanently. The Failed table below lists each post and the reason - most failures include a troubleshooting link.',
							$failed_count,
							'dynamic-og-image-generator'
						) ),
						(int) $failed_count
					);
					?>
				</p>
			</div>
			<?php
			endif;

			// 2. Callback blocked: captures are completing via the polling
			// fallback instead of the instant callback (recorded by the
			// poller when it wins a 15-minute-late job). Informational -
			// captures still work, just slower.
			$doig_cb_blocked_at = (int) get_option( 'doig_callback_blocked_at', 0 );
			if ( $doig_cb_blocked_at > time() - WEEK_IN_SECONDS ) :
			?>
			<div class="notice notice-info inline">
				<p>
					<?php esc_html_e( 'Recent screenshots completed via the slower polling fallback rather than the instant callback. Your host or a security plugin may be blocking requests to the doig/v1/callback endpoint. This is safe - captures still complete - but they can take up to 15 minutes longer than necessary.', 'dynamic-og-image-generator' ); ?>
				</p>
			</div>
			<?php
			endif;

			// 3. Queue backlog: work is waiting but the processor has not
			// run for 30+ minutes - the WP-Cron-is-stalled condition.
			$doig_backlog_queue = (array) get_option( 'doig_regen_queue', array() );
			$doig_last_run_at   = (int) get_option( 'doig_queue_last_run_at', 0 );
			if ( count( $doig_backlog_queue ) > 0 && $doig_last_run_at < time() - 30 * MINUTE_IN_SECONDS ) :
			?>
			<div class="notice notice-warning inline">
				<p>
					<?php
					printf(
						/* translators: %d: number of posts waiting in the capture queue. */
						esc_html( _n(
							'%d post is waiting in the capture queue but the queue processor has not run for over 30 minutes. WP-Cron may be stalled on this site - it usually needs a visitor to fire, or a server-side cron job as a replacement.',
							'%d posts are waiting in the capture queue but the queue processor has not run for over 30 minutes. WP-Cron may be stalled on this site - it usually needs a visitor to fire, or a server-side cron job as a replacement.',
							count( $doig_backlog_queue ),
							'dynamic-og-image-generator'
						) ),
						(int) count( $doig_backlog_queue )
					);
					?>
				</p>
			</div>
			<?php
			endif;
			?>

			<!-- Summary boxes (2.4.0 M1.4: inline styles ported to .doig-summary* CSS classes in admin.css) -->
			<div class="doig-summary">
				<div class="doig-summary-box">
					<span class="doig-summary-value"><?php echo esc_html( (string) $ready_count ); ?></span>
					<span class="doig-summary-label"><?php esc_html_e( 'Cached screenshots', 'dynamic-og-image-generator' ); ?></span>
				</div>
				<div class="doig-summary-box">
					<span class="doig-summary-value<?php echo $pending_count > 0 ? ' doig-summary-value--pending' : ''; ?>"><?php echo esc_html( (string) $pending_count ); ?></span>
					<span class="doig-summary-label"><?php esc_html_e( 'Pending', 'dynamic-og-image-generator' ); ?></span>
				</div>
				<div class="doig-summary-box">
					<span class="doig-summary-value<?php echo $failed_count > 0 ? ' doig-summary-value--failed' : ''; ?>"><?php echo esc_html( (string) $failed_count ); ?></span>
					<span class="doig-summary-label"><?php esc_html_e( 'Failed', 'dynamic-og-image-generator' ); ?></span>
				</div>
				<div class="doig-summary-box">
					<span class="doig-summary-value doig-summary-value--time"><?php echo esc_html( $last_captured_label ); ?></span>
					<span class="doig-summary-label"><?php esc_html_e( 'Last captured', 'dynamic-og-image-generator' ); ?></span>
				</div>
			</div>

			<?php
			// Quick actions sit directly under the status cards (2.5.2). They were
			// previously buried below the Pending / Failed / Recent tables, where a
			// site with a long activity list never saw them. The warning copy runs
			// full width - no max-width container.
			?>
			<h2><?php esc_html_e( 'Quick actions', 'dynamic-og-image-generator' ); ?></h2>
			<p>
				<button type="button" id="doig-regenerate-all" class="button button-primary">
					<?php esc_html_e( 'Regenerate All Screenshots', 'dynamic-og-image-generator' ); ?>
				</button>
				<span id="doig-cache-result" class="doig-inline-result" style="margin-left:8px;"></span>
			</p>
			<p class="description">
				<?php esc_html_e( 'Queues a fresh screenshot for every enabled page and post, regardless of when they were last captured. Existing screenshots continue to serve until the new ones are ready. This may use a significant portion of your monthly quota on large sites.', 'dynamic-og-image-generator' ); ?>
			</p>
			<p class="description">
				<strong class="doig-warning-label"><?php esc_html_e( 'Warning:', 'dynamic-og-image-generator' ); ?></strong>
				<?php esc_html_e( 'Before running bulk operations like this, take a fresh backup of your site (database and uploads directory). Bulk regeneration writes new Media Library attachments for every enabled post and replaces existing attachments.', 'dynamic-og-image-generator' ); ?>
			</p>

			<?php
			// --- Pending list ------------------------------------------------
			$doig_pending_ids = $pending_list_query ? (array) $pending_list_query->posts : array();
			// 2.7.0 M5: pending surfaces join the table (post query cannot
			// see them); keep the 20-row cap after the merge.
			$doig_pending_ids = array_slice(
				array_merge( DOIG_Surfaces::surface_ids_with_status( array( 'pending' ) ), $doig_pending_ids ),
				0,
				20
			);
			if ( ! empty( $doig_pending_ids ) ) :
			?>
			<h2><?php
				/* translators: %d: number of pending screenshots */
				echo esc_html( sprintf( _n( 'Pending screenshot (%d)', 'Pending screenshots (%d)', $pending_count, 'dynamic-og-image-generator' ), $pending_count ) );
			?></h2>
			<p class="description" style="margin:4px 0 8px;">
				<?php esc_html_e( 'These captures are queued or in flight. Most resolve within a minute or two. A capture that stays pending for longer than 10 minutes is usually waiting on a slow proxy callback or a retry backoff.', 'dynamic-og-image-generator' ); ?>
			</p>
			<?php
			$doig_pending_table = new DOIG_Pending_Table( $doig_pending_ids );
			$doig_pending_table->prepare_items();
			$doig_pending_table->display();
			?>
			<?php if ( $pending_count > 20 ) : ?>
			<p class="description" style="margin-top:6px;">
				<?php
				/* translators: %d: number of pending captures not shown in the list */
				echo esc_html( sprintf( __( 'Showing the 20 most recently queued. %d more not shown.', 'dynamic-og-image-generator' ), $pending_count - 20 ) );
				?>
			</p>
			<?php endif; ?>
			<?php endif; ?>

			<?php
			// --- Failed list -------------------------------------------------
			$doig_failed_ids = $failed_list_query ? (array) $failed_list_query->posts : array();
			// 2.7.0 M5: failed surfaces join the table.
			$doig_failed_ids = array_slice(
				array_merge( DOIG_Surfaces::surface_ids_with_status( array( 'failed', 'quota_exceeded' ) ), $doig_failed_ids ),
				0,
				20
			);
			if ( ! empty( $doig_failed_ids ) ) :
			?>
			<h2><?php
				/* translators: %d: number of failed screenshots */
				echo esc_html( sprintf( _n( 'Failed screenshot (%d)', 'Failed screenshots (%d)', $failed_count, 'dynamic-og-image-generator' ), $failed_count ) );
			?></h2>
			<p class="description" style="margin:4px 0 8px;">
				<?php esc_html_e( 'These captures failed. The reason column shows the error reported by the screenshot service. Editing and saving the post re-queues the capture; the Logs tab on the Settings page has the full event history.', 'dynamic-og-image-generator' ); ?>
			</p>
			<?php
			$doig_failed_table = new DOIG_Failed_Table( $doig_failed_ids );
			$doig_failed_table->prepare_items();
			$doig_failed_table->display();
			?>
			<?php if ( $failed_count > 20 ) : ?>
			<p class="description" style="margin-top:6px;">
				<?php
				/* translators: %d: number of failed captures not shown in the list */
				echo esc_html( sprintf( __( 'Showing the 20 most recently modified. %d more not shown.', 'dynamic-og-image-generator' ), $failed_count - 20 ) );
				?>
			</p>
			<?php endif; ?>
			<?php endif; ?>

			<!-- Recent activity -->
			<h2><?php esc_html_e( 'Recent screenshot activity', 'dynamic-og-image-generator' ); ?></h2>
			<?php
			$doig_recent_ids = (array) $recent_query->posts;
			// 2.7.0 M5: ready surfaces join Recent activity, ordered by
			// their cached_at against the post rows, capped at 20.
			$doig_surface_recent = DOIG_Surfaces::surface_ids_with_status( array( 'ready' ) );
			if ( ! empty( $doig_surface_recent ) ) {
				$doig_recent_ids = array_merge( $doig_recent_ids, $doig_surface_recent );
				usort( $doig_recent_ids, function ( $a, $b ) {
					return (int) DOIG_Post_Meta::get( $b, 'cached_at' ) <=> (int) DOIG_Post_Meta::get( $a, 'cached_at' );
				} );
				$doig_recent_ids = array_slice( $doig_recent_ids, 0, 20 );
			}
			if ( ! empty( $doig_recent_ids ) ) :
				$doig_recent_table = new DOIG_Recent_Table( $doig_recent_ids );
				$doig_recent_table->prepare_items();
				$doig_recent_table->display();
			else :
			?>
			<p class="description">
				<?php esc_html_e( 'No screenshots have been captured yet. Once the daily refresh runs or you publish an enabled post or page, captures will appear here.', 'dynamic-og-image-generator' ); ?>
			</p>
			<?php endif; ?>

			<?php
			// Full-size image lightbox (click a Recent activity thumbnail).
			// Populated by admin.js; same pattern as SSAA's preview lightbox.
			?>
			<div id="doig-img-lightbox" class="doig-img-lightbox" hidden>
				<button type="button" class="doig-img-lightbox-close" aria-label="<?php esc_attr_e( 'Close', 'dynamic-og-image-generator' ); ?>">&times;</button>
				<figure class="doig-lightbox-stage">
					<span class="doig-lightbox-imgwrap"><img id="doig-img-lightbox-img" src="" alt="" /></span>
				</figure>
			</div>

			<?php DOIG_Plugin::render_admin_footer( 'dashboard' ); ?>

		</div>
		<?php
	}
}

// Invoke.
doig_render_dashboard_page();
