<?php
/**
 * Capability-aware publishing destination discovery and validation.
 *
 * @package PufferPDF
 */

defined( 'ABSPATH' ) || exit;

/**
 * Resolves WordPress post, user, parent, status, and taxonomy destinations.
 */
final class PufferPDF_Destinations {

	/** Maximum related objects returned or accepted per collection. */
	private const MAX_RELATED_ITEMS = 100;

	/**
	 * Check whether the current user may inspect publishing destinations.
	 *
	 * @return bool|WP_Error
	 */
	public function permissions_check() {
		if ( ! is_user_logged_in() ) {
			return new WP_Error(
				'pufferpdf_authentication_required',
				__( 'Sign in to inspect PufferPDF publishing destinations.', 'pufferpdf' ),
				array( 'status' => 401 )
			);
		}
		if ( ! current_user_can( 'edit_posts' ) ) {
			return new WP_Error(
				'pufferpdf_destination_forbidden',
				__( 'You do not have permission to inspect PufferPDF publishing destinations.', 'pufferpdf' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Return a bounded capability-filtered destination inventory.
	 *
	 * @param WP_REST_Request $request REST request.
	 * @return WP_REST_Response
	 */
	public function get_destinations( WP_REST_Request $request ) {
		$requested_type = sanitize_key( (string) $request->get_param( 'postType' ) );
		$types          = array();

		foreach ( get_post_types( array(), 'objects' ) as $post_type ) {
			if ( $requested_type && $post_type->name !== $requested_type ) {
				continue;
			}
			if ( ! $this->is_eligible_post_type( $post_type ) ) {
				continue;
			}
			if ( ! current_user_can( $post_type->cap->create_posts ) ) {
				continue;
			}
			if ( ! current_user_can( $post_type->cap->delete_posts ) ) {
				continue;
			}

			$types[] = $this->format_post_type( $post_type );
		}

		return rest_ensure_response(
			array(
				'postTypes'       => $types,
				'defaultPostType' => $this->find_default_post_type( $types ),
				'currentUserId'   => get_current_user_id(),
				'defaultAuthorId' => get_current_user_id(),
				'companion'       => array(
					'allowed' => $this->companion_is_available(),
				),
				'limits'          => array(
					'relatedItems' => self::MAX_RELATED_ITEMS,
				),
			)
		);
	}

	/**
	 * Normalize and authorize one requested destination.
	 *
	 * @param array $input Destination payload.
	 * @return array|WP_Error
	 */
	public function normalize_destination( array $input ) {
		$unknown = array_diff(
			array_keys( $input ),
			array( 'postType', 'status', 'parentId', 'authorId', 'terms' )
		);
		if ( ! empty( $unknown ) ) {
			return $this->invalid_destination( __( 'The destination contains unsupported fields.', 'pufferpdf' ) );
		}
		foreach ( array( 'postType', 'status' ) as $string_field ) {
			if ( array_key_exists( $string_field, $input ) && ! is_string( $input[ $string_field ] ) ) {
				return $this->invalid_destination( __( 'Destination names and statuses must use string values.', 'pufferpdf' ) );
			}
		}
		foreach ( array( 'parentId', 'authorId' ) as $integer_field ) {
			if ( array_key_exists( $integer_field, $input ) && ! is_int( $input[ $integer_field ] ) ) {
				return $this->invalid_destination( __( 'Destination IDs must use integer values.', 'pufferpdf' ) );
			}
		}
		if ( array_key_exists( 'terms', $input ) && ! is_array( $input['terms'] ) ) {
			return $this->invalid_destination( __( 'Destination terms must use a taxonomy object.', 'pufferpdf' ) );
		}

		$post_type_name = sanitize_key( isset( $input['postType'] ) ? $input['postType'] : 'post' );
		$post_type      = get_post_type_object( $post_type_name );
		if ( ! $post_type || ! $this->is_eligible_post_type( $post_type ) ) {
			return $this->invalid_destination( __( 'The selected post type is unavailable or cannot accept editable content.', 'pufferpdf' ) );
		}

		if ( ! current_user_can( $post_type->cap->create_posts ) ) {
			return new WP_Error(
				'pufferpdf_destination_forbidden',
				__( 'You do not have permission to create content in the selected post type.', 'pufferpdf' ),
				array( 'status' => 403 )
			);
		}
		if ( ! current_user_can( $post_type->cap->delete_posts ) ) {
			return new WP_Error(
				'pufferpdf_destination_not_reversible',
				__( 'You cannot safely undo content created in the selected post type.', 'pufferpdf' ),
				array( 'status' => 403 )
			);
		}

		$status = sanitize_key( isset( $input['status'] ) ? $input['status'] : 'draft' );
		if ( ! in_array( $status, array( 'draft', 'pending' ), true ) ) {
			return $this->invalid_destination( __( 'Reversible PufferPDF publishing supports only draft or pending status.', 'pufferpdf' ) );
		}
		$author_id = isset( $input['authorId'] ) ? $input['authorId'] : get_current_user_id();
		if ( $author_id < 1 ) {
			return $this->invalid_destination( __( 'The selected author ID is invalid.', 'pufferpdf' ) );
		}
		$author = get_user_by( 'id', $author_id );
		if ( ! $author ) {
			return $this->invalid_destination( __( 'The selected author no longer exists.', 'pufferpdf' ) );
		}
		if ( get_current_user_id() !== $author_id ) {
			if (
				! post_type_supports( $post_type_name, 'author' ) ||
				! current_user_can( $post_type->cap->edit_others_posts ) ||
				! current_user_can( $post_type->cap->delete_others_posts ) ||
				! user_can( $author, $post_type->cap->edit_posts )
			) {
				return new WP_Error(
					'pufferpdf_author_forbidden',
					__( 'You cannot assign the selected author to this post type.', 'pufferpdf' ),
					array( 'status' => 403 )
				);
			}
		}

		$parent_id = isset( $input['parentId'] ) ? $input['parentId'] : 0;
		if ( $parent_id < 0 ) {
			return $this->invalid_destination( __( 'The selected parent ID is invalid.', 'pufferpdf' ) );
		}
		if ( $parent_id ) {
			if ( ! $post_type->hierarchical ) {
				return $this->invalid_destination( __( 'The selected post type does not support parent relationships.', 'pufferpdf' ) );
			}

			$parent = get_post( $parent_id );
			if (
				! $parent ||
				$post_type_name !== $parent->post_type ||
				in_array( $parent->post_status, array( 'trash', 'auto-draft' ), true )
			) {
				return $this->invalid_destination( __( 'The selected parent is not an available object of the same post type.', 'pufferpdf' ) );
			}
			if ( ! current_user_can( 'edit_post', $parent_id ) ) {
				return new WP_Error(
					'pufferpdf_parent_forbidden',
					__( 'You do not have permission to use the selected parent.', 'pufferpdf' ),
					array( 'status' => 403 )
				);
			}
		} elseif ( ! $post_type->hierarchical ) {
			$parent_id = 0;
		}

		$terms_input = isset( $input['terms'] ) ? $input['terms'] : array();
		$terms       = $this->normalize_terms( $post_type_name, $terms_input );
		if ( is_wp_error( $terms ) ) {
			return $terms;
		}

		return array(
			'postType'     => $post_type_name,
			'status'       => $status,
			'parentId'     => $parent_id,
			'authorId'     => $author_id,
			'terms'        => $terms,
			'hierarchical' => (bool) $post_type->hierarchical,
			'viewable'     => is_post_type_viewable( $post_type ),
		);
	}

	/**
	 * Normalize the fixed page destination used for a companion.
	 *
	 * @param string $status Draft or pending.
	 * @return array|WP_Error
	 */
	public function normalize_companion_destination( $status ) {
		return $this->normalize_destination(
			array(
				'postType' => 'page',
				'status'   => $status,
				'parentId' => 0,
				'authorId' => get_current_user_id(),
				'terms'    => array(),
			)
		);
	}

	/**
	 * Return whether a post type may be offered.
	 *
	 * @param WP_Post_Type $post_type Post type object.
	 * @return bool
	 */
	private function is_eligible_post_type( WP_Post_Type $post_type ) {
		$excluded = array(
			'attachment',
			'nav_menu_item',
			'revision',
			'wp_block',
			'wp_font_face',
			'wp_font_family',
			'wp_global_styles',
			'wp_navigation',
			'wp_template',
			'wp_template_part',
			PufferPDF_Records::POST_TYPE,
			'pufferpdf_manifest',
		);

		if (
			in_array( $post_type->name, $excluded, true ) ||
			empty( $post_type->show_ui ) ||
			! post_type_supports( $post_type->name, 'title' ) ||
			! post_type_supports( $post_type->name, 'editor' )
		) {
			return false;
		}

		$eligible = in_array( $post_type->name, array( 'post', 'page' ), true );

		/**
		 * Filter post types that may receive PufferPDF output.
		 *
		 * Integrators opting custom post types in are responsible for any hook or
		 * custom-table side effects outside the core database transaction.
		 *
		 * @param bool         $eligible  Whether the type is eligible by default.
		 * @param WP_Post_Type $post_type Post type object.
		 */
		return (bool) apply_filters( 'pufferpdf_post_type_is_eligible', $eligible, $post_type );
	}

	/**
	 * Format one type and its bounded related destinations.
	 *
	 * @param WP_Post_Type $post_type Post type object.
	 * @return array
	 */
	private function format_post_type( WP_Post_Type $post_type ) {
		$parents = array();
		if ( $post_type->hierarchical ) {
			$query = new WP_Query(
				array(
					'post_type'              => $post_type->name,
					'post_status'            => array_diff( get_post_stati(), array( 'trash', 'auto-draft' ) ),
					'posts_per_page'         => self::MAX_RELATED_ITEMS,
					'orderby'                => 'title',
					'order'                  => 'ASC',
					'no_found_rows'          => true,
					'update_post_meta_cache' => false,
					'update_post_term_cache' => false,
				)
			);
			foreach ( $query->posts as $parent ) {
				if ( current_user_can( 'edit_post', $parent->ID ) ) {
					$parents[] = array(
						'id'    => (int) $parent->ID,
						'title' => get_the_title( $parent ),
					);
				}
			}
		}

		$authors = array();
		if ( post_type_supports( $post_type->name, 'author' ) ) {
			$user_args = array(
				'number'     => self::MAX_RELATED_ITEMS,
				'orderby'    => 'display_name',
				'order'      => 'ASC',
				'capability' => $post_type->cap->edit_posts,
				'fields'     => array( 'ID', 'display_name' ),
			);
			if (
				! current_user_can( $post_type->cap->edit_others_posts ) ||
				! current_user_can( $post_type->cap->delete_others_posts )
			) {
				$user_args['include'] = array( get_current_user_id() );
			}
			foreach ( get_users( $user_args ) as $user ) {
				$authors[] = array(
					'id'   => (int) $user->ID,
					'name' => (string) $user->display_name,
				);
			}
		}

		$taxonomies = array();
		foreach ( get_object_taxonomies( $post_type->name, 'objects' ) as $taxonomy ) {
			if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) {
				continue;
			}

			$term_rows = get_terms(
				array(
					'taxonomy'   => $taxonomy->name,
					'hide_empty' => false,
					'number'     => self::MAX_RELATED_ITEMS,
					'orderby'    => 'name',
					'order'      => 'ASC',
				)
			);
			$terms     = array();
			if ( ! is_wp_error( $term_rows ) ) {
				foreach ( $term_rows as $term ) {
					if ( current_user_can( 'assign_term', $term->term_id ) ) {
						$terms[] = array(
							'id'     => (int) $term->term_id,
							'name'   => (string) $term->name,
							'parent' => (int) $term->parent,
						);
					}
				}
			}

			$taxonomies[] = array(
				'name'         => $taxonomy->name,
				'slug'         => $taxonomy->name,
				'label'        => $taxonomy->labels->name,
				'hierarchical' => (bool) $taxonomy->hierarchical,
				'terms'        => $terms,
			);
		}

		return array(
			'name'            => $post_type->name,
			'value'           => $post_type->name,
			'slug'            => $post_type->name,
			'label'           => $post_type->labels->singular_name,
			'hierarchical'    => (bool) $post_type->hierarchical,
			'viewable'        => is_post_type_viewable( $post_type ),
			'statuses'        => array( 'draft', 'pending' ),
			'defaultAuthorId' => get_current_user_id(),
			'parents'         => $parents,
			'authors'         => $authors,
			'taxonomies'      => $taxonomies,
		);
	}

	/**
	 * Validate term assignments and include WordPress's effective default category.
	 *
	 * @param string $post_type Post type name.
	 * @param array  $input Requested taxonomy map.
	 * @return array|WP_Error
	 */
	private function normalize_terms( $post_type, array $input ) {
		if ( count( $input ) > self::MAX_RELATED_ITEMS ) {
			return $this->invalid_destination( __( 'Too many taxonomy assignments were requested.', 'pufferpdf' ) );
		}

		$normalized = array();
		foreach ( $input as $taxonomy_name => $term_ids ) {
			if ( ! is_string( $taxonomy_name ) ) {
				return $this->invalid_destination( __( 'Taxonomy names must use string object keys.', 'pufferpdf' ) );
			}
			$taxonomy_name = sanitize_key( $taxonomy_name );
			$taxonomy      = get_taxonomy( $taxonomy_name );
			if ( ! $taxonomy || ! is_object_in_taxonomy( $post_type, $taxonomy_name ) ) {
				return $this->invalid_destination( __( 'A requested taxonomy is not registered for the selected post type.', 'pufferpdf' ) );
			}
			if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) {
				return new WP_Error(
					'pufferpdf_terms_forbidden',
					__( 'You do not have permission to assign one of the requested taxonomies.', 'pufferpdf' ),
					array( 'status' => 403 )
				);
			}
			if ( ! is_array( $term_ids ) || count( $term_ids ) > self::MAX_RELATED_ITEMS ) {
				return $this->invalid_destination( __( 'A taxonomy contains an invalid or oversized term list.', 'pufferpdf' ) );
			}
			foreach ( $term_ids as $term_id ) {
				if ( ! is_int( $term_id ) || $term_id < 1 ) {
					return $this->invalid_destination( __( 'Taxonomy term IDs must use positive integer values.', 'pufferpdf' ) );
				}
			}

			$term_ids = array_values( array_unique( $term_ids ) );
			sort( $term_ids, SORT_NUMERIC );
			foreach ( $term_ids as $term_id ) {
				$term = get_term( $term_id, $taxonomy_name );
				if ( ! $term || is_wp_error( $term ) ) {
					return $this->invalid_destination( __( 'A requested taxonomy term no longer exists.', 'pufferpdf' ) );
				}
				if ( ! current_user_can( 'assign_term', $term_id ) ) {
					return new WP_Error(
						'pufferpdf_term_forbidden',
						__( 'You do not have permission to assign one of the requested terms.', 'pufferpdf' ),
						array( 'status' => 403 )
					);
				}
			}

			$normalized[ $taxonomy_name ] = $term_ids;
		}

		if ( 'post' === $post_type && is_object_in_taxonomy( $post_type, 'category' ) ) {
			$category_ids = isset( $normalized['category'] ) ? $normalized['category'] : array();
			if ( empty( $category_ids ) ) {
				$default_category = absint( get_option( 'default_category' ) );
				if ( $default_category ) {
					$normalized['category'] = array( $default_category );
				}
			}
		}

		ksort( $normalized, SORT_STRING );

		return $normalized;
	}

	/**
	 * Pick a stable default type from the formatted inventory.
	 *
	 * @param array $types Formatted types.
	 * @return string
	 */
	private function find_default_post_type( array $types ) {
		foreach ( $types as $type ) {
			if ( 'post' === $type['name'] ) {
				return 'post';
			}
		}

		return empty( $types ) ? '' : (string) $types[0]['name'];
	}

	/**
	 * Return whether the current user can create a companion page.
	 *
	 * @return bool
	 */
	private function companion_is_available() {
		$page_type = get_post_type_object( 'page' );

		return $page_type &&
			$this->is_eligible_post_type( $page_type ) &&
			current_user_can( $page_type->cap->create_posts ) &&
			current_user_can( $page_type->cap->delete_posts );
	}

	/**
	 * Return a consistent invalid-destination error.
	 *
	 * @param string $message Human-readable error.
	 * @return WP_Error
	 */
	private function invalid_destination( $message ) {
		return new WP_Error(
			'pufferpdf_invalid_destination',
			$message,
			array( 'status' => 400 )
		);
	}
}
