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

// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, Squiz.PHP.DiscouragedFunctions.Discouraged
// Justification: REST endpoints use permission_callback (visitor_permission() above verifies both the standard wp_rest nonce AND a per-gallery nonce). The two public AJAX endpoints (serve_image, stream_video) also verify the per-gallery nonce `ccgal_gallery_<id>` on the public branch and a user-bound `ccgal_admin_file_preview` nonce on the admin-preview fallback branch — so static analysis cannot link the conditional `$_GET` reads to the nonce check on the surrounding line. $_GET['file'] is sanitized via CCGAL_Storage_Adapter::sanitize() and then validated against the gallery's folder file map; any non-belonging file ID is rejected before use. ini_set() and set_time_limit() are required for chunked media streaming (potentially 50–100 MB photo/video downloads). All cache-redirects use wp_safe_redirect(); the only outbound redirect to an external storage URL (signed Drive media link in stream_video()) uses a raw header() Location call, not wp_redirect.

class CCGAL_REST_API {

	const NAMESPACE_V1 = 'ccgal/v1';

	public static function init() {
		add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
		add_action( 'wp_ajax_ccgal_serve_image', array( __CLASS__, 'serve_image' ) );
		add_action( 'wp_ajax_nopriv_ccgal_serve_image', array( __CLASS__, 'serve_image' ) );
		add_action( 'wp_ajax_ccgal_stream_video', array( __CLASS__, 'stream_video' ) );
		add_action( 'wp_ajax_nopriv_ccgal_stream_video', array( __CLASS__, 'stream_video' ) );
	}

	public static function register_routes() {
		register_rest_route(
			self::NAMESPACE_V1,
			'/gallery/(?P<id>\d+)/batch',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( __CLASS__, 'get_batch' ),
				'permission_callback' => array( __CLASS__, 'visitor_permission' ),
				'args'                => array(
					'page_token' => array(
						'type'    => 'string',
						'default' => '',
					),
				),
			)
		);

		register_rest_route(
			self::NAMESPACE_V1,
			'/gallery/(?P<id>\d+)/favorite',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( __CLASS__, 'toggle_favorite' ),
				'permission_callback' => array( __CLASS__, 'visitor_permission' ),
				'args'                => array(
					'file_id' => array(
						'type'     => 'string',
						'required' => true,
					),
				),
			)
		);
	}

	public static function visitor_permission( WP_REST_Request $request ) {
		$nonce = $request->get_header( 'X-WP-Nonce' );
		if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
			return new WP_Error( 'ccgal_bad_nonce', __( 'Invalid nonce.', 'customer-cloud-gallery' ), array( 'status' => 403 ) );
		}

		$gallery_id = absint( $request['id'] );
		$gallery    = CCGAL_CPT::get_gallery_config( $gallery_id );
		if ( ! $gallery ) {
			return new WP_Error( 'ccgal_no_gallery', __( 'Gallery not found.', 'customer-cloud-gallery' ), array( 'status' => 404 ) );
		}

		// Draft / private / trashed galleries are not publicly accessible via REST either
		// (mirrors serve_image()/stream_video()); editors of the gallery keep access.
		// Returns 404 (not 403) to avoid confirming the gallery's existence.
		$gallery_post = get_post( $gallery_id );
		if ( ! $gallery_post || ( 'publish' !== $gallery_post->post_status && ! current_user_can( 'edit_post', $gallery_id ) ) ) {
			return new WP_Error( 'ccgal_no_gallery', __( 'Gallery not found.', 'customer-cloud-gallery' ), array( 'status' => 404 ) );
		}

		$gallery_nonce = $request->get_header( 'X-CCGAL-Gallery-Nonce' );
		if ( ! $gallery_nonce ) {
			$gallery_nonce = (string) $request->get_param( 'gallery_nonce' );
		}
		if ( ! $gallery_nonce || ! wp_verify_nonce( $gallery_nonce, 'ccgal_gallery_' . $gallery_id ) ) {
			return new WP_Error( 'ccgal_bad_gallery_nonce', __( 'No access to this gallery.', 'customer-cloud-gallery' ), array( 'status' => 403 ) );
		}

		return true;
	}

	public static function get_batch( WP_REST_Request $request ) {
		$gallery = CCGAL_CPT::get_gallery_config( absint( $request['id'] ) );
		if ( ! $gallery ) {
			return new WP_Error( 'ccgal_no_gallery', __( 'Gallery not found.', 'customer-cloud-gallery' ), array( 'status' => 404 ) );
		}

		if ( empty( $gallery['folder_id'] ) ) {
			return new WP_Error( 'ccgal_no_folder', __( 'No storage folder configured.', 'customer-cloud-gallery' ), array( 'status' => 400 ) );
		}

		$page_token = sanitize_text_field( (string) $request->get_param( 'page_token' ) );
		$adapter    = CCGAL_Storage::adapter_for_gallery( $gallery );

		// Short-lived cache of the listing so N concurrent visitors (and infinite-scroll
		// REST calls that bypass full-page cache) collapse to one Drive/Dropbox round-trip
		// per page per TTL window, instead of a live external API call on every view.
		$cache_key = 'ccgal_batch_' . md5( $gallery['id'] . '|' . $gallery['folder_id'] . '|' . $gallery['sort_order'] . '|' . (int) $gallery['per_page'] . '|' . $page_token );
		$result    = get_transient( $cache_key );
		if ( false === $result ) {
			$result = $adapter->list_folder( $gallery['folder_id'], $gallery['per_page'], $page_token, $gallery['sort_order'] );
			if ( ! is_wp_error( $result ) ) {
				set_transient( $cache_key, $result, 5 * MINUTE_IN_SECONDS );
			}
		}
		if ( is_wp_error( $result ) ) {
			return new WP_REST_Response(
				array(
					'code'    => $result->get_error_code(),
					'message' => $result->get_error_message(),
				),
				500
			);
		}

		$visitor_favorites = array_flip( CCGAL_Favorites::get_favorites_for_visitor( $gallery['id'] ) );

		$items = array();
		foreach ( $result['files'] as $file ) {
			$mime = isset( $file['mimeType'] ) ? (string) $file['mimeType'] : '';
			if ( ! CCGAL_Storage_Adapter::is_media_mime( $mime ) ) {
				continue;
			}

			$file_id = isset( $file['id'] ) ? $adapter->sanitize_file_id( $file['id'] ) : '';
			if ( ! $file_id ) {
				continue;
			}

			$is_video = CCGAL_Storage_Adapter::is_video_mime( $mime );

			$thumb_link = isset( $file['thumbnailLink'] ) ? (string) $file['thumbnailLink'] : '';
			$thumb_direct  = '';
			$medium_direct = '';
			if ( $thumb_link && ! $is_video ) {
				$thumb_direct  = $adapter->resize_thumbnail_link( $thumb_link, 1200 );
				$medium_direct = $adapter->resize_thumbnail_link( $thumb_link, 2048 );
			}

			$items[] = array(
				'id'             => $file_id,
				'name'           => isset( $file['name'] ) ? (string) $file['name'] : '',
				'mime'           => $mime,
				'is_video'       => $is_video,
				'width'          => isset( $file['imageMediaMetadata']['width'] ) ? (int) $file['imageMediaMetadata']['width'] : null,
				'height'         => isset( $file['imageMediaMetadata']['height'] ) ? (int) $file['imageMediaMetadata']['height'] : null,
				'thumb_url'      => $thumb_direct ? $thumb_direct : self::best_url( $file_id, CCGAL_Cache::SIZE_THUMB, $gallery['id'] ),
				'medium_url'     => $medium_direct ? $medium_direct : self::best_url( $file_id, CCGAL_Cache::SIZE_MEDIUM, $gallery['id'] ),
				'thumb_fallback' => self::thumb_url( $file_id, CCGAL_Cache::SIZE_THUMB, $gallery['id'] ),
				'medium_fallback'=> self::thumb_url( $file_id, CCGAL_Cache::SIZE_MEDIUM, $gallery['id'] ),
				'video_url'      => $is_video ? self::video_url( $file_id, $gallery['id'] ) : null,
				'favorited'      => isset( $visitor_favorites[ $file_id ] ),
			);
		}

		return new WP_REST_Response(
			array(
				'gallery_id'      => $gallery['id'],
				'items'           => $items,
				'next_page_token' => $result['next_page_token'],
				'has_more'        => ! empty( $result['next_page_token'] ),
			),
			200
		);
	}

	public static function toggle_favorite( WP_REST_Request $request ) {
		$gallery = CCGAL_CPT::get_gallery_config( absint( $request['id'] ) );
		if ( ! $gallery || ! $gallery['allow_favorites'] || ! $gallery['cookies_enabled'] ) {
			return new WP_Error( 'ccgal_favorites_disabled', __( 'Favorites are disabled for this gallery.', 'customer-cloud-gallery' ), array( 'status' => 403 ) );
		}

		$adapter = CCGAL_Storage::adapter_for_gallery( $gallery );
		$file_id = $adapter->sanitize_file_id( (string) $request->get_param( 'file_id' ) );
		if ( ! $file_id ) {
			return new WP_Error( 'ccgal_bad_file', __( 'File ID missing.', 'customer-cloud-gallery' ), array( 'status' => 400 ) );
		}

		if ( empty( $gallery['folder_id'] ) || ! $adapter->file_in_folder( $file_id, $gallery['folder_id'] ) ) {
			return new WP_Error( 'ccgal_file_not_in_gallery', __( 'File does not belong to this gallery.', 'customer-cloud-gallery' ), array( 'status' => 403 ) );
		}

		$result = CCGAL_Favorites::toggle_favorite( $gallery['id'], $file_id );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return new WP_REST_Response( $result, 200 );
	}

	public static function thumb_url( $file_id, $size, $gallery_id = 0 ) {
		$args = array(
			'action' => 'ccgal_serve_image',
			'file'   => $file_id,
			'size'   => $size,
		);
		if ( $gallery_id ) {
			$args['gallery']  = (int) $gallery_id;
			// Gallery-scoped public nonce. Anyone holding the gallery URL on a
			// rendered page receives the nonce; without it the AJAX endpoint
			// rejects the request. Prevents direct hot-linking by file/gallery-ID
			// guessing from outside the rendered gallery page.
			$args['_wpnonce'] = wp_create_nonce( 'ccgal_gallery_' . (int) $gallery_id );
		} else {
			// Admin-preview path (no gallery context): bind URL to current admin user via nonce.
			// serve_image() rejects the request without a valid nonce on this fallback branch.
			$args['_wpnonce'] = wp_create_nonce( 'ccgal_admin_file_preview' );
		}
		return add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
	}

	public static function best_url( $file_id, $size, $gallery_id = 0 ) {
		if ( CCGAL_Cache::is_cached( $file_id, $size ) ) {
			return CCGAL_Cache::get_cached_url( $file_id, $size );
		}
		return self::thumb_url( $file_id, $size, $gallery_id );
	}

	public static function video_url( $file_id, $gallery_id = 0 ) {
		$args = array(
			'action' => 'ccgal_stream_video',
			'file'   => $file_id,
		);
		if ( $gallery_id ) {
			$args['gallery']  = (int) $gallery_id;
			$args['_wpnonce'] = wp_create_nonce( 'ccgal_gallery_' . (int) $gallery_id );
		} else {
			$args['_wpnonce'] = wp_create_nonce( 'ccgal_admin_file_preview' );
		}
		return add_query_arg( $args, admin_url( 'admin-ajax.php' ) );
	}

	public static function stream_video() {
		$file_id = isset( $_GET['file'] ) ? CCGAL_Storage_Adapter::sanitize( wp_unslash( $_GET['file'] ) ) : '';
		if ( ! $file_id ) {
			status_header( 400 );
			exit;
		}

		$gallery_id = isset( $_GET['gallery'] ) ? absint( $_GET['gallery'] ) : 0;
		$gallery    = null;
		if ( $gallery_id ) {
			// Gallery-scoped public nonce — issued by thumb_url()/video_url() when the
			// gallery is rendered. Without it the endpoint rejects so private file IDs
			// cannot be probed from outside a rendered gallery page.
			$gallery_nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
			if ( ! $gallery_nonce || ! wp_verify_nonce( $gallery_nonce, 'ccgal_gallery_' . $gallery_id ) ) {
				status_header( 403 );
				exit;
			}

			$gallery = CCGAL_CPT::get_gallery_config( $gallery_id );
			if ( ! $gallery ) {
				status_header( 403 );
				exit;
			}

			// Draft / private / trashed galleries must not leak files via the public AJAX path.
			$post = get_post( $gallery['id'] );
			if ( ! $post || 'publish' !== $post->post_status ) {
				if ( ! current_user_can( 'edit_post', $gallery['id'] ) ) {
					status_header( 403 );
					exit;
				}
			}

			$adapter = CCGAL_Storage::adapter_for_gallery( $gallery );
			if ( empty( $gallery['folder_id'] ) || ! $adapter->file_in_folder( $file_id, $gallery['folder_id'] ) ) {
				status_header( 403 );
				exit;
			}
		} elseif ( current_user_can( 'edit_posts' ) ) {
			// Admin-preview fallback (no gallery_id): require nonce bound to current user.
			$preview_nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
			if ( ! $preview_nonce || ! wp_verify_nonce( $preview_nonce, 'ccgal_admin_file_preview' ) ) {
				status_header( 403 );
				exit;
			}
			$adapter = CCGAL_Storage::adapter( 'google_drive' );
		} else {
			status_header( 403 );
			exit;
		}

		$meta = $adapter->get_file_meta( $file_id );
		if ( is_wp_error( $meta ) ) {
			status_header( 500 );
			header( 'Content-Type: text/plain; charset=utf-8' );
			echo esc_html( $meta->get_error_message() );
			exit;
		}

		$mime_raw = isset( $meta['mimeType'] ) ? (string) $meta['mimeType'] : 'application/octet-stream';
		$mime     = preg_replace( '/[^a-zA-Z0-9\/+.\-]/', '', $mime_raw );
		if ( '' === $mime ) {
			$mime = 'application/octet-stream';
		}
		if ( ! CCGAL_Storage_Adapter::is_video_mime( $mime ) ) {
			status_header( 400 );
			exit;
		}

		$signed = $adapter->get_media_download_url( $file_id );
		if ( ! is_wp_error( $signed ) && is_string( $signed ) && '' !== $signed ) {
			nocache_headers();
			header( 'Referrer-Policy: no-referrer' );
			header( 'Location: ' . esc_url_raw( $signed ), true, 302 );
			exit;
		}

		$size = isset( $meta['size'] ) ? (int) $meta['size'] : 0;

		while ( ob_get_level() > 0 ) {
			ob_end_clean();
		}
		if ( function_exists( 'apache_setenv' ) ) {
			@apache_setenv( 'no-gzip', '1' );
		}
		@ini_set( 'zlib.output_compression', 'Off' );
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( 0 );
		}

		$drive_range = '';
		// HTTP_RANGE comes from the client; unslash + cap to the byte-range subset we
		// understand. The preg_match() below acts as the strict whitelist validator —
		// anything that doesn't match `bytes=N-N` is treated as "no range".
		$range_hdr   = isset( $_SERVER['HTTP_RANGE'] ) ? substr( (string) wp_unslash( $_SERVER['HTTP_RANGE'] ), 0, 100 ) : '';

		header( 'Content-Type: ' . $mime );
		header( 'Accept-Ranges: bytes' );
		header( 'X-Content-Type-Options: nosniff' );
		header( 'Cache-Control: private, max-age=3600' );

		if ( $range_hdr && $size > 0 && preg_match( '/bytes=(\d*)-(\d*)/', $range_hdr, $m ) ) {
			$start = ( '' !== $m[1] ) ? (int) $m[1] : 0;
			$end   = ( '' !== $m[2] ) ? (int) $m[2] : $size - 1;
			if ( $end >= $size ) {
				$end = $size - 1;
			}
			if ( $start > $end ) {
				header( 'Content-Range: bytes */' . $size );
				status_header( 416 );
				exit;
			}
			$content_length = $end - $start + 1;
			status_header( 206 );
			header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $size );
			header( 'Content-Length: ' . $content_length );
			$drive_range = $start . '-' . $end;
		} else {
			status_header( 200 );
			if ( $size > 0 ) {
				header( 'Content-Length: ' . $size );
			}
		}

		ignore_user_abort( true );

		$adapter->stream_to_output( $file_id, null, $drive_range );
		exit;
	}

	public static function serve_image() {
		$file_id = isset( $_GET['file'] ) ? CCGAL_Storage_Adapter::sanitize( wp_unslash( $_GET['file'] ) ) : '';
		$size    = isset( $_GET['size'] ) ? sanitize_text_field( wp_unslash( $_GET['size'] ) ) : CCGAL_Cache::SIZE_THUMB;

		if ( ! $file_id ) {
			status_header( 400 );
			exit;
		}

		$gallery_id = isset( $_GET['gallery'] ) ? absint( $_GET['gallery'] ) : 0;
		if ( $gallery_id ) {
			// Gallery-scoped public nonce — issued by thumb_url() at render time.
			// Without it, direct probing by file/gallery-ID guessing is rejected.
			$gallery_nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
			if ( ! $gallery_nonce || ! wp_verify_nonce( $gallery_nonce, 'ccgal_gallery_' . $gallery_id ) ) {
				status_header( 403 );
				exit;
			}

			$gallery = CCGAL_CPT::get_gallery_config( $gallery_id );
			$adapter = $gallery ? CCGAL_Storage::adapter_for_gallery( $gallery ) : CCGAL_Storage::adapter();
			if ( ! $gallery || empty( $gallery['folder_id'] ) || ! $adapter->file_in_folder( $file_id, $gallery['folder_id'] ) ) {
				status_header( 403 );
				exit;
			}

			// Draft / private / trashed galleries must not leak files via the public AJAX path.
			$post = get_post( $gallery['id'] );
			if ( ! $post || 'publish' !== $post->post_status ) {
				if ( ! current_user_can( 'edit_post', $gallery['id'] ) ) {
					status_header( 403 );
					exit;
				}
			}
		} elseif ( current_user_can( 'edit_posts' ) ) {
			// Admin-preview fallback (no gallery_id): require nonce bound to current user.
			$preview_nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
			if ( ! $preview_nonce || ! wp_verify_nonce( $preview_nonce, 'ccgal_admin_file_preview' ) ) {
				status_header( 403 );
				exit;
			}
		} else {
			status_header( 403 );
			exit;
		}

		if ( CCGAL_Cache::is_cached( $file_id, $size ) ) {
			wp_safe_redirect( CCGAL_Cache::get_cached_url( $file_id, $size ), 302 );
			exit;
		}

		$provider = isset( $gallery ) && is_array( $gallery ) ? ( $gallery['provider'] ?? 'google_drive' ) : 'google_drive';
		$url      = CCGAL_Cache::ensure_cached( $file_id, $size, $provider );
		if ( is_wp_error( $url ) ) {
			status_header( 500 );
			header( 'Content-Type: text/plain; charset=utf-8' );
			echo esc_html( $url->get_error_message() );
			exit;
		}

		wp_safe_redirect( $url, 302 );
		exit;
	}
}
