<?php
/**
 * Generic taxonomy term lifecycle engine.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Taxonomy;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are escaped at UI boundaries.

/**
 * Validates, assigns, creates, remaps, diffs, and verifies taxonomy terms.
 */
final class TaxonomyTermService {
	/** @return int[] */
	public function termIds( int $object_id, string $taxonomy ): array {
		$terms = wp_get_object_terms(
			$object_id,
			$taxonomy,
			array(
				'fields'  => 'ids',
				'orderby' => 'term_id',
				'order'   => 'ASC',
			)
		);

		if ( is_wp_error( $terms ) ) {
			throw new \RuntimeException( $terms->get_error_message() );
		}

		$ids = array_values( array_unique( array_map( 'intval', $terms ) ) );
		sort( $ids );

		return $ids;
	}

	/**
	 * @param int[] $term_ids Requested term IDs.
	 * @return array{code:string,message:string}|null
	 */
	public function validationError( string $taxonomy, array $term_ids ): ?array {
		if ( ! taxonomy_exists( $taxonomy ) ) {
			return array(
				'code'    => 'taxonomy_not_registered',
				'message' => 'The registered taxonomy is unavailable.',
			);
		}

		foreach ( array_unique( array_map( 'intval', $term_ids ) ) as $term_id ) {
			if ( $term_id <= 0 || ! term_exists( $term_id, $taxonomy ) ) {
				return array(
					'code'    => 'invalid_term',
					'message' => 'A selected taxonomy term no longer exists.',
				);
			}
		}

		return null;
	}

	/**
	 * @param int[] $term_ids Canonical term IDs.
	 * @return true|\WP_Error
	 */
	public function assign( int $object_id, string $taxonomy, array $term_ids ) {
		$term_ids = array_values( array_unique( array_map( 'intval', $term_ids ) ) );
		sort( $term_ids );
		$error = $this->validationError( $taxonomy, $term_ids );

		if ( null !== $error ) {
			return new \WP_Error( $error['code'], $error['message'] );
		}

		$result = wp_set_object_terms( $object_id, $term_ids, $taxonomy, false );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return $this->termIds( $object_id, $taxonomy ) === $term_ids
			? true
			: new \WP_Error( 'taxonomy_readback_failed', 'Taxonomy assignment readback did not match.' );
	}

	/**
	 * Creates a term only when its registration explicitly permits creation.
	 *
	 * @return int|\WP_Error
	 */
	public function create( TaxonomyRegistration $registration, string $name, int $parent_id = 0 ) {
		if ( ! $registration->allowsTermCreation() ) {
			return new \WP_Error( 'term_creation_disabled', 'This taxonomy registration does not permit term creation.' );
		}

		$taxonomy = get_taxonomy( $registration->taxonomy() );
		if ( ! $taxonomy instanceof \WP_Taxonomy ) {
			return new \WP_Error( 'taxonomy_not_registered', 'The registered taxonomy is unavailable.' );
		}

		$capability = (string) ( $taxonomy->cap->manage_terms ?? 'manage_categories' );
		if ( ! current_user_can( $capability ) ) {
			return new \WP_Error( 'term_creation_denied', 'The current user cannot create terms for this taxonomy.' );
		}

		$created = wp_insert_term( $name, $registration->taxonomy(), array( 'parent' => max( 0, $parent_id ) ) );
		if ( is_wp_error( $created ) ) {
			return $created;
		}

		return (int) ( $created['term_id'] ?? 0 );
	}

	/**
	 * @param int[]          $term_ids Source term IDs.
	 * @param array<int,int> $id_map Source-to-target map.
	 * @return int[]
	 */
	public function remap( array $term_ids, array $id_map ): array {
		$remapped = array();
		foreach ( array_unique( array_map( 'intval', $term_ids ) ) as $term_id ) {
			if ( ! isset( $id_map[ $term_id ] ) ) {
				throw new \RuntimeException( 'A taxonomy term cannot be remapped safely.' );
			}
			$remapped[] = (int) $id_map[ $term_id ];
		}

		return $remapped;
	}

	/**
	 * @param int[] $before Original term IDs.
	 * @param int[] $after New term IDs.
	 * @return array{added:int[],removed:int[]}
	 */
	public function diff( array $before, array $after ): array {
		$before = array_values( array_unique( array_map( 'intval', $before ) ) );
		$after  = array_values( array_unique( array_map( 'intval', $after ) ) );

		return array(
			'added'   => array_values( array_diff( $after, $before ) ),
			'removed' => array_values( array_diff( $before, $after ) ),
		);
	}
}
