<?php

namespace Limb_Chatbot\Includes\Services\Actions;

use Limb_Chatbot\Includes\Data_Objects\Action;
use Limb_Chatbot\Includes\Data_Objects\Action_Plan;
use Limb_Chatbot\Includes\Data_Objects\Action_Plan_Step;
use Limb_Chatbot\Includes\Data_Objects\Chat;
use Limb_Chatbot\Includes\Data_Objects\Message;
use Limb_Chatbot\Includes\Data_Objects\Parameter;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Factories\Data_Collecting_Processor_Factory;
use Limb_Chatbot\Includes\Utilities\Chatbot_Utility;

/**
 * Class Action_Service
 *
 * Orchestrates action execution and parameter collection workflow.
 * Follows SOLID principles:
 * - Single Responsibility: Coordinates action workflow
 * - Open/Closed: Extensible through dependency injection
 * - Liskov Substitution: Uses interfaces and abstractions
 * - Interface Segregation: Depends on specific services only
 * - Dependency Inversion: Depends on abstractions (injected services)
 *
 * @package Limb_Chatbot\Includes\Services\Actions
 * @since 1.0.0
 */
class Action_Service {

	/**
	 * Chat instance
	 *
	 * @var Chat
	 */
	private Chat $chat;

	/**
	 * Action plan builder
	 *
	 * @var Action_Plan_Builder
	 */
	private Action_Plan_Builder $plan_builder;

	/**
	 * Parameter request message builder
	 *
	 * @var Parameter_Request_Message_Builder
	 */
	private Parameter_Request_Message_Builder $parameter_message_builder;

	private ?Chatbot_Utility $chatbot_utility;

	/**
	 * Constructor with dependency injection
	 *
	 * @param  Chat  $chat  Chat instance
	 * @param  Chatbot_Utility|null  $chatbot_utility  Optional chatbot utility for AI features
	 * @param  Action_Plan_Builder|null  $plan_builder  Plan builder (auto-created if null)
	 * @param  Parameter_Request_Message_Builder|null  $parameter_message_builder  Message builder (auto-created if null)
	 *
	 * @since 1.0.0
	 */
	public function __construct(
		Chat $chat,
		?Chatbot_Utility $chatbot_utility = null,
		?Action_Plan_Builder $plan_builder = null,
		?Parameter_Request_Message_Builder $parameter_message_builder = null
	) {
		$this->chat                      = $chat;
		$this->chatbot_utility           = $chatbot_utility ?? null;
		$this->plan_builder              = $plan_builder ?? new Action_Plan_Builder();
		$this->parameter_message_builder = $parameter_message_builder ?? new Parameter_Request_Message_Builder( $chatbot_utility );
	}

	/**
	 * Dispatch an action and start parameter collection
	 *
	 * @param  Action  $action  The action to dispatch
	 *
	 * @return Message|null The first parameter request message or null
	 * @throws \Exception
	 * @since 1.0.0
	 */
	public function dispatch( Action $action ): ?Message {
		$plan = $this->plan_builder->build( $action, $this->chat );
		$this->save_action_plan( $plan );

		// Return parameter collection message if needed
		$next_step = $plan->next_incomplete_step();
		if ( $next_step && $next_step->get_type() === Action_Plan_Step::TYPE_DATA_COLLECTION ) {
			return $this->parameter_message_builder->build( $plan, $next_step );
		}

		return ( new Action_Executor( $this->chat, $this->chatbot_utility ) )->execute( $plan );
	}

	/**
	 * Save action plan to chat meta
	 *
	 * @param  Action_Plan  $plan  The action plan to save
	 *
	 * @return void
	 * @since 1.0.0
	 */
	private function save_action_plan( Action_Plan $plan ): void {
		$this->chat->update_meta( Chat::KEY_CURRENT_ACTION, $plan );
	}

	/**
	 * Check for ongoing action and handle parameter collection with validation
	 *
	 * @param  Message  $user_message  User's message
	 *
	 * @return Message|null Message if action is ongoing, null otherwise
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function check_ongoing_action( Message $user_message ): ?Message {
		$action_plan = $this->chat->current_action_plan();
		if ( ! $action_plan instanceof Action_Plan ) {
			return null;
		}
		if ( $message = $this->check_for_cancellation( $action_plan, $user_message ) ) {
			return $message;
		}
		if ( $message = $this->check_for_undo_parameter( $action_plan, $user_message ) ) {
			return $message;
		}

		// Re-fetch action plan as it may have been updated by check_for_undo_parameter
		$action_plan = $this->chat->current_action_plan();
		if ( ! $action_plan instanceof Action_Plan ) {
			return null;
		}

		$current_step = $action_plan->next_incomplete_step();
		if ( ! $current_step ) {
			// All parameters collected, execute action
			return ( new Action_Executor( $this->chat, $this->chatbot_utility ) )->execute( $action_plan );
		}
		if ( $current_step->is_data_collect_step() ) {
			$data_collect_processor = ( new Data_Collecting_Processor_Factory() )->make( $current_step, $this->chat, $this->chatbot_utility );

			return $data_collect_processor->process( $action_plan, $current_step, $user_message );
		}

		return null;
	}

	/**
	 * Checks if the user message contains a cancellation request for the given action plan.
	 *
	 * Handles three scenarios:
	 * 1. Only action cancellation: Returns AI-generated friendly cancellation message (with fallback)
	 * 2. Action cancellation + text content: Clears action and returns null to allow LLM to process naturally
	 * 3. No cancellation: Returns null to continue normal flow
	 *
	 * @param  Action_Plan  $action_plan  The action plan currently being executed.
	 * @param  Message  $user_message  The user's incoming message containing potential cancellation data.
	 *
	 * @return Message|null  A confirmation message if only cancellation (no text), null otherwise.
	 */
	private function check_for_cancellation( Action_Plan $action_plan, Message $user_message ) {
		$cancel   = false;
		$has_text = false;

		foreach ( $user_message->get_content() as $part ) {
			if ( isset( $part['type'] ) && $part['type'] === Message::CONTENT_TYPE_ACTION_CANCELLATION ) {
				$cancel = true;
			}
			if ( isset( $part['type'] ) && $part['type'] === Message::CONTENT_TYPE_TEXT ) {
				$has_text = true;
			}
		}

		if ( ! $cancel ) {
			return null;
		}

		$this->chat->update_meta( Chat::KEY_CURRENT_ACTION, null );
		if ( $has_text ) {
			return null;
		}

		$stream  = $this->chatbot_utility && $this->chatbot_utility->get_stream();
		$builder = new Action_Cancellation_Message_Builder( $this->chatbot_utility );

		return $builder->build( $action_plan, $stream );
	}

	/**
	 * Checks if the user message contains an undo_parameter request and handles it.
	 *
	 * When type undo_parameter is present:
	 * - Removes from the chat all messages starting from the given parameter question
	 * - Updates the action plan: current and previous parameters are unfilled
	 * - Generates a new parameter request message for the parameter we undid to
	 *
	 * The response message includes removed_messages in included and the new parameter message.
	 *
	 * @param  Action_Plan  $action_plan  The action plan currently being executed.
	 * @param  Message  $user_message  The user's incoming message (may contain undo_parameter content).
	 *
	 * @return Message|null  The new parameter request message with included['removed_messages'], or null.
	 */
	private function check_for_undo_parameter( Action_Plan $action_plan, Message $user_message ): ?Message {
		$undo_data = null;
		foreach ( $user_message->get_content() ?? [] as $part ) {
			if ( isset( $part['type'] ) && $part['type'] === Message::CONTENT_TYPE_UNDO_PARAMETER ) {
				$undo_data = $part[ Message::CONTENT_TYPE_UNDO_PARAMETER ] ?? [];
				break;
			}
		}
		if ( empty( $undo_data ) || ! is_array( $undo_data ) ) {
			return null;
		}

		$message_uuid   = $undo_data['message_uuid'] ?? null;
		$parameter_name = $undo_data['parameter_name'] ?? null;
		if ( ! $message_uuid && ! $parameter_name ) {
			return null;
		}

		$from_message = $this->find_parameter_question_message( $message_uuid, $parameter_name );
		if ( ! $from_message ) {
			return null;
		}

		$all_messages = Message::where(
			[ 'chat_uuid' => $this->chat->get_uuid() ],
			-1,
			1,
			'id',
			'ASC'
		)->get();
		$to_remove = [];
		$found     = false;
		foreach ( $all_messages as $msg ) {
			if ( (int) $msg->id === (int) $from_message->id ) {
				$found = true;
			}
			if ( $found ) {
				$to_remove[] = $msg;
			}
		}
		if ( empty( $to_remove ) ) {
			return null;
		}

		$param_name = $parameter_name ?: $this->get_parameter_name_from_message_content( $from_message );

		if ( ! $param_name ) {
			return null;
		}

		$step_to_redo = $this->find_step_for_parameter_by_name( $action_plan, $param_name );
		if ( ! $step_to_redo ) {
			return null;
		}

		$this->unfill_step_and_next( $action_plan, $step_to_redo );
		$this->save_action_plan( $action_plan );

		foreach ( $to_remove as $msg ) {
			Message::delete( [ 'id' => $msg->id ] );
		}

		$parameter_message = $this->parameter_message_builder->build( $action_plan, $step_to_redo );
		if ( ! $parameter_message ) {
			return null;
		}

		$parameter_message->included['removed_messages'] = array_values( $to_remove );

		return $parameter_message;
	}

	/**
	 * Find the assistant message that is the parameter question (by message_uuid or parameter_name).
	 *
	 * @param  string|null  $message_uuid  Message UUID to find.
	 * @param  string|null  $parameter_name  Parameter name to find in content (used if message_uuid empty).
	 *
	 * @return Message|null
	 */
	private function find_parameter_question_message( ?string $message_uuid, ?string $parameter_name ): ?Message {
		if ( $message_uuid ) {
			$coll = Message::where(
				[ 'chat_uuid' => $this->chat->get_uuid(), 'uuid' => $message_uuid ],
				1
			);
			return $coll->is_empty() ? null : $coll->first();
		}
		$all = Message::where(
			[ 'chat_uuid' => $this->chat->get_uuid(), 'role' => Message::ROLE_ASSISTANT ],
			-1,
			1,
			'id',
			'ASC'
		)->get();
		foreach ( $all as $msg ) {
			$name = $this->get_parameter_name_from_message_content( $msg );
			if ( $name === $parameter_name ) {
				return $msg;
			}
		}
		return null;
	}

	/**
	 * Get the parameter name from an assistant message that contains a parameter content part.
	 * Handles both Parameter object and array (e.g. from DB) in content.
	 *
	 * @param  Message  $message
	 *
	 * @return string|null
	 */
	private function get_parameter_name_from_message_content( Message $message ): ?string {
		foreach ( $message->get_content() ?? [] as $part ) {
			if ( isset( $part['type'] ) && $part['type'] === Message::CONTENT_TYPE_PARAMETER && isset( $part['parameter'] ) ) {
				$p = $part['parameter'];
				if ( $p instanceof Parameter ) {
					return $p->get_name();
				}
				if ( is_array( $p ) && ! empty( $p['name'] ) ) {
					return $p['name'];
				}
			}
		}
		return null;
	}

	/**
	 * Find the action plan step that collects the given parameter by name.
	 *
	 * @param  Action_Plan  $action_plan
	 * @param  string  $parameter_name
	 *
	 * @return Action_Plan_Step|null
	 */
	private function find_step_for_parameter_by_name( Action_Plan $action_plan, string $parameter_name ): ?Action_Plan_Step {
		foreach ( $action_plan->steps as $step ) {
			if ( ! $step instanceof Action_Plan_Step || ! $step->is_data_collect_step() ) {
				continue;
			}
			$data = $step->get_data();
			if ( isset( $data['parameter'] ) && $data['parameter'] instanceof Parameter && $data['parameter']->get_name() === $parameter_name ) {
				return $step;
			}
		}
		return null;
	}

	/**
	 * Set the given step and the next data-collection step to INCOMPLETE and clear their value.
	 *
	 * @param  Action_Plan  $action_plan
	 * @param  Action_Plan_Step  $step
	 *
	 * @return void
	 */
	private function unfill_step_and_next( Action_Plan $action_plan, Action_Plan_Step $step ): void {
		$parameter_steps = [];
		foreach ( $action_plan->steps as $s ) {
			if ( ! $s instanceof Action_Plan_Step || ! $s->is_data_collect_step() ) {
				continue;
			}
			$data = $s->get_data();
			if ( ! isset( $data['parameter'] ) || ! $data['parameter'] instanceof Parameter ) {
				continue;
			}
			$parameter_steps[] = $s;
		}
		$index = null;
		foreach ( $parameter_steps as $i => $s ) {
			if ( $s === $step ) {
				$index = $i;
				break;
			}
		}
		if ( $index === null ) {
			return;
		}
		$steps_to_unfill = [ $parameter_steps[ $index ] ];
		if ( isset( $parameter_steps[ $index + 1 ] ) ) {
			$steps_to_unfill[] = $parameter_steps[ $index + 1 ];
		}
		foreach ( $steps_to_unfill as $s ) {
			$s->set_state( Action_Plan_Step::STATE_INCOMPLETE );
			$data = $s->get_data();
			unset( $data['value'] );
			$s->set_data( $data );
		}
	}
}