<?php
/**
 * Activity Logger Class
 *
 * Handles logging of workflow execution events and activity tracking.
 *
 * @package Sequensy
 * @since 2.0.0
 */

// phpcs:disable WordPress.DB.PreparedSQL -- Storage class uses $wpdb with proper prepare().
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- Dynamic WHERE clause with matching values array.
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages.

declare(strict_types=1);

namespace Sequensy\Storage;

use wpdb;

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class ActivityLogger
 *
 * Provides comprehensive logging for workflow execution events.
 */
class ActivityLogger {

	/**
	 * WordPress database instance.
	 *
	 * @var wpdb
	 */
	private wpdb $wpdb;

	/**
	 * Table name for activity log.
	 *
	 * @var string
	 */
	private string $table_activity_log;

	/**
	 * Log levels (aligned with PSR-3).
	 *
	 * @var array<string>
	 */
	private array $valid_levels = array( 'info', 'warning', 'error', 'critical' );

	/**
	 * Sensitive field names to redact.
	 *
	 * @var array<string>
	 */
	private array $sensitive_fields = array(
		'password',
		'api_key',
		'secret',
		'token',
		'api_secret',
		'access_token',
		'refresh_token',
	);

	/**
	 * Constructor.
	 *
	 * @param wpdb|null $wpdb WordPress database instance. Falls back to global $wpdb.
	 */
	public function __construct( ?wpdb $wpdb = null ) {
		if ( null === $wpdb ) {
			global $wpdb;
		}
		$this->wpdb               = $wpdb;
		$this->table_activity_log = $wpdb->prefix . 'sequensy_activity_log';
	}

	/**
	 * Log a workflow activity event.
	 *
	 * @param int      $workflow_id  Workflow ID.
	 * @param string   $level        Log level (info, warning, error, critical).
	 * @param string   $message      Log message.
	 * @param array    $context      Additional context data.
	 * @param int|null $job_id       Optional Action Scheduler job ID.
	 * @param int|null $execution_id Optional execution ID for per-action queueing.
	 * @return int|false Log ID on success, false on failure.
	 */
	public function log(
		int $workflow_id,
		string $level,
		string $message,
		array $context = array(),
		?int $job_id = null,
		?int $execution_id = null
	): int|false {
		// Validate level.
		if ( ! in_array( $level, $this->valid_levels, true ) ) {
			$level = 'info';
		}

		// Redact sensitive data from context.
		if ( ! empty( $context ) ) {
			$context = $this->redact_sensitive_data( $context );
		}

		// Encode context as JSON.
		$context_json = ! empty( $context ) ? wp_json_encode( $context ) : null;

		// Prepare insert data.
		$insert_data = array(
			'workflow_id' => $workflow_id,
			'level'       => $level,
			'message'     => sanitize_textarea_field( $message ),
			'context'     => $context_json,
		);

		$insert_format = array( '%d', '%s', '%s', '%s' );

		// Add job_id if provided.
		if ( null !== $job_id ) {
			$insert_data['job_id'] = $job_id;
			$insert_format[]       = '%d';
		}

		// Add execution_id if provided.
		if ( null !== $execution_id ) {
			$insert_data['execution_id'] = $execution_id;
			$insert_format[]             = '%d';
		}

		// Insert log entry.
		$result = $this->wpdb->insert(
			$this->table_activity_log,
			$insert_data,
			$insert_format
		);

		// Also log to WordPress debug.log if WP_DEBUG_LOG is enabled.
		if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
			$log_prefix = sprintf( '[Sequensy Pro Workflow #%d]', $workflow_id );
			if ( null !== $execution_id ) {
				$log_prefix .= sprintf( ' [Exec #%d]', $execution_id );
			}
			$log_message = sprintf(
				'%s [%s] %s',
				$log_prefix,
				strtoupper( $level ),
				$message
			);
			if ( ! empty( $context ) ) {
				$log_message .= ' | Context: ' . wp_json_encode( $context );
			}
			error_log( $log_message );
		}

		return $result ? $this->wpdb->insert_id : false;
	}

	/**
	 * Get activity logs with filters and pagination.
	 *
	 * @param array $filters Filters (workflow_id, execution_id, level, date_from, date_to).
	 * @param int   $limit   Number of results to return.
	 * @param int   $offset  Offset for pagination.
	 * @return array Array of log objects.
	 */
	public function get_logs( array $filters = array(), int $limit = 50, int $offset = 0 ): array {
		/**
		 * Maximum number of activity log entries to return.
		 *
		 * Free: 50. Pro: 0 (unlimited, uses request limit as-is).
		 *
		 * @param int $max_limit Maximum entries. 0 = use request limit as-is.
		 */
		$global_limit = apply_filters( 'sequensy_activity_log_limit', 50 );
		if ( $global_limit > 0 && ( $limit <= 0 || $limit > $global_limit ) ) {
			$limit = $global_limit;
		}

		// Build WHERE clause with hardcoded column names.
		$where  = array( '1=1' );
		$values = array();

		if ( ! empty( $filters['workflow_id'] ) ) {
			$where[]  = 'workflow_id = %d';
			$values[] = absint( $filters['workflow_id'] );
		}

		if ( ! empty( $filters['execution_id'] ) ) {
			$where[]  = 'execution_id = %d';
			$values[] = absint( $filters['execution_id'] );
		}

		if ( ! empty( $filters['level'] ) ) {
			$where[]  = 'level = %s';
			$values[] = sanitize_text_field( $filters['level'] );
		}

		if ( ! empty( $filters['date_from'] ) ) {
			$where[]  = 'created_at >= %s';
			$values[] = sanitize_text_field( $filters['date_from'] );
		}

		if ( ! empty( $filters['date_to'] ) ) {
			$where[]  = 'created_at <= %s';
			$values[] = sanitize_text_field( $filters['date_to'] );
		}

		$where_clause = implode( ' AND ', $where );
		$values[]     = $limit;
		$values[]     = $offset;

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders -- Table name is a class property; where_clause and values built from literal strings above.
		$logs = $this->wpdb->get_results(
			$this->wpdb->prepare(
				"SELECT * FROM {$this->table_activity_log}"
				. " WHERE $where_clause ORDER BY created_at DESC"
				. ' LIMIT %d OFFSET %d',
				$values
			)
		);

		// Decode context JSON for each log.
		foreach ( $logs as $log ) {
			if ( ! empty( $log->context ) ) {
				$log->context = json_decode( $log->context, true );
			}
		}

		return $logs;
	}

	/**
	 * Get total count of logs matching filters.
	 *
	 * @param array $filters Filters (workflow_id, execution_id, level, date_from, date_to).
	 * @return int Total count.
	 */
	public function get_logs_count( array $filters = array() ): int {
		// Build WHERE clause with hardcoded column names.
		$where  = array( '1=1' );
		$values = array();

		if ( ! empty( $filters['workflow_id'] ) ) {
			$where[]  = 'workflow_id = %d';
			$values[] = absint( $filters['workflow_id'] );
		}

		if ( ! empty( $filters['execution_id'] ) ) {
			$where[]  = 'execution_id = %d';
			$values[] = absint( $filters['execution_id'] );
		}

		if ( ! empty( $filters['level'] ) ) {
			$where[]  = 'level = %s';
			$values[] = sanitize_text_field( $filters['level'] );
		}

		if ( ! empty( $filters['date_from'] ) ) {
			$where[]  = 'created_at >= %s';
			$values[] = sanitize_text_field( $filters['date_from'] );
		}

		if ( ! empty( $filters['date_to'] ) ) {
			$where[]  = 'created_at <= %s';
			$values[] = sanitize_text_field( $filters['date_to'] );
		}

		$where_clause = implode( ' AND ', $where );

		// Always use prepare() - pass 1 as dummy value when no filters to satisfy prepare() requirement.
		if ( empty( $values ) ) {
			$values[] = 1;
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is a class property; where_clause built from literal strings above.
			return (int) $this->wpdb->get_var(
				$this->wpdb->prepare(
					"SELECT COUNT(*) FROM {$this->table_activity_log} WHERE $where_clause AND %d=%d",
					$values[0],
					$values[0]
				)
			);
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders -- Table name is a class property; where_clause and values built from literal strings above.
		return (int) $this->wpdb->get_var(
			$this->wpdb->prepare(
				"SELECT COUNT(*) FROM {$this->table_activity_log} WHERE $where_clause",
				$values
			)
		);
	}

	/**
	 * Redact sensitive data from context.
	 *
	 * @param array $context Context data to redact.
	 * @return array Redacted context data.
	 */
	private function redact_sensitive_data( array $context ): array {
		foreach ( $context as $key => $value ) {
			// Check if key contains sensitive field name.
			$key_lower = strtolower( (string) $key );
			foreach ( $this->sensitive_fields as $sensitive_field ) {
				if ( strpos( $key_lower, $sensitive_field ) !== false ) {
					// Redact the value.
					if ( is_string( $value ) && strlen( $value ) > 4 ) {
						// Show first 2 and last 2 characters.
						$context[ $key ] = substr( $value, 0, 2 ) . '***' . substr( $value, -2 );
					} else {
						$context[ $key ] = '[REDACTED]';
					}
					break;
				}
			}

			// Recursively redact nested arrays.
			if ( is_array( $value ) ) {
				$context[ $key ] = $this->redact_sensitive_data( $value );
			}
		}

		return $context;
	}

	/**
	 * Delete old logs.
	 *
	 * @param int $days Number of days to keep logs.
	 * @return int Number of deleted rows.
	 */
	public function delete_old_logs( int $days = 30 ): int {
		$date = gmdate( 'Y-m-d H:i:s', strtotime( "-$days days" ) );

		$result = $this->wpdb->query(
			$this->wpdb->prepare(
				"DELETE FROM {$this->table_activity_log} WHERE created_at < %s",
				$date
			)
		);

		return false !== $result ? $result : 0;
	}

	/**
	 * Delete activity log entries older than the specified number of days.
	 *
	 * @param int $days Number of days to retain.
	 * @return int Number of deleted rows.
	 */
	public function prune( int $days = 90 ): int {
		$result = $this->wpdb->query(
			$this->wpdb->prepare(
				"DELETE FROM {$this->table_activity_log} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
				$days
			)
		);

		return false !== $result ? (int) $result : 0;
	}

	/**
	 * Delete all logs for a specific workflow.
	 *
	 * @param int $workflow_id Workflow ID.
	 * @return int Number of deleted rows.
	 */
	public function delete_workflow_logs( int $workflow_id ): int {
		$result = $this->wpdb->delete(
			$this->table_activity_log,
			array( 'workflow_id' => $workflow_id ),
			array( '%d' )
		);

		return false !== $result ? $result : 0;
	}

	/**
	 * Convenience method: Log info level.
	 *
	 * @param int      $workflow_id  Workflow ID.
	 * @param string   $message      Log message.
	 * @param array    $context      Additional context data.
	 * @param int|null $job_id       Optional Action Scheduler job ID.
	 * @param int|null $execution_id Optional execution ID for per-action queueing.
	 * @return int|false Log ID on success, false on failure.
	 */
	public function info(
		int $workflow_id,
		string $message,
		array $context = array(),
		?int $job_id = null,
		?int $execution_id = null
	): int|false {
		return $this->log( $workflow_id, 'info', $message, $context, $job_id, $execution_id );
	}

	/**
	 * Convenience method: Log warning level.
	 *
	 * @param int      $workflow_id  Workflow ID.
	 * @param string   $message      Log message.
	 * @param array    $context      Additional context data.
	 * @param int|null $job_id       Optional Action Scheduler job ID.
	 * @param int|null $execution_id Optional execution ID for per-action queueing.
	 * @return int|false Log ID on success, false on failure.
	 */
	public function warning(
		int $workflow_id,
		string $message,
		array $context = array(),
		?int $job_id = null,
		?int $execution_id = null
	): int|false {
		return $this->log( $workflow_id, 'warning', $message, $context, $job_id, $execution_id );
	}

	/**
	 * Convenience method: Log error level.
	 *
	 * @param int      $workflow_id  Workflow ID.
	 * @param string   $message      Log message.
	 * @param array    $context      Additional context data.
	 * @param int|null $job_id       Optional Action Scheduler job ID.
	 * @param int|null $execution_id Optional execution ID for per-action queueing.
	 * @return int|false Log ID on success, false on failure.
	 */
	public function error(
		int $workflow_id,
		string $message,
		array $context = array(),
		?int $job_id = null,
		?int $execution_id = null
	): int|false {
		return $this->log( $workflow_id, 'error', $message, $context, $job_id, $execution_id );
	}

	/**
	 * Convenience method: Log critical level.
	 *
	 * @param int      $workflow_id  Workflow ID.
	 * @param string   $message      Log message.
	 * @param array    $context      Additional context data.
	 * @param int|null $job_id       Optional Action Scheduler job ID.
	 * @param int|null $execution_id Optional execution ID for per-action queueing.
	 * @return int|false Log ID on success, false on failure.
	 */
	public function critical(
		int $workflow_id,
		string $message,
		array $context = array(),
		?int $job_id = null,
		?int $execution_id = null
	): int|false {
		return $this->log( $workflow_id, 'critical', $message, $context, $job_id, $execution_id );
	}

	/**
	 * Delete all logs for a specific execution.
	 *
	 * @param int $execution_id Execution ID.
	 * @return int Number of deleted rows.
	 */
	public function delete_execution_logs( int $execution_id ): int {
		$result = $this->wpdb->delete(
			$this->table_activity_log,
			array( 'execution_id' => $execution_id ),
			array( '%d' )
		);

		return false !== $result ? $result : 0;
	}
}
