<?php
/**
 * ACF compatibility — field-configuration-aware scanner.
 *
 * Reads ACF field-group definitions and scans postmeta accordingly, picking
 * up media IDs and URLs from image / file / gallery fields and all nested
 * variants inside repeater, flexible_content, group, and clone fields.
 *
 * @package CoffeeWP_Media_Cleanup
 */

namespace CWPM\Compat;

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders -- ACF compat scans wp_postmeta to detect ACF-managed attachments; readonly analytics, no cache benefit.

class Acf {

	/** @var array<string,int> URL → attachment ID cache (per-request). */
	private static $url_to_id_cache = array();

	/** @var array|null Cached, flattened field map (field_key => normalized config). */
	private static $field_map_cache = null;

	/**
	 * Is the Advanced Custom Fields plugin active (either edition)?
	 *
	 * @return bool
	 */
	public static function is_active() {
		return class_exists( 'ACF' )
			|| class_exists( 'acf' )
			|| function_exists( 'acf_get_field_groups' );
	}

	/**
	 * Return every attachment ID referenced by ACF postmeta across the site.
	 *
	 * @return int[] Unique positive attachment IDs.
	 */
	public static function get_used_attachment_ids() {
		$sources = self::collect_sources();
		return array_values( array_unique( array_column( $sources, 'id' ) ) );
	}

	/**
	 * Return every ACF media reference as
	 * [ 'id' => int, 'field_name' => string, 'source_type' => string ] triples.
	 *
	 * Deduplicated by (id, field_name, source_type).
	 *
	 * @return array
	 */
	public static function get_used_sources() {
		$sources = self::collect_sources();

		$seen   = array();
		$unique = array();
		foreach ( $sources as $s ) {
			$key = $s['id'] . '|' . $s['field_name'] . '|' . $s['source_type'] . '|' . $s['post_title'];
			if ( ! isset( $seen[ $key ] ) ) {
				$seen[ $key ] = true;
				$unique[]     = $s;
			}
		}
		return $unique;
	}

	/**
	 * Internal: scan all ACF storage locations and return raw
	 * [ 'id', 'field_name', 'source_type' ] triples (may contain duplicates).
	 *
	 * @return array
	 */
	private static function collect_sources() {
		if ( ! self::is_active() ) {
			return array();
		}

		$fields = self::get_field_map();
		if ( empty( $fields ) ) {
			return array();
		}

		global $wpdb;
		$sources = array();

		$rows = $wpdb->get_results(
			"SELECT pm.post_id, pm.meta_key, pm.meta_value, p.post_type, p.post_title
			 FROM {$wpdb->postmeta} pm
			 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
			 WHERE pm.meta_key LIKE '\\_%'
			 AND pm.meta_value LIKE 'field\\_%'"
		);

		foreach ( $rows as $row ) {
			$field_key = $row->meta_value;
			if ( ! isset( $fields[ $field_key ] ) ) {
				continue;
			}
			$config = $fields[ $field_key ];
			if ( empty( $config['media'] ) ) {
				continue;
			}

			$value_key   = ltrim( $row->meta_key, '_' );
			$value       = get_post_meta( (int) $row->post_id, $value_key, true );
			$source_type = (string) $row->post_type;
			$post_title  = (string) $row->post_title;
			if ( '' === $value || null === $value ) {
				continue;
			}

			self::collect_from_value( $value, $config, $source_type, $post_title, $sources );
		}

		self::scan_termmeta( $fields, $sources );
		self::scan_usermeta( $fields, $sources );
		self::scan_options( $fields, $sources );

		return $sources;
	}

	/**
	 * Build a flat `field_key => config` map from every `acf-field` post.
	 *
	 * @return array<string, array>
	 */
	private static function get_field_map() {
		if ( null !== self::$field_map_cache ) {
			return self::$field_map_cache;
		}

		global $wpdb;

		$rows = $wpdb->get_results(
			"SELECT ID, post_name, post_content, post_excerpt
			 FROM {$wpdb->posts}
			 WHERE post_type = 'acf-field'
			 AND post_status = 'publish'"
		);

		$map = array();
		foreach ( $rows as $row ) {
			$settings = maybe_unserialize( $row->post_content );
			if ( ! is_array( $settings ) ) {
				continue;
			}
			$key  = $row->post_name;
			$name = isset( $settings['name'] ) ? (string) $settings['name'] : $row->post_excerpt;
			$type = isset( $settings['type'] ) ? (string) $settings['type'] : '';

			$return_format = isset( $settings['return_format'] ) ? (string) $settings['return_format'] : '';

			$map[ $key ] = array(
				'key'           => $key,
				'name'          => $name,
				'type'          => $type,
				'return_format' => $return_format,
				'media'         => in_array( $type, array( 'image', 'file', 'gallery', 'wysiwyg' ), true ),
			);
		}

		self::$field_map_cache = $map;
		return $map;
	}

	/**
	 * Extract attachment sources from a single field value according to its config.
	 *
	 * @param mixed  $value       Raw postmeta value.
	 * @param array  $config      Field config from get_field_map().
	 * @param string $source_type Post type (or 'term'/'user'/'options').
	 * @param string $post_title  Human-readable label (post title, or context label for non-post storage).
	 * @param array  &$sources    Collected triples.
	 */
	private static function collect_from_value( $value, $config, $source_type, $post_title, &$sources ) {
		$type       = $config['type'];
		$field_name = $config['name'];
		$maybe      = maybe_unserialize( $value );

		switch ( $type ) {
			case 'image':
			case 'file':
				if ( is_numeric( $maybe ) && (int) $maybe > 0 ) {
					$sources[] = array(
						'id'          => (int) $maybe,
						'field_name'  => $field_name,
						'source_type' => $source_type,
						'post_title'  => $post_title,
					);
				} elseif ( is_string( $maybe ) && '' !== $maybe ) {
					self::collect_from_url( $maybe, $field_name, $source_type, $post_title, $sources );
				} elseif ( is_array( $maybe ) && ! empty( $maybe['ID'] ) ) {
					$sources[] = array(
						'id'          => (int) $maybe['ID'],
						'field_name'  => $field_name,
						'source_type' => $source_type,
						'post_title'  => $post_title,
					);
				} elseif ( is_array( $maybe ) && ! empty( $maybe['url'] ) ) {
					self::collect_from_url( $maybe['url'], $field_name, $source_type, $post_title, $sources );
				}
				break;

			case 'gallery':
				if ( is_array( $maybe ) ) {
					foreach ( $maybe as $entry ) {
						if ( is_numeric( $entry ) && (int) $entry > 0 ) {
							$sources[] = array(
								'id'          => (int) $entry,
								'field_name'  => $field_name,
								'source_type' => $source_type,
								'post_title'  => $post_title,
							);
						} elseif ( is_array( $entry ) ) {
							if ( ! empty( $entry['ID'] ) ) {
								$sources[] = array(
									'id'          => (int) $entry['ID'],
									'field_name'  => $field_name,
									'source_type' => $source_type,
									'post_title'  => $post_title,
								);
							} elseif ( ! empty( $entry['url'] ) ) {
								self::collect_from_url( $entry['url'], $field_name, $source_type, $post_title, $sources );
							}
						} elseif ( is_string( $entry ) ) {
							if ( ctype_digit( $entry ) ) {
								$sources[] = array(
									'id'          => (int) $entry,
									'field_name'  => $field_name,
									'source_type' => $source_type,
									'post_title'  => $post_title,
								);
							} else {
								self::collect_from_url( $entry, $field_name, $source_type, $post_title, $sources );
							}
						}
					}
				}
				break;

			case 'wysiwyg':
				if ( is_string( $maybe ) && '' !== $maybe ) {
					if ( preg_match_all( '/wp-image-(\d+)/', $maybe, $matches ) ) {
						foreach ( $matches[1] as $img_id ) {
							$sources[] = array(
								'id'          => (int) $img_id,
								'field_name'  => $field_name,
								'source_type' => $source_type,
								'post_title'  => $post_title,
							);
						}
					}
					if ( preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\']/', $maybe, $url_matches ) ) {
						foreach ( $url_matches[1] as $img_url ) {
							self::collect_from_url( $img_url, $field_name, $source_type, $post_title, $sources );
						}
					}
				}
				break;

			default:
				break;
		}
	}

	/**
	 * Resolve uploads URL → attachment ID and collect a source entry.
	 *
	 * @param string $url         Raw URL.
	 * @param string $field_name  ACF field name.
	 * @param string $source_type Post type or storage context.
	 * @param string $post_title  Human-readable label.
	 * @param array  &$sources    Collected sources.
	 */
	private static function collect_from_url( $url, $field_name, $source_type, $post_title, &$sources ) {
		$url = trim( (string) $url );
		if ( '' === $url ) {
			return;
		}

		if ( array_key_exists( $url, self::$url_to_id_cache ) ) {
			if ( self::$url_to_id_cache[ $url ] > 0 ) {
				$sources[] = array(
					'id'          => self::$url_to_id_cache[ $url ],
					'field_name'  => $field_name,
					'source_type' => $source_type,
					'post_title'  => $post_title,
				);
			}
			return;
		}

		$aid                           = function_exists( 'attachment_url_to_postid' )
			? (int) attachment_url_to_postid( $url )
			: 0;
		self::$url_to_id_cache[ $url ] = $aid;
		if ( $aid > 0 ) {
			$sources[] = array(
				'id'          => $aid,
				'field_name'  => $field_name,
				'source_type' => $source_type,
				'post_title'  => $post_title,
			);
		}
	}

	/**
	 * Scan termmeta for ACF values on taxonomy terms.
	 *
	 * @param array $fields   Field map.
	 * @param array &$sources Collected sources.
	 */
	private static function scan_termmeta( $fields, &$sources ) {

		global $wpdb;
		$rows = $wpdb->get_results(
			"SELECT term_id, meta_key, meta_value
			 FROM {$wpdb->termmeta}
			 WHERE meta_key LIKE '\\_%'
			 AND meta_value LIKE 'field\\_%'"
		);
		foreach ( $rows as $row ) {
			if ( ! isset( $fields[ $row->meta_value ] ) ) {
				continue;
			}
			$config = $fields[ $row->meta_value ];
			if ( empty( $config['media'] ) ) {
				continue;
			}
			$value_key = ltrim( $row->meta_key, '_' );
			$value     = get_term_meta( (int) $row->term_id, $value_key, true );
			if ( '' === $value ) {
				continue;
			}
			$term      = get_term( (int) $row->term_id );
			$term_name = ( $term && ! is_wp_error( $term ) ) ? $term->name : 'Term';
			self::collect_from_value( $value, $config, 'term', $term_name, $sources );
		}
	}

	/**
	 * Scan usermeta for ACF values on user profiles.
	 *
	 * @param array $fields   Field map.
	 * @param array &$sources Collected sources.
	 */
	private static function scan_usermeta( $fields, &$sources ) {

		global $wpdb;
		$rows = $wpdb->get_results(
			"SELECT user_id, meta_key, meta_value
			 FROM {$wpdb->usermeta}
			 WHERE meta_key LIKE '\\_%'
			 AND meta_value LIKE 'field\\_%'"
		);
		foreach ( $rows as $row ) {
			if ( ! isset( $fields[ $row->meta_value ] ) ) {
				continue;
			}
			$config = $fields[ $row->meta_value ];
			if ( empty( $config['media'] ) ) {
				continue;
			}
			$value_key = ltrim( $row->meta_key, '_' );
			$value     = get_user_meta( (int) $row->user_id, $value_key, true );
			if ( '' === $value ) {
				continue;
			}
			$user_data  = get_userdata( (int) $row->user_id );
			$user_label = $user_data ? $user_data->display_name : 'User';
			self::collect_from_value( $value, $config, 'user', $user_label, $sources );
		}
	}

	/**
	 * Scan the options table for ACF Options-Page values (keys prefixed `options_`).
	 *
	 * @param array $fields   Field map.
	 * @param array &$sources Collected sources.
	 */
	private static function scan_options( $fields, &$sources ) {

		global $wpdb;
		$rows = $wpdb->get_results(
			"SELECT option_name, option_value
			 FROM {$wpdb->options}
			 WHERE option_name LIKE '\\_options\\_%'
			 AND option_value LIKE 'field\\_%'"
		);
		foreach ( $rows as $row ) {
			if ( ! isset( $fields[ $row->option_value ] ) ) {
				continue;
			}
			$config = $fields[ $row->option_value ];
			if ( empty( $config['media'] ) ) {
				continue;
			}
			$value_key = preg_replace( '/^_/', '', $row->option_name );
			$value     = get_option( $value_key );
			if ( '' === $value || false === $value ) {
				continue;
			}
			self::collect_from_value( $value, $config, 'options', 'Options', $sources );
		}
	}

	/**
	 * Test-only: reset caches so repeat tests start fresh.
	 */
	public static function reset_cache_for_tests() {
		self::$url_to_id_cache = array();
		self::$field_map_cache = null;
	}
}
