<?php

namespace UnusedMediaFinder;

use WP_PowerSuite\Core\AbstractModule;

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

class Module extends AbstractModule {

	private const MODULE_ID             = 'unused-media-finder';
	private const MODULE_VERSION        = '1.1.0';
	private const SCAN_BATCH            = 200;
	private const POSTS_BATCH           = 100;
	private const DELETE_BATCH          = 50;
	private const SCAN_TRANSIENT_PREFIX = 'wpps_umf_scan_';
	private const SCAN_TTL              = 900;

	/** @var array<string, int> */
	private array $filename_id_cache = [];

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Unused Media Finder', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Find media files not used anywhere on your site and delete them to reclaim storage space. Clean up orphaned images from deleted posts, old test uploads, and imported content.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'ajax' ];
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		add_action( 'wp_ajax_wpps_umf_scan', [ $this, 'ajaxScanUnusedMedia' ] );
		add_action( 'wp_ajax_wpps_umf_delete', [ $this, 'ajaxDeleteUnusedMedia' ] );
	}

	private function getUsedFeaturedImageIds(): array {
		global $wpdb;

		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != ''",
				'_thumbnail_id'
			)
		);

		return array_map( 'intval', array_filter( (array) $ids ) );
	}

	private function getScannablePostTypes(): array {
		$types = get_post_types( [ 'public' => true ], 'names' );
		unset( $types['attachment'] );

		return array_values( $types );
	}

	private function extractUsedIdsFromContent( string $content ): array {
		if ( $content === '' ) {
			return [];
		}

		$used = [];

		if ( preg_match_all( '/wp-image-(\d+)/', $content, $matches ) ) {
			foreach ( $matches[1] as $id ) {
				$used[] = (int) $id;
			}
		}

		if ( preg_match_all( '/\[gallery[^\]]*ids=["\']([^"\']+)["\']/', $content, $gallery_matches ) ) {
			foreach ( $gallery_matches[1] as $id_list ) {
				foreach ( preg_split( '/\s*,\s*/', $id_list ) as $id ) {
					$id = (int) $id;
					if ( $id > 0 ) {
						$used[] = $id;
					}
				}
			}
		}

		if ( preg_match_all( '/<!-- wp:(?:image|gallery|cover|media-text)[^>]*-->.*?<!-- \/wp:(?:image|gallery|cover|media-text) -->/s', $content, $block_matches ) ) {
			foreach ( $block_matches[0] as $block ) {
				if ( preg_match_all( '/"id"\s*:\s*(\d+)/', $block, $id_matches ) ) {
					foreach ( $id_matches[1] as $id ) {
						$used[] = (int) $id;
					}
				}
			}
		}

		if ( preg_match_all( '/wp-content\/uploads\/[^"\'\s>]+\/(\d{4}\/\d{2}\/)?([^"\'\s>]+)/', $content, $url_matches ) ) {
			foreach ( $url_matches[2] as $filename ) {
				$attachment_id = $this->findAttachmentIdByFilename( $filename );
				if ( $attachment_id > 0 ) {
					$used[] = $attachment_id;
				}
			}
		}

		return $used;
	}

	private function findAttachmentIdByFilename( string $filename ): int {
		$filename = wp_basename( $filename );
		if ( $filename === '' ) {
			return 0;
		}

		if ( isset( $this->filename_id_cache[ $filename ] ) ) {
			return $this->filename_id_cache[ $filename ];
		}

		global $wpdb;

		$id = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_wp_attached_file' AND meta_value LIKE %s LIMIT 1",
				'%' . $wpdb->esc_like( $filename )
			)
		);

		$this->filename_id_cache[ $filename ] = $id > 0 ? $id : 0;

		return $this->filename_id_cache[ $filename ];
	}

	private function buildUnusedItem( int $attachment_id ): ?array {
		$attachment = get_post( $attachment_id );
		if ( ! $attachment || $attachment->post_type !== 'attachment' ) {
			return null;
		}

		$file = get_attached_file( $attachment_id );
		$size = $file && file_exists( $file ) ? (int) filesize( $file ) : 0;
		$title = $attachment->post_title ? $attachment->post_title : basename( $file ?: '' );
		$url   = wp_get_attachment_url( $attachment_id );
		$thumb = wp_get_attachment_image_src( $attachment_id, [ 60, 60 ], true );

		return [
			'id'        => $attachment_id,
			'title'     => $title,
			'url'       => $url ?: '',
			'thumbnail' => $thumb && ! empty( $thumb[0] ) ? $thumb[0] : wp_mime_type_icon( get_post_mime_type( $attachment_id ) ),
			'size'      => $size,
			'mime'      => get_post_mime_type( $attachment_id ) ?: '',
		];
	}

	private function initScanState(): array {
		$used_ids = [];
		foreach ( $this->getUsedFeaturedImageIds() as $id ) {
			$used_ids[ (int) $id ] = true;
		}

		return [
			'phase'             => 'posts',
			'posts_cursor'      => 0,
			'attachment_cursor' => 0,
			'used_ids'          => $used_ids,
			'unused'            => [],
		];
	}

	private function mergeUsedIds( array &$state, array $found ): void {
		foreach ( $found as $id ) {
			$id = (int) $id;
			if ( $id > 0 ) {
				$state['used_ids'][ $id ] = true;
			}
		}
	}

	private function advanceScanBatch( array $state ): array {
		global $wpdb;

		$new_items  = [];
		$post_types = $this->getScannablePostTypes();

		if ( $state['phase'] === 'posts' && empty( $post_types ) ) {
			$state['phase'] = 'attachments';
		}

		if ( $state['phase'] === 'posts' && ! empty( $post_types ) ) {
			$placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
			$sql          = "SELECT ID, post_content FROM {$wpdb->posts}
				WHERE post_type IN ($placeholders)
				AND post_status NOT IN ('auto-draft','inherit')
				AND ID > %d
				ORDER BY ID ASC
				LIMIT %d";
			$prepare_args = array_merge( $post_types, [ (int) $state['posts_cursor'], self::POSTS_BATCH ] );

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- placeholders built from post type slugs.
			$rows = $wpdb->get_results( $wpdb->prepare( $sql, ...$prepare_args ), ARRAY_A );

			if ( empty( $rows ) ) {
				$state['phase'] = 'attachments';
			} else {
				foreach ( $rows as $row ) {
					$post_id = (int) $row['ID'];
					$state['posts_cursor'] = $post_id;
					$this->mergeUsedIds( $state, $this->extractUsedIdsFromContent( (string) ( $row['post_content'] ?? '' ) ) );
				}

				if ( count( $rows ) < self::POSTS_BATCH ) {
					$state['phase'] = 'attachments';
				}
			}
		}

		if ( $state['phase'] === 'attachments' ) {
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT ID FROM {$wpdb->posts}
					WHERE post_type = 'attachment'
					AND post_status = 'inherit'
					AND ID > %d
					ORDER BY ID ASC
					LIMIT %d",
					(int) $state['attachment_cursor'],
					self::SCAN_BATCH
				),
				ARRAY_A
			);

			if ( empty( $rows ) ) {
				return [
					'state'     => $state,
					'new_items' => $new_items,
					'done'      => true,
				];
			}

			foreach ( $rows as $row ) {
				$attachment_id = (int) $row['ID'];
				$state['attachment_cursor'] = $attachment_id;

				if ( isset( $state['used_ids'][ $attachment_id ] ) ) {
					continue;
				}

				$item = $this->buildUnusedItem( $attachment_id );
				if ( $item !== null ) {
					$state['unused'][] = $item;
					$new_items[]       = $item;
				}
			}

			if ( count( $rows ) < self::SCAN_BATCH ) {
				return [
					'state'     => $state,
					'new_items' => $new_items,
					'done'      => true,
				];
			}
		}

		return [
			'state'     => $state,
			'new_items' => $new_items,
			'done'      => false,
		];
	}

	public function ajaxScanUnusedMedia(): void {
		check_ajax_referer( 'wpps_umf_scan', 'nonce' );

		if ( ! current_user_can( 'upload_files' ) || ! current_user_can( 'delete_posts' ) ) {
			wp_send_json_error( [ 'message' => __( 'You do not have permission to manage media.', 'wp-powersuite' ) ] );
		}

		$scan_id  = isset( $_POST['scan_id'] ) ? sanitize_key( wp_unslash( (string) $_POST['scan_id'] ) ) : '';
		$continue = ! empty( $_POST['continue'] );

		if ( $continue && $scan_id !== '' ) {
			$state = get_transient( self::SCAN_TRANSIENT_PREFIX . $scan_id );
			if ( ! is_array( $state ) ) {
				wp_send_json_error( [ 'message' => __( 'Scan session expired. Please scan again.', 'wp-powersuite' ) ] );
			}
		} else {
			$this->filename_id_cache = [];
			$scan_id                 = wp_generate_password( 16, false, false );
			$state                   = $this->initScanState();
		}

		$result = $this->advanceScanBatch( $state );
		set_transient( self::SCAN_TRANSIENT_PREFIX . $scan_id, $result['state'], self::SCAN_TTL );

		wp_send_json_success( [
			'scan_id'      => $scan_id,
			'done'         => $result['done'],
			'unused'       => $result['new_items'],
			'total_unused' => count( $result['state']['unused'] ),
			'phase'        => $result['state']['phase'],
		] );
	}

	public function ajaxDeleteUnusedMedia(): void {
		check_ajax_referer( 'wpps_umf_delete', 'nonce' );

		if ( ! current_user_can( 'upload_files' ) || ! current_user_can( 'delete_posts' ) ) {
			wp_send_json_error( [ 'message' => __( 'You do not have permission to delete media.', 'wp-powersuite' ) ] );
		}

		$scan_id = isset( $_POST['scan_id'] ) ? sanitize_key( wp_unslash( (string) $_POST['scan_id'] ) ) : '';
		if ( $scan_id === '' ) {
			wp_send_json_error( [ 'message' => __( 'Scan session missing. Please scan again before deleting.', 'wp-powersuite' ) ] );
		}

		$state = get_transient( self::SCAN_TRANSIENT_PREFIX . $scan_id );
		if ( ! is_array( $state ) || empty( $state['unused'] ) ) {
			wp_send_json_error( [ 'message' => __( 'Scan session expired or empty. Please scan again.', 'wp-powersuite' ) ] );
		}

		$unused_lookup = [];
		foreach ( $state['unused'] as $item ) {
			if ( isset( $item['id'] ) ) {
				$unused_lookup[ (int) $item['id'] ] = true;
			}
		}

		$ids = [];
		if ( isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ) {
			$ids = array_values( array_filter( array_map( 'absint', wp_unslash( $_POST['ids'] ) ) ) );
		}

		if ( empty( $ids ) ) {
			wp_send_json_error( [ 'message' => __( 'No media items selected.', 'wp-powersuite' ) ] );
		}

		if ( count( $ids ) > self::DELETE_BATCH ) {
			wp_send_json_error( [
				'message' => sprintf(
					/* translators: %d: maximum number of files per delete request */
					__( 'You can delete at most %d files per request.', 'wp-powersuite' ),
					self::DELETE_BATCH
				),
			] );
		}

		$deleted = [];
		$failed  = [];

		foreach ( $ids as $id ) {
			if ( ! isset( $unused_lookup[ $id ] ) ) {
				$failed[] = [
					'id'      => $id,
					'message' => __( 'Attachment not found or is in use.', 'wp-powersuite' ),
				];
				continue;
			}

			if ( ! current_user_can( 'delete_post', $id ) ) {
				continue;
			}

			$result = wp_delete_attachment( $id, true );
			if ( $result ) {
				$deleted[] = $id;
				unset( $unused_lookup[ $id ] );
			} else {
				$failed[] = [
					'id'      => $id,
					'message' => __( 'Deletion failed.', 'wp-powersuite' ),
				];
			}
		}

		$state['unused'] = array_values(
			array_filter(
				$state['unused'],
				static function ( $item ) use ( $unused_lookup ) {
					return isset( $item['id'] ) && isset( $unused_lookup[ (int) $item['id'] ] );
				}
			)
		);

		set_transient( self::SCAN_TRANSIENT_PREFIX . $scan_id, $state, self::SCAN_TTL );

		wp_send_json_success( [
			'deleted'       => $deleted,
			'failed'        => $failed,
			'total_deleted' => count( $deleted ),
		] );
	}
}
