<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class SmallPict_Image_Processor {
	private $api;
	private $transport;

	public function __construct( $api, $transport ) {
		$this->api       = $api;
		$this->transport = $transport;
	}

	/**
	 * Hook into wp_generate_attachment_metadata
	 */
	public function trigger_optimization( $metadata, $attachment_id ) {
		// Auto-optimize check defaults to true or setting
		$opt = get_option( 'smallpict_auto_optimize' );
		// If option doesn't exist (false), default to true. If exists, cast to bool.
		$auto_optimize = ( $opt === false ) ? true : (bool) $opt;

		if ( ! $auto_optimize ) {
			return $metadata;
		}

		// P-P2: Skip synchronous processing in WP-CLI and REST bulk import contexts.
		// These can process hundreds of images and would time out with sync polling.
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return $metadata;
		}
		// Also detect WP All Import and similar REST-based bulk upload contexts
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST && ! is_admin() ) {
			return $metadata;
		}

		// Avoid infinite loop if we update metadata ourselves
		if ( get_post_meta( $attachment_id, '_smallpict_processed', true ) ) {
			return $metadata;
		}

		// Run Processing
		$result = $this->process_attachment( $attachment_id, $metadata );

		if ( is_wp_error( $result ) ) {
			// Log error but don't break upload
			// error_log('SmallPict Error: ' . $result->get_error_message());
			return $metadata;
		}

		// If result contains metadata, use it (This ensures format change persists)
		if ( is_array( $result ) ) {
			update_post_meta( $attachment_id, '_smallpict_processed', true );
			return $result;
		}

		return $metadata; // Fallback
	}

	/**
	 * Main Entry Point: Process a specific attachment AND its thumbnails
	 */
	public function process_attachment( $attachment_id, $metadata = null ) {
		$main_file_path = get_attached_file( $attachment_id );
		if ( ! file_exists( $main_file_path ) ) {
			return new WP_Error( 'file_missing', 'File not found on disk.' );
		}

		$main_mime       = get_post_mime_type( $attachment_id );
		$upload_dir_info = wp_upload_dir();
		$base_dir        = $upload_dir_info['basedir'];

		// If metadata is null (rare), try to reconstruct or just process main
		if ( ! is_array( $metadata ) ) {
			$metadata = array();
		}

		// 1. Process Main Image
			// error_log("SmallPict: Processing Main Image " . basename($main_file_path));
		$main_result = $this->optimize_file_pipeline( $main_file_path, $main_mime );

		if ( ! is_wp_error( $main_result ) ) {
			// Update DB _wp_attached_file immediately for main file
			// Determine relative path based on new full path
			// We assume the file stays in same month folder, just name/ext changes.
			if ( isset( $main_result['new_path'] ) ) {
				$new_filename = basename( $main_result['new_path'] );
				$old_rel_path = get_post_meta( $attachment_id, '_wp_attached_file', true );
				$rel_dir      = dirname( $old_rel_path );
				$new_rel_path = ( $rel_dir === '.' ? '' : $rel_dir . '/' ) . $new_filename;

				update_post_meta( $attachment_id, '_wp_attached_file', $new_rel_path );

				// Update MIME if changed
				if ( isset( $main_result['new_mime'] ) && $main_result['new_mime'] ) {
					wp_update_post(
						array(
							'ID'             => $attachment_id,
							'post_mime_type' => $main_result['new_mime'],
						)
					);
				}

				// Update Metadata Logic
				$metadata['file']     = $new_rel_path;
				$metadata['filesize'] = filesize( $main_result['new_path'] );

				// Update Dimensions (Critical for Aggressive Resize)
				$new_dims = getimagesize( $main_result['new_path'] );
				if ( $new_dims ) {
					$metadata['width']  = $new_dims[0];
					$metadata['height'] = $new_dims[1];
				}
			}
		} else {
			// error_log("SmallPict Error Main: " . $main_result->get_error_message());

			// Notify User if limit reached
			$code = $main_result->get_error_code();
			if ( $code === 'file_too_large' || $code === 'quota_exceeded' ) {
				$msg = 'SmallPict: Optimization skipped for ' . basename( $main_file_path ) . '. ' . $main_result->get_error_message();
				set_transient( 'smallpict_upload_warning_' . get_current_user_id(), $msg, 45 );
				update_post_meta( $attachment_id, 'smallpict_warning', $msg );

				// If main file skipped due to limits, do NOT process thumbnails to avoid eating quota on partial optimization
				$skip_subs = true;
			}
		}

		// 2. Process Thumbnails / Sub-sizes
		// DETECT HUGE IMAGE SCENARIO: Use dimensions or if 'sizes' is suspiciously empty for a large image
		$is_huge = false;
		if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) {
			if ( $metadata['width'] > 2560 || $metadata['height'] > 2560 ) {
				$is_huge = true;
			}
		}

		// Standard Loop for EXISTING sizes (Small images or if WP managed to make them)
		if ( ! isset( $skip_subs ) && isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) && ! empty( $metadata['sizes'] ) ) {
			$dir_path = dirname( $main_file_path );

			foreach ( $metadata['sizes'] as $size_key => $size_info ) {
				$file_name = $size_info['file'];
				$mime_type = isset( $size_info['mime-type'] ) ? $size_info['mime-type'] : $main_mime;
				$full_path = $dir_path . '/' . $file_name;

				if ( ! file_exists( $full_path ) ) {
					continue;
				}

				// error_log("SmallPict: Processing Sub-size [$size_key] " . $file_name);

				$sub_result = $this->optimize_file_pipeline( $full_path, $mime_type );

				if ( ! is_wp_error( $sub_result ) ) {
					// Update Metadata for this size
					$new_sub_filename                            = basename( $sub_result['new_path'] );
					$metadata['sizes'][ $size_key ]['file']      = $new_sub_filename;
					$metadata['sizes'][ $size_key ]['mime-type'] = $sub_result['new_mime'];
				} else {
					// error_log("SmallPict Error Sub-size [$size_key]: " . $sub_result->get_error_message());
				}
			}
		}
		// HUGE IMAGE FALLBACK: Generate Missing Sizes Remotely
		elseif ( $is_huge && ! isset( $skip_subs ) ) {
			// error_log("SmallPict: Detected Huge Image with no sizes. Generating remotely...");

			$registered_sizes = wp_get_registered_image_subsizes();
			$dir_path         = dirname( $main_file_path );
			$orig_w           = $metadata['width'];
			$orig_h           = $metadata['height'];

			foreach ( $registered_sizes as $size_key => $size_data ) {
				// Calculate dimensions using WP logic
				$dims = image_resize_dimensions( $orig_w, $orig_h, $size_data['width'], $size_data['height'], $size_data['crop'] );
				if ( ! $dims ) {
					continue;
				}

				$dst_w = $dims[4];
				$dst_h = $dims[5];

				// Limit check: Don't generate if target is bigger than original? (WP handles this in image_resize_dimensions usually returns false)

				// error_log("SmallPict: Generating Remote Thumbnail [$size_key] {$dst_w}x{$dst_h}");

				// Construct Override Options for Backend
				$options_override = array(
					'resize_policy' => array(
						'enabled'    => true,
						'width'      => $dst_w,
						'height'     => $dst_h,
						'crop'       => $size_data['crop'] ? 'center' : 'fit',
						'aggressive' => true,
					),
				);

				// Target Filename Base
				$filename_base = pathinfo( $main_file_path, PATHINFO_FILENAME );
				$suffix        = "{$dst_w}x{$dst_h}";
				$output_base   = $dir_path . '/' . $filename_base . '-' . $suffix;

				// TRIGGER REMOTE GENERATION using MAIN FILE as source
				$sub_result = $this->optimize_file_pipeline( $main_file_path, $main_mime, $options_override, $output_base );

				if ( ! is_wp_error( $sub_result ) ) {
						$new_sub_filename               = basename( $sub_result['new_path'] );
						$metadata['sizes'][ $size_key ] = array(
							'file'      => $new_sub_filename,
							'width'     => $dst_w,
							'height'    => $dst_h,
							'mime-type' => $sub_result['new_mime'],
						);
				} else {
					// error_log("SmallPict: Failed to generate $size_key: " . $sub_result->get_error_message());
				}
			}
		}

		return $metadata;
	}

	/**
	 * Reusable Pipeline: Upload -> Job -> Download -> Replace
	 * Returns array ['new_path', 'new_mime'] or WP_Error
	 */
	private function optimize_file_pipeline( $file_path, $mime_type, $options_override = array(), $output_base = null ) {
		$filesize = filesize( $file_path );
		$filename = basename( $file_path );

		// 1. Upload Loop
		$upload_req = $this->api->request_upload_url( $filename, $mime_type );
		if ( is_wp_error( $upload_req ) ) {
			return $upload_req;
		}

		$upload_url = $upload_req['input']['presigned_put_url'];
		$bucket     = $upload_req['input']['bucket'];
		$object_key = $upload_req['input']['object_key'];
		$job_id     = $upload_req['job_id'];

		// Actual Upload
		$up_res = $this->transport->upload_to_s3( $upload_url, $file_path, $mime_type );
		if ( is_wp_error( $up_res ) ) {
			return $up_res;
		}

		// 2. Prepare Options
		$target_format = get_option( 'smallpict_output_format', 'webp' );
		if ( $target_format === 'original' ) {
			$parts = explode( '/', $mime_type );
			$ext   = end( $parts );
			if ( $ext == 'jpeg' ) {
				$ext = 'jpg';
			}
			$target_format = $ext;
		}

		if ( $target_format === 'jpeg' ) {
			$target_format = 'jpg'; // Normalize
		}

		// Capabilities Check: Enforce allowed formats locally to prevent backend error
		$caps            = get_option( 'smallpict_account_capabilities', array() );
		$allowed_formats = isset( $caps['formats'] ) ? $caps['formats'] : array( 'webp' ); // Default safe

		if ( ! in_array( $target_format, $allowed_formats ) ) {
			// error_log("SmallPict: Format '$target_format' not allowed for this plan. Falling back to WebP.");
			$target_format = 'webp';
		}

		$options = array(
			'target_format' => $target_format,
			'quality'       => (int) get_option( 'smallpict_quality', 80 ),
			'mode'          => get_option( 'smallpict_compression_mode', 'lossy' ),
		);

		// Merge Overrides BEFORE adding standard resize/target_size logic
		// This allows caller to set specific resize_policy
		if ( ! empty( $options_override ) ) {
			$options = array_merge( $options, $options_override );
		}

		// Only apply Best-effort / Aggressive Defaults if NOT overriding resize
		if ( ! isset( $options['resize_policy'] ) ) {
			// ... Standard Logic ...
			if ( get_option( 'smallpict_target_size_enabled' ) ) {
				// Target Size requires Lossy compression to work effectively
				$options['mode'] = 'lossy';

				$options['size_target'] = array(
					'enabled'        => true,
					'target_kb'      => (int) get_option( 'smallpict_target_kb', 200 ),
					'best_effort'    => true,
					'min_quality'    => 50,
					'max_iterations' => 6,
				);
			}

			if ( get_option( 'smallpict_aggressive_resize' ) ) {
				$options['resize_policy'] = array(
					'enabled'          => true,
					'aggressive'       => true,
					'max_long_edge_px' => (int) get_option( 'smallpict_max_edge', 1600 ),
				);
			}
		}

		// 3. Client Hints
		$client_hints = array( 'content_length' => $filesize );
		$img_size     = getimagesize( $file_path );
		if ( $img_size ) {
			$client_hints['width']  = $img_size[0];
			$client_hints['height'] = $img_size[1];
		}

		// 4. Submit Job
		$job_res = $this->api->submit_job( $job_id, $object_key, $bucket, $options, $client_hints );
		if ( is_wp_error( $job_res ) ) {
			return $job_res;
		}

		// 5. Poll
		$status      = 'queued';
		$result_data = null;

		if ( isset( $job_res['status'] ) && $job_res['status'] === 'completed' ) {
			$status      = 'completed';
			$result_data = $job_res['result'];
		} else {
			// P-P1: Exponential backoff polling (2s, 3s, 4s, 5s → max 5s per attempt)
			// Max 20 attempts × avg ~4s = ~40s worst-case (down from 30×2s = 60s)
			$attempts     = 0;
			$max_attempts = 20;
			while ( $attempts < $max_attempts ) {
				$sleep_s = min( 5, 2 + $attempts );
				sleep( $sleep_s );
				$check = $this->api->get_job_status( $job_id );
				if ( ! is_wp_error( $check ) ) {
					$status = $check['status'];
					if ( $status === 'completed' ) {
						$result_data = $check['result'];
						break;
					}
					if ( $status === 'failed' ) {
						return new WP_Error( 'job_failed', isset( $check['error'] ) ? $check['error'] : 'Job Failed' );
					}
				}
				++$attempts;
			}
		}

		if ( $status !== 'completed' ) {
			return new WP_Error( 'timeout', 'Job timed out.' );
		}

		// 6. Replace File
		$replace_res = $this->replace_file_on_disk( $file_path, $result_data, $output_base );

		// 7. Record Usage (Only if replace successful or at least optimization occurred?)
		// If we bail on replace, we still used server resources. So we should probably record usage upon 'completed' status.
		// But for user fairness, if download fails, maybe don't charge?
		// Let's charge if we successfully downloaded and replaced.
		if ( ! is_wp_error( $replace_res ) ) {
			SmallPict_Quota_Manager::record_usage( $filesize );
		}

		return $replace_res;
	}

	/**
	 * Downloads result and replaces local file (handling extension change)
	 * If output_base is set, it creates a NEW file instead of replacing.
	 */
	private function replace_file_on_disk( $original_path, $result_data, $output_base = null ) {
		$download_url = $result_data['presigned_get_url'];

		// Use output_base as reference if set, else original
		$ref_path  = $output_base ? $output_base : $original_path;
		$temp_path = $ref_path . '.tmp';

		$down_res = $this->transport->download_from_s3( $download_url, $temp_path );
		if ( is_wp_error( $down_res ) ) {
			return $down_res;
		}

		if ( filesize( $temp_path ) < 10 ) {
			wp_delete_file( $temp_path );
			return new WP_Error( 'download_corrupt', 'Downloaded file too small.' );
		}

		// Determine new extension
		$meta     = isset( $result_data['meta'] ) ? $result_data['meta'] : array();
		$fmt      = isset( $meta['format'] ) ? strtoupper( $meta['format'] ) : '';
		$new_ext  = pathinfo( $original_path, PATHINFO_EXTENSION ); // Default to current
		$new_mime = '';

		if ( $fmt === 'WEBP' ) {
			$new_ext  = 'webp';
			$new_mime = 'image/webp'; } elseif ( $fmt === 'AVIF' ) {
			$new_ext  = 'avif';
			$new_mime = 'image/avif'; } elseif ( $fmt === 'JPEG' ) {
				$new_ext  = 'jpg';
				$new_mime = 'image/jpeg'; } elseif ( $fmt === 'PNG' ) {
				$new_ext  = 'png';
				$new_mime = 'image/png'; }

				$dir             = dirname( $ref_path );
				$filename_no_ext = pathinfo( $ref_path, PATHINFO_FILENAME );
				$new_path        = $dir . '/' . $filename_no_ext . '.' . $new_ext;

				if ( $new_path !== $original_path && ! $output_base ) {
					// Delete old (Only if replacing main file)
					if ( file_exists( $original_path ) ) {
						wp_delete_file( $original_path );
					}

					// Rename temp to new (Try using WP_Filesystem)
					global $wp_filesystem;
					if ( empty( $wp_filesystem ) ) {
						require_once ABSPATH . 'wp-admin/includes/file.php';
						WP_Filesystem();
					}

					if ( ! $wp_filesystem->move( $temp_path, $new_path, true ) ) {
						// Fallback: Copy + Unlink (Cross-volume issue)
						if ( copy( $temp_path, $new_path ) ) {
							wp_delete_file( $temp_path );
						} else {
							return new WP_Error( 'rename_failed', 'Rename/Copy failed' );
						}
					}
				} else {
					// Overwrite or New File
					global $wp_filesystem;
					if ( empty( $wp_filesystem ) ) {
						require_once ABSPATH . 'wp-admin/includes/file.php';
						WP_Filesystem();
					}

					if ( ! $wp_filesystem->move( $temp_path, $new_path, true ) ) {
						// Fallback
						if ( copy( $temp_path, $new_path ) ) {
							wp_delete_file( $temp_path );
						} else {
							wp_delete_file( $temp_path );
							return new WP_Error( 'replace_failed', 'Replace/Create failed' );
						}
					}
				}

				return array(
					'new_path' => $new_path,
					'new_mime' => $new_mime,
				);
	}
}
