<?php
/**
 * Bidirectional relational field sync.
 *
 * @package CustomFields
 * @since 1.3.0
 */

namespace CustomFields;

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

/**
 * Keeps inverse post/term meta in sync for bidirectional relational fields.
 */
final class Relationships {

	/**
	 * Sync bidirectional links after a post's custom fields are saved.
	 *
	 * @param int                                $post_id Post ID.
	 * @param array<string, mixed>               $group   Field group definition.
	 * @param array<string, array{old: mixed, new: mixed}> $saved meta_key => old/new pair.
	 */
	public function syncPost( int $post_id, array $group, array $saved ): void {
		$this->syncObject( $post_id, $group, $saved );
	}

	/**
	 * Sync bidirectional links after a term's custom fields are saved.
	 *
	 * @param int                                $term_id Term ID.
	 * @param array<string, mixed>               $group   Field group definition.
	 * @param array<string, array{old: mixed, new: mixed}> $saved meta_key => old/new pair.
	 */
	public function syncTerm( int $term_id, array $group, array $saved ): void {
		$this->syncObject( $term_id, $group, $saved );
	}

	/**
	 * @param int                                $source_id Source object ID.
	 * @param array<string, mixed>               $group     Field group definition.
	 * @param array<string, array{old: mixed, new: mixed}> $saved meta_key => old/new pair.
	 */
	private function syncObject( int $source_id, array $group, array $saved ): void {
		$group_key = (string) ( $group['key'] ?? '' );
		$fields    = isset( $group['fields'] ) && is_array( $group['fields'] ) ? $group['fields'] : [];

		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) || empty( $field['bidirectional'] ) ) {
				continue;
			}

			$type = (string) ( $field['type'] ?? '' );
			if ( ! in_array( $type, [ 'post_object', 'relationship', 'term', 'user' ], true ) ) {
				continue;
			}

			$target = $this->parseTarget( (string) ( $field['bidirectional_target'] ?? '' ) );
			if ( $target === null ) {
				continue;
			}

			$meta_key = MetaKeys::resolve( $group_key, $field );
			if ( $meta_key === '' || ! isset( $saved[ $meta_key ] ) ) {
				continue;
			}

			$pair    = $saved[ $meta_key ];
			$field_multiple = $type === 'relationship' || ! empty( $field['multiple'] );
			$new_ids        = $this->normalizeIds( $pair['new'] ?? null, $field_multiple );
			$old_ids        = $this->normalizeIds( $pair['old'] ?? null, $field_multiple );

			$add    = array_diff( $new_ids, $old_ids );
			$remove = array_diff( $old_ids, $new_ids );

			$inverse_meta = MetaKeys::resolve( $target['group_key'], $target['field'] );
			if ( $inverse_meta === '' ) {
				continue;
			}

			foreach ( $add as $related_id ) {
				$this->addInverse( $type, (int) $related_id, $inverse_meta, $source_id, $target['multiple'] );
			}
			foreach ( $remove as $related_id ) {
				$this->removeInverse( $type, (int) $related_id, $inverse_meta, $source_id, $target['multiple'] );
			}
		}
	}

	/**
	 * @param string $target Raw target "group_key|field_key".
	 * @return array{group_key: string, field: array<string, mixed>, multiple: bool}|null
	 */
	private function parseTarget( string $target ): ?array {
		if ( ! str_contains( $target, '|' ) ) {
			return null;
		}

		[ $group_key, $field_key ] = array_pad( explode( '|', $target, 2 ), 2, '' );
		$group_key = sanitize_key( $group_key );
		$field_key = sanitize_key( $field_key );
		if ( $group_key === '' || $field_key === '' ) {
			return null;
		}

		$storage = new Storage();
		$group   = $storage->get( $group_key );
		if ( ! is_array( $group ) ) {
			return null;
		}

		foreach ( (array) ( $group['fields'] ?? [] ) as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			$matches_key  = (string) ( $field['key'] ?? '' ) === $field_key;
			$matches_name = (string) ( $field['name'] ?? '' ) === $field_key;
			if ( ! $matches_key && ! $matches_name ) {
				continue;
			}

			return [
				'group_key' => $group_key,
				'field'     => $field,
				'multiple'  => (string) ( $field['type'] ?? '' ) === 'relationship' || ! empty( $field['multiple'] ),
			];
		}

		return null;
	}

	/**
	 * @param mixed $value    Stored value.
	 * @param bool  $multiple Whether field allows multiple.
	 * @return array<int, int>
	 */
	private function normalizeIds( mixed $value, bool $multiple ): array {
		if ( $multiple ) {
			$ids = is_array( $value ) ? $value : [];
			return array_values( array_filter( array_map( 'absint', $ids ) ) );
		}

		$id = absint( $value );

		return $id > 0 ? [ $id ] : [];
	}

	/**
	 * @param string $type         Relational field type.
	 * @param int    $related_id   Related object ID.
	 * @param string $inverse_meta Inverse meta key.
	 * @param int    $source_id    Source object ID.
	 * @param bool   $multiple     Inverse stores multiple IDs.
	 */
	private function addInverse( string $type, int $related_id, string $inverse_meta, int $source_id, bool $multiple ): void {
		if ( $related_id <= 0 || $source_id <= 0 ) {
			return;
		}
		if ( ! $this->canEditRelatedObject( $type, $related_id ) ) {
			return;
		}

		if ( $type === 'term' ) {
			$current = get_term_meta( $related_id, $inverse_meta, true );
		} elseif ( $type === 'user' ) {
			$current = get_user_meta( $related_id, $inverse_meta, true );
		} else {
			$current = get_post_meta( $related_id, $inverse_meta, true );
		}

		if ( $multiple ) {
			$list = is_array( $current ) ? array_map( 'absint', $current ) : [];
			if ( ! in_array( $source_id, $list, true ) ) {
				$list[] = $source_id;
			}
			$this->updateInverse( $type, $related_id, $inverse_meta, $list );
			return;
		}

		$this->updateInverse( $type, $related_id, $inverse_meta, $source_id );
	}

	/**
	 * @param string $type         Relational field type.
	 * @param int    $related_id   Related object ID.
	 * @param string $inverse_meta Inverse meta key.
	 * @param int    $source_id    Source object ID.
	 * @param bool   $multiple     Inverse stores multiple IDs.
	 */
	private function removeInverse( string $type, int $related_id, string $inverse_meta, int $source_id, bool $multiple ): void {
		if ( $related_id <= 0 || $source_id <= 0 ) {
			return;
		}
		if ( ! $this->canEditRelatedObject( $type, $related_id ) ) {
			return;
		}

		if ( $type === 'term' ) {
			$current = get_term_meta( $related_id, $inverse_meta, true );
		} elseif ( $type === 'user' ) {
			$current = get_user_meta( $related_id, $inverse_meta, true );
		} else {
			$current = get_post_meta( $related_id, $inverse_meta, true );
		}

		if ( $multiple ) {
			$list = is_array( $current ) ? array_map( 'absint', $current ) : [];
			$list = array_values( array_diff( $list, [ $source_id ] ) );
			if ( $list === [] ) {
				$this->deleteInverse( $type, $related_id, $inverse_meta );
			} else {
				$this->updateInverse( $type, $related_id, $inverse_meta, $list );
			}
			return;
		}

		if ( (int) $current === $source_id ) {
			$this->deleteInverse( $type, $related_id, $inverse_meta );
		}
	}

	/**
	 * @param string $type       Object type.
	 * @param int    $object_id  Object ID.
	 * @param string $meta_key   Meta key.
	 * @param mixed  $value      Value to store.
	 */
	private function updateInverse( string $type, int $object_id, string $meta_key, mixed $value ): void {
		if ( $type === 'term' ) {
			update_term_meta( $object_id, $meta_key, $value );
			return;
		}
		if ( $type === 'user' ) {
			update_user_meta( $object_id, $meta_key, $value );
			return;
		}
		update_post_meta( $object_id, $meta_key, $value );
	}

	/**
	 * @param string $type      Object type.
	 * @param int    $object_id Object ID.
	 * @param string $meta_key  Meta key.
	 */
	private function deleteInverse( string $type, int $object_id, string $meta_key ): void {
		if ( $type === 'term' ) {
			delete_term_meta( $object_id, $meta_key );
			return;
		}
		if ( $type === 'user' ) {
			delete_user_meta( $object_id, $meta_key );
			return;
		}
		delete_post_meta( $object_id, $meta_key );
	}

	private function canEditRelatedObject( string $type, int $object_id ): bool {
		if ( $type === 'term' ) {
			return current_user_can( 'edit_term', $object_id );
		}
		if ( $type === 'user' ) {
			return current_user_can( 'edit_user', $object_id );
		}

		return current_user_can( 'edit_post', $object_id );
	}
}
