<?php
/**
 * File Path: includes/admin/class-mbs-admin-book-list-columns.php
 *
 * Books List Table — Custom Columns and Sort Orders
 *
 * Adds a "Series Order" column to the Books admin list table showing each
 * book's series name and series number.  Makes the Series, Author, and
 * Collections taxonomy columns sortable via custom SQL clauses injected
 * through the posts_clauses filter.  Also establishes a default sort when
 * the admin has not explicitly chosen a sort:
 *
 *   1. Series name A — Z (books with no series sort last)
 *   2. series_number ascending (NULL last within a series)
 *   3. Post title A — Z
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Admin
 * @since      1.0.0
 */

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

/**
 * Class MBS_Admin_Book_List_Columns
 *
 * Registers all Books-list column and sort-order customizations.
 */
class MBS_Admin_Book_List_Columns {

	/**
	 * Orderby token for the default three-level sort (no explicit user choice).
	 */
	private const ORDERBY_DEFAULT = 'mbs_default';

	/**
	 * Orderby token for series-name sort.
	 */
	private const ORDERBY_SERIES = 'mbs_sort_series';

	/**
	 * Orderby token for series-number (series order) sort.
	 */
	private const ORDERBY_SERIES_ORDER = 'mbs_series_order';

	/**
	 * Orderby token for author-name sort.
	 */
	private const ORDERBY_AUTHOR = 'mbs_sort_author';

	/**
	 * Orderby token for collection-name sort.
	 */
	private const ORDERBY_COLLECTION = 'mbs_sort_collection';

	// -------------------------------------------------------------------------
	// Bootstrap
	// -------------------------------------------------------------------------

	/**
	 * Constructor — registers all hooks.
	 */
	public function __construct() {
		add_filter( 'manage_' . MBS_POST_TYPE . '_posts_columns',          array( $this, 'add_columns' ) );
		add_filter( 'manage_edit-' . MBS_POST_TYPE . '_sortable_columns',  array( $this, 'sortable_columns' ) );
		add_action( 'manage_' . MBS_POST_TYPE . '_posts_custom_column',    array( $this, 'render_column' ), 10, 2 );
		add_action( 'pre_get_posts',                                        array( $this, 'apply_default_sort' ) );
		add_filter( 'posts_clauses',                                        array( $this, 'apply_sort_clauses' ), 10, 2 );
		add_action( 'admin_head',                                           array( $this, 'inject_import_button' ) );
	}

	// -------------------------------------------------------------------------
	// Column registration
	// -------------------------------------------------------------------------

	/**
	 * Inject the Series Order column immediately after the Title column.
	 *
	 * @param array<string,string> $columns Existing column map.
	 * @return array<string,string>
	 */
	public function add_columns( array $columns ): array {
		// Stash the Tags label before the loop so we can re-insert it after Collections.
		$tags_key   = 'taxonomy-' . MBS_TAX_TAG;
		$tags_label = isset( $columns[ $tags_key ] ) ? $columns[ $tags_key ] : null;

		$new = array();
		foreach ( $columns as $key => $label ) {
			// Skip Tags here — it will be added after Collections.
			if ( $key === $tags_key ) {
				continue;
			}
			$new[ $key ] = $label;
			if ( 'taxonomy-' . MBS_TAX_SERIES === $key ) {
				$new['mbs_series_order'] = __( 'Series Order', 'my-book-showroom' );
			}
			if ( 'taxonomy-' . MBS_TAX_COLLECTION === $key && null !== $tags_label ) {
				$new[ $tags_key ] = $tags_label;
			}
		}
		return $new;
	}

	/**
	 * Register sortable column tokens for Series, Author, Collections, and Series Order.
	 *
	 * Series, Author, and Collections columns are auto-registered by WordPress
	 * (show_admin_column => true) with keys taxonomy-{slug}. Registering them
	 * here makes their headers clickable sort links.
	 *
	 * @param array<string,string|array> $sortable Existing sortable-column map.
	 * @return array<string,string|array>
	 */
	public function sortable_columns( array $sortable ): array {
		$sortable[ 'taxonomy-' . MBS_TAX_SERIES ]     = self::ORDERBY_SERIES;
		$sortable[ 'taxonomy-' . MBS_TAX_AUTHOR ]     = self::ORDERBY_AUTHOR;
		$sortable[ 'taxonomy-' . MBS_TAX_COLLECTION ] = self::ORDERBY_COLLECTION;
		$sortable['mbs_series_order']                 = self::ORDERBY_SERIES_ORDER;
		return $sortable;
	}

	// -------------------------------------------------------------------------
	// Column renderer
	// -------------------------------------------------------------------------

	/**
	 * Render the Series Order column value for a single row.
	 *
	 * Displays only the position number "#N" since the series name is already
	 * shown in the adjacent Series column. Outputs an em-dash when the book is
	 * assigned to a series but has no position set.
	 * Trailing zeros are stripped from DECIMAL values: "2.00" becomes "2".
	 *
	 * @param string $column  Column key.
	 * @param int    $post_id Current post ID.
	 * @return void
	 */
	public function render_column( string $column, int $post_id ): void {
		if ( 'mbs_series_order' !== $column ) {
			return;
		}

		$terms = get_the_terms( $post_id, MBS_TAX_SERIES );
		if ( empty( $terms ) || is_wp_error( $terms ) ) {
			echo '<span aria-label="' . esc_attr__( 'No series', 'my-book-showroom' ) . '">&mdash;</span>';
			return;
		}

		// ARCH-SERIES-001: series position is a book-level attribute stored in post_meta.
		$sn_raw     = (string) get_post_meta( $post_id, 'mbs_series_position', true );
		$sn_display = '';
		if ( '' !== $sn_raw ) {
			// Trim trailing zeros: "2.00" => "2", "2.50" => "2.5".
			$sn_display = rtrim( rtrim( $sn_raw, '0' ), '.' );
		}

		if ( '' !== $sn_display ) {
			echo '#' . esc_html( $sn_display );
		} else {
			echo '<span aria-label="' . esc_attr__( 'No position', 'my-book-showroom' ) . '">&mdash;</span>';
		}
	}

	// -------------------------------------------------------------------------
	// Sort hooks
	// -------------------------------------------------------------------------

	/**
	 * Set the default three-level sort when no explicit orderby is requested.
	 *
	 * Fires on pre_get_posts. Sets orderby to ORDERBY_DEFAULT so that
	 * apply_sort_clauses() can inject the full JOIN + ORDER BY.
	 *
	 * @param WP_Query $query Current query.
	 * @return void
	 */
	public function apply_default_sort( WP_Query $query ): void {
		if ( ! $this->is_books_list( $query ) ) {
			return;
		}
		// Only take over when no explicit sort has been chosen by the user.
		if ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}
		$query->set( 'orderby', self::ORDERBY_DEFAULT );
		$query->set( 'order', 'ASC' );
	}

	/**
	 * Inject JOIN / ORDER BY / GROUP BY clauses for custom sort tokens.
	 *
	 * Handles:
	 *   mbs_default       — series name ASC, series_number ASC (NULLs last), title ASC
	 *   mbs_sort_series   — series name, then title
	 *   mbs_series_order  — series_number (NULLs last), then title
	 *   mbs_sort_author   — author name, then title
	 *   mbs_sort_collection — collection name, then title
	 *
	 * @param array<string,string> $clauses SQL clause fragments from WP_Query.
	 * @param WP_Query             $query   Current query.
	 * @return array<string,string>
	 */
	public function apply_sort_clauses( array $clauses, WP_Query $query ): array {
		if ( ! $this->is_books_list( $query ) ) {
			return $clauses;
		}

		$orderby = $query->get( 'orderby' );
		if ( ! in_array( $orderby, array(
			self::ORDERBY_DEFAULT,
			self::ORDERBY_SERIES,
			self::ORDERBY_SERIES_ORDER,
			self::ORDERBY_AUTHOR,
			self::ORDERBY_COLLECTION,
		), true ) ) {
			return $clauses;
		}

		global $wpdb;
		$order    = ( 'DESC' === strtoupper( (string) $query->get( 'order' ) ) ) ? 'DESC' : 'ASC';

		switch ( $orderby ) {

			case self::ORDERBY_DEFAULT:
				$clauses['join']    .= $this->series_join( $wpdb );
				$clauses['join']    .= "
					 LEFT JOIN {$wpdb->prefix}mbs_book_meta mbs_bm_sp
					        ON mbs_bm_sp.post_id = {$wpdb->posts}.ID ";
				$clauses['groupby']  = "{$wpdb->posts}.ID";
				$clauses['orderby']  = "ISNULL(MIN(mbs_t_s.name)) ASC, MIN(mbs_t_s.name) ASC, "
				                      . "ISNULL(mbs_bm_sp.series_position) ASC, mbs_bm_sp.series_position ASC, "
				                      . "{$wpdb->posts}.post_title ASC";
				break;

			case self::ORDERBY_SERIES:
				$clauses['join']    .= $this->series_join( $wpdb );
				$clauses['groupby']  = "{$wpdb->posts}.ID";
				$clauses['orderby']  = "MIN(mbs_t_s.name) {$order}, {$wpdb->posts}.post_title ASC";
				break;

			case self::ORDERBY_SERIES_ORDER:
				$clauses['join']    .= "
					 LEFT JOIN {$wpdb->prefix}mbs_book_meta mbs_bm_sp
					        ON mbs_bm_sp.post_id = {$wpdb->posts}.ID ";
				$clauses['groupby']  = "{$wpdb->posts}.ID";
				$clauses['orderby']  = "ISNULL(mbs_bm_sp.series_position) ASC, mbs_bm_sp.series_position {$order}, {$wpdb->posts}.post_title ASC";
				break;

			case self::ORDERBY_AUTHOR:
				$clauses['join']    .= $this->taxonomy_join( $wpdb, MBS_TAX_AUTHOR, 'a' );
				$clauses['groupby']  = "{$wpdb->posts}.ID";
				$clauses['orderby']  = "MIN(mbs_t_a.name) {$order}, {$wpdb->posts}.post_title ASC";
				break;

			case self::ORDERBY_COLLECTION:
				$clauses['join']    .= $this->taxonomy_join( $wpdb, MBS_TAX_COLLECTION, 'c' );
				$clauses['groupby']  = "{$wpdb->posts}.ID";
				$clauses['orderby']  = "MIN(mbs_t_c.name) {$order}, {$wpdb->posts}.post_title ASC";
				break;
		}

		return $clauses;
	}

	// -------------------------------------------------------------------------
	// Private helpers
	// -------------------------------------------------------------------------

	/**
	 * Whether the given WP_Query is the main query on the Books admin list screen.
	 *
	 * @param WP_Query $query Query to inspect.
	 * @return bool
	 */
	private function is_books_list( WP_Query $query ): bool {
		return is_admin() && $query->is_main_query() && MBS_POST_TYPE === $query->get( 'post_type' );
	}

	/**
	 * Build LEFT JOIN fragments for the mbs_series taxonomy.
	 *
	 * SQL aliases used:
	 *   mbs_tr_s  — term_relationships
	 *   mbs_tt_s  — term_taxonomy
	 *   mbs_t_s   — terms
	 *
	 * @param wpdb $wpdb Global wpdb instance.
	 * @return string SQL fragment (safe to concatenate onto $clauses['join']).
	 */
	private function series_join( $wpdb ): string {
		$sql  = "
			 LEFT JOIN {$wpdb->term_relationships} mbs_tr_s ON mbs_tr_s.object_id = {$wpdb->posts}.ID
";
		$sql .= "			 LEFT JOIN {$wpdb->term_taxonomy} mbs_tt_s
";
		$sql .= "			        ON mbs_tt_s.term_taxonomy_id = mbs_tr_s.term_taxonomy_id
";
		$sql .= "			       AND mbs_tt_s.taxonomy = '" . esc_sql( MBS_TAX_SERIES ) . "'
";
		$sql .= "			 LEFT JOIN {$wpdb->terms} mbs_t_s ON mbs_t_s.term_id = mbs_tt_s.term_id ";
		return $sql;
	}

	/**
	 * Build LEFT JOIN fragments for an arbitrary flat taxonomy.
	 *
	 * SQL aliases use the supplied one-letter suffix to avoid collision:
	 *   mbs_tr_{suffix}, mbs_tt_{suffix}, mbs_t_{suffix}
	 *
	 * @param wpdb   $wpdb     Global wpdb instance.
	 * @param string $taxonomy Taxonomy slug (e.g. MBS_TAX_AUTHOR).
	 * @param string $suffix   One-letter alias suffix.
	 * @return string SQL fragment (safe to concatenate onto $clauses['join']).
	 */
	private function taxonomy_join( $wpdb, string $taxonomy, string $suffix ): string {
		$tr = 'mbs_tr_' . $suffix;
		$tt = 'mbs_tt_' . $suffix;
		$t  = 'mbs_t_'  . $suffix;
		$sql  = "
			 LEFT JOIN {$wpdb->term_relationships} {$tr} ON {$tr}.object_id = {$wpdb->posts}.ID
";
		$sql .= "			 LEFT JOIN {$wpdb->term_taxonomy} {$tt}
";
		$sql .= "			        ON {$tt}.term_taxonomy_id = {$tr}.term_taxonomy_id
";
		$sql .= "			       AND {$tt}.taxonomy = '" . esc_sql( $taxonomy ) . "'
";
		$sql .= "			 LEFT JOIN {$wpdb->terms} {$t} ON {$t}.term_id = {$tt}.term_id ";
		return $sql;
	}

	// -------------------------------------------------------------------------
	// Import button — Books list page
	// -------------------------------------------------------------------------

	/**
	 * Inject an Import button next to the Add New Book title-area button.
	 *
	 * WordPress has no PHP-only filter for the post-list title area, so a
	 * minimal DOMContentLoaded snippet inserts the anchor after the first
	 * .page-title-action element.  wp_json_encode() handles all escaping.
	 *
	 * @return void
	 */
	public function inject_import_button(): void {
		$screen = get_current_screen();
		if ( ! $screen || 'edit-' . MBS_POST_TYPE !== $screen->id ) {
			return;
		}
		$url   = admin_url( 'admin.php?page=my-book-showroom-settings&tab=import-export' );
		$label = __( 'Import', 'my-book-showroom' );
		printf(
			'<script>document.addEventListener("DOMContentLoaded",function(){'
			. 'var f=document.querySelector(".wrap .page-title-action");'
			. 'if(f){var b=document.createElement("a");'
			. 'b.href=%s;b.className="page-title-action";'
			. 'b.textContent=%s;'
			. 'f.insertAdjacentElement("afterend",b);}});</script>',
			wp_json_encode( $url ),
			wp_json_encode( $label )
		);
	}
}
