<?php
/**
 * Admin REST endpoints for taxonomy consolidation.
 *
 * Lets the premium backend audit a site's taxonomy and apply operator-confirmed
 * fuzzy-duplicate merges. Authenticated via X-License-Key. See workplan
 * FEAT-TaxonomyConsolidationAdmin.md for the operator workflow.
 *
 * @package Simple_Ai_Auto_Post
 */

if ( ! defined( 'WPINC' ) ) {
	die;
}

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

/**
 * Register the taxonomy admin REST routes.
 */
function saiap_register_taxonomy_admin_routes() {
	register_rest_route(
		'clearpost/v1',
		'/taxonomy/audit',
		array(
			'methods'             => 'GET',
			'callback'            => 'saiap_handle_taxonomy_audit',
			'permission_callback' => 'saiap_verify_taxonomy_admin_permission',
		)
	);

	register_rest_route(
		'clearpost/v1',
		'/taxonomy/merge',
		array(
			'methods'             => 'POST',
			'callback'            => 'saiap_handle_taxonomy_merge',
			'permission_callback' => 'saiap_verify_taxonomy_admin_permission',
			'args'                => array(
				'proposal' => array(
					'required' => true,
					'type'     => 'object',
				),
			),
		)
	);

}
add_action( 'rest_api_init', 'saiap_register_taxonomy_admin_routes' );

/**
 * License-key permission check, mirroring the insert-post pattern.
 *
 * @param WP_REST_Request $request The REST request.
 * @return bool|WP_Error
 */
function saiap_verify_taxonomy_admin_permission( $request ) {
	$license_key_header = $request->get_header( 'X-License-Key' );

	if ( empty( $license_key_header ) ) {
		return new WP_Error( 'missing_license_key', 'X-License-Key header is required', array( 'status' => 401 ) );
	}

	$stored_license_key = get_option( 'saiap_license_key', '' );
	if ( empty( $stored_license_key ) ) {
		if ( saiap_try_recover_license_key( $license_key_header ) ) {
			return true;
		}
		return new WP_Error( 'no_license_configured', 'No license key configured on this site', array( 'status' => 403 ) );
	}

	if ( ! hash_equals( $stored_license_key, $license_key_header ) ) {
		return new WP_Error( 'invalid_license_key', 'Invalid license key', array( 'status' => 403 ) );
	}

	return true;
}

/**
 * GET /clearpost/v1/taxonomy/audit
 *
 * Returns the list of categories and post tags with their post counts and a
 * small sample of post titles per term, for the backend's LLM clustering pass.
 *
 * Response shape:
 * {
 *   taxonomies: {
 *     category: [ { term_id, name, slug, count, sample_post_titles: string[] }, ... ],
 *     post_tag: [ ... ]
 *   }
 * }
 *
 * @return WP_REST_Response
 */
function saiap_handle_taxonomy_audit() {
	$result = array(
		'taxonomies' => array(),
	);

	$taxonomy_slugs = array( 'category', 'post_tag' );

	foreach ( $taxonomy_slugs as $tax_slug ) {
		if ( ! taxonomy_exists( $tax_slug ) ) {
			continue;
		}

		$terms = get_terms(
			array(
				'taxonomy'   => $tax_slug,
				'hide_empty' => false,
				'number'     => 500,
			)
		);

		if ( is_wp_error( $terms ) ) {
			continue;
		}

		$entries = array();
		foreach ( $terms as $term ) {
			$sample_post_titles = array();

			if ( $term->count > 0 ) {
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- bounded by posts_per_page=3 and run from a license-key-authed admin endpoint, not a page-render path.
				$posts = get_posts(
					array(
						'posts_per_page' => 3,
						'post_status'    => 'publish',
						'tax_query'      => array(
							array(
								'taxonomy' => $tax_slug,
								'field'    => 'term_id',
								'terms'    => $term->term_id,
							),
						),
					)
				);

				foreach ( $posts as $p ) {
					$sample_post_titles[] = get_the_title( $p );
				}
			}

			$entries[] = array(
				'term_id'            => (int) $term->term_id,
				'name'               => $term->name,
				'slug'               => $term->slug,
				'count'              => (int) $term->count,
				'sample_post_titles' => $sample_post_titles,
			);
		}

		$result['taxonomies'][ $tax_slug ] = $entries;
	}

	return rest_ensure_response( $result );
}

/**
 * POST /clearpost/v1/taxonomy/merge
 *
 * Applies a consolidation proposal: merge fuzzy-duplicate terms into canonical
 * ones (reassigning posts), and delete listed orphan terms.
 *
 * Body:
 * {
 *   proposal: {
 *     category: [
 *       {
 *         canonical: { name, slug },
 *         merge_from: [ { term_id, name } ]
 *       }
 *     ],
 *     post_tag: [ ... ],
 *     delete_orphans: [ { term_id, name } ]
 *   }
 * }
 *
 * Returns per-cluster results plus aggregate counters.
 *
 * @param WP_REST_Request $request
 * @return WP_REST_Response|WP_Error
 */
function saiap_handle_taxonomy_merge( $request ) {
	$proposal = $request->get_param( 'proposal' );

	if ( empty( $proposal ) || ! is_array( $proposal ) ) {
		return new WP_Error( 'invalid_proposal', 'Missing or invalid proposal', array( 'status' => 400 ) );
	}

	$result = array(
		'merged'           => array(),
		'deleted_orphans'  => array(),
		'reassigned_posts' => 0,
		'errors'           => array(),
	);

	$taxonomy_slugs = array( 'category', 'post_tag' );

	foreach ( $taxonomy_slugs as $tax_slug ) {
		if ( empty( $proposal[ $tax_slug ] ) || ! is_array( $proposal[ $tax_slug ] ) ) {
			continue;
		}
		if ( ! taxonomy_exists( $tax_slug ) ) {
			continue;
		}

		foreach ( $proposal[ $tax_slug ] as $cluster_index => $cluster ) {
			$canonical = isset( $cluster['canonical'] ) ? $cluster['canonical'] : null;
			$merge_from = isset( $cluster['merge_from'] ) ? $cluster['merge_from'] : array();

			if ( ! is_array( $canonical ) || empty( $canonical['name'] ) || empty( $canonical['slug'] ) ) {
				$result['errors'][] = sprintf( '%s cluster %d: missing canonical name/slug', $tax_slug, $cluster_index );
				continue;
			}

			if ( ! is_array( $merge_from ) || count( $merge_from ) === 0 ) {
				$result['errors'][] = sprintf( '%s cluster "%s": no merge_from terms', $tax_slug, $canonical['name'] );
				continue;
			}

			// Defensive cap: refuse very large clusters — likely an LLM hallucination.
			if ( count( $merge_from ) > 20 ) {
				$result['errors'][] = sprintf( '%s cluster "%s": refusing to merge >20 terms in one cluster', $tax_slug, $canonical['name'] );
				continue;
			}

			// Find or create the canonical term. Prefer slug match (stable identifier).
			$canonical_term = get_term_by( 'slug', sanitize_title( $canonical['slug'] ), $tax_slug );
			if ( ! $canonical_term ) {
				$canonical_term = get_term_by( 'name', $canonical['name'], $tax_slug );
			}
			if ( ! $canonical_term ) {
				$created = wp_insert_term( $canonical['name'], $tax_slug, array( 'slug' => sanitize_title( $canonical['slug'] ) ) );
				if ( is_wp_error( $created ) ) {
					$result['errors'][] = sprintf( '%s "%s": failed to create canonical — %s', $tax_slug, $canonical['name'], $created->get_error_message() );
					continue;
				}
				$canonical_term_id = (int) $created['term_id'];
			} else {
				$canonical_term_id = (int) $canonical_term->term_id;
			}

			$cluster_result = array(
				'taxonomy'         => $tax_slug,
				'canonical'        => array(
					'term_id' => $canonical_term_id,
					'name'    => $canonical['name'],
					'slug'    => sanitize_title( $canonical['slug'] ),
				),
				'merged_term_ids'  => array(),
				'reassigned_posts' => 0,
			);

			foreach ( $merge_from as $source ) {
				$source_term_id = isset( $source['term_id'] ) ? (int) $source['term_id'] : 0;
				if ( $source_term_id <= 0 || $source_term_id === $canonical_term_id ) {
					continue;
				}

				$post_ids = get_objects_in_term( $source_term_id, $tax_slug );
				if ( is_wp_error( $post_ids ) ) {
					$result['errors'][] = sprintf( 'source term %d: %s', $source_term_id, $post_ids->get_error_message() );
					continue;
				}

				foreach ( (array) $post_ids as $post_id ) {
					$set = wp_set_object_terms( (int) $post_id, array( $canonical_term_id ), $tax_slug, true );
					if ( is_wp_error( $set ) ) {
						$result['errors'][] = sprintf( 'post %d → canonical %d: %s', $post_id, $canonical_term_id, $set->get_error_message() );
						continue;
					}
					$cluster_result['reassigned_posts']++;
					$result['reassigned_posts']++;
				}

				$deleted = wp_delete_term( $source_term_id, $tax_slug );
				if ( is_wp_error( $deleted ) ) {
					$result['errors'][] = sprintf( 'delete source %d: %s', $source_term_id, $deleted->get_error_message() );
					continue;
				}
				$cluster_result['merged_term_ids'][] = $source_term_id;
			}

			$result['merged'][] = $cluster_result;
		}
	}

	// Delete listed orphans (terms with count == 0 that the operator opted to remove).
	if ( ! empty( $proposal['delete_orphans'] ) && is_array( $proposal['delete_orphans'] ) ) {
		foreach ( $proposal['delete_orphans'] as $orphan ) {
			$term_id  = isset( $orphan['term_id'] ) ? (int) $orphan['term_id'] : 0;
			$tax_slug = isset( $orphan['taxonomy'] ) ? sanitize_key( $orphan['taxonomy'] ) : '';
			if ( $term_id <= 0 || empty( $tax_slug ) || ! taxonomy_exists( $tax_slug ) ) {
				continue;
			}
			$deleted = wp_delete_term( $term_id, $tax_slug );
			if ( is_wp_error( $deleted ) ) {
				$result['errors'][] = sprintf( 'delete orphan %d: %s', $term_id, $deleted->get_error_message() );
				continue;
			}
			$result['deleted_orphans'][] = array(
				'term_id'  => $term_id,
				'taxonomy' => $tax_slug,
			);
		}
	}

	return rest_ensure_response( $result );
}

