<?php
/**
 * KooKoa Folders — Tree & navigation AJAX handlers.
 *
 * Read the folder tree for the sidebar, the flat folder list for dropdowns,
 * and persist the per-post-type sort preference. Registered and dispatched by
 * KookoaWPF_Ajax.
 *
 * @package KookoaWPF
 * @license GPL-2.0-or-later
 */

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

/**
 * AJAX handlers for reading the folder tree / flat list and saving sort order.
 */
class KookoaWPF_Ajax_Tree extends KookoaWPF_Ajax_Handler {

	/**
	 * Singleton instance.
	 *
	 * @var self|null
	 */
	private static $instance = null;

	/**
	 * Return the singleton instance.
	 *
	 * @return self
	 */
	public static function instance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor. The action map lives on the KookoaWPF_Ajax dispatcher, so
	 * this collaborator registers nothing of its own.
	 */
	private function __construct() {
		// Registration is owned by KookoaWPF_Ajax (the dispatch surface).
	}

	/**
	 * Get folder tree
	 */
	public function get_tree(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$post_type = self::require_managed_post_type(
			isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''
		);

		if ( empty( $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing post type.', 'kookoa-folders' ) ) );
		}

		// Optional override: callers (e.g. the media modal sidebar) can ask
		// for the tree to be returned in a specific sort order, ignoring the
		// saved sort_pref. Used by contexts that have no sort UI and want
		// predictable manual ordering. Whitelisted to the same set as the
		// sort_folders handler so a malicious caller can't inject anything.
		$force_sort_by = '';
		if ( isset( $_POST['force_sort_by'] ) ) {
			$candidate = sanitize_key( wp_unslash( $_POST['force_sort_by'] ) );
			if ( in_array( $candidate, array( 'name', 'date', 'count', 'custom' ), true ) ) {
				$force_sort_by = $candidate;
			}
		}

		// Flush term count cache only when a mutation flag is set.
		// This avoids destroying persistent object cache (Redis/Memcached)
		// on every tree load. Mutation handlers set this transient.
		$taxonomy = KookoaWPF_Folders::get_taxonomy( $post_type );
		if ( get_transient( 'kookoawpf_tree_dirty_' . $post_type ) ) {
			$term_ids = get_terms(
				array(
					'taxonomy'   => $taxonomy,
					'hide_empty' => false,
					'fields'     => 'ids',
				)
			);
			if ( ! is_wp_error( $term_ids ) && ! empty( $term_ids ) ) {
				clean_term_cache( $term_ids, $taxonomy );
			}
			delete_transient( 'kookoawpf_tree_dirty_' . $post_type );
		}

		$tree      = KookoaWPF_Folders::get_jstree_data( $post_type, $force_sort_by );
		$count     = KookoaWPF_Folders::get_count( $post_type );
		$uncat     = KookoaWPF_Folders::get_uncategorized_count( $post_type );
		$sort_pref = get_option(
			'kookoawpf_sort_' . $post_type,
			array(
				'by'  => 'custom',
				'dir' => 'ASC',
			)
		);

		// Total items count for "All Items" nav.
		// Must include the same statuses used by get_uncategorized_count()
		// so that folder counts + uncategorized ≤ total_items.
		$counts_obj = wp_count_posts( $post_type );
		if ( 'attachment' === $post_type ) {
			$total_items = isset( $counts_obj->inherit ) ? (int) $counts_obj->inherit : 0;
			// Subtract attachments soft-trashed by PRO Cleanup. They keep
			// post_status='inherit' (the trash is a postmeta flag), so
			// wp_count_posts() includes them — but Media Library hides them
			// and get_uncategorized_count() already excludes them.
			$total_items = max( 0, $total_items - KookoaWPF_Folders::get_cleanup_trashed_count() );
		} else {
			$total_items = 0;
			foreach ( array( 'publish', 'draft', 'pending', 'private', 'future' ) as $st ) {
				$total_items += isset( $counts_obj->$st ) ? (int) $counts_obj->$st : 0;
			}
		}

		// Count folders across ALL post types (for export button state).
		$total_all_pts = 0;
		foreach ( kookoawpf()->get_supported_post_types() as $pt ) {
			$total_all_pts += KookoaWPF_Folders::get_count( $pt );
		}

		wp_send_json_success(
			array(
				'tree'              => $tree,
				'total_folders'     => $count,
				'total_folders_all' => $total_all_pts,
				'total_items'       => $total_items,
				'uncategorized'     => $uncat,
				'can_create_folder' => true,
				'sort'              => $sort_pref,
			)
		);
	}

	/**
	 * Sort folders by criteria (name, date, count, or manual).
	 */
	public function sort_folders(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$sort_by   = isset( $_POST['sort_by'] ) ? sanitize_key( wp_unslash( $_POST['sort_by'] ) ) : 'name';
		$sort_dir  = isset( $_POST['sort_dir'] ) ? strtoupper( sanitize_key( wp_unslash( $_POST['sort_dir'] ) ) ) : 'ASC';
		$post_type = self::require_managed_post_type(
			isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''
		);

		if ( empty( $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing post type.', 'kookoa-folders' ) ) );
		}

		// Validate sort direction.
		if ( ! in_array( $sort_dir, array( 'ASC', 'DESC' ), true ) ) {
			$sort_dir = 'ASC';
		}

		// Validate sort_by.
		$allowed = array( 'name', 'date', 'count', 'custom' );
		if ( ! in_array( $sort_by, $allowed, true ) ) {
			$sort_by = 'name';
		}

		// Save sort preference only — do NOT rewrite folder_order meta.
		// This preserves the user's manual drag-and-drop order so they can
		// switch back to "Manual order" and get their original arrangement.
		update_option(
			'kookoawpf_sort_' . $post_type,
			array(
				'by'  => $sort_by,
				'dir' => $sort_dir,
			)
		);

		$message = ( 'custom' === $sort_by )
			? __( 'Manual order restored.', 'kookoa-folders' )
			: __( 'Folders sorted.', 'kookoa-folders' );

		wp_send_json_success( array( 'message' => $message ) );
	}

	/**
	 * Get flat folder list for dropdowns (Bulk Organize)
	 */
	public function get_folder_list(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$post_type = self::require_managed_post_type(
			isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''
		);

		if ( empty( $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing post type.', 'kookoa-folders' ) ) );
		}

		$tree = KookoaWPF_Folders::get_tree( $post_type );
		$flat = array();
		// Smart folders are query-based virtual views — they can't host children
		// or accept item assignments, so they must never appear in dropdown
		// selectors (Move to Folder, Bulk Organize, parent picker, grid filter).
		KookoaWPF_Folders::flatten_tree( $tree, $flat, 0, true );

		wp_send_json_success( array( 'folders' => $flat ) );
	}
}
