<?php

declare( strict_types=1 );

namespace StrykeWP\Modules\Backup;

/**
 * Controls the complete backup process: DB export, ZIP archiving, Google Drive upload, logging.
 *
 * Migrated from STRYKE_Backup_Runner. Global constant STRYKE_LOG_TABLE replaced by
 * BackupSettings::LOG_TABLE. STRYKE_Google_API replaced by GoogleDriveClient.
 *
 * @package StrykeWP\Modules\Backup
 */
class BackupRunner {

	private string $temp_dir;
	private string $zip_file_path = '';
	private string $sql_file_path = '';
	private int    $log_id        = 0;
	private string $backup_destination;

	public function __construct() {
		$upload_dir               = wp_upload_dir();
		$this->temp_dir           = $upload_dir['basedir'] . '/stryke-temp-backup/';
		$this->backup_destination = BackupSettings::get_options()['backup_destination'] ?? 'local';

		if ( ! file_exists( $this->temp_dir ) ) {
			wp_mkdir_p( $this->temp_dir );
		}
		$this->protect_directory();
	}

	/**
	 * Ensures the temp directory is not web-accessible.
	 * Creates .htaccess and index.php if missing (idempotent).
	 */
	private function protect_directory(): void {
		$htaccess = $this->temp_dir . '.htaccess';
		if ( ! file_exists( $htaccess ) ) {
			if ( false === file_put_contents( $htaccess, "Order deny,allow\nDeny from all\n" ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
				error_log( 'STRYKE Backup: .htaccess konnte nicht geschrieben werden: ' . $htaccess );
			}
		}

		$index = $this->temp_dir . 'index.php';
		if ( ! file_exists( $index ) ) {
			if ( false === file_put_contents( $index, "<?php\n// Silence is golden.\n" ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
				error_log( 'STRYKE Backup: index.php konnte nicht geschrieben werden: ' . $index );
			}
		}
	}

	// -----------------------------------------------------------------------
	// Entry Point
	// -----------------------------------------------------------------------

	/**
	 * Runs the full backup sequence.
	 *
	 * Supports two destinations:
	 *  - 'gdrive': Creates ZIP, uploads to Google Drive, then removes local file.
	 *  - 'local':  Creates ZIP, keeps it on the server for manual download.
	 *
	 * After success, sends an email notification if enabled.
	 *
	 * @return bool True on success, false on any step failure.
	 */
	public function run(): bool {
		global $wpdb;

		set_time_limit( 0 );
		@ini_set( 'memory_limit', '512M' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors, WordPress.PHP.IniSet

		// Failsafe: clean up temp files even on fatal errors.
		register_shutdown_function( fn() => $this->cleanup() );

		$start_time = current_time( 'mysql' );

		// Create initial "In Progress" log entry.
		$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			BackupSettings::get_table_name(),
			array(
				'timestamp'            => $start_time,
				'filename'             => 'In Progress',
				'filesize'             => 0,
				'status'               => 'In Progress',
				'google_drive_file_id' => null,
			)
		);
		$this->log_id = (int) $wpdb->insert_id;

		$backup_timestamp = gmdate( 'Ymd_His' );

		try {
			// 1. DB export.
			$this->sql_file_path = $this->temp_dir . "db_backup_{$backup_timestamp}.sql";
			if ( ! $this->export_database() ) {
				return $this->log_failure( 'Datenbank-Export fehlgeschlagen.' );
			}

			// 2. ZIP archive.
			$archive_name        = "stryke_backup_{$backup_timestamp}.zip";
			$this->zip_file_path = $this->temp_dir . $archive_name;
			if ( ! $this->create_zip_archive( $archive_name ) ) {
				return $this->log_failure( 'ZIP-Archivierung fehlgeschlagen.' );
			}

			$file_size      = (int) filesize( $this->zip_file_path );
			$gdrive_file_id = null;

			// 3. Destination-specific handling.
			if ( 'gdrive' === $this->backup_destination ) {
				$api            = new GoogleDriveClient();
				$gdrive_file_id = $api->upload_file( $this->zip_file_path, $archive_name );
				if ( ! $gdrive_file_id ) {
					return $this->log_failure( 'Google Drive Upload fehlgeschlagen.' );
				}
			}
			// For 'local' mode the ZIP stays in temp_dir — cleanup() will skip it.

			// 4. Log success.
			$this->log_success( $archive_name, $file_size, $gdrive_file_id );

			return true;
		} finally {
			$this->cleanup();
		}
	}

	/**
	 * Removes temp files left on disk.
	 * SQL dump is always removed. ZIP is only removed when backup_destination
	 * is 'gdrive' (already uploaded) — for 'local' mode the ZIP must stay.
	 * Safe to call multiple times (idempotent).
	 */
	private function cleanup(): void {
		// SQL dump — always remove.
		if ( '' !== $this->sql_file_path && file_exists( $this->sql_file_path ) ) {
			@unlink( $this->sql_file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
		}

		// ZIP — only remove when Google Drive (already uploaded).
		if ( 'gdrive' === $this->backup_destination && '' !== $this->zip_file_path && file_exists( $this->zip_file_path ) ) {
			@unlink( $this->zip_file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
		}
	}

	// -----------------------------------------------------------------------
	// DB Export
	// -----------------------------------------------------------------------

	private function export_database(): bool {
		global $wpdb;

		$handle = fopen( $this->sql_file_path, 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen
		if ( false === $handle ) {
			error_log( 'STRYKE DB Error: SQL-Datei konnte nicht geöffnet werden.' );
			return false;
		}

		$tables = $wpdb->get_results( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . '%' ), ARRAY_N );

		foreach ( $tables as $table_row ) {
			$table = $table_row[0];

			$create_table = $wpdb->get_row( "SHOW CREATE TABLE `{$table}`", ARRAY_N ); // phpcs:ignore WordPress.DB.PreparedSQL
			if ( false === fwrite( $handle, "DROP TABLE IF EXISTS `{$table}`;\n" ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
				fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
				error_log( 'STRYKE DB Error: Schreibfehler bei Tabelle ' . $table );
				return false;
			}
			if ( false === fwrite( $handle, $create_table[1] . ";\n\n" ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
				fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
				error_log( 'STRYKE DB Error: Schreibfehler bei CREATE TABLE ' . $table );
				return false;
			}

			// Chunked export: 1000 rows at a time to prevent memory exhaustion.
			$chunk_size = 1000;
			$offset     = 0;

			while ( true ) {
				$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL
					$wpdb->prepare( "SELECT * FROM `{$table}` LIMIT %d OFFSET %d", $chunk_size, $offset ),
					ARRAY_A
				);

				if ( empty( $rows ) ) {
					break;
				}

				$row_values = array();
				foreach ( $rows as $row ) {
					$escaped = array_map(
						static fn( $v ) => null === $v ? 'NULL' : "'" . esc_sql( $v ) . "'",
						$row
					);
					$row_values[] = '(' . implode( ', ', $escaped ) . ')';
				}

				$insert_sql = "INSERT INTO `{$table}` VALUES \n" . implode( ",\n", $row_values ) . ";\n\n";
				if ( false === fwrite( $handle, $insert_sql ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite
					fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
					error_log( 'STRYKE DB Error: Schreibfehler bei INSERT INTO ' . $table );
					return false;
				}

				$offset += $chunk_size;

				// Free memory between chunks.
				unset( $rows, $row_values );
			}
		}

		fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose
		return true;
	}

	// -----------------------------------------------------------------------
	// ZIP Archiving
	// -----------------------------------------------------------------------

	private function create_zip_archive( string $archive_name ): bool {
		if ( ! class_exists( 'ZipArchive' ) ) {
			error_log( 'STRYKE ZIP Error: ZipArchive Klasse nicht verfügbar.' );
			return false;
		}

		$zip = new \ZipArchive();
		if ( true !== $zip->open( $this->zip_file_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) ) {
			error_log( 'STRYKE ZIP Error: ZIP-Archiv konnte nicht erstellt werden.' );
			return false;
		}

		$root_path = ABSPATH;

		$exclude_patterns = array(
			'wp-content/cache/',
			'wp-content/upgrade/',
			'wp-content/stryke-temp-backup/',
		);

		$add_recursively = function ( string $path ) use ( &$zip, $root_path, $exclude_patterns, &$add_recursively ): void {
			$items = new \FilesystemIterator( $path );
			foreach ( $items as $item ) {
				// Skip symlinks to avoid traversal outside ABSPATH.
				if ( $item->isLink() ) {
					continue;
				}

				$file_path = $item->getRealPath();
				if ( false === $file_path || ! str_starts_with( $file_path, $root_path ) ) {
					continue;
				}

				$local_path = str_replace( $root_path, '', $file_path );

				foreach ( $exclude_patterns as $pattern ) {
					if ( str_starts_with( $local_path, $pattern ) ) {
						continue 2;
					}
				}

				if ( $item->isDir() ) {
					$zip->addEmptyDir( $local_path );
					$add_recursively( $file_path );
				} else {
					if ( ! $zip->addFile( $file_path, $local_path ) ) {
						error_log( 'STRYKE ZIP Warning: Datei konnte nicht hinzugefügt werden: ' . $local_path );
					}
				}
			}
		};

		// Root files.
		foreach ( array( 'wp-config.php', '.htaccess', basename( $this->sql_file_path ) ) as $file ) {
			$full = $root_path . $file;
			if ( file_exists( $full ) ) {
				$zip->addFile( $full, $file );
			}
		}

		// wp-content.
		$wp_content = $root_path . 'wp-content/';
		if ( file_exists( $wp_content ) ) {
			$zip->addEmptyDir( 'wp-content' );
			$add_recursively( $wp_content );
		}

		$zip->close();
		return true;
	}

	// -----------------------------------------------------------------------
	// Logging
	// -----------------------------------------------------------------------

	private function log_success( string $filename, int $filesize, ?string $gdrive_id ): void {
		global $wpdb;
		$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			BackupSettings::get_table_name(),
			array(
				'filename'             => $filename,
				'filesize'             => $filesize,
				'status'               => 'Success',
				'google_drive_file_id' => $gdrive_id,
			),
			array( 'ID' => $this->log_id )
		);
	}

	private function log_failure( string $message ): bool {
		global $wpdb;
		$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			BackupSettings::get_table_name(),
			array(
				'filename'             => 'Failed',
				'filesize'             => 0,
				'status'               => 'Failed',
				'google_drive_file_id' => null,
			),
			array( 'ID' => $this->log_id )
		);
		error_log( 'STRYKE Backup FAILED: ' . $message );
		return false;
	}
}
