<?php
/**
 * Thumbnail AJAX handlers — regenerate, delete-orphans, toggle sizes.
 *
 * @package CoffeeWP_Media_Cleanup
 */

namespace CWPM\Ajax;

use CWPM\Core\Db;
use CWPM\Core\Logger;
use CWPM\Core\Trash;
use CWPM\Service\ThumbnailService;
use CWPM\Thumbnail\Sizes;

defined( 'ABSPATH' ) || exit;

// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified centrally in BaseHandler::verify_request().

class ThumbnailHandler extends BaseHandler {

	/**
	 * Constructor — register AJAX actions.
	 */
	public function __construct() {
		add_action( 'wp_ajax_cwpm_regenerate_thumbnails', array( $this, 'regenerate_thumbnails' ) );
		add_action( 'wp_ajax_cwpm_delete_orphan_thumbnails', array( $this, 'delete_orphan_thumbnails' ) );
		add_action( 'wp_ajax_cwpm_delete_orphan_file_thumbs', array( $this, 'delete_orphan_file_thumbs' ) );
		add_action( 'wp_ajax_cwpm_toggle_thumbnail_size', array( $this, 'toggle_thumbnail_size' ) );
	}

	/**
	 * Toggle a single thumbnail size on/off.
	 */
	public function toggle_thumbnail_size() {
		check_ajax_referer( 'cwpm_nonce', 'nonce' );
		$this->verify_request();

		$size    = sanitize_text_field( wp_unslash( $_POST['size'] ?? '' ) );
		$enabled = filter_var( wp_unslash( $_POST['enabled'] ?? true ), FILTER_VALIDATE_BOOLEAN );

		if ( empty( $size ) ) {
			wp_send_json_error( 'Missing size parameter', 400 );
		}

		$registered = wp_get_registered_image_subsizes();
		if ( ! isset( $registered[ $size ] ) ) {
			wp_send_json_error( 'Invalid size name', 400 );
		}

		$disabled = Sizes::get_disabled();

		if ( $enabled ) {
			$disabled = array_values( array_diff( $disabled, array( $size ) ) );
		} else {
			$disabled[] = $size;
			$disabled   = array_values( array_unique( $disabled ) );
		}

		Sizes::save_disabled( $disabled );
		wp_send_json_success(
			array(
				'size'     => $size,
				'enabled'  => $enabled,
				'disabled' => $disabled,
			)
		);
	}

	/**
	 * Regenerate thumbnail sizes for a batch of attachments.
	 *
	 * Accepts a comma-separated list of attachment IDs and calls
	 * wp_generate_attachment_metadata() on each one to regenerate any
	 * missing intermediate sizes.
	 */
	public function regenerate_thumbnails() {
		check_ajax_referer( 'cwpm_nonce', 'nonce' );
		$this->verify_request();

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- parse_attachment_ids() splits + casts to int per-item.
		$ids = $this->parse_attachment_ids( wp_unslash( $_POST['attachment_ids'] ?? '' ) );

		require_once ABSPATH . 'wp-admin/includes/image.php';

		$results   = array_map( array( $this, 'regenerate_single_attachment' ), $ids );
		$succeeded = count(
			array_filter(
				$results,
				function ( $r ) {
					return $r['success'];
				}
			)
		);

		Logger::info( $this->format_regen_log( $results, $succeeded ) );

		wp_send_json_success(
			array(
				'results'   => $results,
				'succeeded' => $succeeded,
				'failed'    => count( $results ) - $succeeded,
			)
		);
	}

	/**
	 * Parse the comma-separated attachment_ids POST field into a clean int[].
	 * Sends a 400 response and exits when input is empty or contains no valid IDs.
	 *
	 * @param string $raw Raw POST value.
	 * @return int[]
	 */
	private function parse_attachment_ids( $raw ) {
		$raw = sanitize_text_field( (string) $raw );

		if ( '' === $raw ) {
			wp_send_json_error( 'No attachment IDs provided', 400 );
		}

		$ids = array_values( array_filter( array_map( 'absint', explode( ',', $raw ) ) ) );

		if ( empty( $ids ) ) {
			wp_send_json_error( 'Invalid attachment IDs', 400 );
		}

		return $ids;
	}

	/**
	 * Regenerate every intermediate size for a single attachment.
	 *
	 * @param int $attach_id
	 * @return array{id:int,success:bool,size_bytes:int,error?:string,sizes?:string[]}
	 */
	private function regenerate_single_attachment( $attach_id ) {
		$file = get_attached_file( $attach_id );

		if ( ! $file || ! file_exists( $file ) ) {
			return array(
				'id'         => $attach_id,
				'success'    => false,
				'error'      => 'File not found on disk',
				'size_bytes' => 0,
			);
		}

		$missing_before = $this->collect_missing_thumb_files( $file, wp_get_attachment_metadata( $attach_id ) );

		$metadata = wp_generate_attachment_metadata( $attach_id, $file );

		if ( is_wp_error( $metadata ) ) {
			return array(
				'id'         => $attach_id,
				'success'    => false,
				'error'      => $metadata->get_error_message(),
				'size_bytes' => 0,
			);
		}

		wp_update_attachment_metadata( $attach_id, $metadata );

		$size_bytes = $this->sum_regenerated_thumb_sizes( dirname( $file ), $missing_before );

		$rel_path = get_post_meta( $attach_id, '_wp_attached_file', true );
		if ( $rel_path ) {
			Db::update_result_action( $rel_path, 'regenerate-thumbnails', 'regenerated' );
		}

		return array(
			'id'         => $attach_id,
			'success'    => true,
			'size_bytes' => $size_bytes,
			'sizes'      => $missing_before,
		);
	}

	/**
	 * Identify thumbnail files referenced in the attachment metadata that are
	 * missing on disk.
	 *
	 * @param string $file Absolute path to the original attachment.
	 * @param mixed  $meta Attachment metadata array (or non-array on miss).
	 * @return string[] Filenames (no path) of missing thumbnails.
	 */
	private function collect_missing_thumb_files( $file, $meta ) {
		if ( ! is_array( $meta ) || empty( $meta['sizes'] ) ) {
			return array();
		}

		$missing  = array();
		$file_dir = dirname( $file );

		foreach ( $meta['sizes'] as $size_data ) {
			if ( empty( $size_data['file'] ) ) {
				continue;
			}
			if ( ! file_exists( $file_dir . '/' . $size_data['file'] ) ) {
				$missing[] = $size_data['file'];
			}
		}

		return $missing;
	}

	/**
	 * Sum the on-disk size of newly-created thumbnail files.
	 *
	 * @param string   $file_dir Absolute directory containing the thumbnails.
	 * @param string[] $filenames Thumbnail filenames to measure.
	 * @return int Total bytes.
	 */
	private function sum_regenerated_thumb_sizes( $file_dir, $filenames ) {
		$bytes = 0;
		foreach ( $filenames as $thumb_filename ) {
			$path = $file_dir . '/' . $thumb_filename;
			if ( file_exists( $path ) ) {
				$bytes += (int) filesize( $path );
			}
		}
		return $bytes;
	}

	/**
	 * Build the human-readable log line summarising a regeneration batch.
	 *
	 * @param array $results   Per-attachment regeneration outcomes.
	 * @param int   $succeeded Count of successful entries.
	 * @return string
	 */
	private function format_regen_log( $results, $succeeded ) {
		$entries = array();
		foreach ( $results as $r ) {
			if ( empty( $r['success'] ) ) {
				continue;
			}
			$path      = get_post_meta( $r['id'], '_wp_attached_file', true ) ?: 'attachment #' . $r['id'];
			$sizes     = ! empty( $r['sizes'] ) ? '(' . implode( ', ', $r['sizes'] ) . ')' : '(all sizes)';
			$entries[] = $path . ' ' . $sizes;
		}

		$detail = ! empty( $entries ) ? ' [' . implode( '; ', $entries ) . ']' : '';
		return 'Thumbnails regenerated for ' . $succeeded . ' attachment(s).' . $detail;
	}

	/**
	 * Move orphan thumbnail files to the trash folder.
	 *
	 * Accepts a JSON-encoded array of { id: int, sizes: string[] } objects.
	 * For each attachment / size pair, the thumbnail file is moved to
	 * uploads/cwpm-trash/ if it exists on disk.
	 */
	public function delete_orphan_thumbnails() {
		check_ajax_referer( 'cwpm_nonce', 'nonce' );
		$this->verify_request();

		$raw = sanitize_text_field( wp_unslash( $_POST['items'] ?? '' ) );

		if ( empty( $raw ) ) {
			wp_send_json_error( 'No items provided', 400 );
		}

		$items = json_decode( $raw, true );

		if ( ! is_array( $items ) || empty( $items ) ) {
			wp_send_json_error( 'Invalid items format', 400 );
		}

		wp_send_json_success( ThumbnailService::delete_orphan_thumbnails( $items ) );
	}

	/**
	 * Move orphan file thumbnail derivatives to the trash.
	 *
	 * These are thumbnail-sized variants (e.g. image-150x150.jpg) of orphan files
	 * that have no attachment record. Accepts a JSON-encoded array of rel_paths
	 * relative to the uploads directory.
	 */
	public function delete_orphan_file_thumbs() {
		check_ajax_referer( 'cwpm_nonce', 'nonce' );
		$this->verify_request();

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON string decoded + per-item sanitized below.
		$raw = wp_unslash( $_POST['rel_paths'] ?? '' );

		if ( empty( $raw ) ) {
			wp_send_json_error( 'No rel_paths provided', 400 );
		}

		$rel_paths = json_decode( $raw, true );

		if ( ! is_array( $rel_paths ) || empty( $rel_paths ) ) {
			wp_send_json_error( 'Invalid rel_paths format', 400 );
		}

		$upload_dir = wp_upload_dir();
		$base_dir   = $upload_dir['basedir'];
		$results    = array();

		foreach ( $rel_paths as $rel_path ) {
			$rel_path = sanitize_text_field( $rel_path );
			$abs_path = $base_dir . '/' . ltrim( $rel_path, '/' );
			$rel_dir  = dirname( $rel_path );

			if ( '.' === $rel_dir ) {
				$rel_dir = '';
			}

			$moved     = Trash::move_to_trash( $abs_path, $rel_dir );
			$results[] = array(
				'rel_path'   => $rel_path,
				'success'    => $moved,
				'size_bytes' => 0,
			);
		}

		$succeeded = count(
			array_filter(
				$results,
				function ( $r ) {
					return $r['success'];
				}
			)
		);

		$trashed_files = array_filter( array_column( $results, 'rel_path' ) );
		$detail        = ! empty( $trashed_files ) ? ' [' . implode( ', ', $trashed_files ) . ']' : '';
		Logger::info( 'Orphan file thumbnails trashed: ' . $succeeded . ' file(s).' . $detail );

		wp_send_json_success(
			array(
				'results'   => $results,
				'succeeded' => $succeeded,
				'failed'    => count( $results ) - $succeeded,
			)
		);
	}
}
