<?php
/**
 * Progress Tracker.
 *
 * Tracks progress of long-running background operations.
 *
 * @package   Agenistrator
 * @since     1.0.0
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://agenistrator.com/
 *
 * SPDX-FileCopyrightText: 2025-2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

use Agenistrator\Events\Progress_Event;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Progress_Tracker class.
 */
class Progress_Tracker {

	/**
	 * Operation identifier (e.g., 'embeddings').
	 *
	 * @var string
	 */
	private $operation;

	/**
	 * Unique instance identifier for this operation run.
	 *
	 * @var string
	 */
	private $instance_id;

	/**
	 * Private constructor - use static factory methods.
	 *
	 * @param string $operation   The operation identifier.
	 * @param string $instance_id The unique instance ID.
	 */
	private function __construct( $operation, $instance_id ) {
		$this->operation   = $operation;
		$this->instance_id = $instance_id;
	}

	/**
	 * Start a new operation.
	 *
	 * Generates a unique instance ID and pushes an initial progress event.
	 *
	 * @param string $operation The operation identifier (e.g., 'embeddings').
	 * @return self
	 * @since     1.0.0
	 */
	public static function start( $operation ) {
		$instance_id = uniqid( '', true );
		$tracker     = new self( $operation, $instance_id );
		$tracker->update( 0, 0 );
		return $tracker;
	}

	/**
	 * Get currently running operations.
	 *
	 * Returns all running instances, allowing multiple concurrent operations
	 * of the same type.
	 *
	 * @param string|null $operation Optional. Filter by operation type.
	 * @return self[] Array of Progress_Tracker instances for running operations.
	 * @since     1.0.0
	 */
	public static function get_running( $operation = null ) {
		$events = Event_Queue::get_instance()->get_events_by_type( 'progress' );

		// Track cancelled/completed operations and latest event per instance
		$cancelled          = array();
		$latest_by_instance = array();

		foreach ( $events as $event ) {
			$instance_id = $event->get_instance_id();
			if ( ! $instance_id ) {
				continue;
			}

			// Track any instance that has been cancelled or completed
			if ( $event->get_status() !== Progress_Event::STATUS_RUNNING ) {
				$cancelled[ $instance_id ] = true;
			}

			// Keep only the latest event per instance
			if ( ! isset( $latest_by_instance[ $instance_id ] ) ) {
				$latest_by_instance[ $instance_id ] = $event;
			}
		}

		// Remove cancelled/completed operations from the list
		$latest_by_instance = array_diff_key( $latest_by_instance, $cancelled );

		// Create tracker instances for remaining running operations
		$trackers = array();
		foreach ( $latest_by_instance as $instance_id => $event ) {
			$op = $event->get_operation();
			if ( null !== $operation && $op !== $operation ) {
				continue;
			}
			$trackers[] = new self( $op, $instance_id );
		}

		return $trackers;
	}

	/**
	 * Get a running tracker by its instance ID.
	 *
	 * @param string $instance_id The instance ID to look up.
	 * @return self|null The tracker if found and running, null otherwise.
	 * @since     1.0.0
	 */
	public static function get_by_instance_id( $instance_id ) {
		$events = Event_Queue::get_instance()->get_events_by_type( 'progress' );

		$latest_event = null;

		foreach ( $events as $event ) {
			if ( $event->get_instance_id() !== $instance_id ) {
				continue;
			}

			// If this instance has been cancelled/completed at any point then it is not running
			if ( $event->get_status() !== Progress_Event::STATUS_RUNNING ) {
				return null;
			}

			// Keep the first (latest) event for this instance
			if ( null === $latest_event ) {
				$latest_event = $event;
			}
		}

		// If no events found, return null
		if ( null === $latest_event ) {
			return null;
		}

		return new self( $latest_event->get_operation(), $instance_id );
	}

	/**
	 * Get the operation identifier.
	 *
	 * @return string
	 * @since     1.0.0
	 */
	public function get_operation() {
		return $this->operation;
	}

	/**
	 * Get the instance ID.
	 *
	 * @return string
	 * @since     1.0.0
	 */
	public function get_instance_id() {
		return $this->instance_id;
	}

	/**
	 * Get the latest event for this tracker's operation and instance.
	 *
	 * @return Progress_Event|null Event or null if not found.
	 */
	private function get_latest_event() {
		$events = Event_Queue::get_instance()->get_events_by_type( 'progress' );
		foreach ( $events as $event ) {
			if ( $event->get_operation() === $this->operation
				&& $event->get_instance_id() === $this->instance_id ) {
				return $event;
			}
		}
		return null;
	}

	/**
	 * Get current progress value.
	 *
	 * @return int
	 * @since     1.0.0
	 */
	public function get_current() {
		$event = $this->get_latest_event();
		return $event ? $event->get_current() : 0;
	}

	/**
	 * Get total items count.
	 *
	 * @return int
	 * @since     1.0.0
	 */
	public function get_total() {
		$event = $this->get_latest_event();
		return $event ? $event->get_total() : 0;
	}

	/**
	 * Get operation status.
	 *
	 * @return string
	 * @since     1.0.0
	 */
	public function get_status() {
		$event = $this->get_latest_event();
		return $event ? $event->get_status() : Progress_Event::STATUS_RUNNING;
	}

	/**
	 * Get created timestamp.
	 *
	 * @return int
	 * @since     1.0.0
	 */
	public function get_created_at() {
		$event = $this->get_latest_event();
		return $event ? (int) $event->get_created_at() : 0;
	}

	/**
	 * Get percent complete.
	 *
	 * @return float Percentage (0-100).
	 * @since     1.0.0
	 */
	public function get_percent_complete() {
		$event = $this->get_latest_event();
		if ( ! $event ) {
			return 0.0;
		}
		$total = $event->get_total();
		if ( $total <= 0 ) {
			return 0.0;
		}
		return round( ( $event->get_current() / $total ) * 100, 1 );
	}

	/**
	 * Update progress.
	 *
	 * @param int    $current Current progress value.
	 * @param int    $total   Total items to process.
	 * @param string $status  Operation status.
	 * @since     1.0.0
	 */
	public function update( $current, $total, $status = Progress_Event::STATUS_RUNNING ) {
		$event = new Progress_Event( $this->operation, $current, $total, $status, $this->instance_id );
		$event->push();
	}

	/**
	 * Mark operation as complete.
	 *
	 * @param int $total Total items processed.
	 * @since     1.0.0
	 */
	public function complete( $total ) {
		$this->update( $total, $total, Progress_Event::STATUS_COMPLETED );
	}

	/**
	 * Cancel this operation.
	 * @since     1.0.0
	 */
	public function cancel() {
		$event = new Progress_Event( $this->operation, 0, 0, Progress_Event::STATUS_CANCELLED, $this->instance_id );
		$event->push();
	}

	/**
	 * Check if this operation should stop.
	 *
	 * Returns true if:
	 * - The operation was explicitly cancelled (cancelled event with our instance_id), or
	 * - The operation was superseded by a newer instance (a running event with a different instance_id).
	 *
	 * This method scans all recent events to find cancel events, since a cancel event
	 * may be "buried" by subsequent update events due to race conditions.
	 *
	 * @return bool
	 * @since     1.0.0
	 */
	public function is_cancelled() {
		$events = Event_Queue::get_instance()->get_events_by_type( 'progress' );

		foreach ( $events as $event ) {
			if ( $event->get_operation() !== $this->operation ) {
				continue;
			}

			$status      = $event->get_status();
			$instance_id = $event->get_instance_id();

			// Explicit cancellation of our instance (scan all events, don't stop early)
			if ( Progress_Event::STATUS_CANCELLED === $status && $instance_id === $this->instance_id ) {
				return true;
			}
		}

		return false;
	}
}
