<?php
/**
 * File Path: includes/class-mbs-series-sync.php
 *
 * Series Sync Service
 *
 * Registers WordPress hooks that keep every series's persisted reading
 * order in sync with the actual membership of the series:
 *
 *   · `set_object_terms`  — when a book's series assignments change, append
 *                           newly-assigned series / prune removed series
 *                           from the respective reading orders.
 *   · `trashed_post`      — when a book is trashed, prune it from all
 *                           persisted reading orders.
 *   · `deleted_post`      — same, when a book is permanently deleted.
 *
 * Why auto-append rather than silent drift:
 *     Without these hooks, the admin carefully sets a reading order, then
 *     assigns a new book to the series via the book editor, and the new
 *     book is invisible to any reading-order consumer (Context 3 enhanced
 *     series template, etc.). Auto-append guarantees the reading order is
 *     always a superset of the actual series membership.
 *
 * Why no-op when reading order is uninitialized (per R1):
 *     We only auto-append into an *already-persisted* order. When the
 *     series has no persisted order, the DAO computes the default on read
 *     from series_number; we don't want auto-append to silently write the
 *     default behind the admin's back.
 *
 * Instantiated once at plugin bootstrap (MBS_Plugin::init()) so the hooks
 * fire in every context — admin, cron, WP-CLI, REST — wherever a book's
 * series terms are modified.
 *
 * All raw SQL lives in the DAO (MBS_Series_Reading_Order_DAO); this class
 * is pure hook orchestration.
 *
 * @package    My_Book_Showroom
 * @subpackage Includes
 * @since      1.0.0
 */

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

class MBS_Series_Sync {

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function __construct() {
		add_action( 'set_object_terms', array( $this, 'on_set_object_terms' ), 10, 6 );
		add_action( 'trashed_post',     array( $this, 'on_post_removed' ),     10, 1 );
		add_action( 'deleted_post',     array( $this, 'on_post_removed' ),     10, 1 );
	}

	// -------------------------------------------------------------------------
	// Hook handlers
	// -------------------------------------------------------------------------

	/**
	 * Handle a change in a post's taxonomy terms.
	 *
	 * Fires for EVERY taxonomy; we short-circuit on anything other than
	 * mbs_series on an mbs_book post.
	 *
	 * Diff computation:
	 *   added   = $tt_ids   - $old_tt_ids  →  append book to these series
	 *   removed = $old_tt_ids - $tt_ids    →  prune book from these series
	 *
	 * @param int    $object_id    The post ID whose terms changed.
	 * @param mixed  $terms        The submitted term values (unused — we use tt_ids).
	 * @param int[]  $tt_ids       New term_taxonomy_ids after the operation.
	 * @param string $taxonomy     The taxonomy slug.
	 * @param bool   $append       Whether the operation was an append (unused).
	 * @param int[]  $old_tt_ids   Previous term_taxonomy_ids before the operation.
	 * @return void
	 */
	public function on_set_object_terms( int $object_id, $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids ): void {
		if ( MBS_TAX_SERIES !== $taxonomy ) {
			return;
		}
		if ( get_post_type( $object_id ) !== MBS_POST_TYPE ) {
			return;
		}

		$new_term_ids = self::resolve_term_ids( $tt_ids );
		$old_term_ids = self::resolve_term_ids( $old_tt_ids );

		$added   = array_diff( $new_term_ids, $old_term_ids );
		$removed = array_diff( $old_term_ids, $new_term_ids );

		foreach ( $added as $term_id ) {
			MBS_Series_Reading_Order_DAO::append_book( (int) $term_id, $object_id );
		}

		foreach ( $removed as $term_id ) {
			MBS_Series_Reading_Order_DAO::prune_book( (int) $term_id, $object_id );
		}
	}

	/**
	 * Handle a book being trashed or permanently deleted.
	 *
	 * Prunes the book from every series's persisted reading order so stale
	 * IDs don't accumulate.
	 *
	 * @param int $post_id The post being removed.
	 * @return void
	 */
	public function on_post_removed( int $post_id ): void {
		if ( get_post_type( $post_id ) !== MBS_POST_TYPE ) {
			return;
		}
		MBS_Series_Reading_Order_DAO::prune_from_all_series( $post_id );
	}

	// -------------------------------------------------------------------------
	// Internal
	// -------------------------------------------------------------------------

	/**
	 * Map an array of term_taxonomy_ids to their term_ids, dropping zeros.
	 *
	 * @param int[] $tt_ids
	 * @return int[]
	 */
	private static function resolve_term_ids( array $tt_ids ): array {
		$out = array();
		foreach ( $tt_ids as $tt_id ) {
			$term_id = MBS_Series_Reading_Order_DAO::term_id_from_tt_id( (int) $tt_id );
			if ( $term_id > 0 ) {
				$out[] = $term_id;
			}
		}
		return $out;
	}
}
