<?php
/**
 * Dashboard list tables (2.5.2).
 *
 * The three Dashboard tables (Pending / Failed / Recent activity) are
 * WP_List_Table subclasses rather than hand-rolled markup, so they inherit
 * WordPress's own list-table conventions for free: the bold `row-title`
 * link, the hover-revealed `Edit | View` row actions with their `|`
 * separators and screen-reader text, post-state suffixes, the responsive
 * primary-column toggle, and core's admin font/spacing.
 *
 * Nothing in this plugin styles those - core's list-tables CSS does,
 * exactly as it does on the Posts and Pages screens. That is the whole
 * point of extending WP_List_Table: the row-actions hover is pure core
 * CSS, no JS. Same pattern SSAA uses (see its class-results-table.php).
 *
 * These are read-only status tables: no bulk actions, no sorting, no
 * pagination, so display_tablenav() is a deliberate no-op.
 *
 * @package Dynamic_OG_Image_Generator
 */

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

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Shared behaviour for the three Dashboard tables.
 */
abstract class DOIG_Dashboard_Table extends WP_List_Table {

	/**
	 * Post IDs to render, in display order.
	 *
	 * @var int[]
	 */
	protected $post_ids = array();

	/**
	 * @param int[]  $post_ids Post IDs in display order.
	 * @param string $plural   Table class suffix (also the core table CSS class).
	 */
	public function __construct( array $post_ids, $plural ) {
		parent::__construct(
			array(
				'singular' => '',
				'plural'   => $plural,
				'ajax'     => false,
			)
		);
		$this->post_ids = array_map( 'intval', $post_ids );
	}

	/**
	 * Items are just post IDs; each column method pulls what it needs.
	 */
	public function prepare_items() {
		$this->_column_headers = array( $this->get_columns(), array(), array() );

		$this->items = array();
		foreach ( $this->post_ids as $post_id ) {
			$this->items[] = array( 'post_id' => $post_id );
		}
	}

	/**
	 * Title is the primary column - core then attaches the row actions and
	 * the responsive toggle button to it.
	 */
	protected function get_default_primary_column_name() {
		return 'title';
	}

	/**
	 * Read-only status tables: suppress the bulk-action / pagination bars.
	 *
	 * @param string $which 'top' or 'bottom'.
	 */
	protected function display_tablenav( $which ) {}

	/**
	 * The Title cell: `<strong><a class="row-title">` + core post states,
	 * with core-generated row actions appended after the </strong>.
	 *
	 * @param array $item Row (post_id).
	 * @return string
	 */
	protected function column_title( $item ) {
		$post_id = (int) $item['post_id'];

		// 2.7.0 M5: surface rows - label from the registry, View link to
		// the live page, a 'Site-wide page' state chip, no Edit action
		// (there is no editor for a surface).
		if ( DOIG_Surfaces::is_surface_id( $post_id ) ) {
			$key   = DOIG_Surfaces::key_for_id( $post_id );
			$label = DOIG_Surfaces::label_for_key( $key );
			$url   = DOIG_Surfaces::url_for_key( $key );

			$inner  = esc_html( $label );
			$inner .= ' &mdash; <span class="post-state">' . esc_html__( 'Site-wide page', 'dynamic-og-image-generator' ) . '</span>';

			$actions = array();
			if ( '' !== $url ) {
				$actions['view'] = '<a href="' . esc_url( $url ) . '" target="_blank" rel="noopener">' . esc_html__( 'View', 'dynamic-og-image-generator' ) . '</a>';
			}
			return '<strong>' . $inner . '</strong>' . $this->row_actions( $actions );
		}

		$post    = get_post( $post_id );

		$title = get_the_title( $post_id );
		$title = ( '' !== $title ) ? $title : __( '(no title)', 'dynamic-og-image-generator' );

		$edit_link = get_edit_post_link( $post_id );
		$inner     = $edit_link
			? '<a class="row-title" href="' . esc_url( $edit_link ) . '">' . esc_html( $title ) . '</a>'
			: esc_html( $title );

		// WordPress-standard post-state suffix (Draft, Private, ...), rendered
		// INSIDE the <strong> exactly as core's posts list does.
		if ( $post && function_exists( 'get_post_states' ) ) {
			$states = (array) get_post_states( $post );
			if ( ! empty( $states ) ) {
				$separator = function_exists( 'wp_get_list_item_separator' ) ? wp_get_list_item_separator() : ', ';
				$total     = count( $states );
				$index     = 0;
				$inner    .= ' &mdash; ';
				foreach ( $states as $state ) {
					$index++;
					$tail   = ( $index < $total ) ? $separator : '';
					$inner .= '<span class="post-state">' . esc_html( $state ) . esc_html( $tail ) . '</span>';
				}
			}
		}

		return '<strong>' . $inner . '</strong>' . $this->row_actions( $this->build_row_actions( $post ) );
	}

	/**
	 * Actions array; the keys become core's `<span class="{key}">` wrappers
	 * and core inserts the ` | ` separators and the `.row-actions` div.
	 *
	 * @param WP_Post|null $post Post object.
	 * @return array
	 */
	protected function build_row_actions( $post ) {
		$actions = array();

		if ( ! $post ) {
			return $actions;
		}

		$edit_link = get_edit_post_link( $post->ID );
		if ( $edit_link ) {
			$actions['edit'] = '<a href="' . esc_url( $edit_link ) . '">' . esc_html__( 'Edit', 'dynamic-og-image-generator' ) . '</a>';
		}

		$permalink = get_permalink( $post->ID );
		if ( $permalink ) {
			$actions['view'] = '<a href="' . esc_url( $permalink ) . '" target="_blank" rel="noopener">' . esc_html__( 'View', 'dynamic-og-image-generator' ) . '</a>';
		}

		// Inherit plugin-added row actions (Edit with Elementor, Divi, ...)
		// from the same filters core's posts/pages screens run. Quick Edit is
		// dropped - it has no meaning on a read-only status table.
		$filter = is_post_type_hierarchical( $post->post_type ) ? 'page_row_actions' : 'post_row_actions';
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- core filter, deliberately inherited.
		$actions = (array) apply_filters( $filter, $actions, $post );
		unset( $actions['inline hide-if-no-js'], $actions['inline'] );

		return $actions;
	}

	/**
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_type( $item ) {
		if ( DOIG_Surfaces::is_surface_id( (int) $item['post_id'] ) ) {
			return esc_html__( 'Site-wide page', 'dynamic-og-image-generator' );
		}
		$post_type = get_post_type( (int) $item['post_id'] );
		$type_obj  = get_post_type_object( $post_type );
		return esc_html( $type_obj ? $type_obj->labels->singular_name : (string) $post_type );
	}

	/**
	 * Unknown column: render nothing rather than dumping the raw row.
	 *
	 * @param array  $item        Row.
	 * @param string $column_name Column key.
	 * @return string
	 */
	protected function column_default( $item, $column_name ) {
		return '';
	}

	/**
	 * "N ago" cell with an absolute UTC timestamp in the title attribute.
	 *
	 * @param int $timestamp Unix timestamp.
	 * @return string
	 */
	protected static function time_ago_cell( $timestamp ) {
		$timestamp = (int) $timestamp;
		if ( $timestamp <= 0 ) {
			return esc_html__( 'Unknown', 'dynamic-og-image-generator' );
		}

		$label = sprintf(
			/* translators: %s: human-readable time-since */
			__( '%s ago', 'dynamic-og-image-generator' ),
			human_time_diff( $timestamp, time() )
		);

		return '<span title="' . esc_attr( gmdate( 'Y-m-d H:i:s', $timestamp ) . ' UTC' ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * An em-dash placeholder for "nothing to show here".
	 *
	 * @return string
	 */
	protected static function dash() {
		return '<span aria-hidden="true">&#8212;</span>';
	}
}

/**
 * Recent screenshot activity: the 20 most recent ready captures.
 */
class DOIG_Recent_Table extends DOIG_Dashboard_Table {

	/**
	 * @param int[] $post_ids Post IDs.
	 */
	public function __construct( array $post_ids ) {
		parent::__construct( $post_ids, 'doig-recent' );
	}

	public function get_columns() {
		return array(
			'title'    => __( 'Title', 'dynamic-og-image-generator' ),
			'type'     => __( 'Type', 'dynamic-og-image-generator' ),
			'image'    => __( 'Image', 'dynamic-og-image-generator' ),
			'source'   => __( 'Source', 'dynamic-og-image-generator' ),
			'captured' => __( 'Captured', 'dynamic-og-image-generator' ),
		);
	}

	/**
	 * The cached WebP, as a lightbox trigger. Cached filenames are
	 * timestamp-unique per capture, so no cache-busting arg is needed.
	 *
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_image( $item ) {
		$url = (string) DOIG_Post_Meta::get( (int) $item['post_id'], 'cached_url' );

		// Status says ready but the cache entry is gone (e.g. cache cleared);
		// show a dash, never a broken image.
		if ( '' === $url ) {
			return self::dash();
		}

		return '<img class="doig-recent-thumb doig-zoomable" loading="lazy" width="90" height="47" src="'
			. esc_url( $url ) . '" data-full="' . esc_url( $url ) . '" alt="" />';
	}

	/**
	 * The resolved Image source - answers "why is this the image?".
	 *
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_source( $item ) {
		$post_id = (int) $item['post_id'];

		if ( DOIG_Surfaces::is_surface_id( $post_id ) ) {
			$mode   = DOIG_Source_Resolver::surface_mode( $post_id );
			$labels = DOIG_Source_Resolver::surface_mode_labels();
			return isset( $labels[ $mode ] ) ? esc_html( $labels[ $mode ] ) : self::dash();
		}

		$mode = DOIG_Source_Resolver::resolve_mode( $post_id );

		$labels = array(
			DOIG_Source_Resolver::MODE_SCREENSHOT             => __( 'Auto-screenshot', 'dynamic-og-image-generator' ),
			DOIG_Source_Resolver::MODE_FEATURED               => __( 'Featured image', 'dynamic-og-image-generator' ),
			DOIG_Source_Resolver::MODE_FEATURED_WITH_FALLBACK => __( 'Featured, screenshot fallback', 'dynamic-og-image-generator' ),
			DOIG_Source_Resolver::MODE_CUSTOM                 => __( 'Custom image', 'dynamic-og-image-generator' ),
			DOIG_Source_Resolver::MODE_OFF                    => __( 'Off', 'dynamic-og-image-generator' ),
		);

		return isset( $labels[ $mode ] ) ? esc_html( $labels[ $mode ] ) : self::dash();
	}

	/**
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_captured( $item ) {
		return self::time_ago_cell( DOIG_Post_Meta::get( (int) $item['post_id'], 'cached_at' ) );
	}
}

/**
 * Pending screenshots: queued or in flight.
 */
class DOIG_Pending_Table extends DOIG_Dashboard_Table {

	/**
	 * @param int[] $post_ids Post IDs.
	 */
	public function __construct( array $post_ids ) {
		parent::__construct( $post_ids, 'doig-pending' );
	}

	public function get_columns() {
		return array(
			'title'    => __( 'Title', 'dynamic-og-image-generator' ),
			'type'     => __( 'Type', 'dynamic-og-image-generator' ),
			'queued'   => __( 'Queued', 'dynamic-og-image-generator' ),
			'attempts' => __( 'Attempts', 'dynamic-og-image-generator' ),
			'job_id'   => __( 'Job ID', 'dynamic-og-image-generator' ),
		);
	}

	/**
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_queued( $item ) {
		return self::time_ago_cell( DOIG_Post_Meta::get( (int) $item['post_id'], 'scheduled_at' ) );
	}

	/**
	 * Submission attempts so far, from the retry queue. Surfaces a retrying
	 * job (and, at the cap, one that is about to be marked failed) instead of
	 * leaving it silent. The option is a LIST of entries, not keyed by post.
	 *
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_attempts( $item ) {
		static $attempts_by_post = null;

		if ( null === $attempts_by_post ) {
			$attempts_by_post = array();
			foreach ( (array) get_option( DOIG_Cron::RETRY_OPTION, array() ) as $entry ) {
				if ( isset( $entry['post_id'] ) ) {
					$attempts_by_post[ (int) $entry['post_id'] ] = isset( $entry['attempt_count'] ) ? (int) $entry['attempt_count'] : 0;
				}
			}
		}

		$post_id = (int) $item['post_id'];
		if ( empty( $attempts_by_post[ $post_id ] ) ) {
			return self::dash();
		}

		$count = $attempts_by_post[ $post_id ];
		$label = sprintf(
			/* translators: 1: attempts made, 2: maximum attempts */
			__( '%1$d of %2$d', 'dynamic-og-image-generator' ),
			$count,
			DOIG_Cron::MAX_SUBMISSION_ATTEMPTS
		);

		$classes = 'doig-attempts';
		if ( $count >= DOIG_Cron::MAX_SUBMISSION_ATTEMPTS ) {
			$classes .= ' doig-attempts--exhausted';
		}

		return '<span class="' . esc_attr( $classes ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_job_id( $item ) {
		$job_id = (string) DOIG_Post_Meta::get( (int) $item['post_id'], 'job_id' );
		if ( '' === $job_id ) {
			return self::dash();
		}
		return '<code class="doig-job-id">' . esc_html( $job_id ) . '</code>';
	}
}

/**
 * Failed screenshots (including quota-exceeded).
 */
class DOIG_Failed_Table extends DOIG_Dashboard_Table {

	/**
	 * @param int[] $post_ids Post IDs.
	 */
	public function __construct( array $post_ids ) {
		parent::__construct( $post_ids, 'doig-failed' );
	}

	public function get_columns() {
		return array(
			'title'  => __( 'Title', 'dynamic-og-image-generator' ),
			'type'   => __( 'Type', 'dynamic-og-image-generator' ),
			'reason' => __( 'Reason', 'dynamic-og-image-generator' ),
		);
	}

	/**
	 * @param array $item Row.
	 * @return string
	 */
	protected function column_reason( $item ) {
		$post_id   = (int) $item['post_id'];
		$err_code  = (string) DOIG_Post_Meta::get( $post_id, 'error_code' );
		$err_msg   = (string) DOIG_Post_Meta::get( $post_id, 'error_message' );
		$status    = (string) DOIG_Post_Meta::get( $post_id, 'status' );

		if ( '' !== $err_msg ) {
			$short = ( strlen( $err_msg ) > 100 ) ? substr( $err_msg, 0, 100 ) . '…' : $err_msg;
		} elseif ( 'quota_exceeded' === $status ) {
			$short = __( 'Monthly quota exceeded.', 'dynamic-og-image-generator' );
		} else {
			$short = __( 'No error detail recorded.', 'dynamic-og-image-generator' );
		}

		$out = '<span title="' . esc_attr( $err_msg ) . '">';
		if ( '' !== $err_code ) {
			$out .= '<code class="doig-error-code">' . esc_html( $err_code ) . '</code><br />';
		}
		$out .= '<span class="doig-error-msg">' . esc_html( $short ) . '</span>';
		$out .= '</span>';

		// 2.6.2: per-code guidance - a short "what do I do now" plus the
		// matching troubleshooting deep link. Augments the raw message
		// above, never replaces it. Unmapped codes render as before.
		$guidance = class_exists( 'DOIG_Error_Guidance' ) ? DOIG_Error_Guidance::get( $err_code ) : null;
		if ( null !== $guidance ) {
			$out .= '<br /><span class="doig-error-guidance description">' . esc_html( $guidance['hint'] ) . ' ';
			$out .= '<a href="' . esc_url( $guidance['url'] ) . '" target="_blank" rel="noreferrer">'
				. esc_html__( 'Troubleshooting guide', 'dynamic-og-image-generator' )
				. ' &rarr;</a></span>';
		}

		return $out;
	}
}
