<?php
/**
 * KooKoa Folders — Export/Import AJAX handlers.
 *
 * Extracted from class-kookoawpf-ajax.php (blocco 31).
 *
 * @package KookoaWPF
 */

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

/**
 * Handles folder export and import via AJAX.
 */
class KookoaWPF_Export {

	/**
	 * 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;
	}

	/**
	 * Register AJAX actions for export and import.
	 */
	private function __construct() {
		add_action( 'wp_ajax_kookoawpf_export_folders', array( $this, 'export_folders' ) );
		add_action( 'wp_ajax_kookoawpf_import_folders', array( $this, 'import_folders' ) );
	}

	/**
	 * Verify capability + RBAC for an export/import handler.
	 *
	 * Each public handler verifies its nonce inline as its first statement;
	 * this helper covers only the capability/role check that is identical
	 * across handlers, so the nonce check stays at the call site of every
	 * endpoint that authenticates its own request.
	 */
	private function verify_request(): void {
		if ( ! current_user_can( 'manage_options' ) || ! kookoawpf_user_has_access() ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'kookoa-folders' ) ) );
		}
	}

	/**
	 * Export folder structure as JSON.
	 */
	public function export_folders(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		$post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : '';
		$scope     = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : 'single';

		if ( 'all' === $scope ) {
			$export    = array();
			$supported = kookoawpf()->get_supported_post_types();
			foreach ( $supported as $pt ) {
				$tree = KookoaWPF_Folders::get_tree( $pt );
				if ( ! empty( $tree ) ) {
					$export[ $pt ] = self::prepare_export_tree( $tree );
				}
			}
		} else {
			if ( empty( $post_type ) ) {
				wp_send_json_error( array( 'message' => __( 'Missing post type.', 'kookoa-folders' ) ) );
			}
			// Symmetric with the import side: only allow post types this
			// plugin actually manages, so an admin can't export a tree for
			// a slug we don't own.
			$supported = kookoawpf()->get_supported_post_types();
			if ( ! in_array( $post_type, $supported, true ) ) {
				wp_send_json_error( array( 'message' => __( 'Unsupported post type.', 'kookoa-folders' ) ) );
			}
			$tree   = KookoaWPF_Folders::get_tree( $post_type );
			$export = array( $post_type => self::prepare_export_tree( $tree ) );
		}

		$data = array(
			'plugin'      => 'kookoa-folders',
			'version'     => KOOKOAWPF_VERSION,
			'exported_at' => current_time( 'mysql' ),
			'site_url'    => get_site_url(),
			'folders'     => $export,
		);

		wp_send_json_success( array( 'json' => $data ) );
	}

	/**
	 * Prepare tree nodes for export (strip runtime data, keep structure + meta).
	 *
	 * @param array<int, array<string, mixed>> $tree Hierarchical folder tree array.
	 * @return array<int, array<string, mixed>>
	 */
	private static function prepare_export_tree( $tree ) {
		$result = array();
		foreach ( $tree as $node ) {
			$item = array(
				'name'   => $node['text'],
				'color'  => $node['color'],
				'locked' => $node['locked'],
				'pinned' => $node['pinned'],
				'order'  => $node['order'],
				'icon'   => isset( $node['icon'] ) ? $node['icon'] : '',
			);
			// Smart folder bookkeeping. The tree from KookoaWPF_Folders::get_tree
			// carries `is_dynamic` but NOT `dynamic_rules` (the rules payload
			// is stored as termmeta and only ever materialised at runtime by
			// KookoaWPF_Dynamic::build_query_payload). Without re-serialising
			// both pieces here the export drops every smart folder back to a
			// plain folder on import — backups / "replace all" cycles silently
			// lose the rule sets.
			if ( ! empty( $node['is_dynamic'] ) ) {
				$item['is_dynamic'] = true;
				$rules              = KookoaWPF_Folders::get_meta( (int) $node['id'], 'dynamic_rules' );
				if ( is_array( $rules ) ) {
					$item['dynamic_rules'] = $rules;
				}
			}
			if ( ! empty( $node['children'] ) ) {
				$item['children'] = self::prepare_export_tree( $node['children'] );
			}
			$result[] = $item;
		}
		return $result;
	}

	/**
	 * Import folder structure from JSON.
	 */
	public function import_folders(): void {
		check_ajax_referer( 'kookoawpf_nonce', 'nonce' );
		$this->verify_request();

		// JSON payload: sanitize_textarea_field strips tags and control characters
		// (which a legitimate folder-export JSON never contains) while preserving
		// the newlines and braces json_decode() needs. Each leaf value is
		// re-sanitised per-field inside import_tree_recursive().
		$json_raw = isset( $_POST['json_data'] ) ? sanitize_textarea_field( wp_unslash( $_POST['json_data'] ) ) : '';
		$mode     = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : 'merge';

		if ( empty( $json_raw ) ) {
			wp_send_json_error( array( 'message' => __( 'No JSON data provided.', 'kookoa-folders' ) ) );
		}

		$data = json_decode( $json_raw, true );

		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid JSON format.', 'kookoa-folders' ) ) );
		}

		if ( ! isset( $data['plugin'] ) || 'kookoa-folders' !== $data['plugin'] ) {
			wp_send_json_error( array( 'message' => __( 'This JSON file is not a valid KooKoa Folders export.', 'kookoa-folders' ) ) );
		}

		if ( ! isset( $data['folders'] ) || ! is_array( $data['folders'] ) ) {
			wp_send_json_error( array( 'message' => __( 'No folder data found in the file.', 'kookoa-folders' ) ) );
		}

		$supported = kookoawpf()->get_supported_post_types();
		$imported  = 0;
		$skipped   = 0;

		foreach ( $data['folders'] as $post_type => $tree ) {
			$post_type = sanitize_key( $post_type );

			if ( ! in_array( $post_type, $supported, true ) ) {
				++$skipped;
				continue;
			}

			$taxonomy = KookoaWPF_Folders::get_taxonomy( $post_type );

			if ( 'replace' === $mode ) {
				$existing = get_terms(
					array(
						'taxonomy'   => $taxonomy,
						'hide_empty' => false,
						'fields'     => 'ids',
					)
				);
				if ( ! is_wp_error( $existing ) ) {
					foreach ( $existing as $tid ) {
						KookoaWPF_Folders::delete_all_meta( $tid );
						wp_delete_term( $tid, $taxonomy );
					}
				}
			}

			$count     = $this->import_tree_recursive( $tree, 0, $post_type, $taxonomy );
			$imported += $count;
		}

		wp_send_json_success(
			array(
				'message'  => sprintf(
				/* translators: %d: number of folders imported */
					__( 'Import complete: %d folders imported.', 'kookoa-folders' ),
					$imported
				),
				'imported' => $imported,
				'skipped'  => $skipped,
			)
		);
	}

	/**
	 * Recursively import folder tree.
	 *
	 * @param array<string, mixed> $tree      Array of folder node data to import.
	 * @param int                  $parent_id Parent term ID (0 for root).
	 * @param string               $post_type The post type slug.
	 * @param string               $taxonomy  The taxonomy name.
	 * @param int                  $depth     Current recursion depth.
	 * @return int Number of folders imported.
	 */
	private function import_tree_recursive( $tree, $parent_id, $post_type, $taxonomy, $depth = 0 ) {
		$count = 0;

		if ( ! is_array( $tree ) ) {
			return 0; }
		if ( $depth > 20 ) {
			return 0; }

		foreach ( $tree as $node ) {
			$name = isset( $node['name'] ) ? sanitize_text_field( $node['name'] ) : '';
			// Cap at MySQL term name column length to avoid silent truncation / DB errors.
			if ( strlen( $name ) > 200 ) {
				$name = substr( $name, 0, 200 );
			}
			if ( empty( $name ) ) {
				continue; }

			$existing = get_terms(
				array(
					'taxonomy'   => $taxonomy,
					'name'       => $name,
					'parent'     => $parent_id,
					'hide_empty' => false,
					'number'     => 1,
				)
			);

			if ( ! is_wp_error( $existing ) && ! empty( $existing ) ) {
				$term_id = $existing[0]->term_id;
			} else {
				$result = wp_insert_term( $name, $taxonomy, array( 'parent' => $parent_id ) );
				if ( is_wp_error( $result ) ) {
					if ( $result->get_error_code() === 'term_exists' ) {
						$existing_id = absint( $result->get_error_data() );
						$verify      = get_term( $existing_id, $taxonomy );
						if ( $verify && ! is_wp_error( $verify ) ) {
							$term_id = $existing_id;
						} else {
							$unique_slug = sanitize_title( $name ) . '-' . substr( md5( uniqid( '', true ) ), 0, 6 );
							$retry       = wp_insert_term(
								$name,
								$taxonomy,
								array(
									'parent' => $parent_id,
									'slug'   => $unique_slug,
								)
							);
							if ( is_wp_error( $retry ) ) {
								continue;
							}
							$term_id = $retry['term_id'];
							++$count;
						}
					} else {
						continue;
					}
				} else {
					$term_id = $result['term_id'];
					++$count;
				}
			}

			if ( isset( $node['color'] ) && $node['color'] ) {
				KookoaWPF_Folders::update_meta( $term_id, 'folder_color', sanitize_hex_color( $node['color'] ) );
			}
			if ( isset( $node['order'] ) ) {
				KookoaWPF_Folders::update_meta( $term_id, 'folder_order', intval( $node['order'] ) );
			}
			if ( ! empty( $node['locked'] ) ) {
				KookoaWPF_Folders::update_meta( $term_id, 'folder_locked', true );
			}
			if ( ! empty( $node['pinned'] ) ) {
				KookoaWPF_Folders::update_meta( $term_id, 'folder_pinned', true );
			}
			if ( ! empty( $node['icon'] ) ) {
				$icon_clean = sanitize_text_field( $node['icon'] );
				if ( strlen( $icon_clean ) > 64 ) {
					$icon_clean = substr( $icon_clean, 0, 64 );
				}
				KookoaWPF_Folders::update_meta( $term_id, 'folder_icon', $icon_clean );
			}
			// Smart folder reconstruction — paired with the export-side
			// `is_dynamic` + `dynamic_rules` capture in prepare_export_tree().
			// Without re-applying both meta the term comes back as a plain
			// folder and the user silently loses the rule-based automation.
			// `field` is normalised with sanitize_key() to match how
			// KookoaWPF_Dynamic::build_query_payload() reads it; `value`
			// keeps the scalar payload as text and is re-sanitised per
			// rule type at query-build time. Rule count is capped at 6 to
			// stay aligned with the UI/storage limit documented in CLAUDE.md
			// section 7.
			if ( ! empty( $node['is_dynamic'] ) ) {
				KookoaWPF_Folders::update_meta( $term_id, 'is_dynamic', 1 );
				$clean_rules = array();
				if ( isset( $node['dynamic_rules'] ) && is_array( $node['dynamic_rules'] ) ) {
					foreach ( $node['dynamic_rules'] as $rule ) {
						if ( ! is_array( $rule ) ) {
							continue;
						}
						$field = isset( $rule['field'] ) ? sanitize_key( $rule['field'] ) : '';
						if ( '' === $field ) {
							continue;
						}
						$value         = isset( $rule['value'] ) ? sanitize_text_field( (string) $rule['value'] ) : '';
						$clean_rules[] = array(
							'field' => $field,
							'value' => $value,
						);
						if ( count( $clean_rules ) >= 6 ) {
							break;
						}
					}
				}
				KookoaWPF_Folders::update_meta( $term_id, 'dynamic_rules', $clean_rules );
			}
			if ( ! empty( $node['children'] ) && is_array( $node['children'] ) ) {
				$count += $this->import_tree_recursive( $node['children'], $term_id, $post_type, $taxonomy, $depth + 1 );
			}
		}

		return $count;
	}
}
