<?php

namespace ContentFolders;

use WP_PowerSuite\Core\AbstractModule;

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

class Module extends AbstractModule {

	private const MODULE_ID      = 'content-folders';
	private const MODULE_VERSION = '1.0.1';
	private const TAXONOMY       = 'wpps_media_folder';
	private const FOLDER_COOKIE  = 'wpps_media_folder';

	private ?array $settings_cache = null;
	private ?array $all_terms_cache = null;
	private ?int $unassigned_count_cache = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Categorize Media', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Organize the Media Library with a folder tree sidebar, drag-and-drop assignment, and settings in a modal.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'ajax' ];
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		add_action( 'init', [ $this, 'registerTaxonomy' ], 15 );
		add_action( 'add_attachment', [ $this, 'assignDefaultFolderToAttachment' ] );

		if ( wp_doing_ajax() ) {
			add_filter( 'ajax_query_attachments_args', [ $this, 'filterMediaGrid' ] );
			add_action( 'wp_ajax_wpps_folders_add', [ $this, 'ajaxAddFolder' ] );
			add_action( 'wp_ajax_wpps_folders_update', [ $this, 'ajaxUpdateFolder' ] );
			add_action( 'wp_ajax_wpps_folders_delete', [ $this, 'ajaxDeleteFolder' ] );
			add_action( 'wp_ajax_wpps_folders_settings_save', [ $this, 'ajaxSaveSidebarState' ] );
			add_action( 'wp_ajax_wpps_folders_reorder', [ $this, 'ajaxReorderFolders' ] );
			add_action( 'wp_ajax_wpps_folders_assign_media', [ $this, 'ajaxAssignMediaToFolder' ] );
			return;
		}

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ] );
		add_action( 'load-upload.php', [ $this, 'registerUploadPageHooks' ] );
		add_action( 'load-upload.php', [ $this, 'handleBulkAssignSubmit' ] );
	}

	public function registerUploadPageHooks(): void {
		if ( ! current_user_can( 'upload_files' ) ) {
			return;
		}

		$this->setMediaGridFolderCookie();
		add_filter( 'admin_body_class', [ $this, 'addMediaPageBodyClass' ] );
		add_action( 'admin_footer', [ $this, 'outputMediaSidebar' ] );
		add_action( 'admin_notices', [ $this, 'bulkAssignFormNotice' ] );
		add_filter( 'manage_media_columns', [ $this, 'addFolderColumn' ] );
		add_action( 'manage_media_custom_column', [ $this, 'renderFolderColumn' ], 10, 2 );
		add_action( 'restrict_manage_posts', [ $this, 'outputFolderFilter' ], 10, 2 );
		add_filter( 'pre_get_posts', [ $this, 'filterByFolder' ] );
		add_filter( 'bulk_actions-upload', [ $this, 'addBulkActions' ] );
		add_filter( 'handle_bulk_actions-upload', [ $this, 'handleBulkActions' ], 10, 3 );
	}

	/**
	 * Keep the media-grid folder cookie in sync with the current Media Library URL.
	 * Clear it on "All Media" so object-cache hosts / sticky cookies cannot keep filtering.
	 */
	private function setMediaGridFolderCookie(): void {
		$path = defined( 'ADMIN_COOKIE_PATH' ) ? ADMIN_COOKIE_PATH : '/';

		if ( ! isset( $_GET[ self::TAXONOMY ] ) || ! is_scalar( $_GET[ self::TAXONOMY ] ) ) {
			$this->clearMediaGridFolderCookie( $path );
			return;
		}

		$value = sanitize_text_field( wp_unslash( (string) $_GET[ self::TAXONOMY ] ) );
		if ( $value === '' ) {
			$this->clearMediaGridFolderCookie( $path );
			return;
		}

		$expire = time() + DAY_IN_SECONDS;
		setcookie( self::FOLDER_COOKIE, $value, $expire, $path, '', is_ssl(), true );
		$_COOKIE[ self::FOLDER_COOKIE ] = $value;
	}

	private function clearMediaGridFolderCookie( string $path ): void {
		setcookie( self::FOLDER_COOKIE, '', time() - YEAR_IN_SECONDS, $path, '', is_ssl(), true );
		unset( $_COOKIE[ self::FOLDER_COOKIE ] );
	}

	public function registerTaxonomy(): void {
		$labels = [
			'name'              => _x( 'Folders', 'taxonomy general name', 'wp-powersuite' ),
			'singular_name'     => _x( 'Folder', 'taxonomy singular name', 'wp-powersuite' ),
			'all_items'         => __( 'All Folders', 'wp-powersuite' ),
			'edit_item'         => __( 'Edit Folder', 'wp-powersuite' ),
			'update_item'       => __( 'Update Folder', 'wp-powersuite' ),
			'add_new_item'      => __( 'Add New Folder', 'wp-powersuite' ),
			'new_item_name'     => __( 'New Folder Name', 'wp-powersuite' ),
			'menu_name'         => _x( 'Folders', 'taxonomy menu name', 'wp-powersuite' ),
			'search_items'      => __( 'Search Folders', 'wp-powersuite' ),
			'parent_item'       => __( 'Parent Folder', 'wp-powersuite' ),
			'parent_item_colon' => __( 'Parent Folder:', 'wp-powersuite' ),
		];

		register_taxonomy( self::TAXONOMY, 'attachment', [
			'labels'                => $labels,
			'hierarchical'          => true,
			'public'                => false,
			'show_ui'               => false,
			'show_in_menu'          => false,
			'show_in_rest'          => false,
			'show_admin_column'     => false,
			'update_count_callback' => [ $this, 'updateAttachmentTermCount' ],
			'query_var'             => true,
			'rewrite'               => false,
			'capabilities'          => [
				'manage_terms' => 'manage_categories',
				'edit_terms'   => 'manage_categories',
				'delete_terms' => 'manage_categories',
				'assign_terms' => 'upload_files',
			],
		] );
	}

	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}
		$saved    = $this->moduleSettings()->all( [] );
		$defaults = [
			'sidebar_width'   => 280,
			'default_folder'  => 0,
			'sidebar_visible' => true,
			'folder_order'    => [],
		];
		$this->settings_cache = array_merge( $defaults, is_array( $saved ) ? $saved : [] );
		return $this->settings_cache;
	}

	private function clearSettingsCache(): void {
		$this->settings_cache = null;
	}

	private function clearTermsCache(): void {
		$this->all_terms_cache       = null;
		$this->unassigned_count_cache = null;
	}

	private function getAllFolderTerms(): array {
		if ( $this->all_terms_cache !== null ) {
			return $this->all_terms_cache;
		}
		$terms = get_terms( [
			'taxonomy'   => self::TAXONOMY,
			'hide_empty' => false,
			'orderby'    => 'name',
			'order'      => 'ASC',
		] );
		$this->all_terms_cache = is_wp_error( $terms ) ? [] : $terms;
		return $this->all_terms_cache;
	}

	public function addMediaPageBodyClass( string $classes ): string {
		return $classes . ' wpps-media-folders-page';
	}

	public function outputMediaSidebar(): void {
		$settings  = $this->getSettings();
		$width     = max( 200, min( 400, (int) ( $settings['sidebar_width'] ?? 280 ) ) );
		$visible   = ! empty( $settings['sidebar_visible'] );
		$taxonomy  = self::TAXONOMY;
		$base_url  = admin_url( 'upload.php' );
		$current   = isset( $_GET[ $taxonomy ] ) ? sanitize_text_field( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
		$all_count = (int) ( wp_count_posts( 'attachment' )->inherit ?? 0 );
		$unassigned = $this->getUnassignedCount();
		$folder_tree = $this->getFolderTree( $settings, $current );
		$all_terms = $this->getAllFolderTerms();
		$default_folder      = (int) ( $settings['default_folder'] ?? 0 );
		$can_manage_settings = \WP_PowerSuite\Core\NetworkScope::canManagePlugin();

		include __DIR__ . '/views/media-sidebar.php';
	}

	private function getFolderTree( array $settings, string $current = '' ): array {
		$all = $this->getAllFolderTerms();
		if ( empty( $all ) ) {
			return [];
		}
		$by_parent = [];
		foreach ( $all as $term ) {
			$pid = (int) $term->parent;
			if ( ! isset( $by_parent[ $pid ] ) ) {
				$by_parent[ $pid ] = [];
			}
			$by_parent[ $pid ][] = $term;
		}
		$order_ids = array_map( 'intval', $settings['folder_order'] ?? [] );
		$sort_top = function ( $a, $b ) use ( $order_ids ) {
			$pos_a = array_search( (int) $a->term_id, $order_ids, true );
			$pos_b = array_search( (int) $b->term_id, $order_ids, true );
			if ( $pos_a === false && $pos_b === false ) {
				return strcasecmp( $a->name, $b->name );
			}
			if ( $pos_a === false ) {
				return 1;
			}
			if ( $pos_b === false ) {
				return -1;
			}
			return $pos_a - $pos_b;
		};
		$build_children = function ( $parent_id ) use ( &$build_children, $by_parent, $current ) {
			if ( ! isset( $by_parent[ $parent_id ] ) ) {
				return [];
			}
			$list = $by_parent[ $parent_id ];
			usort( $list, function ( $a, $b ) {
				return strcasecmp( $a->name, $b->name );
			} );
			foreach ( $list as $term ) {
				$term->children = $build_children( (int) $term->term_id );
				$term->total_count = (int) $term->count;
				foreach ( $term->children as $child ) {
					$term->total_count += isset( $child->total_count ) ? (int) $child->total_count : (int) $child->count;
				}
				$term->has_active_descendant = ( $current !== '' && $term->slug === $current );
				foreach ( $term->children as $child ) {
					if ( ! empty( $child->has_active_descendant ) ) {
						$term->has_active_descendant = true;
						break;
					}
				}
			}
			return $list;
		};
		$top = $by_parent[0] ?? [];
		usort( $top, $sort_top );
		foreach ( $top as $term ) {
			$term->children = $build_children( (int) $term->term_id );
			$term->total_count = (int) $term->count;
			foreach ( $term->children as $child ) {
				$term->total_count += isset( $child->total_count ) ? (int) $child->total_count : (int) $child->count;
			}
			$term->has_active_descendant = ( $current !== '' && $term->slug === $current );
			foreach ( $term->children as $child ) {
				if ( ! empty( $child->has_active_descendant ) ) {
					$term->has_active_descendant = true;
					break;
				}
			}
		}
		return $top;
	}

	private function getUnassignedCount(): int {
		if ( $this->unassigned_count_cache !== null ) {
			return $this->unassigned_count_cache;
		}
		global $wpdb;
		$taxonomy = self::TAXONOMY;
		$tt_ids   = $wpdb->get_col( $wpdb->prepare(
			"SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s",
			$taxonomy
		) );
		if ( empty( $tt_ids ) ) {
			$total = wp_count_posts( 'attachment' );
			$this->unassigned_count_cache = (int) ( $total->inherit ?? 0 ) + (int) ( $total->private ?? 0 );
			return $this->unassigned_count_cache;
		}
		$placeholders = implode( ',', array_fill( 0, count( $tt_ids ), '%d' ) );
		$count = $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->posts} p
			WHERE p.post_type = 'attachment' AND (p.post_status = 'inherit' OR p.post_status = 'private')
			AND p.ID NOT IN (
				SELECT tr.object_id FROM {$wpdb->term_relationships} tr
				WHERE tr.term_taxonomy_id IN ($placeholders)
			)",
			...$tt_ids
		) );
		$this->unassigned_count_cache = (int) $count;
		return $this->unassigned_count_cache;
	}

	public function updateAttachmentTermCount( array $term_taxonomy_ids, \WP_Taxonomy $taxonomy ): void {
		global $wpdb;
		if ( $taxonomy->name !== self::TAXONOMY || empty( $term_taxonomy_ids ) ) {
			return;
		}
		$placeholders = implode( ',', array_fill( 0, count( $term_taxonomy_ids ), '%d' ) );
		$results = $wpdb->get_results( $wpdb->prepare(
			"SELECT tr.term_taxonomy_id, COUNT(*) AS cnt
			FROM {$wpdb->term_relationships} tr
			INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id AND p.post_type = 'attachment'
			AND (p.post_status = 'inherit' OR p.post_status = 'private')
			WHERE tr.term_taxonomy_id IN ($placeholders)
			GROUP BY tr.term_taxonomy_id",
			...$term_taxonomy_ids
		), OBJECT_K );
		foreach ( $term_taxonomy_ids as $tt_id ) {
			$count = isset( $results[ (string) $tt_id ] ) ? (int) $results[ (string) $tt_id ]->cnt : 0;
			$wpdb->update( $wpdb->term_taxonomy, [ 'count' => $count ], [ 'term_taxonomy_id' => $tt_id ] );
		}

		// Direct SQL updates leave persistent object-cache term counts / get_terms lists stale.
		clean_term_cache( $term_taxonomy_ids, '', false );
		clean_taxonomy_cache( self::TAXONOMY );
		$this->clearTermsCache();
	}

	public function enqueueScripts( string $hook ): void {
		global $pagenow;
		if ( $pagenow !== 'upload.php' && $hook !== 'upload.php' ) {
			return;
		}

		$settings = $this->getSettings();
		$width    = max( 200, min( 400, (int) ( $settings['sidebar_width'] ?? 280 ) ) );
		$visible  = ! empty( $settings['sidebar_visible'] );
		$plugin_url = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$base_url   = $plugin_url . 'modules/' . self::MODULE_ID . '/assets/';

		wp_enqueue_style( 'wp-powersuite-content-folders', $base_url . 'content-folders.css', [], self::MODULE_VERSION );
		wp_enqueue_script( 'jquery-ui-sortable' );
		wp_enqueue_script( 'wp-powersuite-content-folders', $base_url . 'content-folders.js', [ 'jquery', 'jquery-ui-sortable' ], self::MODULE_VERSION, true );

		$menu_width      = (int) apply_filters( 'wpps_content_folders_admin_menu_width', 160 );
		$menu_width_fold = (int) apply_filters( 'wpps_content_folders_admin_menu_folded_width', 36 );
		$sidebar_left    = $menu_width;
		$content_margin  = $visible ? ( $menu_width + $width ) : $menu_width;
		$content_folded  = $visible ? ( $menu_width_fold + $width ) : $menu_width_fold;
		wp_add_inline_style( 'wp-powersuite-content-folders', sprintf(
			'.wpps-media-folders-sidebar { width: %1$dpx; left: %2$dpx; right: auto; } body.wp-admin.wpps-media-folders-page #wpcontent { margin-left: %3$dpx !important; } body.folded.wp-admin .wpps-media-folders-sidebar { left: %4$dpx; right: auto; } body.folded.wp-admin.wpps-media-folders-page #wpcontent { margin-left: %5$dpx !important; }',
			$width,
			$sidebar_left,
			$content_margin,
			$menu_width_fold,
			$content_folded
		) );

		$taxonomy       = self::TAXONOMY;
		$current_folder = isset( $_GET[ $taxonomy ] ) && is_scalar( $_GET[ $taxonomy ] ) ? sanitize_text_field( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
		$folder_list    = array_map(
			static function ( $t ) {
				return [ 'term_id' => (int) $t->term_id, 'name' => $t->name, 'parent' => (int) $t->parent ];
			},
			$this->getAllFolderTerms()
		);
		wp_localize_script( 'wp-powersuite-content-folders', 'wppsContentFolders', [
			'ajaxUrl'              => admin_url( 'admin-ajax.php' ),
			'uploadPageUrl'        => admin_url( 'upload.php' ),
			'nonce'                => wp_create_nonce( 'wpps_content_folders' ),
			'currentFolder'        => $current_folder,
			'taxonomy'             => $taxonomy,
			'folderList'           => $folder_list,
			'adminMenuWidth'       => $menu_width,
			'adminMenuWidthFolded' => $menu_width_fold,
			'settings'             => [
				'sidebar_width'  => $width,
				'default_folder' => (int) ( $settings['default_folder'] ?? 0 ),
			],
			'strings'              => [
				'confirmDelete'  => __( 'Delete this folder? Items will be unassigned.', 'wp-powersuite' ),
				'saved'          => __( 'Saved.', 'wp-powersuite' ),
				'error'          => __( 'An error occurred.', 'wp-powersuite' ),
				'assignToFolder' => __( 'Assign to folder', 'wp-powersuite' ),
				'parentNone'     => __( 'None (top level)', 'wp-powersuite' ),
			],
		] );
	}

	public function addFolderColumn( array $columns ): array {
		$new = [];
		foreach ( $columns as $key => $label ) {
			$new[ $key ] = $label;
			if ( $key === 'title' || $key === 'name' ) {
				$new['wpps_folder'] = __( 'Folder', 'wp-powersuite' );
			}
		}
		if ( ! isset( $new['wpps_folder'] ) ) {
			$new['wpps_folder'] = __( 'Folder', 'wp-powersuite' );
		}
		return $new;
	}

	public function renderFolderColumn( string $column_name, int $post_id ): void {
		if ( $column_name !== 'wpps_folder' ) {
			return;
		}
		$folders    = $this->getAllFolderTerms();
		$current    = get_the_terms( $post_id, self::TAXONOMY );
		$current_id = 0;
		if ( $current && ! is_wp_error( $current ) && ! empty( $current ) ) {
			$current_id = (int) $current[0]->term_id;
		}
		echo '<select class="wpps-folder-assign-select" data-attachment-id="' . esc_attr( (string) $post_id ) . '" aria-label="' . esc_attr__( 'Assign to folder', 'wp-powersuite' ) . '">';
		echo '<option value="0"' . selected( $current_id, 0, false ) . '>— ' . esc_html__( 'None', 'wp-powersuite' ) . ' —</option>';
		foreach ( $folders as $term ) {
			echo '<option value="' . esc_attr( (string) $term->term_id ) . '"' . selected( $current_id, $term->term_id, false ) . '>' . esc_html( $term->name ) . '</option>';
		}
		echo '</select>';
	}

	public function outputFolderFilter( string $post_type, string $which ): void {
		if ( $which !== 'top' || $post_type !== 'attachment' ) {
			return;
		}
		$taxonomy = self::TAXONOMY;
		$selected = isset( $_REQUEST[ $taxonomy ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ $taxonomy ] ) ) : '';
		$terms    = array_filter(
			$this->getAllFolderTerms(),
			static function ( $term ) {
				return (int) $term->parent === 0;
			}
		);
		?>
		<select name="<?php echo esc_attr( $taxonomy ); ?>" id="wpps-folder-filter">
			<option value=""><?php esc_html_e( 'All folders', 'wp-powersuite' ); ?></option>
			<option value="-1" <?php selected( $selected, '-1' ); ?>><?php esc_html_e( 'Unassigned', 'wp-powersuite' ); ?></option>
			<?php foreach ( $terms as $term ) : ?>
				<option value="<?php echo esc_attr( $term->slug ); ?>" <?php selected( $selected, $term->slug ); ?>><?php echo esc_html( $term->name ); ?></option>
			<?php endforeach; ?>
		</select>
		<?php
	}

	public function filterByFolder( \WP_Query $query ): void {
		if ( ! is_admin() || $query->get( 'post_type' ) !== 'attachment' ) {
			return;
		}
		global $pagenow;
		if ( $pagenow !== 'upload.php' ) {
			return;
		}
		$taxonomy = self::TAXONOMY;
		if ( ! isset( $_REQUEST[ $taxonomy ] ) ) {
			return;
		}
		$value = sanitize_text_field( wp_unslash( $_REQUEST[ $taxonomy ] ) );
		if ( $value === '' ) {
			return;
		}
		$query->set( $taxonomy, null );
		if ( $value === '-1' ) {
			$query->set( 'tax_query', [
				[ 'taxonomy' => $taxonomy, 'operator' => 'NOT EXISTS' ],
			] );
			return;
		}
		$query->set( 'tax_query', [
			[ 'taxonomy' => $taxonomy, 'field' => 'slug', 'terms' => $value ],
		] );
	}

	public function filterMediaGrid( array $query ): array {
		$value = $this->resolveMediaGridFolderFilter();
		if ( $value === null || $value === '' ) {
			return $query;
		}

		unset( $query[ self::TAXONOMY ] );

		$clause = null;
		if ( $value === '-1' || $value === -1 ) {
			$clause = [
				'taxonomy' => self::TAXONOMY,
				'operator' => 'NOT EXISTS',
			];
		} elseif ( is_numeric( $value ) && (int) $value > 0 ) {
			$term_id = (int) $value;
			$term    = get_term( $term_id, self::TAXONOMY );
			if ( ! $term || is_wp_error( $term ) ) {
				return $query;
			}
			$clause = [
				'taxonomy' => self::TAXONOMY,
				'field'    => 'term_id',
				'terms'    => [ $term_id ],
			];
		} else {
			$term = get_term_by( 'slug', $value, self::TAXONOMY );
			if ( ! $term || is_wp_error( $term ) ) {
				return $query;
			}
			$clause = [
				'taxonomy' => self::TAXONOMY,
				'field'    => 'term_id',
				'terms'    => [ (int) $term->term_id ],
			];
		}

		$existing = isset( $query['tax_query'] ) && is_array( $query['tax_query'] ) ? $query['tax_query'] : [];
		if ( $existing !== [] && ! isset( $existing['relation'] ) ) {
			$existing = array_merge( [ 'relation' => 'AND' ], $existing );
		}
		$existing[]         = $clause;
		$query['tax_query'] = $existing;

		return $query;
	}

	/**
	 * Resolve the active folder filter for query-attachments.
	 * Prefer the explicit media query arg; only use the cookie on Media Library requests.
	 */
	private function resolveMediaGridFolderFilter(): ?string {
		if ( isset( $_REQUEST['query'][ self::TAXONOMY ] ) && is_scalar( $_REQUEST['query'][ self::TAXONOMY ] ) ) {
			$value = sanitize_text_field( wp_unslash( (string) $_REQUEST['query'][ self::TAXONOMY ] ) );
			return $value !== '' ? $value : null;
		}

		if ( ! $this->isQueryAttachmentsFromMediaLibrary() ) {
			return null;
		}

		if ( isset( $_COOKIE[ self::FOLDER_COOKIE ] ) && is_scalar( $_COOKIE[ self::FOLDER_COOKIE ] ) ) {
			$value = sanitize_text_field( wp_unslash( (string) $_COOKIE[ self::FOLDER_COOKIE ] ) );
			return $value !== '' ? $value : null;
		}

		return null;
	}

	private function isQueryAttachmentsFromMediaLibrary(): bool {
		if ( empty( $_SERVER['HTTP_REFERER'] ) ) {
			return false;
		}

		$referer = wp_parse_url( wp_unslash( (string) $_SERVER['HTTP_REFERER'] ) );
		if ( ! is_array( $referer ) ) {
			return false;
		}

		$path = (string) ( $referer['path'] ?? '' );
		return $path !== '' && ( str_ends_with( $path, '/upload.php' ) || str_ends_with( $path, 'upload.php' ) );
	}

	public function assignDefaultFolderToAttachment( int $attachment_id ): void {
		$settings = $this->getSettings();
		$term_id  = (int) ( $settings['default_folder'] ?? 0 );
		if ( $term_id <= 0 ) {
			return;
		}
		$term = get_term( $term_id, self::TAXONOMY );
		if ( $term && ! is_wp_error( $term ) ) {
			wp_set_object_terms( $attachment_id, $term_id, self::TAXONOMY );
		}
	}

	private function verifyAjaxNonce(): void {
		if ( ! check_ajax_referer( 'wpps_content_folders', 'nonce', false ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid security token.', 'wp-powersuite' ) ] );
		}
		if ( ! current_user_can( 'upload_files' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxAddFolder(): void {
		$this->verifyAjaxNonce();
		$name      = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		$parent_id = isset( $_POST['parent_id'] ) ? (int) $_POST['parent_id'] : 0;
		if ( ! $name ) {
			wp_send_json_error( [ 'message' => __( 'Invalid request.', 'wp-powersuite' ) ] );
		}
		if ( ! current_user_can( 'manage_categories' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		if ( $parent_id > 0 ) {
			$parent = get_term( $parent_id, self::TAXONOMY );
			if ( ! $parent || is_wp_error( $parent ) ) {
				wp_send_json_error( [ 'message' => __( 'Invalid parent folder.', 'wp-powersuite' ) ] );
			}
		}
		$result = wp_insert_term( $name, self::TAXONOMY, [ 'parent' => $parent_id ] );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( [ 'message' => $result->get_error_message() ] );
		}
		$this->clearTermsCache();
		wp_send_json_success( [ 'term_id' => $result['term_id'], 'name' => $name, 'slug' => $result['slug'] ] );
	}

	public function ajaxUpdateFolder(): void {
		$this->verifyAjaxNonce();
		$term_id   = isset( $_POST['term_id'] ) ? (int) $_POST['term_id'] : 0;
		$name      = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		$parent_id = isset( $_POST['parent_id'] ) ? (int) $_POST['parent_id'] : 0;
		if ( ! $term_id || ! $name ) {
			wp_send_json_error( [ 'message' => __( 'Invalid request.', 'wp-powersuite' ) ] );
		}
		if ( ! current_user_can( 'manage_categories' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$term = get_term( $term_id, self::TAXONOMY );
		if ( ! $term || is_wp_error( $term ) ) {
			wp_send_json_error( [ 'message' => __( 'Folder not found.', 'wp-powersuite' ) ] );
		}
		if ( $parent_id > 0 ) {
			$parent = get_term( $parent_id, self::TAXONOMY );
			if ( ! $parent || is_wp_error( $parent ) ) {
				wp_send_json_error( [ 'message' => __( 'Invalid parent folder.', 'wp-powersuite' ) ] );
			}
			if ( (int) $parent_id === $term_id ) {
				wp_send_json_error( [ 'message' => __( 'A folder cannot be its own parent.', 'wp-powersuite' ) ] );
			}
			if ( $this->termIsDescendantOf( $parent_id, $term_id ) ) {
				wp_send_json_error( [ 'message' => __( 'Cannot move folder inside its own subfolder.', 'wp-powersuite' ) ] );
			}
		}
		$result = wp_update_term( $term_id, self::TAXONOMY, [ 'name' => $name, 'parent' => $parent_id ] );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( [ 'message' => $result->get_error_message() ] );
		}
		$this->clearTermsCache();
		wp_send_json_success( [ 'term_id' => $term_id, 'name' => $name ] );
	}

	private function termIsDescendantOf( int $term_id, int $ancestor_id ): bool {
		$current = get_term( $term_id, self::TAXONOMY );
		while ( $current && ! is_wp_error( $current ) && (int) $current->parent > 0 ) {
			if ( (int) $current->parent === $ancestor_id ) {
				return true;
			}
			$current = get_term( (int) $current->parent, self::TAXONOMY );
		}
		return false;
	}

	public function ajaxDeleteFolder(): void {
		$this->verifyAjaxNonce();
		$term_id = isset( $_POST['term_id'] ) ? (int) $_POST['term_id'] : 0;
		if ( ! $term_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid request.', 'wp-powersuite' ) ] );
		}
		if ( ! current_user_can( 'manage_categories' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$term = get_term( $term_id, self::TAXONOMY );
		if ( ! $term || is_wp_error( $term ) ) {
			wp_send_json_error( [ 'message' => __( 'Folder not found.', 'wp-powersuite' ) ] );
		}
		$result = wp_delete_term( $term_id, self::TAXONOMY );
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( [ 'message' => $result->get_error_message() ] );
		}
		$this->clearTermsCache();
		wp_send_json_success();
	}

	public function ajaxAssignMediaToFolder(): void {
		$this->verifyAjaxNonce();
		$attachment_id = isset( $_POST['attachment_id'] ) ? (int) $_POST['attachment_id'] : 0;
		$term_id       = isset( $_POST['term_id'] ) ? (int) $_POST['term_id'] : 0;
		if ( ! $attachment_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid request.', 'wp-powersuite' ) ] );
		}
		$post = get_post( $attachment_id );
		if ( ! $post || $post->post_type !== 'attachment' ) {
			wp_send_json_error( [ 'message' => __( 'Invalid attachment.', 'wp-powersuite' ) ] );
		}
		if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		if ( $term_id > 0 ) {
			$term = get_term( $term_id, self::TAXONOMY );
			if ( ! $term || is_wp_error( $term ) ) {
				wp_send_json_error( [ 'message' => __( 'Invalid folder.', 'wp-powersuite' ) ] );
			}
			wp_set_object_terms( $attachment_id, $term_id, self::TAXONOMY );
			wp_update_term_count_now( [ $term->term_taxonomy_id ], self::TAXONOMY );
		} else {
			$previous = get_the_terms( $attachment_id, self::TAXONOMY );
			wp_set_object_terms( $attachment_id, [], self::TAXONOMY );
			if ( $previous && ! is_wp_error( $previous ) && ! empty( $previous ) ) {
				wp_update_term_count_now( [ $previous[0]->term_taxonomy_id ], self::TAXONOMY );
			}
		}
		$this->clearTermsCache();
		wp_send_json_success();
	}

	public function ajaxSaveSidebarState(): void {
		$this->verifyAjaxNonce();
		$settings = $this->getSettings();
		if ( isset( $_POST['sidebar_width'] ) ) {
			$w = (int) $_POST['sidebar_width'];
			$settings['sidebar_width'] = max( 200, min( 400, $w ) );
		}
		if ( isset( $_POST['sidebar_visible'] ) ) {
			$settings['sidebar_visible'] = ! empty( $_POST['sidebar_visible'] );
		}
		if ( isset( $_POST['default_folder'] ) && \WP_PowerSuite\Core\NetworkScope::canManagePlugin() ) {
			$folder_id = (int) $_POST['default_folder'];
			if ( $folder_id === 0 ) {
				$settings['default_folder'] = 0;
			} else {
				$term = get_term( $folder_id, self::TAXONOMY );
				if ( $term && ! is_wp_error( $term ) ) {
					$settings['default_folder'] = $folder_id;
				}
			}
		}
		$this->clearSettingsCache();
		$this->moduleSettings()->save( $settings );
		wp_send_json_success();
	}

	public function ajaxReorderFolders(): void {
		$this->verifyAjaxNonce();
		if ( ! current_user_can( 'manage_categories' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$order = isset( $_POST['order'] ) && is_array( $_POST['order'] ) ? array_map( 'intval', $_POST['order'] ) : [];
		$order = array_filter( $order );
		$top_level = get_terms( [
			'taxonomy'   => self::TAXONOMY,
			'hide_empty' => false,
			'parent'     => 0,
			'fields'     => 'ids',
		] );
		$valid_ids = is_array( $top_level ) ? array_flip( $top_level ) : [];
		$order = array_values( array_filter( $order, function ( $id ) use ( $valid_ids ) {
			return isset( $valid_ids[ $id ] );
		} ) );
		$settings = $this->getSettings();
		$settings['folder_order'] = $order;
		$this->clearSettingsCache();
		$this->moduleSettings()->save( $settings );
		wp_send_json_success();
	}

	public function addBulkActions( array $actions ): array {
		$actions['wpps_assign_to_folder']   = __( 'Assign to folder', 'wp-powersuite' );
		$actions['wpps_remove_from_folder'] = __( 'Remove from folder', 'wp-powersuite' );
		return $actions;
	}

	public function handleBulkActions( string $redirect, string $action, array $post_ids ): string {
		if ( $action === 'wpps_remove_from_folder' && ! empty( $post_ids ) ) {
			$taxonomy = self::TAXONOMY;
			$updated = 0;
			$term_ids_updated = [];
			foreach ( $post_ids as $id ) {
				$id = (int) $id;
				if ( ! $id || ! current_user_can( 'edit_post', $id ) ) {
					continue;
				}
				$post = get_post( $id );
				if ( ! $post || $post->post_type !== 'attachment' ) {
					continue;
				}
				$previous = get_the_terms( $id, $taxonomy );
				wp_set_object_terms( $id, [], $taxonomy );
				if ( $previous && ! is_wp_error( $previous ) && ! empty( $previous ) ) {
					$term_ids_updated[ $previous[0]->term_taxonomy_id ] = true;
				}
				$updated++;
			}
			foreach ( array_keys( $term_ids_updated ) as $tt_id ) {
				wp_update_term_count_now( [ $tt_id ], $taxonomy );
			}
			$redirect = add_query_arg( 'wpps_removed', $updated, $redirect );
		}
		if ( $action === 'wpps_assign_to_folder' && ! empty( $post_ids ) ) {
			$redirect = add_query_arg( [
				'wpps_bulk_assign' => '1',
				'media'            => implode( ',', array_map( 'intval', $post_ids ) ),
			], $redirect );
		}
		return $redirect;
	}

	public function bulkAssignFormNotice(): void {
		if ( ! current_user_can( 'upload_files' ) ) {
			return;
		}
		if ( isset( $_GET['wpps_assigned'] ) ) {
			$n = max( 0, (int) $_GET['wpps_assigned'] );
			echo '<div class="notice notice-success is-dismissible"><p>';
			printf(
				/* translators: 1: number, 2: number. */
				esc_html( _n( '%d item assigned to folder.', '%d items assigned to folder.', $n, 'wp-powersuite' ) ),
				$n
			);
			echo '</p></div>';
			return;
		}
		if ( isset( $_GET['wpps_removed'] ) ) {
			$n = max( 0, (int) $_GET['wpps_removed'] );
			echo '<div class="notice notice-success is-dismissible"><p>';
			printf(
				/* translators: 1: number, 2: number. */
				esc_html( _n( '%d item removed from folder.', '%d items removed from folder.', $n, 'wp-powersuite' ) ),
				$n
			);
			echo '</p></div>';
			return;
		}
		if ( ! isset( $_GET['wpps_bulk_assign'], $_GET['media'] ) ) {
			return;
		}
		$media = sanitize_text_field( wp_unslash( $_GET['media'] ) );
		$ids   = array_filter( array_map( 'intval', explode( ',', $media ) ) );
		if ( empty( $ids ) ) {
			return;
		}
		$all_terms = $this->getAllFolderTerms();
		$count = count( $ids );
		$url   = admin_url( 'upload.php' );
		?>
		<div class="notice notice-info wpps-bulk-assign-notice">
			<p>
				<?php
				printf(
					/* translators: 1: number, 2: number. */
					esc_html( _n( 'Assign %d item to folder:', 'Assign %d items to folder:', $count, 'wp-powersuite' ) ),
					$count
				);
				?>
			</p>
			<form method="post" action="<?php echo esc_url( $url ); ?>" style="margin: 8px 0;">
				<?php wp_nonce_field( 'wpps_bulk_assign', 'wpps_bulk_assign_nonce' ); ?>
				<input type="hidden" name="wpps_bulk_assign_do" value="1" />
				<?php foreach ( $ids as $id ) : ?>
					<input type="hidden" name="media[]" value="<?php echo esc_attr( (string) $id ); ?>" />
				<?php endforeach; ?>
				<select name="wpps_folder_id" id="wpps-bulk-folder-select" required>
					<option value=""><?php esc_html_e( '— Select folder —', 'wp-powersuite' ); ?></option>
					<?php foreach ( $all_terms as $term ) : ?>
						<option value="<?php echo esc_attr( (string) $term->term_id ); ?>"><?php echo esc_html( $term->name ); ?></option>
					<?php endforeach; ?>
				</select>
				<button type="submit" class="button button-primary"><?php esc_html_e( 'Apply', 'wp-powersuite' ); ?></button>
				<a href="<?php echo esc_url( $url ); ?>" class="button"><?php esc_html_e( 'Cancel', 'wp-powersuite' ); ?></a>
			</form>
		</div>
		<?php
	}

	public function handleBulkAssignSubmit(): void {
		if ( ! isset( $_POST['wpps_bulk_assign_do'], $_POST['wpps_bulk_assign_nonce'], $_POST['media'] ) ) {
			return;
		}
		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpps_bulk_assign_nonce'] ) ), 'wpps_bulk_assign' ) ) {
			return;
		}
		if ( ! current_user_can( 'upload_files' ) ) {
			return;
		}
		$folder_id = isset( $_POST['wpps_folder_id'] ) ? (int) $_POST['wpps_folder_id'] : 0;
		$term = $folder_id > 0 ? get_term( $folder_id, self::TAXONOMY ) : null;
		if ( ! $term || is_wp_error( $term ) ) {
			return;
		}
		$ids = array_filter( array_map( 'intval', (array) $_POST['media'] ) );
		$taxonomy = self::TAXONOMY;
		$updated = 0;
		foreach ( $ids as $id ) {
			$post = get_post( $id );
			if ( ! $post || $post->post_type !== 'attachment' ) {
				continue;
			}
			if ( ! current_user_can( 'edit_post', $id ) ) {
				continue;
			}
			wp_set_object_terms( $id, $folder_id, $taxonomy );
			$updated++;
		}
		wp_update_term_count_now( [ $term->term_taxonomy_id ], $taxonomy );
		wp_safe_redirect( add_query_arg( 'wpps_assigned', $updated, admin_url( 'upload.php' ) ) );
		exit;
	}
}

