<?php
/**
 * Livada SEO — Activity Log
 *
 * Append-only log of plugin actions: settings updates, redirect CRUD,
 * AI suggestions, schema changes, audit runs, IndexNow pings, imports.
 * Useful for debugging issues, tracking who changed what, and auditing
 * automated agent activity (MCP / WP-CLI).
 *
 * Storage: dedicated `{prefix}livada_seo_activity_log` table with a hard
 * cap of 1000 rows (auto-trimmed on insert) so the table stays bounded.
 *
 * @package Livada_SEO
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Livada_SEO_Activity_Log {

	/** Maximum rows kept in the log; older ones are pruned on insert. */
	const MAX_ROWS = 1000;

	/** Schema version stored in `livada_seo_activity_log_version` option. */
	const TABLE_VERSION = '1.0';

	// ── Bootstrap ─────────────────────────────────────────────────────────────

	public static function init(): void {
		add_action( 'admin_init', [ self::class, 'maybe_create_table' ] );
		add_action( 'admin_menu', [ self::class, 'register_menu' ], 30 );

		// Wire the events we care about into log entries.
		add_action( 'update_option_livada_seo_settings',  [ self::class, 'on_settings_updated' ], 10, 2 );
		add_action( 'update_option_livada_seo_locations', [ self::class, 'on_locations_updated' ], 10, 2 );

		// AJAX clear handler for the admin viewer.
		add_action( 'wp_ajax_livada_seo_activity_clear', [ self::class, 'ajax_clear' ] );
	}

	public static function maybe_create_table(): void {
		$option   = 'livada_seo_activity_log_version';
		$current  = (string) get_option( $option, '' );
		if ( self::TABLE_VERSION === $current ) {
			return;
		}

		global $wpdb;
		$table   = $wpdb->prefix . 'livada_seo_activity_log';
		$charset = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
			user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			actor VARCHAR(50) NOT NULL DEFAULT 'web',
			event VARCHAR(60) NOT NULL,
			object_type VARCHAR(40) NULL,
			object_id BIGINT UNSIGNED NULL,
			summary VARCHAR(255) NULL,
			data LONGTEXT NULL,
			PRIMARY KEY (id),
			KEY event (event),
			KEY created_at (created_at)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );

		update_option( $option, self::TABLE_VERSION, false );
	}

	public static function register_menu(): void {
		add_submenu_page(
			'livada-seo',
			__( 'Activity Log — Livada SEO', 'livada-seo' ),
			__( 'Activity Log', 'livada-seo' ),
			'manage_options',
			'livada-seo-activity',
			[ self::class, 'render_page' ]
		);
	}

	// ── Public API ────────────────────────────────────────────────────────────

	/**
	 * Records a single activity event.
	 *
	 * @param string                    $event        Short slug, e.g. "settings_updated", "ai_suggest"
	 * @param string                    $summary      Human-readable one-liner.
	 * @param array<string,mixed>|null  $data         Optional structured payload (JSON-encoded).
	 * @param array{type?:string,id?:int} $object     Optional object reference.
	 */
	public static function log( string $event, string $summary = '', ?array $data = null, array $object = [] ): void {
		global $wpdb;
		$table = $wpdb->prefix . 'livada_seo_activity_log';

		// Defensive: don't crash if the table hasn't been created yet.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$exists = (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
		if ( '' === $exists ) {
			return;
		}

		$actor = self::detect_actor();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->insert(
			$table,
			[
				'created_at'  => current_time( 'mysql' ),
				'user_id'     => (int) get_current_user_id(),
				'actor'       => $actor,
				'event'       => substr( $event, 0, 60 ),
				'object_type' => isset( $object['type'] ) ? substr( (string) $object['type'], 0, 40 ) : null,
				'object_id'   => isset( $object['id'] )   ? (int) $object['id']                     : null,
				'summary'     => substr( (string) $summary, 0, 255 ),
				'data'        => null === $data ? null : wp_json_encode( $data ),
			],
			[ '%s', '%d', '%s', '%s', '%s', '%d', '%s', '%s' ]
		);

		// Cap row count.
		self::maybe_trim();
	}

	/**
	 * Detects the calling context: 'cli', 'rest', 'cron', 'ajax', or 'web'.
	 * Useful to distinguish operator clicks from automated agent activity.
	 */
	private static function detect_actor(): string {
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return 'cli';
		}
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return 'rest';
		}
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			return 'cron';
		}
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return 'ajax';
		}
		return 'web';
	}

	/**
	 * Keeps the table at most MAX_ROWS rows by deleting the oldest entries
	 * when needed. Cheap because the index on `id` makes the LIMIT subquery O(1).
	 */
	private static function maybe_trim(): void {
		global $wpdb;
		$table = $wpdb->prefix . 'livada_seo_activity_log';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
		if ( $count <= self::MAX_ROWS ) {
			return;
		}

		$excess = $count - self::MAX_ROWS;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$table} ORDER BY id ASC LIMIT %d",
				$excess
			)
		);
	}

	// ── Hooked event listeners ────────────────────────────────────────────────

	public static function on_settings_updated( $old_value, $new_value ): void {
		// Compute a tiny diff summary: how many keys changed.
		$old_keys = is_array( $old_value ) ? array_keys( $old_value ) : [];
		$new_keys = is_array( $new_value ) ? array_keys( $new_value ) : [];
		$changed  = 0;

		if ( is_array( $old_value ) && is_array( $new_value ) ) {
			foreach ( $new_value as $k => $v ) {
				$old = $old_value[ $k ] ?? null;
				if ( serialize( $old ) !== serialize( $v ) ) {
					$changed++;
				}
			}
		}

		self::log(
			'settings_updated',
			sprintf(
				/* translators: %d: number of keys changed */
				_n( '%d setting key updated', '%d setting keys updated', $changed, 'livada-seo' ),
				$changed
			),
			[ 'changed_keys' => $changed, 'total_keys' => count( $new_keys ) ]
		);
	}

	public static function on_locations_updated( $old_value, $new_value ): void {
		$old_count = is_array( $old_value ) ? count( $old_value ) : 0;
		$new_count = is_array( $new_value ) ? count( $new_value ) : 0;

		self::log(
			'locations_updated',
			sprintf(
				/* translators: 1: old count, 2: new count */
				__( 'Locations: %1$d → %2$d', 'livada-seo' ),
				$old_count,
				$new_count
			),
			[ 'before' => $old_count, 'after' => $new_count ]
		);
	}

	// ── Admin viewer ──────────────────────────────────────────────────────────

	public static function render_page(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		global $wpdb;
		$table = $wpdb->prefix . 'livada_seo_activity_log';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$rows = $wpdb->get_results(
			"SELECT id, created_at, user_id, actor, event, object_type, object_id, summary
			 FROM {$table}
			 ORDER BY id DESC
			 LIMIT 200"
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );

		?>
		<div class="wrap livada-seo-wrap">
			<h1>
				<?php esc_html_e( 'Activity Log', 'livada-seo' ); ?>
				<button type="button" id="livada-activity-clear" class="page-title-action" style="margin-left:12px; color:#b32d2e;">
					<?php esc_html_e( 'Clear log', 'livada-seo' ); ?>
				</button>
			</h1>
			<p class="description">
				<?php
				printf(
					/* translators: %d: total log row count */
					esc_html( _n( '%d entry stored (capped at 1000).', '%d entries stored (capped at 1000).', $total, 'livada-seo' ) ),
					(int) $total
				);
				?>
			</p>

			<table class="wp-list-table widefat fixed striped">
				<thead>
					<tr>
						<th style="width: 14%;"><?php esc_html_e( 'When', 'livada-seo' ); ?></th>
						<th style="width: 8%;"><?php esc_html_e( 'Actor', 'livada-seo' ); ?></th>
						<th style="width: 10%;"><?php esc_html_e( 'User', 'livada-seo' ); ?></th>
						<th style="width: 16%;"><?php esc_html_e( 'Event', 'livada-seo' ); ?></th>
						<th style="width: 14%;"><?php esc_html_e( 'Object', 'livada-seo' ); ?></th>
						<th><?php esc_html_e( 'Summary', 'livada-seo' ); ?></th>
					</tr>
				</thead>
				<tbody>
					<?php if ( empty( $rows ) ) : ?>
						<tr><td colspan="6"><?php esc_html_e( 'No activity yet.', 'livada-seo' ); ?></td></tr>
					<?php else : ?>
						<?php foreach ( $rows as $r ) :
							$user_label = $r->user_id > 0
								? ( get_userdata( (int) $r->user_id )->user_login ?? sprintf( '#%d', (int) $r->user_id ) )
								: '—';
							$obj_label = '';
							if ( $r->object_type && $r->object_id ) {
								$obj_label = $r->object_type . ' #' . (int) $r->object_id;
							}
						?>
							<tr>
								<td><small><?php echo esc_html( $r->created_at ); ?></small></td>
								<td><code><?php echo esc_html( $r->actor ); ?></code></td>
								<td><?php echo esc_html( $user_label ); ?></td>
								<td><code><?php echo esc_html( $r->event ); ?></code></td>
								<td><small><?php echo esc_html( $obj_label ); ?></small></td>
								<td><?php echo esc_html( $r->summary ); ?></td>
							</tr>
						<?php endforeach; ?>
					<?php endif; ?>
				</tbody>
			</table>
		</div>

		<script>
		(function () {
			var btn = document.getElementById('livada-activity-clear');
			if (!btn) return;
			var data = window.livadaSEO || {};
			var ajaxUrl = data.ajaxUrl || '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>';
			var nonce   = data.nonce   || '<?php echo esc_js( wp_create_nonce( 'livada_seo_admin_nonce' ) ); ?>';

			btn.addEventListener('click', function () {
				if (!window.confirm('<?php echo esc_js( __( 'Clear all activity log entries?', 'livada-seo' ) ); ?>')) return;
				var body = new URLSearchParams({ action: 'livada_seo_activity_clear', nonce: nonce });
				fetch(ajaxUrl, {
					method: 'POST', credentials: 'same-origin',
					headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					body: body.toString()
				}).then(function () { window.location.reload(); });
			});
		})();
		</script>
		<?php
	}

	public static function ajax_clear(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ], 403 );
		}

		global $wpdb;
		$table = $wpdb->prefix . 'livada_seo_activity_log';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$wpdb->query( "TRUNCATE TABLE {$table}" );

		self::log( 'log_cleared', __( 'Activity log cleared by user', 'livada-seo' ) );

		wp_send_json_success();
	}
}
