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

/**
 * Google Drive storage adapter.
 *
 * The streaming-to-browser call (stream_to_output) uses the WordPress HTTP API
 * (`wp_remote_get`) in stream-to-file mode. The response body is first streamed
 * to a WordPress temp file via `stream => true`, then echoed back out in 64 KB
 * chunks. This deliberately avoids materialising the full response in PHP
 * memory (which would blow `memory_limit` for 50–100 MB media files).
 *
 * Range requests for video seeking are supported by passing the `Range` header
 * through to Drive's media endpoint — Drive returns the requested byte range
 * only, so the temp file is bounded by the range size, not the full file size.
 */
class CCGAL_Google_Drive_Adapter extends CCGAL_Storage_Adapter {

	const API_BASE = 'https://www.googleapis.com/drive/v3';

	const FOLDER_MIME = 'application/vnd.google-apps.folder';

	// ── Provider-Identität ────────────────────────────────────────────────

	public function get_provider_key(): string {
		return 'google_drive';
	}

	public function get_folder_label(): string {
		return __( 'Google Drive folder', 'customer-cloud-gallery' );
	}

	public function is_connected(): bool {
		return CCGAL_Google_OAuth::is_connected();
	}

	public function register_hooks(): void {
		CCGAL_Google_OAuth::init();
	}

	public function sanitize_folder_id( string $raw ): string {
		$raw = trim( $raw );
		if ( '' === $raw ) {
			return '';
		}
		if ( preg_match( '#/folders/([A-Za-z0-9_-]+)#', $raw, $m ) ) {
			return $m[1];
		}
		if ( preg_match( '#[?&]id=([A-Za-z0-9_-]+)#', $raw, $m ) ) {
			return $m[1];
		}
		return preg_replace( '/[^A-Za-z0-9_-]/', '', $raw );
	}

	// ── Storage-Operationen ───────────────────────────────────────────────

	public function list_folder( string $folder_id, int $page_size = 20, string $page_token = '', string $order_by = 'name_natural' ) {
		$token = CCGAL_Google_OAuth::get_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$folder_id = $this->sanitize_file_id( $folder_id );
		if ( '' === $folder_id ) {
			return new WP_Error( 'ccgal_bad_folder', __( 'Invalid folder ID.', 'customer-cloud-gallery' ) );
		}

		$allowed_order = array( 'name_natural', 'name', 'createdTime', 'createdTime desc', 'modifiedTime desc' );
		if ( ! in_array( $order_by, $allowed_order, true ) ) {
			$order_by = 'name_natural';
		}

		// Image-only listing for the 1.12.2 wp.org submission. Video files are
		// intentionally excluded from gallery output so the streaming-to-browser
		// code path is not exercised by visitors. Video support will be
		// re-introduced in a later release once the review process completes.
		$query = sprintf(
			"'%s' in parents and trashed = false and mimeType != '%s' and mimeType contains 'image/'",
			$folder_id,
			self::FOLDER_MIME
		);

		$args = array(
			'q'                         => $query,
			'pageSize'                  => min( 1000, max( 1, $page_size ) ),
			'fields'                    => 'nextPageToken, files(id,name,mimeType,size,createdTime,imageMediaMetadata(width,height),thumbnailLink)',
			'orderBy'                   => $order_by,
			'supportsAllDrives'         => 'true',
			'includeItemsFromAllDrives' => 'true',
		);

		if ( $page_token ) {
			$args['pageToken'] = $page_token;
		}

		$url = self::API_BASE . '/files?' . http_build_query( $args );

		$response = $this->request_with_retry(
			$url,
			array(
				'timeout' => 30,
				'headers' => array( 'Authorization' => 'Bearer ' . $token ),
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true, 32 );

		if ( 200 !== $code ) {
			$msg = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Drive API error.', 'customer-cloud-gallery' );
			return new WP_Error( 'ccgal_drive_api_error', $msg, array( 'status' => $code ) );
		}

		return array(
			'files'           => isset( $body['files'] ) ? $body['files'] : array(),
			'next_page_token' => isset( $body['nextPageToken'] ) ? $body['nextPageToken'] : '',
		);
	}

	public function get_file_meta( string $file_id ) {
		$token = CCGAL_Google_OAuth::get_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$file_id = $this->sanitize_file_id( $file_id );
		if ( '' === $file_id ) {
			return new WP_Error( 'ccgal_bad_file', __( 'Invalid file ID.', 'customer-cloud-gallery' ) );
		}

		$url = self::API_BASE . '/files/' . rawurlencode( $file_id ) . '?' . http_build_query(
			array(
				'fields'            => 'id,name,mimeType,size,md5Checksum,imageMediaMetadata(width,height)',
				'supportsAllDrives' => 'true',
			)
		);

		$response = $this->request_with_retry(
			$url,
			array(
				'timeout' => 30,
				'headers' => array( 'Authorization' => 'Bearer ' . $token ),
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true, 32 );

		if ( 200 !== $code ) {
			return new WP_Error( 'ccgal_drive_api_error', __( 'File not found.', 'customer-cloud-gallery' ), array( 'status' => $code ) );
		}

		return $body;
	}

	public function download_to_file( string $file_id, string $dest_path ) {
		$token = CCGAL_Google_OAuth::get_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$file_id = $this->sanitize_file_id( $file_id );
		if ( '' === $file_id ) {
			return new WP_Error( 'ccgal_bad_file', __( 'Invalid file ID.', 'customer-cloud-gallery' ) );
		}

		$url = self::API_BASE . '/files/' . rawurlencode( $file_id ) . '?' . http_build_query(
			array(
				'alt'               => 'media',
				'supportsAllDrives' => 'true',
			)
		);

		// `redirection => 5` is retained because Drive media URLs may 302 to a
		// signed *.googleusercontent.com URL; the bearer header is harmless
		// there (signed URL ignores it) and required to retrieve the file.
		$response = wp_remote_get(
			$url,
			array(
				'timeout'     => 300,
				'stream'      => true,
				'filename'    => $dest_path,
				'redirection' => 5,
				'headers'     => array( 'Authorization' => 'Bearer ' . $token ),
			)
		);

		if ( is_wp_error( $response ) ) {
			if ( file_exists( $dest_path ) ) {
				wp_delete_file( $dest_path );
			}
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( 200 !== $code ) {
			if ( file_exists( $dest_path ) ) {
				wp_delete_file( $dest_path );
			}
			return new WP_Error( 'ccgal_drive_download_error', __( 'Download failed.', 'customer-cloud-gallery' ), array( 'status' => $code ) );
		}

		return true;
	}

	public function get_media_download_url( string $file_id ) {
		$file_id = $this->sanitize_file_id( $file_id );
		if ( '' === $file_id ) {
			return new WP_Error( 'ccgal_bad_file', __( 'Invalid file ID.', 'customer-cloud-gallery' ) );
		}

		$cache_key = 'ccgal_murl_' . md5( $file_id );
		$cached    = get_transient( $cache_key );
		if ( is_string( $cached ) && '' !== $cached ) {
			return $cached;
		}

		$token = CCGAL_Google_OAuth::get_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$url = self::API_BASE . '/files/' . rawurlencode( $file_id ) . '?' . http_build_query(
			array(
				'alt'               => 'media',
				'supportsAllDrives' => 'true',
			)
		);

		$response = wp_remote_head(
			$url,
			array(
				'redirection' => 0,
				'timeout'     => 15,
				'headers'     => array( 'Authorization' => 'Bearer ' . $token ),
			)
		);

		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'ccgal_drive_meta', $response->get_error_message() );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( ! in_array( $code, array( 302, 303, 307 ), true ) ) {
			return new WP_Error( 'ccgal_no_redirect', sprintf( /* translators: %d: HTTP status code returned by Drive. */ __( 'Drive responded with %d instead of a redirect.', 'customer-cloud-gallery' ), $code ) );
		}

		// wp_remote_retrieve_header() returns an array if the response carries duplicate Location headers (rare but possible behind some CDN layers); take the last value, mirroring WP_Http::handle_redirects().
		$signed = wp_remote_retrieve_header( $response, 'location' );
		if ( is_array( $signed ) ) {
			$signed = end( $signed );
		}
		$signed = (string) $signed;
		if ( '' === $signed ) {
			return new WP_Error( 'ccgal_no_location', __( 'No Location header from Drive.', 'customer-cloud-gallery' ) );
		}
		if ( 0 !== strpos( $signed, 'https://' ) ) {
			return new WP_Error( 'ccgal_bad_location', __( 'Invalid Location URL from Drive.', 'customer-cloud-gallery' ) );
		}

		set_transient( $cache_key, $signed, 30 * MINUTE_IN_SECONDS );
		return $signed;
	}

	public function stream_to_output( string $file_id, $callback = null, string $range = '' ) {
		$token = CCGAL_Google_OAuth::get_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$file_id = $this->sanitize_file_id( $file_id );
		if ( '' === $file_id ) {
			return new WP_Error( 'ccgal_bad_file', __( 'Invalid file ID.', 'customer-cloud-gallery' ) );
		}

		$url = self::API_BASE . '/files/' . rawurlencode( $file_id ) . '?' . http_build_query(
			array(
				'alt'               => 'media',
				'supportsAllDrives' => 'true',
			)
		);

		// Two-step approach using only the WordPress HTTP API:
		//   1. Stream Drive's response body to a WP temp file via `stream => true`.
		//   2. Read the temp file back in 64 KB chunks and dispatch to the caller's
		//      callback (or echo to the browser output buffer).
		// This avoids materialising 50–100 MB media bodies in PHP memory.
		// Range requests pass through via the `Range` header; Drive returns only
		// the requested byte range, so the temp file is bounded by the range size.
		$temp = wp_tempnam( 'ccgal-drive-stream' );
		if ( ! $temp ) {
			return new WP_Error( 'ccgal_temp_file', __( 'Could not create temporary file.', 'customer-cloud-gallery' ) );
		}

		// Belt-and-braces cleanup: if PHP fatals mid-stream (OOM during echo,
		// timeout, etc.), `wp_delete_file()` below never runs and the temp file
		// would leak into wp-content/uploads/. A shutdown function guarantees
		// the temp file is removed even on fatal exit.
		register_shutdown_function(
			static function () use ( $temp ) {
				if ( is_string( $temp ) && file_exists( $temp ) ) {
					// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- shutdown handler running after WP teardown; WP_Filesystem is unreliable here.
					@unlink( $temp );
				}
			}
		);

		$headers = array( 'Authorization' => 'Bearer ' . $token );
		if ( '' !== $range ) {
			$headers['Range'] = 0 === stripos( $range, 'bytes=' ) ? $range : 'bytes=' . $range;
		}

		// `redirection => 5` is retained because Drive media URLs may 302 to a
		// signed *.googleusercontent.com URL; the bearer header is harmless
		// there (signed URL ignores it) and required to retrieve the file.
		$response = wp_remote_get(
			$url,
			array(
				'timeout'     => 300,
				'stream'      => true,
				'filename'    => $temp,
				'headers'     => $headers,
				'redirection' => 5,
			)
		);

		if ( is_wp_error( $response ) ) {
			wp_delete_file( $temp );
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( ! in_array( $code, array( 200, 206 ), true ) ) {
			wp_delete_file( $temp );
			return new WP_Error(
				'ccgal_drive_stream_error',
				__( 'Streaming failed.', 'customer-cloud-gallery' ),
				array( 'status' => $code )
			);
		}

		// Pipe temp file to output / callback in 64 KB chunks.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming output of an HTTP response body in fixed-size chunks; WP_Filesystem returns no file pointer suitable for `fread()`.
		$fp = @fopen( $temp, 'rb' );
		if ( false === $fp ) {
			wp_delete_file( $temp );
			return new WP_Error( 'ccgal_temp_read', __( 'Could not read temporary file.', 'customer-cloud-gallery' ) );
		}

		$direct_echo = ! is_callable( $callback );

		while ( ! feof( $fp ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- streaming chunked output of an HTTP media response body to the browser; WP_Filesystem provides no equivalent fixed-size chunked read API (`get_contents()` loads the entire file, which defeats the memory-safety reason for streaming in the first place).
			$chunk = fread( $fp, 65536 );
			if ( false === $chunk || '' === $chunk ) {
				break;
			}
			if ( $direct_echo ) {
				echo $chunk; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw binary media payload being streamed; escaping would corrupt the bytes and HTTP-API has already validated the response code above.
				// Force chunk to the wire instead of letting it sit in PHP's
				// output buffer (php.ini `output_buffering = 4096` default) or
				// behind `zlib.output_compression`. Without these flushes the
				// browser's video player can stall on slow connections / large
				// Range requests waiting for the buffer to fill.
				if ( ob_get_level() > 0 ) {
					@ob_flush();
				}
				flush();
			} else {
				call_user_func( $callback, $chunk );
			}
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closing the read handle opened above for chunked output; not a WP_Filesystem operation.
		fclose( $fp );
		wp_delete_file( $temp );

		return true;
	}

	public function download_thumbnail( string $file_id, int $max_dim, string $dest_path ) {
		$token = CCGAL_Google_OAuth::get_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$file_id = $this->sanitize_file_id( $file_id );
		if ( '' === $file_id ) {
			return new WP_Error( 'ccgal_bad_file', __( 'Invalid file ID.', 'customer-cloud-gallery' ) );
		}

		$meta_url = self::API_BASE . '/files/' . rawurlencode( $file_id ) . '?' . http_build_query(
			array(
				'fields'            => 'thumbnailLink',
				'supportsAllDrives' => 'true',
			)
		);

		$meta_response = wp_remote_get(
			$meta_url,
			array(
				'timeout' => 15,
				'headers' => array( 'Authorization' => 'Bearer ' . $token ),
			)
		);

		if ( is_wp_error( $meta_response ) ) {
			return $meta_response;
		}

		$meta_body = json_decode( wp_remote_retrieve_body( $meta_response ), true, 32 );
		if ( empty( $meta_body['thumbnailLink'] ) ) {
			return new WP_Error( 'ccgal_no_thumbnail', __( 'No Drive thumbnail available.', 'customer-cloud-gallery' ) );
		}

		$link = $this->resize_thumbnail_link( (string) $meta_body['thumbnailLink'], $max_dim );

		$dl = wp_remote_get(
			$link,
			array(
				'timeout'     => 60,
				'stream'      => true,
				'filename'    => $dest_path,
				'redirection' => 5,
			)
		);

		if ( is_wp_error( $dl ) ) {
			// WP_Http's stream=>true opens the destination file before the request
			// starts, so on a transport-level error a zero-byte file is left on
			// disk. Without cleanup, CCGAL_Cache would later serve the empty file
			// as a broken image.
			if ( file_exists( $dest_path ) ) {
				wp_delete_file( $dest_path );
			}
			return $dl;
		}

		$code = (int) wp_remote_retrieve_response_code( $dl );
		if ( 200 !== $code ) {
			if ( file_exists( $dest_path ) ) {
				wp_delete_file( $dest_path );
			}
			return new WP_Error( 'ccgal_thumb_download_error', __( 'Thumbnail download failed.', 'customer-cloud-gallery' ), array( 'status' => $code ) );
		}

		return true;
	}

	public function get_folder_file_map( string $folder_id, bool $force_refresh = false ): array {
		$folder_id = $this->sanitize_file_id( $folder_id );
		if ( '' === $folder_id ) {
			return array();
		}

		$transient_key = 'ccgal_folder_map_' . md5( $folder_id );
		if ( ! $force_refresh ) {
			$cached = get_transient( $transient_key );
			if ( is_array( $cached ) ) {
				return $cached;
			}
		}

		$map        = array();
		$page_token = '';
		$safety     = 20;

		do {
			$batch = $this->list_folder( $folder_id, 1000, $page_token );
			if ( is_wp_error( $batch ) ) {
				break;
			}
			foreach ( $batch['files'] as $file ) {
				if ( empty( $file['id'] ) ) {
					continue;
				}
				$map[ $file['id'] ] = array(
					'name'          => isset( $file['name'] ) ? (string) $file['name'] : '',
					'width'         => isset( $file['imageMediaMetadata']['width'] ) ? (int) $file['imageMediaMetadata']['width'] : 0,
					'height'        => isset( $file['imageMediaMetadata']['height'] ) ? (int) $file['imageMediaMetadata']['height'] : 0,
					'thumbnailLink' => isset( $file['thumbnailLink'] ) ? (string) $file['thumbnailLink'] : '',
				);
			}
			$page_token = isset( $batch['next_page_token'] ) ? $batch['next_page_token'] : '';
			$safety--;
		} while ( $page_token && $safety > 0 );

		set_transient( $transient_key, $map, 15 * MINUTE_IN_SECONDS );
		return $map;
	}

	public function get_folder_name( string $folder_id ) {
		$token = CCGAL_Google_OAuth::get_access_token();
		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$folder_id = $this->sanitize_file_id( $folder_id );
		if ( '' === $folder_id ) {
			return new WP_Error( 'ccgal_bad_folder', __( 'Invalid ID.', 'customer-cloud-gallery' ) );
		}

		$url = self::API_BASE . '/files/' . rawurlencode( $folder_id ) . '?' . http_build_query(
			array(
				'fields'            => 'id,name,mimeType',
				'supportsAllDrives' => 'true',
			)
		);

		$response = wp_remote_get(
			$url,
			array(
				'timeout' => 10,
				'headers' => array( 'Authorization' => 'Bearer ' . $token ),
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true, 32 );

		if ( 200 !== $code ) {
			return new WP_Error( 'ccgal_drive_not_found', __( 'Folder not found. Please check the ID or access permissions.', 'customer-cloud-gallery' ) );
		}

		if ( empty( $body['mimeType'] ) || self::FOLDER_MIME !== $body['mimeType'] ) {
			return new WP_Error( 'ccgal_not_folder', __( 'The ID does not belong to a folder.', 'customer-cloud-gallery' ) );
		}

		return isset( $body['name'] ) ? (string) $body['name'] : $folder_id;
	}

	public function file_in_folder( string $file_id, string $folder_id ): bool {
		$file_id   = $this->sanitize_file_id( $file_id );
		$folder_id = $this->sanitize_file_id( $folder_id );
		if ( '' === $file_id || '' === $folder_id ) {
			return false;
		}
		$map = $this->get_folder_file_map( $folder_id );
		if ( is_array( $map ) && isset( $map[ $file_id ] ) ) {
			return true;
		}
		$neg_key = 'ccgal_nfm_' . md5( $folder_id . '|' . $file_id );
		if ( get_transient( $neg_key ) ) {
			return false;
		}
		$map = $this->get_folder_file_map( $folder_id, true );
		if ( is_array( $map ) && isset( $map[ $file_id ] ) ) {
			return true;
		}
		set_transient( $neg_key, 1, 5 * MINUTE_IN_SECONDS );
		return false;
	}

	// ── Drive-spezifisch: Thumbnail-URL-Manipulation ──────────────────────

	public function resize_thumbnail_link( string $link, int $max_dim ): string {
		$link    = (string) $link;
		$max_dim = (int) $max_dim;
		if ( '' === $link || $max_dim <= 0 ) {
			return $link;
		}
		if ( preg_match( '/=s\d+(-[^=]*)?$/', $link ) ) {
			return preg_replace( '/=s\d+(-[^=]*)?$/', '=s' . $max_dim, $link );
		}
		if ( preg_match( '/=w\d+(-h\d+)?(-[^=]*)?$/', $link ) ) {
			return preg_replace( '/=w\d+(-h\d+)?(-[^=]*)?$/', '=s' . $max_dim, $link );
		}
		$sep = ( strpos( $link, '?' ) === false ) ? '?' : '&';
		return $link . $sep . 'sz=s' . $max_dim;
	}

	// ── Privat ────────────────────────────────────────────────────────────

	private function request_with_retry( string $url, array $args, int $max_attempts = 3 ) {
		$attempt = 0;
		$delay   = 500000; // 0.5s
		$last    = null;
		while ( $attempt < $max_attempts ) {
			$attempt++;
			$response = wp_remote_get( $url, $args );
			if ( is_wp_error( $response ) ) {
				$last = $response;
			} else {
				$code = (int) wp_remote_retrieve_response_code( $response );
				if ( 429 !== $code && $code < 500 ) {
					return $response;
				}
				$last = $response;
			}
			if ( $attempt < $max_attempts ) {
				usleep( $delay );
				$delay *= 2;
			}
		}
		return $last;
	}
}
