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

/********************************************************************
 * Copyright (C) 2025 Hassine RIANI (https://upvimeo.com)
 *
 * This file is part of Upvimeo
 *
 * Upvimeo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * Upvimeo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Upvimeo. If not, see <https://www.gnu.org/licenses/>.
 **********************************************************************/

/**
 * Class UpVimeo_File_Chunk_Handler
 *
 * This class handles all the chunk file uploading logic.
 *
 * @license GPLv2
 * @copyright Hassine RIANI (https://upvimeo.com)
 */
abstract class UpVimeo_File_Chunk_Handler
{

	/**
	 * Unique string for this specific handler.
	 * @var string
	 */
	protected $prefix;

	/**
	 * The tmp dir that the tmp files are uploaded.
	 * @var string
	 */
	protected $tmp_dir;

	/**
	 * The $_FILES array input name of the files.
	 * @var mixed
	 */
	protected $input_name;

	/**
	 * Max file size allowed per upload.
	 * @var string
	 */
	protected $max_file_size;

	/**
	 * The logging utility
	 * @var UpVimeo_Logger
	 */
	protected $logger;

	/**
	 * UpVimeo_File_Chunk_Handler constructor.
	 *
	 * @param $prefix
	 * @param array $params
	 */
	public function __construct($prefix, $params = array())
	{
		$this->prefix = $prefix;

		// Set tmp dir.
		if (isset($params['tmp_dir'])) {
			$this->tmp_dir = $params['tmp_dir'];
			if (!file_exists($this->tmp_dir)) {
				_doing_it_wrong(__METHOD__, 'Tmp dir path does not exist.', esc_html(UPVIMEO_VERSION));
			}
		} else {
			_doing_it_wrong(__METHOD__, 'No tmp dir path specified.', esc_html(UPVIMEO_VERSION));
		}

		// Set input name
		if (isset($params['input_name'])) {
			$this->input_name = $params['input_name'];
		} else {
			_doing_it_wrong(__METHOD__, 'No input name specified.', esc_html(UPVIMEO_VERSION));
		}

		// Max filesize
		if (isset($params['max_file_size'])) {
			$this->max_file_size = $params['max_file_size'];
		} else {
			_doing_it_wrong(__METHOD__, 'No max file size specified.', esc_html(UPVIMEO_VERSION));
		}
	}

	/**
	 * Returns the handler prefix.
	 *
	 * @return mixed
	 */
	public function get_prefix()
	{
		return $this->prefix;
	}

	/**
	 * Register the chunk upload endpoints
	 */
	public function register()
	{

		add_action('wp_ajax_' . $this->prefix . '_file_upload_speed_test', array($this, 'ajax_speed_test'));
		if ( defined('UPVIMEO_IS_PRO') && UPVIMEO_IS_PRO ) {
			add_action('wp_ajax_nopriv_' . $this->prefix . '_file_upload_speed_test', array($this, 'ajax_speed_test'));
		}

		add_action('wp_ajax_' . $this->prefix . '_upload_chunk_init', array($this, 'ajax_upload_init'));
		if ( defined('UPVIMEO_IS_PRO') && UPVIMEO_IS_PRO ) {
			add_action('wp_ajax_nopriv_' . $this->prefix . '_upload_chunk_init', array(
				$this,
				'ajax_upload_init'
			));
		}
		add_action('wp_ajax_' . $this->prefix . '_upload_chunk', array($this, 'ajax_upload_chunk'));
		if ( defined('UPVIMEO_IS_PRO') && UPVIMEO_IS_PRO ) {
			add_action('wp_ajax_nopriv_' . $this->prefix . '_upload_chunk', array($this, 'ajax_upload_chunk'));
		}
		add_action('wp_ajax_' . $this->prefix . '_file_chunks_uploaded', array($this, 'ajax_upload_finalize'));
		if ( defined('UPVIMEO_IS_PRO') && UPVIMEO_IS_PRO ) {
			add_action('wp_ajax_nopriv_' . $this->prefix . '_file_chunks_uploaded', array(
				$this,
				'ajax_upload_finalize'
			));
		}

		add_action('wp_ajax_' . $this->prefix . '_remove_file', array($this, 'ajax_remove_file'));
		if ( defined('UPVIMEO_IS_PRO') && UPVIMEO_IS_PRO ) {
			add_action('wp_ajax_nopriv_' . $this->prefix . '_remove_file', array($this, 'ajax_remove_file'));
		}


		// phpcs:ignore WordPress.Security.NonceVerification
		if (!empty($_POST[$this->prefix . '_slow']) && 'true' === $_POST[$this->prefix . '_slow'] && !empty($this->validate())) {
			add_filter('upvimeo_file_upload_chunk_parallel', '__return_false');
			add_filter('upvimeo_file_upload_chunk_size', array($this, 'get_slow_connection_chunk_size'));
		}
	}

	/**
	 * Clean up the tmp folder - remove all old files every day (filterable interval).
	 */
	protected function clean_tmp_files()
	{

		$files = glob(trailingslashit($this->get_tmp_dir()) . '*');

		if (!is_array($files) || empty($files)) {
			return;
		}

		$lifespan = (int) apply_filters('upvimeo_field_clean_tmp_files_lifespan', DAY_IN_SECONDS);

		foreach ($files as $file) {
			if ('index.html' === $file || !is_file($file)) {
				continue;
			}

			// In some cases filemtime() can return false, in that case - pretend this is a new file and do nothing.
			$modified = (int) filemtime($file);
			if (empty($modified)) {
				$modified = time();
			}

			if ((time() - $modified) >= $lifespan) {
				@wp_delete_file($file); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			}
		}
	}

	/**
	 * Speed test endpoint.
	 */
	public function ajax_speed_test()
	{
		// If the user lacks backend upload permissions (Guest or Subscriber),
		// treat this as a frontend upload request which requires PRO.
		if ( ! current_user_can( 'upload_files' ) && ! current_user_can( 'manage_options' ) ) {
			if ( ! defined('UPVIMEO_IS_PRO') || ! UPVIMEO_IS_PRO ) {
				wp_send_json_error( array( 'message' => 'Unauthorized access. Pro version required for frontend uploads.' ), 403 );
			}
		}
		check_ajax_referer('upvimeo_ajax_nonce', 'security');
		wp_send_json_success();
	}

	/**
	 * Remove the file from the temporary directory.
	 */
	public function ajax_remove_file()
	{
		$default_error = esc_html__('Something went wrong while removing the file.', 'upvimeo-direct-uploads');

		// If the user lacks backend upload permissions (Guest or Subscriber),
		// treat this as a frontend upload request which requires PRO.
		if ( ! current_user_can( 'upload_files' ) && ! current_user_can( 'manage_options' ) ) {
			if ( ! defined('UPVIMEO_IS_PRO') || ! UPVIMEO_IS_PRO ) {
				wp_send_json_error( array( 'message' => 'Unauthorized access. Pro version required for frontend uploads.' ), 403 );
			}
		}

		check_ajax_referer('upvimeo_ajax_nonce', '_wpnonce');

		$validated_form_field = $this->validate();
		if (empty($validated_form_field)) {
			wp_send_json_error($default_error, 400);
		}

		if (empty($_POST['file'])) {
			wp_send_json_error($default_error, 403);
		}

		$file     = sanitize_file_name(wp_unslash($_POST['file']));
		$tmp_path = wp_normalize_path($this->get_tmp_dir() . '/' . $file);

		// Requested file does not exist, which is good.
		if (!is_file($tmp_path)) {
			wp_send_json_success($file);
		}

		// Path-traversal guard: confirm the resolved path is still inside the tmp directory.
		$real_tmp_dir = realpath($this->get_tmp_dir());
		$real_path    = realpath($tmp_path);
		if (false === $real_tmp_dir || false === $real_path || strpos($real_path, trailingslashit($real_tmp_dir)) !== 0) {
			wp_send_json_error($default_error, 403);
		}

		if (@wp_delete_file($tmp_path)) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			wp_send_json_success($file);
		}

		wp_send_json_error($default_error, 400);
	}

	/**
	 * Initializes the chunk upload process.
	 *
	 * No data is being send by the client, they expecting an authorization from this method
	 * before sending any chunk.
	 *
	 * The server may return different configs to the uploader client (smaller chunks, disable
	 * parallel uploads etc).
	 *
	 * This method would validate the file extension, maximum size and other things.
	 */
	public function ajax_upload_init()
	{
		$default_error = esc_html__('Something went wrong, please try again.', 'upvimeo-direct-uploads');

		// If the user lacks backend upload permissions (Guest or Subscriber),
		// treat this as a frontend upload request which requires PRO.
		if ( ! current_user_can( 'upload_files' ) && ! current_user_can( 'manage_options' ) ) {
			if ( ! defined('UPVIMEO_IS_PRO') || ! UPVIMEO_IS_PRO ) {
				wp_send_json_error( array( 'message' => 'Unauthorized access. Pro version required for frontend uploads.' ), 403 );
			}
		}

		check_ajax_referer('upvimeo_ajax_nonce', '_wpnonce');

		$validated_form_field = $this->validate();
		if (empty($validated_form_field)) {
			wp_send_json_error($default_error);
		}

		$handler = UpVimeo_File_Chunk::from_current_request(array(
			'input_name' => $this->input_name,
			'tmp_dir' => $this->tmp_dir,
			'chunk_size' => $this->get_chunk_size(),
		));

		if (!$handler || !$handler->create_metadata()) {
			wp_send_json_error($default_error, 403);
		}

		$error = 0;
		$name = sanitize_file_name(wp_unslash($handler->get_file_name()));
		$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));

		$errors = array();
		$errors = array_merge($errors, (array) upvimeo_validate_file_upload_error($error));
		$errors = array_merge($errors, (array) upvimeo_validate_request_upload_max_filesize($this->get_max_file_size(), [$handler->get_file_size()]));
		$errors = array_merge($errors, (array) upvimeo_validate_file_extension($extension));
		$errors = array_filter($errors);
		$errors = array_unique($errors);
		$errors = array_values($errors);

		if (count($errors) > 0) {
			wp_send_json_error(implode(',', $errors));
		}

		wp_send_json(
			[
				'success' => true,
				'data' => [
					'dzchunksize' => $handler->get_chunk_size(),
					'parallelChunkUploads' => apply_filters('upvimeo_file_upload_chunk_parallel', false),
				],
			]
		);
	}

	/**
	 * Upload the files using chunks.
	 */
	public function ajax_upload_chunk()
	{
		$default_error = esc_html__('Something went wrong, please try again.', 'upvimeo-direct-uploads');

		// If the user lacks backend upload permissions (Guest or Subscriber),
		// treat this as a frontend upload request which requires PRO.
		if ( ! current_user_can( 'upload_files' ) && ! current_user_can( 'manage_options' ) ) {
			if ( ! defined('UPVIMEO_IS_PRO') || ! UPVIMEO_IS_PRO ) {
				wp_send_json_error( array( 'message' => 'Unauthorized access. Pro version required for frontend uploads.' ), 403 );
			}
		}

		check_ajax_referer('upvimeo_ajax_nonce', '_wpnonce');

		$validated_form_field = $this->validate();
		if (empty($validated_form_field)) {
			wp_send_json_error($default_error);
		}

		$handler = UpVimeo_File_Chunk::from_current_request(array(
			'input_name' => $this->input_name,
			'tmp_dir' => $this->tmp_dir,
			'chunk_size' => $this->get_chunk_size(),
		));

		if (!$handler || !$handler->load_metadata()) {
			wp_send_json_error($default_error, 403);
		}

		if (!$handler->write()) {
			wp_send_json_error($default_error, 403);
		}

		wp_send_json(['success' => true]);
	}

	/**
	 * Ajax handler for finalizing a chunked upload.
	 */
	public function ajax_upload_finalize()
	{
		$default_error = esc_html__('Something went wrong, please try again.', 'upvimeo-direct-uploads');

		// If the user lacks backend upload permissions (Guest or Subscriber),
		// treat this as a frontend upload request which requires PRO.
		if ( ! current_user_can( 'upload_files' ) && ! current_user_can( 'manage_options' ) ) {
			if ( ! defined('UPVIMEO_IS_PRO') || ! UPVIMEO_IS_PRO ) {
				wp_send_json_error( array( 'message' => 'Unauthorized access. Pro version required for frontend uploads.' ), 403 );
			}
		}

		check_ajax_referer('upvimeo_ajax_nonce', '_wpnonce');

		$handler = UpVimeo_File_Chunk::from_current_request(array(
			'input_name' => $this->input_name,
			'tmp_dir' => $this->tmp_dir,
			'chunk_size' => $this->get_chunk_size(),
		));
		if (!$handler || !$handler->load_metadata()) {
			wp_send_json_error($default_error, 403);
		}

		$file_name = $handler->get_file_name();
		$file_user_name = $handler->get_file_user_name();
		$extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));

		$valid_ext = upvimeo_validate_file_extension($extension);
		if (!empty($valid_ext)) {
			wp_send_json_error($valid_ext, 403);
		}

		$tmp_dir = $this->get_tmp_dir();
		$tmp_name = $this->get_tmp_file_name($extension);
		$tmp_path = wp_normalize_path($tmp_dir . '/' . $tmp_name);

		if (!$handler->finalize($tmp_path)) {
			wp_send_json_error($default_error, 403);
		}

		$is_valid_type = upvimeo_validate_wp_filetype_and_ext($tmp_path, sanitize_file_name($file_name));
		if (false !== $is_valid_type) {
			@wp_delete_file($tmp_path);
			wp_send_json_error($is_valid_type, 403);
		}

		$this->clean_tmp_files();

		wp_send_json_success(
			array(
				'name' => $file_name,
				'file' => pathinfo($tmp_path, PATHINFO_FILENAME) . '.' . pathinfo($tmp_path, PATHINFO_EXTENSION),
				'file_user_name' => $file_user_name,
				'size' => filesize($tmp_path),
			)
		);
	}

	/**
	 * Returns the tmp dir
	 * @return string
	 */
	protected function get_tmp_dir()
	{
		return $this->tmp_dir;
	}

	/**
	 * Maximum chunk for slow connections.
	 *
	 * @return int
	 */
	public function get_slow_connection_chunk_size()
	{

		$max_allowed_size = $this->get_max_allowed_chunk_size(true);
		$max_file_size = $this->get_max_file_size();

		if (is_numeric($max_file_size) && $max_file_size > 0) {
			$value = min($max_allowed_size, $max_file_size);
		} else {
			$value = $max_allowed_size;
		}

		return $value;
	}

	/**
	 * Maximum size for a chunk in file uploads.
	 *
	 * @return int
	 */
	public function get_chunk_size()
	{

		$max_allowed_size = $this->get_max_allowed_chunk_size();
		$max_file_size = $this->get_max_file_size();

		if (is_numeric($max_file_size) && $max_file_size > 0) {
			$value = min($max_allowed_size, $max_file_size);
		} else {
			$value = $max_allowed_size;
		}

		return $value;
	}

	/**
	 * Maximum allowed file size by server constraints
	 *
	 * @param bool $slow
	 *
	 * @return mixed
	 */
	public function get_max_allowed_chunk_size($slow = false)
	{

		if (!$slow) {
			$default_size = apply_filters('upvimeo_file_upload_chunk_size', 2 * 1024 * 1024);
		} else {
			$default_size = 512 * 1024;
		}
		$max_allowed_size = wp_max_upload_size();

		return min($default_size, $max_allowed_size);
	}

	/**
	 * Returns the max file size
	 */
	public function get_max_file_size()
	{
		return $this->max_file_size;
	}

	/**
	 * Validate field meta and check if it is valid.
	 *
	 * @return bool
	 */
	abstract protected function validate();

	/**
	 * Returns the tmp filename
	 *
	 * @param $extension
	 *
	 * @return string
	 */
	abstract protected function get_tmp_file_name($extension);
}
