<?php
namespace R2_Offload\Media;

use R2_Offload\DB;
use R2_Offload\Logger;
use R2_Offload\Settings;
use R2_Offload\Storage\Storage_Client;

defined( 'ABSPATH' ) || exit;

final class Pruner {

	private Storage_Client $storage;
	private Settings $settings;
	private Logger $logger;

	public function __construct( Storage_Client $storage, Settings $settings, Logger $logger ) {
		$this->storage  = $storage;
		$this->settings = $settings;
		$this->logger   = $logger;
		add_action( 'r2_offload_after_offloaded', [ $this, 'maybe_auto_prune' ], 10, 2 );
	}

	public function maybe_auto_prune( int $attachment_id, array $paths ): void {
		if ( ! $this->settings->get( 'auto_prune' ) ) {
			return;
		}
		if ( get_post_meta( $attachment_id, '_r2_offload_pruned', true ) ) {
			return;
		}
		$bytes = 0;
		foreach ( $paths as $local ) {
			$bytes += (int) @filesize( $local );
			@unlink( $local );
		}
		update_post_meta( $attachment_id, '_r2_offload_pruned', 1 );
		$this->logger->info( 'Auto-pruned ' . $attachment_id, 'prune', [ 'bytes' => $bytes ] );
	}

	public function prune( bool $dry_run = false, int $limit = 0 ): array {
		global $wpdb;
		$table = DB::items_table();
		$sql   = "SELECT attachment_id, object_prefix FROM {$table} WHERE status='offloaded'";
		if ( $limit > 0 ) {
			$sql .= $wpdb->prepare( ' LIMIT %d', $limit );
		}
		$rows = (array) $wpdb->get_results( $sql );

		$uploads = wp_get_upload_dir();
		$base    = trailingslashit( $uploads['basedir'] );

		$considered = count( $rows );
		$pruned     = 0;
		$freed      = 0;
		$skipped    = 0;
		$errors     = [];

		foreach ( $rows as $row ) {
			$id = (int) $row->attachment_id;

			if ( get_post_meta( $id, '_r2_offload_pruned', true ) ) {
				$skipped++;
				continue;
			}

			$paths = Path_Resolver::attachment_files( $id );
			if ( empty( $paths ) ) {
				$skipped++;
				$errors[] = [ 'id' => $id, 'reason' => 'no local files' ];
				continue;
			}

			// Use prefix stored at upload time; fall back to current effective prefix.
			$prefix = trim( (string) $row->object_prefix, '/' );
			if ( $prefix === '' ) {
				$prefix = $this->settings->effective_prefix();
			}

			$all_verified = true;
			foreach ( $paths as $local ) {
				$relative = ltrim( str_replace( $base, '', $local ), '/' );
				$object   = $prefix !== '' ? $prefix . '/' . $relative : $relative;
				$info     = $this->storage->head_object( $object );
				if ( ! $info ) {
					$all_verified = false;
					$errors[]     = [ 'id' => $id, 'reason' => 'missing_on_remote', 'object' => $object ];
					break;
				}
				$lsize = (int) @filesize( $local );
				if ( $lsize > 0 && isset( $info['size'] ) && (int) $info['size'] !== $lsize ) {
					$all_verified = false;
					$errors[]     = [ 'id' => $id, 'reason' => 'size_mismatch', 'object' => $object, 'local' => $lsize, 'remote' => (int) $info['size'] ];
					break;
				}
			}
			if ( ! $all_verified ) {
				continue;
			}

			$bytes       = 0;
			$unlink_fail = [];
			foreach ( $paths as $local ) {
				$bytes += (int) @filesize( $local );
				if ( $dry_run ) {
					continue;
				}
				if ( ! @unlink( $local ) && file_exists( $local ) ) {
					$unlink_fail[] = [
						'path'     => $local,
						'writable' => is_writable( $local ),
						'owner'    => function_exists( 'fileowner' ) ? @fileowner( $local ) : null,
					];
				}
			}

			if ( ! empty( $unlink_fail ) ) {
				$errors[] = [ 'id' => $id, 'reason' => 'unlink_failed', 'details' => $unlink_fail ];
				$this->logger->error( 'Prune unlink failed for ' . $id, 'prune', [ 'fail' => $unlink_fail ] );
				continue;
			}

			if ( ! $dry_run ) {
				update_post_meta( $id, '_r2_offload_pruned', 1 );
				$this->logger->info( 'Pruned local files for ' . $id, 'prune', [ 'bytes' => $bytes, 'files' => count( $paths ) ] );
			}
			$pruned++;
			$freed += $bytes;
		}

		$this->logger->info(
			'Prune run complete',
			'prune',
			[ 'considered' => $considered, 'pruned' => $pruned, 'skipped' => $skipped, 'bytes_freed' => $freed, 'dry_run' => $dry_run ]
		);

		return [
			'considered'  => $considered,
			'pruned'      => $pruned,
			'skipped'     => $skipped,
			'bytes_freed' => $freed,
			'dry_run'     => $dry_run,
			'errors'      => $errors,
		];
	}

	public function restore( int $attachment_id ): array {
		global $wpdb;
		$item = $wpdb->get_row( $wpdb->prepare( "SELECT object_prefix FROM " . DB::items_table() . " WHERE attachment_id=%d", $attachment_id ) );
		$meta = wp_get_attachment_metadata( $attachment_id );
		if ( ! $meta ) {
			return [ 'ok' => false, 'message' => 'No metadata.' ];
		}
		$uploads = wp_get_upload_dir();
		$base    = trailingslashit( $uploads['basedir'] );
		$prefix  = $item && $item->object_prefix ? trim( (string) $item->object_prefix, '/' ) : $this->settings->effective_prefix();

		$original = get_attached_file( $attachment_id );
		if ( ! $original ) {
			return [ 'ok' => false, 'message' => 'No attached file path.' ];
		}

		$targets = [ $original ];
		$dir     = trailingslashit( dirname( $original ) );
		if ( ! empty( $meta['sizes'] ) ) {
			foreach ( $meta['sizes'] as $s ) {
				if ( ! empty( $s['file'] ) ) {
					$targets[] = $dir . $s['file'];
				}
			}
		}

		$restored = 0;
		foreach ( $targets as $local ) {
			$relative = ltrim( str_replace( $base, '', $local ), '/' );
			$object   = $prefix !== '' ? $prefix . '/' . $relative : $relative;
			wp_mkdir_p( dirname( $local ) );
			if ( $this->storage->download_object( $object, $local ) ) {
				$restored++;
			}
		}
		delete_post_meta( $attachment_id, '_r2_offload_pruned' );
		$this->logger->info( 'Restored ' . $restored . ' files for ' . $attachment_id, 'prune' );
		return [ 'ok' => $restored > 0, 'restored' => $restored ];
	}
}
