<?php
/**
 * Sync event repository.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Sync;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Repository stores local sync queue rows.

/**
 * Stores local events for future Cloud synchronization.
 */
class SyncEventRepository {
	const STATUS_PENDING = 'pending';
	const STATUS_SENT    = 'sent';
	const STATUS_FAILED  = 'failed';
	const STATUS_SKIPPED = 'skipped';

	/**
	 * Enqueue a sync event.
	 *
	 * @param string              $event_type Event type.
	 * @param string              $object_type Object type.
	 * @param int|string          $object_id Object id.
	 * @param array<string,mixed> $payload Payload.
	 * @return int
	 */
	public function enqueue( $event_type, $object_type, $object_id, array $payload ) {
		global $wpdb;

		$encoded = wp_json_encode( $payload );
		if ( ! is_string( $encoded ) ) {
			return 0;
		}

		$inserted = $wpdb->insert(
			$wpdb->prefix . 'subin_sync_events',
			array(
				'event_type'   => $this->sanitize_event_type( $event_type ),
				'object_type'  => sanitize_key( (string) $object_type ),
				'object_id'    => sanitize_text_field( (string) $object_id ),
				'payload_json' => $encoded,
				'status'       => self::STATUS_PENDING,
				'attempts'     => 0,
				'last_error'   => '',
				'created_at'   => current_time( 'mysql' ),
				'updated_at'   => current_time( 'mysql' ),
				'sent_at'      => null,
			),
			array( '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s' )
		);

		return $inserted ? absint( $wpdb->insert_id ) : 0;
	}

	/**
	 * Sanitize event type while preserving dot-separated names.
	 *
	 * @param string $event_type Event type.
	 * @return string
	 */
	private function sanitize_event_type( $event_type ) {
		return preg_replace( '/[^a-z0-9_.-]/', '', strtolower( (string) $event_type ) );
	}

	/**
	 * Get pending events eligible for a sync attempt.
	 *
	 * @param int $limit Limit.
	 * @return array<int,object>
	 */
	public function get_pending_batch( $limit = 25 ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_sync_events';
		$limit = max( 1, min( 100, absint( $limit ) ) );
		$now   = current_time( 'mysql' );

		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$table}
				WHERE status IN (%s, %s)
					AND attempts < 6
					AND (
						attempts = 0
						OR updated_at <= DATE_SUB(%s, INTERVAL LEAST(3600, POW(2, attempts) * 60) SECOND)
					)
				ORDER BY id ASC
				LIMIT %d",
				self::STATUS_PENDING,
				self::STATUS_FAILED,
				$now,
				$limit
			) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
	}

	/**
	 * Mark events as sent.
	 *
	 * @param array<int,int> $event_ids Event ids.
	 * @return void
	 */
	public function mark_sent( array $event_ids ) {
		global $wpdb;

		$event_ids = array_values( array_filter( array_map( 'absint', $event_ids ) ) );
		if ( empty( $event_ids ) ) {
			return;
		}

		$table        = $wpdb->prefix . 'subin_sync_events';
		$placeholders = implode( ',', array_fill( 0, count( $event_ids ), '%d' ) );
		$params       = array_merge(
			array(
				self::STATUS_SENT,
				current_time( 'mysql' ),
				current_time( 'mysql' ),
			),
			$event_ids
		);

		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table}
				SET status = %s, sent_at = %s, updated_at = %s, last_error = ''
				WHERE id IN ({$placeholders})",
				...$params
			) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
	}

	/**
	 * Mark events as failed.
	 *
	 * @param array<int,int> $event_ids Event ids.
	 * @param string         $error Error message.
	 * @return void
	 */
	public function mark_failed( array $event_ids, $error ) {
		global $wpdb;

		$event_ids = array_values( array_filter( array_map( 'absint', $event_ids ) ) );
		if ( empty( $event_ids ) ) {
			return;
		}

		$table        = $wpdb->prefix . 'subin_sync_events';
		$placeholders = implode( ',', array_fill( 0, count( $event_ids ), '%d' ) );
		$params       = array_merge(
			array(
				self::STATUS_FAILED,
				sanitize_textarea_field( (string) $error ),
				current_time( 'mysql' ),
			),
			$event_ids
		);

		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table}
				SET status = %s, attempts = attempts + 1, last_error = %s, updated_at = %s
				WHERE id IN ({$placeholders})",
				...$params
			) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
	}

	/**
	 * Mark events as skipped.
	 *
	 * @param array<int,int> $event_ids Event ids.
	 * @param string         $reason Reason.
	 * @return void
	 */
	public function mark_skipped( array $event_ids, $reason ) {
		global $wpdb;

		$event_ids = array_values( array_filter( array_map( 'absint', $event_ids ) ) );
		if ( empty( $event_ids ) ) {
			return;
		}

		$table        = $wpdb->prefix . 'subin_sync_events';
		$placeholders = implode( ',', array_fill( 0, count( $event_ids ), '%d' ) );
		$params       = array_merge(
			array(
				self::STATUS_SKIPPED,
				sanitize_textarea_field( (string) $reason ),
				current_time( 'mysql' ),
			),
			$event_ids
		);

		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table}
				SET status = %s, last_error = %s, updated_at = %s
				WHERE id IN ({$placeholders})",
				...$params
			) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
	}

	/**
	 * Count queued events by status.
	 *
	 * @param string $status Status.
	 * @return int
	 */
	public function count_by_status( $status = self::STATUS_PENDING ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_sync_events';

		return absint(
			$wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$table} WHERE status = %s",
					sanitize_key( (string) $status )
				) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			)
		);
	}

	/**
	 * Count events grouped by status.
	 *
	 * @return array<string,int>
	 */
	public function count_all_by_statuses() {
		global $wpdb;

		$table  = $wpdb->prefix . 'subin_sync_events';
		$counts = array(
			self::STATUS_PENDING => 0,
			self::STATUS_FAILED  => 0,
			self::STATUS_SENT    => 0,
			self::STATUS_SKIPPED => 0,
		);
		$rows   = $wpdb->get_results( "SELECT status, COUNT(*) AS total FROM {$table} GROUP BY status", ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		foreach ( $rows as $row ) {
			$status = sanitize_key( (string) ( $row['status'] ?? '' ) );
			if ( array_key_exists( $status, $counts ) ) {
				$counts[ $status ] = absint( $row['total'] ?? 0 );
			}
		}

		return $counts;
	}

	/**
	 * Get recent sync events.
	 *
	 * @param int $limit Limit.
	 * @return array<int,object>
	 */
	public function recent( $limit = 10 ) {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_sync_events';
		$limit = max( 1, min( 50, absint( $limit ) ) );

		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, event_type, object_type, object_id, status, attempts, last_error, created_at, updated_at, sent_at
				FROM {$table}
				ORDER BY id DESC
				LIMIT %d",
				$limit
			) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		);
	}

	/**
	 * Move failed events back to pending.
	 *
	 * @return int|false Number of updated rows, or false on failure.
	 */
	public function retry_failed() {
		global $wpdb;

		return $wpdb->update(
			$wpdb->prefix . 'subin_sync_events',
			array(
				'status'     => self::STATUS_PENDING,
				'attempts'   => 0,
				'last_error' => '',
				'updated_at' => current_time( 'mysql' ),
			),
			array( 'status' => self::STATUS_FAILED ),
			array( '%s', '%d', '%s', '%s' ),
			array( '%s' )
		);
	}

	/**
	 * Get the highest local event id.
	 *
	 * @return int
	 */
	public function latest_id() {
		global $wpdb;

		$table = $wpdb->prefix . 'subin_sync_events';

		return absint( $wpdb->get_var( "SELECT MAX(id) FROM {$table}" ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}
}
