<?php
/**
 * Reporter.
 *
 * Handles generation, storage, listing, deletion, and emailing of diagnostic
 * reports. Reports are stored as zip files containing JSON data.
 *
 * @package   Agenistrator
 * @since     1.0.0
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://maximum.software/
 *
 * SPDX-FileCopyrightText: 2025-2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

// Direct access prevention
if ( ! defined( 'ABSPATH' ) ) {
	return;
}

/**
 * Reporter class.
 */
class Reporter implements Maintenance {
	use Singleton;

	/** Report format version */
	private const REPORT_VERSION = '1.0';

	/** Maximum number of events to include in report */
	private const MAX_EVENTS_IN_REPORT = 1000;

	/** Reports directory name under uploads */
	private const REPORTS_DIR = 'agenistrator/reports';

	/** Email address to send reports to */
	private const REPORT_EMAIL = 'agenistrator-reports@maximum.software';

	/** Cached upload directory base path */
	private $upload_basedir = null;

	/** Cached upload directory base URL */
	private $upload_baseurl = null;

	/** Cached reports directory path (null = not initialized, false = error, string = path) */
	private $reports_dir = null;

	/**
	 * Constructor
	 * @since     1.0.0
	 */
	protected function __construct() {
		$this->init_upload_dir();
	}

	/**
	 * Initialize upload directory cache
	 */
	private function init_upload_dir() {
		$upload_dir = wp_upload_dir( null, false );
		if ( empty( $upload_dir['error'] ) ) {
			$this->upload_basedir = $upload_dir['basedir'];
			$this->upload_baseurl = $upload_dir['baseurl'];
		}
	}

	/**
	 * Register hooks
	 * @since     1.0.0
	 */
	public function register_hooks() {
		// No hooks needed
	}

	/**
	 * Plugin activation tasks
	 * @since     1.0.0
	 */
	public function activate_plugin() {
		// No activation tasks needed
	}

	/**
	 * Plugin deactivation tasks
	 * @since     1.0.0
	 */
	public function deactivate_plugin() {
		// No deactivation tasks needed
	}

	/**
	 * Uninstall - remove reports directory
	 * @since     1.0.0
	 */
	public function uninstall_plugin() {
		$this->delete_all_reports( true );
	}

	/**
	 * Get the reports directory path, creating it if needed
	 *
	 * @return string Absolute path to reports directory
	 * @throws \Exception If directory cannot be created
	 * @since     1.0.0
	 */
	public function get_reports_directory() {
		// Return cached result if available
		if ( null !== $this->reports_dir ) {
			return $this->reports_dir;
		}

		// Check if upload dir is available
		if ( null === $this->upload_basedir ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Upload directory is not available', 'agenistrator' ) );
		}

		$reports_dir = path_join( $this->upload_basedir, self::REPORTS_DIR );

		// Create directory if it doesn't exist
		if ( ! file_exists( $reports_dir ) ) {
			if ( ! wp_mkdir_p( $reports_dir ) ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
				throw new \Exception( __( 'Failed to create reports directory', 'agenistrator' ) );
			}
		}

		// Ensure index.php exists to prevent directory listing
		$index_file = path_join( $reports_dir, 'index.php' );
		if ( ! file_exists( $index_file ) ) {
			WordPress_Utils::get_wp_filesystem()->put_contents( $index_file, '<?php // Silence is golden', FS_CHMOD_FILE );
		}

		$this->reports_dir = $reports_dir;
		return $reports_dir;
	}

	/**
	 * Generate a problem report
	 *
	 * @param int    $user_id     The user ID
	 * @param int    $thread_id   The thread ID
	 * @param string $description User's description of the issue
	 * @return string The report ID (UUID)
	 * @throws \Exception If report generation fails
	 * @since     1.0.0
	 */
	public function generate_report( $user_id, $thread_id, $description = '' ) {
		$reports_dir = $this->get_reports_directory();
		$report_id   = wp_generate_uuid4();
		$filename    = $this->get_filename_for_report_id( $report_id );
		$final_path  = path_join( $reports_dir, $filename );

		// Create zip in temp directory first, then move via WP_Filesystem
		$temp_path = path_join( get_temp_dir(), $filename );

		$report_data = $this->get_report_data( $user_id, $thread_id, $description );

		$zip    = new \ZipArchive();
		$result = $zip->open( $temp_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE );
		if ( true !== $result ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Failed to create zip file', 'agenistrator' ) );
		}

		$json = wp_json_encode( $report_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
		$zip->addFromString( 'report.json', $json );
		$zip->close();

		// Move from temp to final location using WP_Filesystem
		$wp_filesystem = WordPress_Utils::get_wp_filesystem();
		if ( ! $wp_filesystem->move( $temp_path, $final_path, true ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Failed to move zip file to reports directory', 'agenistrator' ) );
		}

		if ( ! $wp_filesystem->exists( $final_path ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Failed to write zip file', 'agenistrator' ) );
		}

		return $report_id;
	}

	/**
	 * Collect all data for the report
	 *
	 * @param int    $user_id     The user ID
	 * @param int    $thread_id   The thread ID
	 * @param string $description User's description of the issue
	 * @return array Report data structure
	 */
	private function get_report_data( $user_id, $thread_id, $description = '' ) {
		$user = get_userdata( $user_id );

		// Get chat log
		$chat_log_manager = Chat_Log_Manager::get_instance();
		$messages         = $chat_log_manager->get_chat_log( $thread_id );
		$messages_array   = array_map(
			function ( $msg ) {
				return $msg->to_array();
			},
			$messages
		);

		return array(
			'report_version'      => self::REPORT_VERSION,
			'generated_at'        => gmdate( 'c' ),
			'issue_description'   => $description,
			'reporter'            => array(
				'user_id'      => $user_id,
				'email'        => $user ? $user->user_email : '',
				'display_name' => $user ? $user->display_name : '',
			),
			'site'                => array(
				'name'         => get_bloginfo( 'name' ),
				'url'          => home_url(),
				'admin_email'  => get_option( 'admin_email' ),
				'is_multisite' => is_multisite(),
			),
			'agenistrator_config' => $this->get_agenistrator_config(),
			'agenistrator_stats'  => $this->get_agenistrator_stats(),
			'agenistrator_events' => $this->get_agenistrator_events(),
			'wordpress_stats'     => $this->get_wordpress_stats(),
			'error_log'           => $this->get_error_log_data(),
			'action_scheduler'    => WordPress_Utils::get_action_scheduler_status(),
			'site_health'         => $this->get_site_health_data(),
			'chat_log'            => array(
				'thread_id'     => $thread_id,
				'message_count' => count( $messages_array ),
				'messages'      => $messages_array,
			),
		);
	}

	/**
	 * Get Agenistrator plugin configuration (sanitized - no API keys)
	 *
	 * @return array Sanitized plugin configuration
	 */
	private function get_agenistrator_config() {
		$provider_manager = Provider_Manager::get_instance();
		$llm_manager      = LLM_Manager::get_instance();
		$tool_manager     = Tool_Manager::get_instance();
		$settings         = Settings::get_instance();

		// Get sanitized provider config (include only public parameters)
		$providers_config = array();
		try {
			$all_providers = $provider_manager->get_all_providers();
			foreach ( $all_providers as $provider_id => $provider ) {
				$providers_config[ $provider_id ] = array(
					'type'         => $provider->get_type_id(),
					'display_name' => $provider->get_display_name(),
					'enabled'      => $provider_manager->is_provider_enabled( $provider_id ),
					'parameters'   => $this->get_public_provider_params( $provider ),
				);
			}
		} catch ( \Throwable $e ) {
			$providers_config = array( 'error' => $e->getMessage() );
		}

		// Get tool settings
		$tool_settings = array();
		try {
			$tool_settings = $tool_manager->get_tool_settings();
		} catch ( \Throwable $e ) {
			$tool_settings = array( 'error' => $e->getMessage() );
		}

		return array(
			'providers'     => $providers_config,
			'llm_settings'  => array(
				'chat_model'                  => $llm_manager->get_chat_model_id(),
				'embeddings_model'            => $llm_manager->get_embeddings_model_id(),
				'chat_model_configured'       => $llm_manager->is_chat_model_configured(),
				'embeddings_model_configured' => $llm_manager->is_embeddings_model_configured(),
				'default_context_window'      => $settings->get_config( 'default_context_window', Model_Info::DEFAULT_CONTEXT_WINDOW ),
				'default_max_output_tokens'   => $settings->get_config( 'default_max_output_tokens', Model_Info::DEFAULT_MAX_OUTPUT ),
			),
			'tool_settings' => $tool_settings,
		);
	}

	/**
	 * Get public (non-private) parameters for a provider
	 *
	 * Filters out parameters marked as private or with type 'password'.
	 *
	 * @param Provider_Type $provider The provider instance
	 * @return array Public parameters with their values
	 */
	private function get_public_provider_params( $provider ) {
		$definitions   = $provider->get_parameter_definitions();
		$values        = $provider->get_params();
		$public_params = array();

		foreach ( $definitions as $param_name => $definition ) {
			// Skip password fields (always private)
			if ( isset( $definition['type'] ) && 'password' === $definition['type'] ) {
				continue;
			}

			// Skip explicitly private fields
			if ( ! empty( $definition['private'] ) ) {
				continue;
			}

			// Include the parameter value if it exists
			if ( array_key_exists( $param_name, $values ) ) {
				$public_params[ $param_name ] = $values[ $param_name ];
			}
		}

		return $public_params;
	}

	/**
	 * Get Agenistrator statistics (embeddings, cron, progress)
	 *
	 * @return array Agenistrator statistics
	 */
	private function get_agenistrator_stats() {
		$embeddings_manager = Embeddings_Manager::get_instance();

		$stats = array(
			'embeddings_count'            => 0,
			'current_embedding_model'     => '',
			'current_embedding_dimension' => 0,
			'search_operational'          => false,
			'scheduled_cron_events'       => array(),
			'progress_trackers'           => array(),
		);

		try {
			$stats['embeddings_count']            = $embeddings_manager->get_embeddings_count();
			$stats['current_embedding_model']     = $embeddings_manager->get_config( 'current_embedding_model', '' );
			$stats['current_embedding_dimension'] = (int) $embeddings_manager->get_config( 'current_embedding_dimension', 0 );
			$stats['search_operational']          = $embeddings_manager->is_search_operational();

			// Get all running progress trackers
			$running_trackers = Progress_Tracker::get_running();
			foreach ( $running_trackers as $tracker ) {
				$stats['progress_trackers'][] = array(
					'operation'        => $tracker->get_operation(),
					'instance_id'      => $tracker->get_instance_id(),
					'status'           => $tracker->get_status(),
					'current'          => $tracker->get_current(),
					'total'            => $tracker->get_total(),
					'percent_complete' => $tracker->get_percent_complete(),
				);
			}

			// Get scheduled Agenistrator cron events
			$agenistrator_cron_hooks = array(
				'agenistrator_hourly_cleanup',
				'agenistrator_sync_content_embeddings_batch',
				'agenistrator_cleanup_orphan_embeddings_batch',
				'agenistrator_sync_post_embeddings',
			);
			foreach ( $agenistrator_cron_hooks as $hook ) {
				$next_run = wp_next_scheduled( $hook );
				if ( $next_run ) {
					$stats['scheduled_cron_events'][ $hook ] = gmdate( 'c', $next_run );
				}
			}
		} catch ( \Throwable $e ) {
			$stats['error'] = $e->getMessage();
		}

		return $stats;
	}

	/**
	 * Get recent events from the event queue
	 *
	 * @return array Recent events
	 */
	private function get_agenistrator_events() {
		global $wpdb;

		try {
			$table_name = $wpdb->prefix . 'agenistrator_events';

			// Get recent events directly (all types)
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; table name is hardcoded; caching not applicable.
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is hardcoded
					"SELECT * FROM {$table_name}
					ORDER BY id DESC
					LIMIT %d",
					self::MAX_EVENTS_IN_REPORT
				)
			);

			if ( ! $rows ) {
				return array();
			}

			$events = array();
			foreach ( $rows as $row ) {
				$events[] = array(
					'id'         => (int) $row->id,
					'user_id'    => null === $row->user_id ? null : (int) $row->user_id,
					'thread_id'  => $row->thread_id,
					'event_type' => $row->event_type,
					'data'       => json_decode( $row->event_data, true ),
					'created_at' => (int) $row->created_at,
				);
			}

			return $events;
		} catch ( \Throwable $e ) {
			return array( 'error' => $e->getMessage() );
		}
	}

	/**
	 * Get WordPress instance statistics
	 *
	 * @return array WordPress statistics
	 */
	private function get_wordpress_stats() {
		// Get post counts by type
		$post_types_objects = get_post_types( array(), 'objects' );
		$post_counts        = array();
		$post_types_info    = array();

		foreach ( $post_types_objects as $post_type ) {
			$counts                          = wp_count_posts( $post_type->name );
			$post_counts[ $post_type->name ] = array(
				'publish' => (int) ( $counts->publish ?? 0 ),
				'draft'   => (int) ( $counts->draft ?? 0 ),
				'total'   => array_sum( (array) $counts ),
			);

			$post_types_info[] = array(
				'name'         => $post_type->name,
				'label'        => $post_type->label,
				'public'       => $post_type->public,
				'hierarchical' => $post_type->hierarchical,
				'show_ui'      => $post_type->show_ui,
				'builtin'      => $post_type->_builtin,
			);
		}

		// Get user counts
		$user_counts = count_users();

		// Get comment counts
		$comment_counts = wp_count_comments();

		// Get media library statistics
		$media_stats = $this->get_media_stats();

		// Get taxonomy statistics
		$taxonomy_stats = $this->get_taxonomy_stats();

		// Get menu statistics
		$menu_stats = $this->get_menu_stats();

		return array(
			'post_types' => $post_types_info,
			'posts'      => $post_counts,
			'users'      => array(
				'total'   => $user_counts['total_users'],
				'by_role' => $user_counts['avail_roles'],
			),
			'comments'   => array(
				'total'    => (int) $comment_counts->total_comments,
				'approved' => (int) $comment_counts->approved,
				'pending'  => (int) $comment_counts->moderated,
				'spam'     => (int) $comment_counts->spam,
			),
			'media'      => $media_stats,
			'taxonomies' => $taxonomy_stats,
			'menus'      => $menu_stats,
		);
	}

	/**
	 * Get media library statistics
	 *
	 * @return array Media statistics by mime type
	 */
	private function get_media_stats() {
		$counts = wp_count_attachments();

		$by_type = array();
		$total   = 0;

		foreach ( (array) $counts as $mime_type => $count ) {
			if ( 'trash' === $mime_type ) {
				continue;
			}
			$by_type[ $mime_type ] = (int) $count;
			$total                += (int) $count;
		}

		arsort( $by_type );

		return array(
			'total'   => $total,
			'by_type' => $by_type,
		);
	}

	/**
	 * Get taxonomy statistics
	 *
	 * @return array Taxonomy statistics with term counts
	 */
	private function get_taxonomy_stats() {
		$taxonomies = get_taxonomies( array(), 'objects' );
		$stats      = array();

		foreach ( $taxonomies as $taxonomy ) {
			$term_count = wp_count_terms(
				array(
					'taxonomy'   => $taxonomy->name,
					'hide_empty' => false,
				)
			);

			$stats[] = array(
				'name'         => $taxonomy->name,
				'label'        => $taxonomy->label,
				'public'       => $taxonomy->public,
				'hierarchical' => $taxonomy->hierarchical,
				'builtin'      => $taxonomy->_builtin,
				'object_types' => $taxonomy->object_type,
				'term_count'   => is_wp_error( $term_count ) ? 0 : (int) $term_count,
			);
		}

		return $stats;
	}

	/**
	 * Get menu statistics
	 *
	 * @return array Menu statistics
	 */
	private function get_menu_stats() {
		$menus     = wp_get_nav_menus();
		$menu_list = array();

		foreach ( $menus as $menu ) {
			$menu_list[] = array(
				'id'         => $menu->term_id,
				'name'       => $menu->name,
				'item_count' => $menu->count,
			);
		}

		$locations          = get_registered_nav_menus();
		$assigned_locations = get_nav_menu_locations();

		return array(
			'total'                => count( $menus ),
			'menus'                => $menu_list,
			'registered_locations' => count( $locations ),
			'assigned_locations'   => count( array_filter( $assigned_locations ) ),
		);
	}

	/**
	 * Get error log data
	 *
	 * @return array Error log information
	 */
	private function get_error_log_data() {
		$data = array(
			'debug_enabled'     => defined( 'WP_DEBUG' ) && WP_DEBUG,
			'debug_log_enabled' => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
			'debug_display'     => defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY,
			'log_files'         => array(),
		);

		$log_files = WordPress_Utils::discover_log_files();

		foreach ( $log_files as $log_path ) {
			if ( ! file_exists( $log_path ) || ! is_readable( $log_path ) ) {
				continue;
			}

			$data['log_files'][] = array(
				'path'                => $log_path,
				'size'                => filesize( $log_path ),
				'modified'            => gmdate( 'Y-m-d H:i:s e', filemtime( $log_path ) ),
				'agenistrator_errors' => $this->extract_agenistrator_log_lines( $log_path ),
			);
		}

		return $data;
	}

	/**
	 * Extract log lines containing 'Agenistrator' (case-insensitive)
	 *
	 * Reads the log file from the end and returns up to 1000 matching lines
	 * in chronological order (oldest first).
	 *
	 * @param string $log_path Path to log file
	 * @return array Array of matching log lines (raw strings)
	 */
	private function extract_agenistrator_log_lines( $log_path ) {
		$max_lines = 1000;
		$matches   = array();

		try {
			$file = new \SplFileObject( $log_path, 'r' );
			$file->setFlags( \SplFileObject::DROP_NEW_LINE );
			$file->seek( PHP_INT_MAX );
			$total_lines = $file->key();

			$match_count = 0;
			for ( $i = $total_lines; $i >= 0 && $match_count < $max_lines; $i-- ) {
				$file->seek( $i );
				$line = $file->current();

				if ( '' !== $line && stripos( $line, 'agenistrator' ) !== false ) {
					$matches[] = $line;
					++$match_count;
				}
			}
		} catch ( \Throwable $e ) {
			return array();
		}

		return array_reverse( $matches );
	}

	/**
	 * Get WordPress Site Health debug data
	 *
	 * @return array Site health data
	 */
	private function get_site_health_data() {
		// Load the WP_Debug_Data class if not already loaded
		if ( ! class_exists( 'WP_Debug_Data' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-debug-data.php';
		}

		try {
			$debug_data = \WP_Debug_Data::debug_data();

			// Extract only the fields (values), not the full structure with labels
			$simplified = array();
			foreach ( $debug_data as $section_key => $section ) {
				if ( ! isset( $section['fields'] ) ) {
					continue;
				}

				$simplified[ $section_key ] = array();
				foreach ( $section['fields'] as $field_key => $field ) {
					// Skip private fields
					if ( ! empty( $field['private'] ) ) {
						continue;
					}

					// Get the value, preferring 'debug' over 'value'
					$value                                    = $field['debug'] ?? $field['value'] ?? null;
					$simplified[ $section_key ][ $field_key ] = $value;
				}
			}

			return $simplified;
		} catch ( \Throwable $e ) {
			return array( 'error' => $e->getMessage() );
		}
	}

	/**
	 * List all reports
	 *
	 * @return array Array of report info: report_id, size, created timestamp
	 * @since     1.0.0
	 */
	public function list_reports() {
		// Check if reports directory exists without creating it
		if ( null === $this->upload_basedir ) {
			return array();
		}

		$reports_dir = path_join( $this->upload_basedir, self::REPORTS_DIR );
		if ( ! is_dir( $reports_dir ) ) {
			return array();
		}

		$reports = array();
		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Intentional: graceful fallback if directory is unreadable
		$files = @scandir( $reports_dir );
		if ( false === $files ) {
			return array();
		}

		$files = array_diff( $files, array( '.', '..', 'index.php' ) );
		foreach ( $files as $file ) {
			$report_id = $this->extract_report_id_from_filename( $file );
			if ( ! $report_id ) {
				continue;
			}

			$filepath = path_join( $reports_dir, $file );
			if ( ! is_file( $filepath ) ) {
				continue;
			}

			$reports[] = array(
				'report_id' => $report_id,
				'size'      => filesize( $filepath ),
				'created'   => filemtime( $filepath ),
			);
		}

		// Sort by created time descending (newest first)
		usort(
			$reports,
			function ( $a, $b ) {
				return $b['created'] - $a['created'];
			}
		);

		return $reports;
	}

	/** Filename prefix for report files */
	private const REPORT_PREFIX = 'report-';

	/** Filename suffix for report files */
	private const REPORT_SUFFIX = '.zip';

	/**
	 * Get filename for a report ID
	 *
	 * @param string $report_id The report UUID
	 * @return string The filename
	 */
	private function get_filename_for_report_id( $report_id ) {
		return self::REPORT_PREFIX . strtolower( $report_id ) . self::REPORT_SUFFIX;
	}

	/**
	 * Extract report ID from filename
	 *
	 * @param string $filename The report filename
	 * @return string|false The report ID or false if invalid
	 */
	private function extract_report_id_from_filename( $filename ) {
		$prefix_len = strlen( self::REPORT_PREFIX );
		$suffix_len = strlen( self::REPORT_SUFFIX );

		if ( strncmp( $filename, self::REPORT_PREFIX, $prefix_len ) !== 0 ) {
			return false;
		}
		if ( substr( $filename, -$suffix_len ) !== self::REPORT_SUFFIX ) {
			return false;
		}

		$potential_id = substr( $filename, $prefix_len, -$suffix_len );

		return wp_is_uuid( $potential_id ) ? strtolower( $potential_id ) : false;
	}

	/**
	 * Get report file path
	 *
	 * @param string $report_id The report UUID
	 * @return string File path
	 * @throws \Exception If report ID is invalid or report not found
	 * @since     1.0.0
	 */
	public function get_report_filepath( $report_id ) {
		if ( ! wp_is_uuid( $report_id ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Invalid report ID', 'agenistrator' ) );
		}

		$reports_dir = $this->get_reports_directory();
		$filename    = $this->get_filename_for_report_id( $report_id );
		$filepath    = path_join( $reports_dir, $filename );

		if ( ! file_exists( $filepath ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Report not found', 'agenistrator' ) );
		}

		return $filepath;
	}

	/**
	 * Delete a report
	 *
	 * @param string $report_id The report UUID
	 * @throws \Exception If report cannot be deleted
	 * @since     1.0.0
	 */
	public function delete_report( $report_id ) {
		$filepath = $this->get_report_filepath( $report_id );
		wp_delete_file( $filepath );
	}

	/**
	 * Delete all report files
	 *
	 * @param bool $remove_directory Whether to also remove the reports directory (for uninstall)
	 * @since     1.0.0
	 */
	public function delete_all_reports( $remove_directory = false ) {
		// Nothing to do if upload directory is not available
		if ( null === $this->upload_basedir ) {
			return;
		}

		$reports_dir = path_join( $this->upload_basedir, self::REPORTS_DIR );
		if ( ! is_dir( $reports_dir ) ) {
			return;
		}

		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Intentional: graceful fallback if directory is unreadable
		$files = @scandir( $reports_dir );
		if ( false !== $files ) {
			$wp_filesystem = WordPress_Utils::get_wp_filesystem();
			$files         = array_diff( $files, array( '.', '..', 'index.php' ) );
			foreach ( $files as $file ) {
				$filepath = path_join( $reports_dir, $file );
				if ( is_file( $filepath ) ) {
					$wp_filesystem->delete( $filepath );
				}
			}
		}

		if ( $remove_directory ) {
			$wp_filesystem = WordPress_Utils::get_wp_filesystem();
			// Remove index.php before removing directory
			$index_file = path_join( $reports_dir, 'index.php' );
			if ( file_exists( $index_file ) ) {
				$wp_filesystem->delete( $index_file );
			}
			$wp_filesystem->rmdir( $reports_dir );
			// Reset cached directory path
			$this->reports_dir = null;
		}
	}

	/**
	 * Send a report via email
	 *
	 * @param string $report_id The report UUID
	 * @throws \Exception If report cannot be sent
	 * @since     1.0.0
	 */
	public function send_report( $report_id ) {
		$filepath    = $this->get_report_filepath( $report_id );
		$filename    = $this->get_filename_for_report_id( $report_id );
		$site_domain = wp_parse_url( home_url(), PHP_URL_HOST );
		$subject     = "[Agenistrator] Report from {$site_domain}";

		$site_name   = get_bloginfo( 'name' );
		$admin_email = get_option( 'admin_email' );
		$report_url  = $this->get_report_url( $report_id );

		$message = String_Utils::render(
			'report-email',
			array(
				'site_name'   => $site_name,
				'site_domain' => $site_domain,
				'report_url'  => $report_url,
				'filename'    => $filename,
				'admin_email' => $admin_email,
			)
		);

		$headers = array(
			'Content-Type: text/html; charset=UTF-8',
			"From: {$site_name} <{$admin_email}>",
			"Reply-To: {$admin_email}",
		);

		$attachments = array( $filepath );

		$result = wp_mail( self::REPORT_EMAIL, $subject, $message, $headers, $attachments );
		if ( ! $result ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Failed to send report email', 'agenistrator' ) );
		}
	}

	/**
	 * Get download URL for a report
	 *
	 * @param string $report_id The report UUID
	 * @return string Download URL
	 * @throws \Exception If report ID is invalid or upload URL unavailable
	 * @since     1.0.0
	 */
	public function get_report_url( $report_id ) {
		if ( ! wp_is_uuid( $report_id ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Invalid report ID', 'agenistrator' ) );
		}

		if ( null === $this->upload_baseurl ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Upload URL is not available', 'agenistrator' ) );
		}

		$filename = $this->get_filename_for_report_id( $report_id );
		return trailingslashit( $this->upload_baseurl ) . trailingslashit( self::REPORTS_DIR ) . $filename;
	}
}
