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

class CCGAL_Activator {

	const DB_VERSION = '1.10';

	public static function activate() {
		// One-shot migration from the legacy `wpg_` prefix to `ccgal_`. Must run BEFORE
		// create_tables() / create_cache_dir() so the new prefix names operate on data
		// that has already been moved, not on fresh-empty stubs alongside the legacy
		// entries.
		self::migrate_legacy_prefix();

		self::create_tables();
		self::create_cache_dir();

		// Print-shop is a Pro feature → only create the auto-page when Pro is actually active.
		// Free users would otherwise end up with a "Print Order" page that contains an
		// unregistered shortcode and renders as raw text.
		if ( function_exists( 'ccgal_is_pro' ) && ccgal_is_pro() ) {
			self::create_print_page();
		}

		CCGAL_CPT::register_post_type();

		// Schedule the daily cache-cleanup cron once at activation time, not on every page load.
		if ( ! wp_next_scheduled( 'ccgal_cache_cleanup' ) ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'ccgal_cache_cleanup' );
		}

		flush_rewrite_rules();
		update_option( 'ccgal_db_version', self::DB_VERSION, false );

		// One-shot migration: rewrite legacy shortcodes in existing post_content.
		self::migrate_legacy_shortcodes();

		if ( class_exists( 'CCGAL_Welcome' ) ) {
			CCGAL_Welcome::mark_for_redirect();
		}
	}

	/**
	 * One-shot migration of every persisted artifact from the legacy `wpg_` prefix
	 * to the new `ccgal_` prefix (required by wp.org review for 4+-char prefixes).
	 *
	 * Runs only when the legacy `wpg_db_version` option is still present AND the
	 * new `ccgal_db_version` is not yet set — so it is idempotent across activate()
	 * fires and skips fresh installs.
	 *
	 * Migrates: options, post-meta, post types, custom tables, cron hooks, and the
	 * thumbnail cache directory. Browser visitor cookies keep the legacy name
	 * 'wpg_visitor' (intentional — renaming would orphan existing favourites).
	 */
	private static function migrate_legacy_prefix() {
		if ( ! get_option( 'wpg_db_version' ) || get_option( 'ccgal_db_version' ) ) {
			return;
		}

		// Big tables (post_meta on large sites) plus three sequential UPDATEs +
		// up to five RENAME TABLE statements can blow past PHP's default 30s
		// `max_execution_time` on shared hosts. Migration runs once per site —
		// extending the limit is acceptable.
		if ( function_exists( 'set_time_limit' ) ) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- one-shot migration on activation; the 300s window only exists to keep the prefix-rename UPDATEs running through on shared hosts; not used in any hot path.
			@set_time_limit( 300 );
		}

		global $wpdb;

		// 1. Options — rename every option_name LIKE 'wpg\_%' to 'ccgal_…'. Backslash-
		//    escape the `_` so the LIKE matches a literal underscore, not "any char".
		//    CRITICAL: explicitly exclude `wpg_db_version` from the bulk rename so the
		//    guard option (`get_option('wpg_db_version')` at the top of this method)
		//    can still be read on a subsequent activation if anything below crashes.
		//    `wpg_db_version` is deleted explicitly at the END of this method instead.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- one-shot UPDATE on activation; bulk REPLACE() on option_name is the only sane way to do this.
		$wpdb->query( "UPDATE {$wpdb->options} SET option_name = CONCAT('ccgal_', SUBSTRING(option_name, 5)) WHERE option_name LIKE 'wpg\\_%' AND option_name != 'wpg_db_version'" );

		// 2. Post-meta — rename every meta_key LIKE 'wpg\_%' to 'ccgal_…', plus the
		//    leading-underscore convention `_wpg_system_page` → `_ccgal_system_page`.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- one-shot UPDATE on activation; bulk REPLACE() on meta_key is the only sane way to do this.
		$wpdb->query( "UPDATE {$wpdb->postmeta} SET meta_key = CONCAT('ccgal_', SUBSTRING(meta_key, 5)) WHERE meta_key LIKE 'wpg\\_%'" );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- one-shot UPDATE on activation; bulk REPLACE() on meta_key (`_wpg_` variant) is the only sane way to do this.
		$wpdb->query( "UPDATE {$wpdb->postmeta} SET meta_key = CONCAT('_ccgal_', SUBSTRING(meta_key, 6)) WHERE meta_key LIKE '\\_wpg\\_%'" );

		// 3. Custom post type — convert all `wpg_gallery` posts to `ccgal_gallery`.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot UPDATE on activation, value-bound via $wpdb->prepare(); caching makes no sense for a write query.
		$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_type = %s WHERE post_type = %s", 'ccgal_gallery', 'wpg_gallery' ) );

		// 4. Custom tables — RENAME TABLE is atomic in MySQL. Skip silently if the
		//    legacy table is absent (fresh Pro install) or the new one already exists
		//    (re-activation after partial migration).
		$rename = array(
			'wpg_favorites'   => 'ccgal_favorites',
			'wpg_orders'      => 'ccgal_orders',
			'wpg_order_items' => 'ccgal_order_items',
			'wpg_views'       => 'ccgal_views',       // Pro
			'wpg_downloads'   => 'ccgal_downloads',   // Pro
		);
		$rename_failures = array();
		foreach ( $rename as $old => $new ) {
			$old_table = $wpdb->prefix . $old;
			$new_table = $wpdb->prefix . $new;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$old_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $old_table ) ) === $old_table;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$new_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $new_table ) ) === $new_table;
			if ( $old_exists && ! $new_exists ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- RENAME TABLE does not support $wpdb->prepare() placeholders for identifiers; $old_table and $new_table are composed of $wpdb->prefix plus a constant string suffix from the hard-coded $rename map above, no user input involved.
				$result = $wpdb->query( "RENAME TABLE `{$old_table}` TO `{$new_table}`" );
				if ( false === $result ) {
					$rename_failures[] = $old_table;
				}
			}
		}
		if ( ! empty( $rename_failures ) ) {
			// Surface the failure to the site admin instead of silent corruption.
			// Most likely cause is a missing ALTER permission on shared hosting.
			set_transient(
				'ccgal_migration_table_rename_error',
				$rename_failures,
				DAY_IN_SECONDS
			);

			// CRITICAL: keep `wpg_db_version` in place so the migration block runs
			// again on the next activation. Without this early return, the loop
			// below would delete the marker AND create_tables() (called by the
			// caller) would create fresh empty `ccgal_*` tables alongside the
			// orphaned `wpg_*` tables — effectively orphaning the user's favorites
			// and orders data. Returning here means: fix the DB permissions, then
			// reactivate the plugin and the migration retries from scratch.
			return;
		}

		// 5. Cron — clear the legacy `wpg_cache_cleanup` event. The new
		//    `ccgal_cache_cleanup` is scheduled in activate() right after this method.
		$timestamp = wp_next_scheduled( 'wpg_cache_cleanup' );
		if ( $timestamp ) {
			wp_unschedule_event( $timestamp, 'wpg_cache_cleanup' );
		}
		wp_clear_scheduled_hook( 'wpg_cache_cleanup' );

		// 6. Cache directory — rename uploads/wpg-cache/ to uploads/ccgal-cache/.
		$upload_dir = wp_upload_dir();
		$old_cache  = trailingslashit( $upload_dir['basedir'] ) . 'wpg-cache';
		$new_cache  = trailingslashit( $upload_dir['basedir'] ) . 'ccgal-cache';
		if ( is_dir( $old_cache ) && ! is_dir( $new_cache ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- WP_Filesystem cannot be initialised in register_activation_hook (no FTP credentials); rename() is safe inside our own uploads sub-dir.
			@rename( $old_cache, $new_cache );
		}

		// 7. Delete the legacy db-version marker only on full success — the
		//    early return above keeps it in place when RENAME TABLE failed so a
		//    later activation re-runs the migration once permissions are fixed.
		delete_option( 'wpg_db_version' );
	}

	/**
	 * One-time migration that rewrites every legacy shortcode token directly in
	 * post_content to the new `ccgal_*` prefix. Covers the historic German alias
	 * `[hochzeitsgalerie]` / `[hochzeitsgalerie_druck]`, the interim `[ccg_*]`
	 * forms introduced in 1.12.0, and the still-older `[customer_cloud_gallery*]`
	 * tokens that pre-1.12.0 installs may still carry.
	 *
	 * Idempotent — guarded by the `ccgal_shortcode_migration_v2_done` option so it
	 * runs only once even if `activate()` fires on subsequent updates. Operates
	 * on $wpdb->posts only (shortcodes don't live in postmeta).
	 *
	 * Uses MySQL's REPLACE() inside a single UPDATE per pattern so save_post /
	 * revisions hooks are not fired (would be slow on sites with many pages).
	 */
	private static function migrate_legacy_shortcodes() {
		if ( get_option( 'ccgal_shortcode_migration_v2_done' ) ) {
			return;
		}

		global $wpdb;

		$replacements = array(
			// Pre-1.12.0 legacy.
			array( '[customer_cloud_gallery ',        '[ccgal_gallery ',  '%[customer_cloud_gallery %' ),
			array( '[customer_cloud_gallery]',        '[ccgal_gallery]',  '%[customer_cloud_gallery]%' ),
			array( '[/customer_cloud_gallery]',       '[/ccgal_gallery]', '%[/customer_cloud_gallery]%' ),
			array( '[customer_cloud_gallery_print ',  '[ccgal_print ',    '%[customer_cloud_gallery_print %' ),
			array( '[customer_cloud_gallery_print]',  '[ccgal_print]',    '%[customer_cloud_gallery_print]%' ),
			array( '[/customer_cloud_gallery_print]', '[/ccgal_print]',   '%[/customer_cloud_gallery_print]%' ),
			// 1.12.0 interim `ccg_*`.
			array( '[ccg_gallery ',                   '[ccgal_gallery ',  '%[ccg_gallery %' ),
			array( '[ccg_gallery]',                   '[ccgal_gallery]',  '%[ccg_gallery]%' ),
			array( '[/ccg_gallery]',                  '[/ccgal_gallery]', '%[/ccg_gallery]%' ),
			array( '[ccg_print ',                     '[ccgal_print ',    '%[ccg_print %' ),
			array( '[ccg_print]',                     '[ccgal_print]',    '%[ccg_print]%' ),
			array( '[/ccg_print]',                    '[/ccgal_print]',   '%[/ccg_print]%' ),
			// Historic German alias.
			array( '[hochzeitsgalerie ',              '[ccgal_gallery ',  '%[hochzeitsgalerie %' ),
			array( '[hochzeitsgalerie]',              '[ccgal_gallery]',  '%[hochzeitsgalerie]%' ),
			array( '[/hochzeitsgalerie]',             '[/ccgal_gallery]', '%[/hochzeitsgalerie]%' ),
			array( '[hochzeitsgalerie_druck ',        '[ccgal_print ',    '%[hochzeitsgalerie_druck %' ),
			array( '[hochzeitsgalerie_druck]',        '[ccgal_print]',    '%[hochzeitsgalerie_druck]%' ),
			array( '[/hochzeitsgalerie_druck]',       '[/ccgal_print]',   '%[/hochzeitsgalerie_druck]%' ),
		);

		foreach ( $replacements as $pair ) {
			list( $search, $replace, $like ) = $pair;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-shot migration query on activation; not a hot path and there is nothing to cache.
			$wpdb->query( $wpdb->prepare(
				"UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE post_content LIKE %s",
				$search,
				$replace,
				$like
			) );
		}

		update_option( 'ccgal_shortcode_migration_v2_done', true, false );
		// Clean up the v1 marker — no longer needed.
		delete_option( 'ccgal_legacy_shortcodes_migrated' );
	}

	/**
	 * Creates the Print Order page if it does not exist yet.
	 * Idempotent — safe to call multiple times.
	 * Public so Pro classes can call it on first init after a Free→Pro upgrade.
	 */
	public static function ensure_print_page() {
		if ( ! function_exists( 'ccgal_is_pro' ) || ! ccgal_is_pro() ) {
			return;
		}
		self::create_print_page();
	}

	private static function create_tables() {
		global $wpdb;

		$charset_collate = $wpdb->get_charset_collate();

		$favorites = $wpdb->prefix . 'ccgal_favorites';

		$sql = array();

		$sql[] = "CREATE TABLE {$favorites} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			gallery_id BIGINT UNSIGNED NOT NULL,
			file_id VARCHAR(191) NOT NULL,
			cookie_id VARCHAR(64) NOT NULL,
			visitor_label VARCHAR(191) DEFAULT NULL,
			created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
			PRIMARY KEY (id),
			UNIQUE KEY fav_unique (gallery_id, file_id, cookie_id),
			KEY gallery_file (gallery_id, file_id),
			KEY cookie_idx (cookie_id)
		) {$charset_collate};";

		$orders = $wpdb->prefix . 'ccgal_orders';
		$items  = $wpdb->prefix . 'ccgal_order_items';

		$sql[] = "CREATE TABLE {$orders} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			gallery_id BIGINT UNSIGNED NOT NULL,
			visitor_id VARCHAR(64) NOT NULL DEFAULT '',
			customer_name VARCHAR(200) NOT NULL,
			customer_email VARCHAR(200) NOT NULL,
			customer_phone VARCHAR(50) DEFAULT NULL,
			customer_address TEXT NOT NULL,
			customer_message TEXT DEFAULT NULL,
			subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00,
			shipping DECIMAL(10,2) NOT NULL DEFAULT 0.00,
			total DECIMAL(10,2) NOT NULL DEFAULT 0.00,
			payment_method VARCHAR(20) NOT NULL DEFAULT '',
			status VARCHAR(32) NOT NULL DEFAULT 'neu',
			payment_status VARCHAR(32) NOT NULL DEFAULT 'offen',
			admin_notes TEXT DEFAULT NULL,
			email_log TEXT DEFAULT NULL,
			created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
			updated_at DATETIME DEFAULT NULL,
			PRIMARY KEY (id),
			KEY gallery_id (gallery_id),
			KEY status (status),
			KEY payment_status (payment_status),
			KEY created_at (created_at)
		) {$charset_collate};";

		$sql[] = "CREATE TABLE {$items} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			order_id BIGINT UNSIGNED NOT NULL,
			file_id VARCHAR(191) NOT NULL,
			file_name_snapshot VARCHAR(512) DEFAULT NULL,
			format_key VARCHAR(64) NOT NULL,
			format_name_snapshot VARCHAR(200) NOT NULL,
			quantity INT UNSIGNED NOT NULL DEFAULT 1,
			unit_price DECIMAL(10,2) NOT NULL,
			line_total DECIMAL(10,2) NOT NULL,
			paper_key VARCHAR(64) DEFAULT NULL,
			paper_name_snapshot VARCHAR(200) DEFAULT NULL,
			paper_surcharge DECIMAL(10,2) NOT NULL DEFAULT 0.00,
			PRIMARY KEY (id),
			KEY order_id (order_id)
		) {$charset_collate};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		foreach ( $sql as $query ) {
			dbDelta( $query );
		}
	}

	public static function create_print_page() {
		if ( get_option( 'ccgal_print_page_id' ) ) {
			return;
		}
		$page_id = wp_insert_post( array(
			'post_title'   => __( 'Print Order', 'customer-cloud-gallery' ),
			'post_name'    => 'print-order',
			'post_status'  => 'publish',
			'post_type'    => 'page',
			'post_content' => '[ccgal_print]',
			'meta_input'   => array( '_ccgal_system_page' => '1' ),
		) );
		if ( $page_id && ! is_wp_error( $page_id ) ) {
			update_option( 'ccgal_print_page_id', $page_id, false );
		}
	}

	private static function create_cache_dir() {
		$upload_dir = wp_upload_dir();
		$cache_dir  = trailingslashit( $upload_dir['basedir'] ) . 'ccgal-cache';

		if ( ! file_exists( $cache_dir ) ) {
			wp_mkdir_p( $cache_dir );
		}

		$htaccess = $cache_dir . '/.htaccess';
		if ( ! file_exists( $htaccess ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- one-shot activation write of a static security guard inside our own cache dir under wp-content/uploads; WP_Filesystem would require interactive FTP credentials that aren't available during register_activation_hook.
			file_put_contents(
				$htaccess,
				"Options -Indexes\n# Block direct PHP execution in cache\n<FilesMatch \"\\.(php|phtml|php7|phar)$\">\n\tDeny from all\n</FilesMatch>\n"
			);
		}

		$index = $cache_dir . '/index.html';
		if ( ! file_exists( $index ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- same as above: directory-listing guard inside our own cache dir during activation.
			file_put_contents( $index, '' );
		}
	}
}
