<?php
/**
 * Defines the Jetpack CRM Automation workflow base.
 *
 * @package automattic/jetpack-crm
 * @since 6.2.0
 */

namespace Automattic\Jetpack\CRM\Automation;

use Automattic\Jetpack\CRM\Automation\Data_Types\Data_Type;

/**
 * Adds the Automation_Workflow class.
 *
 * @since 6.2.0
 */
class Automation_Workflow {

	/**
	 * The workflow id.
	 *
	 * @since 6.2.0
	 * @var int|string
	 */
	protected $id;

	/**
	 * The CRM site ID the workflow belongs to.
	 *
	 * @since 6.2.0
	 * @var int
	 */
	protected $zbs_site;

	/**
	 * The WP User who created the workflow.
	 *
	 * @since 6.2.0
	 * @var int
	 */
	protected $zbs_owner;

	/**
	 * The workflow name.
	 *
	 * @since 6.2.0
	 * @var string
	 */
	public $name;

	/**
	 * The workflow description.
	 *
	 * @since 6.2.0
	 * @var string
	 */
	public $description;

	/**
	 * The workflow category.
	 *
	 * @since 6.2.0
	 * @var string
	 */
	public $category;

	/**
	 * The workflow triggers.
	 *
	 * @since 6.2.0
	 * @var string[]
	 */
	public $triggers;

	/**
	 * The workflow initial step id.
	 *
	 * @since 6.2.0
	 * @var int|string|null
	 */
	public $initial_step;

	/**
	 * The workflow steps list
	 *
	 * @since 6.2.0
	 * @var array
	 */
	public $steps;

	/**
	 * The workflow active status.
	 *
	 * @since 6.2.0
	 * @var bool
	 */
	public $active;

	/**
	 * The version of the workflow.
	 *
	 * @since 6.2.0
	 * @var int
	 */
	protected $version;

	/**
	 * A timestamp that reflects when the workflow was created.
	 *
	 * @since 6.2.0
	 * @var int
	 */
	protected $created_at;

	/**
	 * A timestamp that reflects when the workflow was last updated.
	 *
	 * @since 6.2.0
	 * @var int
	 */
	protected $updated_at;

	/**
	 * The automation engine.
	 *
	 * @since 6.2.0
	 * @var Automation_Engine
	 */
	protected $automation_engine;

	/**
	 * The automation logger.
	 *
	 * @since 6.2.0
	 * @var Automation_Logger
	 */
	protected $logger;

	/**
	 * Automation_Workflow constructor.
	 *
	 * @since 6.2.0
	 *
	 * @param array $workflow_data The workflow data to be constructed.
	 */
	public function __construct( array $workflow_data ) {
		$this->id           = $workflow_data['id'] ?? null;
		$this->zbs_site     = $workflow_data['zbs_site'] ?? -1;
		$this->zbs_owner    = $workflow_data['zbs_owner'] ?? -1;
		$this->triggers     = $workflow_data['triggers'] ?? array();
		$this->steps        = $workflow_data['steps'] ?? array();
		$this->initial_step = $workflow_data['initial_step'] ?? '';
		$this->name         = $workflow_data['name'] ?? '';
		$this->description  = $workflow_data['description'] ?? '';
		$this->category     = $workflow_data['category'] ?? '';
		$this->active       = $workflow_data['active'] ?? false;
		$this->version      = $workflow_data['version'] ?? 1;
		$this->created_at   = $workflow_data['created_at'] ?? null;
		$this->updated_at   = $workflow_data['updated_at'] ?? null;
	}

	/**
	 * Get the id of this workflow.
	 *
	 * This will either be a string if the workflow is registered in the codebase,
	 * or an integer if it is a custom workflow stored in the database.
	 *
	 * @since 6.2.0
	 *
	 * @return int|string The workflow id.
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Get the CRM site the workflow should run on.
	 *
	 * @since 6.2.0
	 *
	 * @return int
	 */
	public function get_zbs_site(): int {
		return $this->zbs_site;
	}

	/**
	 * Set the CRM site teh workflow should run on.
	 *
	 * @since 6.2.0
	 *
	 * @param int $site The CRM site the workflow should run on.
	 * @return void
	 */
	public function set_zbs_site( int $site ): void {
		$this->zbs_site = $site;
	}

	/**
	 * Get the CRM owner/creator of the workflow.
	 *
	 * @since 6.2.0
	 *
	 * @return int
	 */
	public function get_zbs_owner(): int {
		return $this->zbs_owner;
	}

	/**
	 * Set the CRM owner/creator of the workflow.
	 *
	 * @since 6.2.0
	 *
	 * @param int $owner The CRM owner/creator of the workflow.
	 * @return void
	 */
	public function set_zbs_owner( int $owner ): void {
		$this->zbs_owner = $owner;
	}

	/**
	 * Get name.
	 *
	 * @since 6.2.0
	 *
	 * @return string
	 */
	public function get_name(): string {
		return $this->name;
	}

	/**
	 * Set name.
	 *
	 * @since 6.2.0
	 *
	 * @param string $name The workflow name.
	 * @return void
	 */
	public function set_name( string $name ): void {
		$this->name = $name;
	}

	/**
	 * Get description.
	 *
	 * @since 6.2.0
	 *
	 * @return string
	 */
	public function get_description(): string {
		return $this->description;
	}

	/**
	 * Set description.
	 *
	 * @since 6.2.0
	 *
	 * @param string $description The workflow description.
	 * @return void
	 */
	public function set_description( string $description ): void {
		$this->description = $description;
	}

	/**
	 * Get category.
	 *
	 * @since 6.2.0
	 *
	 * @return string
	 */
	public function get_category(): string {
		return $this->category;
	}

	/**
	 * Set category.
	 *
	 * @since 6.2.0
	 *
	 * @param string $category The workflow category.
	 * @return void
	 */
	public function set_category( string $category ): void {
		$this->category = $category;
	}

	/**
	 * Get the database schema version.
	 *
	 * @since 6.2.0
	 *
	 * @return int
	 */
	public function get_version(): int {
		return $this->version;
	}

	/**
	 * Get the timestamp for when the workflow was created.
	 *
	 * @since 6.2.0
	 *
	 * @return int|null
	 */
	public function get_created_at(): ?int {
		return $this->created_at;
	}

	/**
	 * Get the timestamp for when the workflow was last updated.
	 *
	 * @since 6.2.0
	 *
	 * @return int|null
	 */
	public function get_updated_at(): ?int {
		return $this->updated_at;
	}

	/**
	 * Set the triggers within the workflow given an array of triggers.
	 *
	 * @since 6.2.0
	 *
	 * @param string[] $triggers An array of triggers to be set.
	 * @return void
	 */
	public function set_triggers( array $triggers ): void {
		$this->triggers = $triggers;
	}

	/**
	 * Get the trigger names of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @return string[] The workflow trigger names.
	 */
	public function get_triggers(): array {
		return $this->triggers;
	}

	/**
	 * Instance the triggers of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @throws Workflow_Exception Throws an exception if there is an issue initializing the trigger.
	 * @return void
	 */
	public function init_triggers(): void {

		$this->get_logger()->log( 'Initializing Workflow triggers...' );

		if ( ! $this->is_active() ) {
			$this->get_logger()->log( 'The workflow is not active. No triggers loaded.' );
			return;
		}

		foreach ( $this->get_triggers() as $trigger_slug ) {
			try {
				$trigger_class = $this->get_engine()->get_trigger_class( $trigger_slug );

				/** @var Base_Trigger $trigger */
				$trigger = new $trigger_class();
				$trigger->init( $this );

				$this->get_logger()->log( 'Trigger initialized: ' . $trigger_slug );

			} catch ( Automation_Exception $e ) {
				throw new Workflow_Exception(
					/* Translators: %s is the error message to be included in the exception string. */
					sprintf( __( 'An error happened initializing the trigger. %s', 'zero-bs-crm' ), $e->getMessage() ),
					Workflow_Exception::ERROR_INITIALIZING_TRIGGER
				);
			}
		}
	}

	/**
	 * Set initial step of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @param int|string|null $step_id The initial step id.
	 * @return void
	 */
	public function set_initial_step( $step_id ): void {
		$this->initial_step = $step_id;
	}

	/**
	 * Set the step list of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @param array $steps The steps of the workflow.
	 */
	public function set_steps( array $steps ) {
		$this->steps = $steps;
	}

	/**
	 * Get the workflow as an array.
	 *
	 * The main use-case to get the workflow as an array is to be stored
	 * in the database or if it is being shared via API.
	 *
	 * @since 6.2.0
	 *
	 * @return array The workflow as an array.
	 */
	public function to_array(): array {
		return array(
			'id'           => $this->get_id(),
			'zbs_site'     => $this->get_zbs_site(),
			'zbs_owner'    => $this->get_zbs_owner(),
			'name'         => $this->get_name(),
			'description'  => $this->get_description(),
			'category'     => $this->get_category(),
			'triggers'     => $this->get_triggers(),
			'steps'        => $this->get_steps(),
			'initial_step' => $this->get_initial_step_index(),
			'active'       => $this->is_active(),
			'version'      => $this->get_version(),
			'created_at'   => $this->get_created_at(),
			'updated_at'   => $this->get_updated_at(),
		);
	}

	/**
	 * Get the initial step data of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @return array|null The initial step data of the workflow.
	 */
	public function get_initial_step(): ?array {
		return $this->steps[ $this->get_initial_step_index() ] ?? null;
	}

	/**
	 * Get the initial step index of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @return int|string|null The index key for the next step of the workflow.
	 */
	public function get_initial_step_index() {
		return $this->initial_step;
	}

	/**
	 * Get the steps of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @return array The steps of the workflow.
	 */
	public function get_steps(): array {
		return $this->steps;
	}

	/**
	 * Get the initial step of this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @param int|string $id The step id.
	 * @return array|null The step data instance.
	 */
	public function get_step( $id ): ?array {
		if ( $id === null ) {
			return null;
		}

		return $this->steps[ $id ] ?? null;
	}

	/**
	 * Start the workflow execution once a trigger is activated.
	 *
	 * @since 6.2.0
	 *
	 * @param Trigger        $trigger An instance of the Trigger class.
	 * @param Data_Type|null $data All relevant object data to be passed through the workflow.
	 * @return bool Whether the workflow was executed successfully.
	 *
	 * @throws Automation_Exception|Workflow_Exception Throws an exception if there is an issue executing the workflow.
	 * @throws Data_Transformer_Exception Throws an exception if there is an issue transforming the data.
	 */
	public function execute( Trigger $trigger, ?Data_Type $data = null ): bool {
		return $this->get_engine()->execute_workflow( $this, $trigger, $data );
	}

	/**
	 * Turn on the workflow.
	 *
	 * @since 6.2.0
	 *
	 * @return void
	 */
	public function turn_on(): void {
		$this->active = true;
	}

	/**
	 * Turn off the workflow.
	 *
	 * @since 6.2.0
	 *
	 * @return void
	 */
	public function turn_off(): void {
		$this->active = false;
	}

	/**
	 * Check if the workflow is active.
	 *
	 * @since 6.2.0
	 *
	 * @return bool Whether the workflow is active.
	 */
	public function is_active(): bool {
		return $this->active;
	}

	/**
	 * Add a trigger to this workflow.
	 *
	 * @since 6.2.0
	 *
	 * @param string $string The name of the trigger to add.
	 * @return void
	 */
	public function add_trigger( string $string ): void {
		$this->triggers[] = $string;
	}

	/**
	 * Set the automation engine.
	 *
	 * @since 6.2.0
	 *
	 * @param Automation_Engine $engine An instance of the Automation_Engine class.
	 * @return void
	 * @throws Workflow_Exception|Automation_Exception Exception if there is an issue with the Engine.
	 */
	public function set_engine( Automation_Engine $engine ): void {
		$this->automation_engine = $engine;

		// Process and check the steps when the engine is set.
		$this->process_steps();
	}

	/**
	 * Get the automation engine.
	 *
	 * @since 6.2.0
	 *
	 * @return Automation_Engine Return an instance of the Automation_Engine class.
	 *
	 * @throws Workflow_Exception Throws an exception if there is no engine instance.
	 */
	protected function get_engine(): Automation_Engine {
		if ( ! $this->automation_engine instanceof Automation_Engine ) {
			throw new Workflow_Exception(
				/* Translators: %s The ID of the workflow. */
				sprintf( '[%s] Cannot run workflow logic without an engine instance', $this->get_id() ),
				Workflow_Exception::MISSING_ENGINE_INSTANCE
			);
		}

		return $this->automation_engine;
	}

	/**
	 * Set Logger.
	 *
	 * @since 6.2.0
	 *
	 * @param Automation_Logger $logger An instance of the Automation_Logger class.
	 * @return void
	 */
	public function set_logger( Automation_Logger $logger ) {
		$this->logger = $logger;
	}

	/**
	 * Get Logger.
	 *
	 * @since 6.2.0
	 *
	 * @return Automation_Logger Return an instance of the Automation_Logger class.
	 */
	public function get_logger(): Automation_Logger {
		return $this->logger ?? Automation_Logger::instance();
	}

	/**
	 * Process the steps of the workflow.
	 *
	 * @throws Workflow_Exception|Automation_Exception Exception if there is an issue processing the steps.
	 * @since 6.2.0
	 */
	private function process_steps() {
		foreach ( $this->steps as $step_data ) {
			if ( ! isset( $step_data['class_name'] ) ) {
				$step_data['class_name'] = $this->get_engine()->get_step_class( $step_data['slug'] );
			}
		}
	}

	/**
	 * Set the timestamp for when the workflow was created.
	 *
	 * @since 6.2.0
	 *
	 * @param int $time The timestamp for when the workflow was created.
	 * @return void
	 */
	public function set_created_at( int $time ): void {
		$this->created_at = $time;
	}

	/**
	 * Set the timestamp for when the workflow was last updated.
	 *
	 * @since 6.2.0
	 *
	 * @param int $time The timestamp for when the workflow was last updated.
	 * @return void
	 */
	public function set_updated_at( int $time ): void {
		$this->updated_at = $time;
	}

	/**
	 * Set the id of the workflow.
	 *
	 * @since 6.2.0
	 *
	 * @param int|string $id The workflow id.
	 * @return void
	 */
	public function set_id( $id ): void {
		$this->id = $id;
	}
}