<?php
namespace R2_Offload\Storage;

use R2_Offload\Logger;
use R2_Offload\Settings;

defined( 'ABSPATH' ) || exit;

final class GCS_Client implements Storage_Client {

	private Settings $settings;
	private Logger $logger;
	private Rate_Limiter $limiter;
	private ?object $bucket = null;

	public function __construct( Settings $settings, Logger $logger ) {
		$this->settings = $settings;
		$this->logger   = $logger;
		$this->limiter  = new Rate_Limiter( $settings, $logger );
	}

	public function is_available(): bool {
		return class_exists( '\\Google\\Cloud\\Storage\\StorageClient' ) && $this->settings->is_configured();
	}

	private function bucket(): object {
		if ( $this->bucket ) {
			return $this->bucket;
		}
		if ( ! class_exists( '\\Google\\Cloud\\Storage\\StorageClient' ) ) {
			throw new \RuntimeException( 'google/cloud-storage SDK missing. Run composer install in plugin directory.' );
		}

		$creds = json_decode( (string) $this->settings->get( 'credentials_json' ), true );
		if ( ! is_array( $creds ) ) {
			throw new \RuntimeException( 'Invalid service account JSON.' );
		}

		$client       = new \Google\Cloud\Storage\StorageClient( [ 'keyFile' => $creds ] );
		$this->bucket = $client->bucket( (string) $this->settings->get( 'bucket' ) );
		return $this->bucket;
	}

	public function upload_file( string $local_path, string $object_name, string $mime = '' ): array {
		if ( ! file_exists( $local_path ) ) {
			throw new \RuntimeException( 'Local file missing: ' . $local_path );
		}

		$options = [
			'name'     => $object_name,
			'metadata' => [
				'cacheControl' => 'public, max-age=31536000, immutable',
			],
		];

		// Legacy ACLs only work on fine-grained (non-UBLA) buckets. Default off for UBLA compatibility.
		if ( $this->settings->get( 'use_legacy_acl' ) ) {
			$options['predefinedAcl'] = $this->settings->get( 'private_mode' ) ? 'private' : 'publicRead';
		}

		if ( $mime !== '' ) {
			$options['metadata']['contentType'] = $mime;
		}

		$stream = fopen( $local_path, 'rb' );
		if ( ! $stream ) {
			throw new \RuntimeException( 'Cannot read local file: ' . $local_path );
		}

		try {
			$bucket = $this->bucket();
			$object = $this->limiter->wrap(
				fn() => $bucket->upload( $stream, $options ),
				'upload:' . $object_name
			);
		} finally {
			if ( is_resource( $stream ) ) {
				fclose( $stream );
			}
		}

		return [
			'name' => $object->name(),
			'size' => (int) filesize( $local_path ),
		];
	}

	public function head_object( string $object_name ): ?array {
		try {
			$bucket = $this->bucket();
			$info   = $this->limiter->wrap(
				function () use ( $bucket, $object_name ) {
					$object = $bucket->object( $object_name );
					if ( ! $object->exists() ) {
						return null;
					}
					return $object->info();
				},
				'head:' . $object_name
			);
			if ( ! $info ) {
				return null;
			}
			return [
				'size' => (int) ( $info['size'] ?? 0 ),
				'md5'  => (string) ( $info['md5Hash'] ?? '' ),
				'etag' => (string) ( $info['etag'] ?? '' ),
			];
		} catch ( \Throwable $e ) {
			$this->logger->warn( 'Head failed: ' . $e->getMessage(), 'gcs', [ 'object' => $object_name ] );
			return null;
		}
	}

	public function download_object( string $object_name, string $target_path ): bool {
		try {
			$bucket = $this->bucket();
			return (bool) $this->limiter->wrap(
				function () use ( $bucket, $object_name, $target_path ) {
					$object = $bucket->object( $object_name );
					if ( ! $object->exists() ) {
						return false;
					}
					$object->downloadToFile( $target_path );
					return file_exists( $target_path );
				},
				'download:' . $object_name
			);
		} catch ( \Throwable $e ) {
			$this->logger->warn( 'Download failed: ' . $e->getMessage(), 'gcs', [ 'object' => $object_name ] );
			return false;
		}
	}

	public function delete_object( string $object_name ): void {
		try {
			$bucket = $this->bucket();
			$this->limiter->wrap(
				fn() => $bucket->object( $object_name )->delete(),
				'delete:' . $object_name
			);
		} catch ( \Throwable $e ) {
			$this->logger->warn( 'Delete failed: ' . $e->getMessage(), 'gcs', [ 'object' => $object_name ] );
		}
	}

	public function test_connection(): array {
		try {
			$exists = $this->bucket()->exists();
			return [ 'ok' => (bool) $exists, 'message' => $exists ? 'Bucket reachable.' : 'Bucket not found.' ];
		} catch ( \Throwable $e ) {
			return [ 'ok' => false, 'message' => $e->getMessage() ];
		}
	}
}
