<?php
/**
 * Activity Logger.
 *
 * Owns the custom table and the write/read API. The table is created lazily on
 * module boot (dbDelta, idempotent) and is intentionally NOT dropped on
 * uninstall unless the user opts in to full data removal — consistent with the
 * safe-lifecycle policy.
 *
 * Each entry: time, user id + display name snapshot, action key, object type,
 * object id, and a short human description. Writes are cheap single inserts.
 *
 * @package Platform\Helm\Modules\Activity_Log
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\Activity_Log;

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

final class ActivityLogger {

	/** Bump when the schema changes. Plain literal (OPcache-safe). */
	private const SCHEMA_VERSION = '1';
	private const SCHEMA_OPTION  = 'platform_helm_activity_schema';

	public function table(): string {
		global $wpdb;
		return $wpdb->prefix . 'platform_helm_activity';
	}

	/**
	 * Create or update the table. Idempotent; safe to call on every boot.
	 */
	public function maybe_install(): void {
		if ( get_option( self::SCHEMA_OPTION ) === self::SCHEMA_VERSION ) {
			return;
		}
		global $wpdb;
		$table   = $this->table();
		$charset = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$table} (
			id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
			logged_at DATETIME NOT NULL,
			user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			user_name VARCHAR(191) NOT NULL DEFAULT '',
			action VARCHAR(64) NOT NULL DEFAULT '',
			object_type VARCHAR(40) NOT NULL DEFAULT '',
			object_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			description TEXT NULL,
			PRIMARY KEY (id),
			KEY logged_at (logged_at),
			KEY action (action),
			KEY user_id (user_id)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
		update_option( self::SCHEMA_OPTION, self::SCHEMA_VERSION, false );
	}

	/**
	 * Record an event.
	 */
	public function log( string $action, string $description = '', string $object_type = '', int $object_id = 0 ): void {
		global $wpdb;

		$user      = wp_get_current_user();
		$user_id   = $user && $user->exists() ? (int) $user->ID : 0;
		$user_name = $user && $user->exists() ? (string) $user->display_name : __( 'System', 'platform-helm' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->insert(
			$this->table(),
			[
				'logged_at'   => current_time( 'mysql' ),
				'user_id'     => $user_id,
				'user_name'   => mb_substr( $user_name, 0, 191 ),
				'action'      => mb_substr( $action, 0, 64 ),
				'object_type' => mb_substr( $object_type, 0, 40 ),
				'object_id'   => $object_id,
				'description' => mb_substr( $description, 0, 2000 ),
			],
			[ '%s', '%d', '%s', '%s', '%s', '%d', '%s' ]
		);
	}

	/**
	 * Read recent entries, newest first.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public function recent( int $limit = 100, int $offset = 0, string $action_filter = '' ): array {
		global $wpdb;
		$table = $this->table();
		$limit = max( 1, min( 500, $limit ) );

		if ( '' !== $action_filter ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results( $wpdb->prepare(
				"SELECT * FROM {$table} WHERE action = %s ORDER BY id DESC LIMIT %d OFFSET %d",
				$action_filter, $limit, max( 0, $offset )
			), ARRAY_A );
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results( $wpdb->prepare(
				"SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d",
				$limit, max( 0, $offset )
			), ARRAY_A );
		}
		return is_array( $rows ) ? $rows : [];
	}

	public function count_all(): int {
		global $wpdb;
		$table = $this->table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
	}

	/**
	 * Fetch rows for CSV export, newest first, optionally filtered by action.
	 * Bounded by a generous hard cap so an export never tries to stream an
	 * unbounded result set into memory.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public function all_for_export( string $action_filter = '', int $cap = 10000 ): array {
		global $wpdb;
		$table = $this->table();
		$cap   = max( 1, min( 50000, $cap ) );

		if ( '' !== $action_filter ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results( $wpdb->prepare(
				"SELECT logged_at, user_name, action, object_type, object_id, description
				 FROM {$table} WHERE action = %s ORDER BY id DESC LIMIT %d",
				$action_filter, $cap
			), ARRAY_A );
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results( $wpdb->prepare(
				"SELECT logged_at, user_name, action, object_type, object_id, description
				 FROM {$table} ORDER BY id DESC LIMIT %d",
				$cap
			), ARRAY_A );
		}
		return is_array( $rows ) ? $rows : [];
	}

	/**
	 * Delete entries older than N days. Used by an optional retention prune.
	 */
	public function prune_older_than( int $days ): int {
		global $wpdb;
		$table = $this->table();
		$cutoff = gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
		return (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE logged_at < %s", $cutoff ) );
	}

	/**
	 * Drop the table — only called on explicit opt-in uninstall.
	 */
	public function drop_table(): void {
		global $wpdb;
		$table = $this->table();
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
		$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
		delete_option( self::SCHEMA_OPTION );
	}
}
