<?php

namespace Simple_History\Services;

use Simple_History\Event_Details\Event_Details_Group;
use Simple_History\Event_Details\Event_Details_Item;
use Simple_History\Event_Details\Event_Details_Item_RAW_Formatter;
use Simple_History\Helpers;
use Simple_History\Loggers\Plugin_Logger;
use Simple_History\Log_Initiators;
use Simple_History\Services\Auto_Backfill_Service;

/**
 * Setup database and upgrade it if needed.
 */
class Setup_Database extends Service {
	/** @var bool Whether this is a fresh install (set in step 1, read in later steps). */
	private $is_fresh_install = false;

	/**
	 * @inheritdoc
	 */
	public function loaded() {
		// Run at prio 5 so it's run before the loggers etc. are setup.
		add_action( 'after_setup_theme', array( $this, 'run_setup_steps' ), 5 );

		add_filter( 'simple_history/row_details_output', [ $this, 'add_row_details_output' ], 10, 2 );
	}

	/**
	 * Check if plugin version have changed, i.e. has been upgraded
	 * If upgrade is detected then maybe modify database and so on for that version
	 */
	public function run_setup_steps() {
		$this->setup_new_to_version_1();
		$this->setup_version_1_to_version_2();
		$this->setup_version_2_to_version_3();
		$this->setup_version_3_to_version_4();
		$this->setup_version_4_to_version_5();
		$this->setup_version_5_to_version_6();
		$this->setup_version_6_to_version_7();
		$this->setup_version_7_to_version_8();
		$this->setup_version_8_to_version_9();
		$this->setup_version_9_to_version_10();
	}

	/**
	 * Recreate missing database tables.
	 *
	 * This is called when a query or insert fails due to missing tables.
	 * It resets the db_version to 0 and runs all setup steps to recreate tables.
	 *
	 * This handles scenarios like:
	 * - Site duplication where tables weren't copied
	 * - MU plugin with orphaned db_version option
	 * - Multisite network activation issues
	 *
	 * @return bool True if tables were recreated, false if already attempted.
	 */
	public static function recreate_tables_if_missing() {
		// Prevent infinite recursion - only try once per request.
		static $already_attempted = false;

		if ( $already_attempted ) {
			return false;
		}

		$already_attempted = true;

		// Reset db_version to 0 to trigger fresh table creation.
		delete_option( 'simple_history_db_version' );

		// Get the Setup_Database service instance and run setup steps.
		$simple_history = \Simple_History\Simple_History::get_instance();
		$setup_service  = $simple_history->get_service( self::class );

		if ( $setup_service instanceof self ) {
			$setup_service->run_setup_steps();

			// Log recovery for debugging purposes.
			if ( WP_DEBUG && WP_DEBUG_LOG ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				error_log( 'Simple History: Auto-recreated missing database tables' );
			}

			return true;
		}

		return false;
	}

	/**
	 * Check if a database error indicates missing tables.
	 *
	 * @param string $error_message The database error message.
	 * @return bool True if error indicates table doesn't exist.
	 */
	public static function is_table_missing_error( $error_message ) {
		// MySQL/MariaDB error for missing table.
		return stripos( $error_message, "doesn't exist" ) !== false
			|| stripos( $error_message, 'does not exist' ) !== false;
	}

	/**
	 * Get the current database version.
	 *
	 * @return int The database version. Version 0 = first install or version earlier than 0.4.
	 */
	private function get_db_version() {
		return (int) get_option( 'simple_history_db_version', false );
	}

	/**
	 * Update the database version to a new version.
	 * This is done by updating the option simple_history_db_version.
	 *
	 * @param int $new_version The new version to set.
	 */
	private function update_db_to_version( $new_version ) {
		update_option( 'simple_history_db_version', $new_version, true );
	}

	/**
	 * If no db_version is set then this
	 * is a version of Simple History < 0.4
	 * or it's a first install
	 * Fix database not using UTF-8.
	 */
	private function setup_new_to_version_1() {
		$db_version = $this->get_db_version();

		// Run step only if previous step was step before this one.
		if ( $db_version !== 0 ) {
			return;
		}

		global $wpdb;
		$table_name = $this->simple_history->get_events_table_name();

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$charset_collate = $wpdb->get_charset_collate();

		// Table creation, used to be in register_activation_hook
		// We change the varchar size to add one num just to force update of encoding. dbdelta didn't see it otherwise.
		$sql =
			'CREATE TABLE ' .
			$table_name .
			" (
			id bigint(20) NOT NULL AUTO_INCREMENT,
			date datetime NOT NULL,
			PRIMARY KEY  (id)
		) {$charset_collate};";

		// Upgrade db / fix utf for varchars.
		dbDelta( $sql );

		$this->update_db_to_version( 1 );

		// Flag auto-backfill to run on next admin page load.
		// This populates the history with existing WordPress data (posts, pages, users)
		// so users don't start with an empty log.
		Auto_Backfill_Service::set_backfill_pending();

		// Flag this as a fresh install so later migration steps can detect it.
		$this->is_fresh_install = true;

		// Show a welcome admin notice on the next admin page load.
		// Only set pending if the option doesn't exist yet (true first install, not table recovery).
		if ( get_option( Welcome_Message_Service::OPTION_NAME ) !== false ) {
			return;
		}

		Welcome_Message_Service::set_pending();
	}

	/**
	 * If db version is 1 then upgrade to 2
	 * Version 2 added the 'action_description' column,
	 * but it's not used any more so don't do it.
	 */
	private function setup_version_1_to_version_2() {
		$db_version = $this->get_db_version();

		// Run step only if previous step was step before this one.
		if ( $db_version !== 1 ) {
			return;
		}

		// Check that all options we use are set to their defaults, if they miss value
		// Each option that is missing a value will make a sql call otherwise = unnecessary.
		$arr_options = array(
			array(
				'name'          => 'simple_history_show_as_page',
				'default_value' => 1,
			),
			array(
				'name'          => 'simple_history_show_on_dashboard',
				'default_value' => 1,
			),
		);

		foreach ( $arr_options as $one_option ) {
			$option_value = get_option( $one_option['name'] );
			if ( $option_value !== false ) {
				continue;
			}

			// Value is not set in db, so set it to a default.
			update_option( $one_option['name'], $one_option['default_value'], true );
		}

		$this->update_db_to_version( 2 );
	}

	/**
	 * If db_version is 2 then upgrade to 3:
	 * - Add some fields to existing table wp_simple_history_contexts
	 * - Add all new table wp_simple_history_contexts
	 *
	 * @since 2.0
	 */
	private function setup_version_2_to_version_3() {
		$db_version = $this->get_db_version();

		// Run step only if previous step was step before this one.
		if ( $db_version !== 2 ) {
			return;
		}

		global $wpdb;
		$table_name          = $this->simple_history->get_events_table_name();
		$table_name_contexts = $this->simple_history->get_contexts_table_name();

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$charset_collate = $wpdb->get_charset_collate();

		// WordPress uses utf8mb4 since 4.2 (4 bytes/char). InnoDB has a 767-byte
		// index limit on older row formats, so prefix-index any varchar(255)
		// column we index. floor(767 / 4) = 191. Matches WP core's pattern.
		$max_index_length = 191;

		// Update old table.
		$sql = "
			CREATE TABLE {$table_name} (
				id bigint(20) NOT NULL AUTO_INCREMENT,
				date datetime NOT NULL,
				logger varchar(30) DEFAULT NULL,
				level varchar(20) DEFAULT NULL,
				message varchar(255) DEFAULT NULL,
				occasionsID varchar(32) DEFAULT NULL,
				initiator varchar(16) DEFAULT NULL,
				PRIMARY KEY  (id),
				KEY date (date),
				KEY loggerdate (logger,date)
			) {$charset_collate};";

		dbDelta( $sql );

		// Add context table.
		$sql = "
			CREATE TABLE IF NOT EXISTS {$table_name_contexts} (
				context_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
				history_id bigint(20) unsigned NOT NULL,
				`key` varchar(255) DEFAULT NULL,
				value longtext,
				PRIMARY KEY  (context_id),
				KEY history_id (history_id),
				KEY `key` (`key`({$max_index_length}))
			) {$charset_collate};
		";

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( $sql );

		// Update possible old items to use SimpleLogger.
		$sql = sprintf(
			'
				UPDATE %1$s
				SET
					logger = \'SimpleLogger\',
					level = \'info\'
				WHERE logger IS NULL
			',
			$table_name
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( $sql );

		$this->update_db_to_version( 3 );

		// Say welcome, however loggers are not added this early so we need to
		// use a filter to load it later.
		add_action( 'simple_history/loggers_loaded', array( $this, 'add_welcome_log_messages' ) );
	}

	/**
	 * If db version = 3
	 * then we need to update database to allow null values for some old columns
	 * that used to work in pre wp 4.1 beta, but since 4.1 wp uses STRICT_ALL_TABLES
	 * WordPress Commit: https://github.com/WordPress/WordPress/commit/f17d168a0f72211a9bfd9d3fa680713069871bb6
	 *
	 * @since 2.0
	 */
	private function setup_version_3_to_version_4() {

		$db_version = $this->get_db_version();

		// Run step only if previous step was step before this one.
		if ( $db_version !== 3 ) {
			return;
		}

		global $wpdb;

		$table_name = $this->simple_history->get_events_table_name();

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		// If old columns exist = this is an old install, then modify the columns so we still can keep them
		// we want to keep them because user may have logged items that they want to keep.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$db_cols = $wpdb->get_col( "DESCRIBE $table_name" );

		if ( in_array( 'action', $db_cols, true ) ) {
			$sql = sprintf(
				'
                        ALTER TABLE %1$s
                        MODIFY `action` varchar(255) NULL,
                        MODIFY `object_type` varchar(255) NULL,
                        MODIFY `object_subtype` varchar(255) NULL,
                        MODIFY `user_id` int(10) NULL,
                        MODIFY `object_id` int(10) NULL,
                        MODIFY `object_name` varchar(255) NULL
                    ',
				$table_name
			);
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->query( $sql );
		}

		$this->update_db_to_version( 4 );
	}

	/**
	 * Uppdate from db version 4 to version 5.
	 *
	 * Set default values for simple_history_detective_mode_enabled and simple_history_experimental_features_enabled
	 * so no additional SQL queries are needed.
	 */
	private function setup_version_4_to_version_5() {
		if ( $this->get_db_version() !== 4 ) {
			return;
		}

		// Set default value for simple_history_detective_mode_enabled and simple_history_experimental_features_enabled.
		$default_values = [
			'simple_history_detective_mode_enabled'        => 0,
			'simple_history_experimental_features_enabled' => 0,
		];

		foreach ( $default_values as $option_name => $default_value ) {
			$option_existing_value = get_option( $option_name );
			$option_value_to_set   = $default_value;

			if ( $option_existing_value !== false ) {
				$option_value_to_set = $option_existing_value;
			}

			// Re-set (possibly existing) value, but with autoload set to true.
			update_option( $option_name, $option_value_to_set, true );
		}

		$this->update_db_to_version( 5 );
	}

	/**
	 * Uppdate from db version 5 to version 6.
	 *
	 * Set default value for option simple_history_show_in_admin_bar to 1.
	 */
	private function setup_version_5_to_version_6() {
		if ( $this->get_db_version() !== 5 ) {
			return;
		}

		// Set default value for simple_history_show_in_admin_bar.
		update_option( 'simple_history_show_in_admin_bar', 1, true );

		$this->update_db_to_version( 6 );
	}

	/**
	 * Update from db version 6 to version 7.
	 *
	 * Sets the install date for the plugin.
	 */
	private function setup_version_6_to_version_7() {
		if ( $this->get_db_version() !== 6 ) {
			return;
		}

		update_option( 'simple_history_install_date_gmt', gmdate( 'Y-m-d H:i:s' ), false );

		$this->update_db_to_version( 7 );
	}

	/**
	 * Update from db version 7 to version 8.
	 *
	 * Ensures the welcome message option exists for upgrading users,
	 * preventing a DB query for a non-existent option on every page load.
	 */
	private function setup_version_7_to_version_8() {
		if ( $this->get_db_version() !== 7 ) {
			return;
		}

		if ( get_option( Welcome_Message_Service::OPTION_NAME ) === false ) {
			update_option( Welcome_Message_Service::OPTION_NAME, 'seen', true );
		}

		$this->update_db_to_version( 8 );
	}

	/**
	 * Update from db version 8 to version 9.
	 *
	 * Stores retention days in the database so it autoloads
	 * and avoids an extra DB query on every page load.
	 * Fresh installs: 30 days. Existing installs: 60 days.
	 */
	private function setup_version_8_to_version_9() {
		if ( $this->get_db_version() !== 8 ) {
			return;
		}

		$retention_days = $this->is_fresh_install ? 30 : 60;
		update_option( 'simple_history_retention_days', $retention_days, true );

		$this->update_db_to_version( 9 );
	}

	/**
	 * Update from db version 9 to version 10.
	 *
	 * Create rows with default values for all settings that did not get one
	 * on install. Without a row, the first save of the settings page creates
	 * the missing options via add_option(), which the settings logger would
	 * record as a "Modified settings" event even when nothing was changed.
	 * Storing the rows also lets the options autoload.
	 */
	private function setup_version_9_to_version_10() {
		if ( $this->get_db_version() !== 9 ) {
			return;
		}

		// RSS is disabled by default, but users that used the plugin before the
		// feed became opt-in (detected via an existing RSS secret) keep it
		// enabled, mirroring the lazy init in the RSS dropin.
		$rss_feed_default = get_option( 'simple_history_rss_secret' ) === false ? '0' : '1';

		// Auto-backfill pending: a missing row means the backfill already ran
		// (the flag used to be deleted afterwards, costing one extra query on
		// every admin page) or the install predates the feature — both mean
		// "not pending", so 0. Fresh installs get the row set to true by
		// setup_new_to_version_1() before this step runs.
		$options_with_defaults = [
			'simple_history_pager_size'           => 20,
			'simple_history_pager_size_dashboard' => 5,
			'simple_history_menu_page_location'   => 'top',
			'simple_history_enable_rss_feed'      => $rss_feed_default,
			'simple_history_reactions_enabled'    => '1',
			Auto_Backfill_Service::PENDING_OPTION => 0,
		];

		foreach ( $options_with_defaults as $option_name => $default_value ) {
			if ( get_option( $option_name ) !== false ) {
				continue;
			}

			update_option( $option_name, $default_value, true );
		}

		$this->update_db_to_version( 10 );
	}

	/**
	 * Add welcome messages to the log.
	 *
	 * Fired from action simple_history/loggers_loaded.
	 * Is only called after database has been upgraded, so only on first install (or upgrade).
	 */
	public function add_welcome_log_messages() {
		// Prevent duplicate entries if table recovery re-registers this callback.
		static $already_run = false;
		if ( $already_run ) {
			return;
		}
		$already_run = true;

		$plugin_logger = $this->simple_history->get_instantiated_logger_by_slug( 'SimplePluginLogger' );

		if ( ! $plugin_logger instanceof Plugin_Logger ) {
			return;
		}

		$plugin_logger->info_message(
			'plugin_installed',
			[
				'plugin_name'        => 'Simple History',
				'plugin_description' =>
					'Plugin that logs various things that occur in WordPress and then presents those events in a very nice GUI.',
				'plugin_url'         => 'https://simple-history.com',
				'plugin_version'     => SIMPLE_HISTORY_VERSION,
				'plugin_author'      => 'Pär Thernström',
			]
		);

		// Add plugin activated message.
		$plugin_logger->info_message(
			'plugin_activated',
			[
				'plugin_slug'  => 'simple-history',
				'plugin_name'  => 'Simple History',
				'plugin_title' => '<a href="https://simple-history.com/">Simple History</a>',
			]
		);

		$welcome_message_1 = __(
			'Simple History is now active and logging events on your site.',
			'simple-history'
		);

		SimpleLogger()->info(
			$welcome_message_1,
			array(
				'_initiator'         => Log_Initiators::WORDPRESS,
				'is_welcome_message' => true,
			)
		);
	}

	/**
	 * Append longer welcome message to the welcome message.
	 *
	 * @param string $html The HTML output.
	 * @param object $row The row object.
	 * @return string|\Simple_History\Event_Details\Event_Details_Group HTML output or Event_Details_Group with welcome message.
	 */
	public function add_row_details_output( $html, $row ) {
		$is_welcome_message = $row->context['is_welcome_message'] ?? false;

		if ( ! $is_welcome_message ) {
			return $html;
		}

		/**
		 * Placeholders:
		 * 1: Emoji
		 * 2: Message
		 */
		$row_template = '
			<div class="sh-FeedIntroduction-row">
				<div><span class="sh-FeedIntroduction-emoji">%1$s</span></div>
				<div>%2$s</div>
			</div>
		';

		$message = '<div class="sh-FeedIntroduction">';

		$message .= sprintf(
			$row_template,
			'📋',
			__( 'Logins, content edits, plugin updates, settings changes, user registrations — every significant action on your site is recorded here automatically.', 'simple-history' )
		);

		$message .= sprintf(
			$row_template,
			'👥',
			__( "Running a team site? Each user's actions are tracked individually, so you always know who changed what and when.", 'simple-history' )
		);

		$message .= sprintf(
			$row_template,
			'⏳',
			sprintf(
				/* translators: %s is a link to the add-ons page */
				__( 'Your existing posts, pages, and users have already been added to give you a head start. Need your full history? <a href="%s" target="_blank">Simple History Premium</a> lets you backfill with custom options.', 'simple-history' ),
				esc_url( Helpers::get_tracking_url( 'https://simple-history.com/add-ons/premium/', 'premium_welcome_backfill' ) )
			)
		);

		$message .= sprintf(
			$row_template,
			'🗓️',
			sprintf(
				/* translators: 1: number of days, 2: link to Premium page */
				__(
					'Events are kept for %1$s days by default. <a href="%2$s" target="_blank">Simple History Premium</a> extends your retention period so you never lose important history.',
					'simple-history'
				),
				Helpers::get_clear_history_interval(),
				esc_url( Helpers::get_tracking_url( 'https://simple-history.com/add-ons/premium/', 'premium_welcome_retention' ) )
			)
		);

		// Close sh-FeedIntroduction.
		$message .= '</div>';

		$item_table_row_raw_formatter = ( new Event_Details_Item_RAW_Formatter() )->set_html_output( $message );

		$welcome_item = ( new Event_Details_Item( 'is_welcome_message' ) )->set_formatter( $item_table_row_raw_formatter );

		$details_group = new Event_Details_Group();
		$details_group->add_items(
			[
				$welcome_item,
			]
		);

		return $details_group;
	}
}
