<?php
/**
 * KooKoa Folders — Folder meta/property AJAX handlers.
 *
 * Single-folder property toggles: colour, icon, lock and pin. Registered and
 * dispatched by KookoaWPF_Ajax; this collaborator holds the handler bodies and
 * the icon-sanitisation helper they share.
 *
 * @package KookoaWPF
 * @license GPL-2.0-or-later
 */

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

/**
 * AJAX handlers for folder colour, icon, lock and pin state.
 */
class KookoaWPF_Ajax_Meta extends KookoaWPF_Ajax_Handler {

	/**
	 * Maximum length for a folder icon stored in term meta. Matches the cap
	 * applied by the JSON import path in KookoaWPF_Export so the same limit
	 * holds whether an icon arrives via live AJAX or via bulk import.
	 *
	 * @var int
	 */
	private const ICON_MAX_LENGTH = 64;

	/**
	 * 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).
	}

	/**
	 * Update folder color
	 */
	public function update_folder_color(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$folder_id = isset( $_POST['folder_id'] ) ? absint( wp_unslash( $_POST['folder_id'] ) ) : 0;
		$post_type = self::require_managed_post_type(
			isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''
		);
		$color     = isset( $_POST['color'] ) ? sanitize_hex_color( wp_unslash( $_POST['color'] ) ) : '';

		if ( ! $folder_id || empty( $post_type ) || empty( $color ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing data.', 'kookoa-folders' ) ) );
		}

		// Ensure the term belongs to the taxonomy of the claimed post type —
		// prevents cross-post-type tampering via enumerated term_ids.
		if ( ! self::folder_in_taxonomy( $folder_id, $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Folder not found.', 'kookoa-folders' ) ) );
		}

		if ( KookoaWPF_Folders::get_meta( $folder_id, 'folder_locked' ) ) {
			wp_send_json_error( array( 'message' => __( 'This folder is locked.', 'kookoa-folders' ) ) );
		}

		KookoaWPF_Folders::update_meta( $folder_id, 'folder_color', $color );

		wp_send_json_success( array( 'message' => __( 'Color updated.', 'kookoa-folders' ) ) );
	}

	/**
	 * Update folder icon (emoji or empty to reset to default)
	 */
	public function update_folder_icon(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$folder_id = isset( $_POST['folder_id'] ) ? absint( wp_unslash( $_POST['folder_id'] ) ) : 0;
		$post_type = self::require_managed_post_type(
			isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''
		);
		$icon      = self::sanitize_folder_icon(
			isset( $_POST['icon'] ) ? sanitize_text_field( wp_unslash( $_POST['icon'] ) ) : ''
		);

		if ( ! $folder_id || empty( $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Missing data.', 'kookoa-folders' ) ) );
		}

		if ( ! self::folder_in_taxonomy( $folder_id, $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Folder not found.', 'kookoa-folders' ) ) );
		}

		if ( KookoaWPF_Folders::get_meta( $folder_id, 'folder_locked' ) ) {
			wp_send_json_error( array( 'message' => __( 'This folder is locked.', 'kookoa-folders' ) ) );
		}

		if ( empty( $icon ) ) {
			KookoaWPF_Folders::delete_meta( $folder_id, 'folder_icon' );
		} else {
			KookoaWPF_Folders::update_meta( $folder_id, 'folder_icon', $icon );
		}

		wp_send_json_success( array( 'message' => __( 'Icon updated.', 'kookoa-folders' ) ) );
	}

	/**
	 * Toggle folder lock.
	 */
	public function toggle_lock(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$folder_id = isset( $_POST['folder_id'] ) ? absint( wp_unslash( $_POST['folder_id'] ) ) : 0;
		$post_type = self::require_managed_post_type(
			isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''
		);
		if ( ! $folder_id || empty( $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid folder.', 'kookoa-folders' ) ) );
		}
		$taxonomy = KookoaWPF_Folders::get_taxonomy( $post_type );
		if ( ! $taxonomy || ! term_exists( $folder_id, $taxonomy ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid folder.', 'kookoa-folders' ) ) );
		}
		$current = KookoaWPF_Folders::get_meta( $folder_id, 'folder_locked' );

		KookoaWPF_Folders::update_meta( $folder_id, 'folder_locked', ! $current );

		wp_send_json_success(
			array(
				'locked'  => ! $current,
				'message' => ! $current ? __( 'Folder locked.', 'kookoa-folders' ) : __( 'Folder unlocked.', 'kookoa-folders' ),
			)
		);
	}

	/**
	 * Toggle folder pin/bookmark.
	 * Pin = move to top of siblings (after existing pinned ones)
	 * Unpin = move to bottom of siblings
	 */
	public function toggle_pin(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$folder_id = isset( $_POST['folder_id'] ) ? absint( wp_unslash( $_POST['folder_id'] ) ) : 0;
		$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' ) ) );
		}

		if ( ! $folder_id || ! self::folder_in_taxonomy( $folder_id, $post_type ) ) {
			wp_send_json_error( array( 'message' => __( 'Folder not found.', 'kookoa-folders' ) ) );
		}

		$current   = KookoaWPF_Folders::get_meta( $folder_id, 'folder_pinned' );
		$new_state = ! $current;

		KookoaWPF_Folders::update_meta( $folder_id, 'folder_pinned', $new_state );

		// Get this folder's siblings (same parent).
		$taxonomy  = KookoaWPF_Folders::get_taxonomy( $post_type );
		$term      = get_term( $folder_id, $taxonomy );
		$parent_id = $term instanceof WP_Term ? $term->parent : 0;

		$siblings = KookoaWPF_Folders::get_children_ordered( $parent_id, $taxonomy );

		if ( ! empty( $siblings ) ) {
			// Prime meta cache to avoid N+1 queries.
			$sib_ids = wp_list_pluck( $siblings, 'term_id' );
			update_termmeta_cache( $sib_ids );

			// Separate pinned and unpinned, preserving current order.
			$pinned  = array();
			$regular = array();

			foreach ( $siblings as $sib ) {
				$is_pinned = KookoaWPF_Folders::get_meta( $sib->term_id, 'folder_pinned' );
				if ( $is_pinned ) {
					$pinned[] = $sib->term_id;
				} else {
					$regular[] = $sib->term_id;
				}
			}

			// Rebuild order: pinned first, then regular — batch via CASE WHEN.
			$all_ordered = array_merge( $pinned, $regular );

			// Only update siblings whose order actually changed.
			$updates = array();
			foreach ( $all_ordered as $index => $tid ) {
				$current_order = (int) KookoaWPF_Folders::get_meta( $tid, 'folder_order' );
				if ( $current_order !== $index ) {
					$updates[ $tid ] = $index;
				}
			}

			if ( ! empty( $updates ) ) {
				foreach ( $updates as $tid => $order ) {
					KookoaWPF_Folders::update_meta( $tid, 'folder_order', $order );
				}
			}
		}

		wp_send_json_success(
			array(
				'pinned'  => $new_state,
				'message' => $new_state ? __( 'Pinned to top.', 'kookoa-folders' ) : __( 'Unpinned.', 'kookoa-folders' ),
			)
		);
	}

	/**
	 * Sanitize and length-cap a folder icon string from a request.
	 *
	 * The caller passes the value already unslashed and run through
	 * sanitize_text_field(). An empty string is preserved so callers can
	 * use it to reset the icon to the default. Non-empty values are
	 * truncated to self::ICON_MAX_LENGTH characters using mb_substr() when
	 * available so emoji and other multibyte characters are not split
	 * mid-codepoint.
	 *
	 * @param string $icon Already-sanitized icon string.
	 * @return string Empty string, or icon truncated to ICON_MAX_LENGTH.
	 */
	private static function sanitize_folder_icon( string $icon ): string {
		if ( '' === $icon ) {
			return '';
		}
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $icon, 0, self::ICON_MAX_LENGTH );
		}
		return substr( $icon, 0, self::ICON_MAX_LENGTH );
	}
}
