<?php

namespace Limb_Chatbot\Includes\Data_Objects;

use Limb_Chatbot\Includes\Services\Collection;
use Limb_Chatbot\Includes\Services\Data_Object_Collection;

class Action extends WPDB_Data_Object {
	const TABLE_NAME = 'lbaic_actions';
	const FILLABLE = [
		'id',
		'title',
		'name',
		'type',
		'access_roles',
		'config',
		'ai_instructions',
		'status',
		'created_at',
		'updated_at'
	];

	const STATUS_INACTIVE = 0;
	const STATUS_ACTIVE = 1;
	const TYPE_CONVERSATIONAL = 1;
	const ROLE_EVERYONE = 'everyone';
	const TYPE_STATIC = 2;
	const DEFAULT_CONFIG = [
		'show_fields'   => 1,
		'callback_text' => null,
	];
	const CONFIG_KEY_SHOW_FIELDS = 'show_fields';

	public ?int $type = null;
	public ?array $access_roles = null;
	public ?array $config = null;

	public ?int $status;

	public string $title;

	/**
	 * @var string Action name
	 */
	public string $name;

	/**
	 * @var string AI instructions
	 */
	public string $ai_instructions;

	/**
	 * @var string Created timestamp
	 */
	public $created_at;

	/**
	 * @var string Updated timestamp
	 */
	public $updated_at;

	/**
	 * @json_excluded
	 * @var array Meta properties to load
	 */
	public array $meta_properties = array( 'parameters', 'action_callbacks', 'entries_count' );

	/**
	 * @json_excluded
	 * @var mixed Included relations
	 */
	public array $included = [];

	/**
	 * When set, parameters() returns this collection instead of loading from the database (synthetic actions).
	 *
	 * @var Data_Object_Collection|null
	 */
	private ?Data_Object_Collection $runtime_parameters = null;

	public function __construct( $instance = null ) {
		if ( is_array( $instance ) && ! empty( $instance['access_roles'] ) && $this->isJson( $instance['access_roles'] ) ) {
			$instance['access_roles'] = json_decode( $instance['access_roles'], true );
		} elseif ( is_object( $instance ) && ! empty( $instance->access_roles ) && $this->isJson( $instance->access_roles ) ) {
			$instance->access_roles = json_decode( $instance->access_roles, true );
		}
		if ( is_array( $instance ) && ! empty( $instance['config'] ) && $this->isJson( $instance['config'] ) ) {
			$instance['config'] = json_decode( $instance['config'], true );
		} elseif ( is_object( $instance ) && ! empty( $instance->config ) && $this->isJson( $instance->config ) ) {
			$instance->config = json_decode( $instance->config, true );
		}
		parent::__construct( $instance );
	}

	/**
	 * Find action by name
	 *
	 * @param  string  $action_name  Action name
	 *
	 * @return static|null
	 */
	public static function find_by_name( $action_name ) {
		return self::where( [ 'name' => $action_name ] )->first() ?? null;
	}

	/**
	 * Restore a synthetic action from JSON stored on the action plan (not from the database).
	 *
	 * @param  array{action: array<string, mixed>, parameters: array<int, array<string, mixed>>}  $snapshot
	 */
	public static function from_inline_snapshot( array $snapshot ): ?self {
		if ( empty( $snapshot['action'] ) || ! is_array( $snapshot['action'] ) ) {
			return null;
		}
		$action = new self( $snapshot['action'] );
		$params = [];
		if ( ! empty( $snapshot['parameters'] ) && is_array( $snapshot['parameters'] ) ) {
			foreach ( $snapshot['parameters'] as $row ) {
				if ( is_array( $row ) ) {
					$params[] = new Parameter( $row );
				}
			}
		}
		$action->set_runtime_parameters( new Data_Object_Collection( $params ) );

		return $action;
	}

	public static function find_by_title( string $value ) {
		return Action::where( [ 'title' => $value ] )->first() ?? null;
	}

	/**
	 * Attach a callback to this action
	 *
	 * @param  int  $callback_id  Callback ID
	 * @param  int  $order  Execution order (default 1)
	 * @param  int  $is_required  Whether callback is required (default 1)
	 *
	 * @return Action_Callback
	 * @throws \Exception
	 */
	public function attach_callback( int $callback_id, int $order = 1, int $is_required = 1 ): Action_Callback {
		// Check if callback already exists for this action
		$existing = Action_Callback::where( [
			'action_id' => $this->get_id(),
			'id'        => $callback_id
		] )->first();

		if ( $existing ) {
			// Update existing callback
			$existing->set_order( $order );
			$existing->set_is_required( $is_required );
			$existing->touch();
			$existing->save();

			return $existing;
		}

		// Create new callback linked to this action
		$callback = new Action_Callback();
		$callback->set_action_id( $this->get_id() );
		$callback->set_id( $callback_id );
		$callback->set_order( $order );
		$callback->set_is_required( $is_required );
		$callback->save();

		return $callback;
	}

	/**
	 * Check if action has any callbacks
	 *
	 * @return null|Collection
	 */
	public function has_callbacks(): ?Collection {
		$callbacks = $this->get_callbacks();

		return $callbacks->is_empty() ? null : $callbacks;
	}

	/**
	 * Get action callbacks ordered by execution order
	 *
	 * @return Data_Object_Collection Collection of Action_Callback objects
	 */
	public function get_callbacks(): Data_Object_Collection {
		return Action_Callback::where( [ 'action_id' => $this->get_id() ], - 1, - 1, '`order`', 'asc' );
	}

	/**
	 * Get created timestamp
	 *
	 * @return string
	 */
	public function get_created_at() {
		return $this->created_at;
	}

	/**
	 * Set created timestamp
	 *
	 * @param  string  $created_at
	 *
	 * @return void
	 */
	public function set_created_at( $created_at ): void {
		$this->created_at = $created_at;
	}

	/**
	 * Get updated timestamp
	 *
	 * @return string
	 */
	public function get_updated_at() {
		return $this->updated_at;
	}

	/**
	 * Set updated timestamp
	 *
	 * @param  string  $updated_at
	 *
	 * @return void
	 */
	public function set_updated_at( $updated_at ): void {
		$this->updated_at = $updated_at;
	}

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

	/**
	 * Set action name
	 *
	 * @param  string  $name
	 *
	 * @return void
	 */
	public function set_name( string $name ): void {
		$this->name = $name;
	}

	/**
	 * Get AI instructions
	 *
	 * @return string|null
	 */
	public function get_ai_instructions(): ?string {
		return $this->ai_instructions;
	}

	/**
	 * Set AI instructions
	 *
	 * @param  string|null  $ai_instructions
	 *
	 * @return void
	 */
	public function set_ai_instructions( ?string $ai_instructions ): void {
		$this->ai_instructions = $ai_instructions;
	}

	/**
	 * Get parameters (alias for parameters() method)
	 *
	 * @return Data_Object_Collection
	 */
	public function get_parameters(): Data_Object_Collection {
		return $this->parameters();
	}

	/**
	 * Get action parameters
	 *
	 * @param  Data_Object_Collection|null  $parameters
	 *
	 * @return void
	 */
	public function set_runtime_parameters( ?Data_Object_Collection $parameters ): void {
		$this->runtime_parameters = $parameters;
	}

	public function parameters(): Data_Object_Collection {
		if ( $this->runtime_parameters instanceof Data_Object_Collection ) {
			return $this->runtime_parameters;
		}

		return Parameter::where( [ 'action_id' => $this->get_id() ],  - 1, - 1, '`order`', 'asc' );
	}

	public function action_callbacks(): Data_Object_Collection {
		return Action_Callback::where( [ 'action_id' => $this->get_id() ] );
	}

	/**
	 * Delete actions matching the where condition
	 *
	 * Also deletes associated parameters and action callbacks
	 *
	 * @param  array  $where  Where condition for deletion
	 *
	 * @return bool True on success, false on failure
	 */
	public static function delete( $where ): bool {
		$items = self::where( $where );
		if ( ! $items->is_empty() ) {
			$items->each( function ( Action $action ) {
				Parameter::delete( [ 'action_id' => $action->get_id() ] );
				Action_Callback::delete( [ 'action_id' => $action->get_id() ] );
			} );
		}

		return parent::delete( $where );
	}

	public function get_status(): ?int {
		return $this->status;
	}

	public function set_status( ?int $status ): void {
		$this->status = $status;
	}

	public function has_callback( $name ) {
		return ! Action_Callback::where(['action_id' => $this->id, 'name' => $name])->is_empty();
	}

	public function get_title(): string {
		return $this->title;
	}

	public function set_title( string $title ): void {
		$this->title = $title;
	}

	public function get_type(): ?int {
		return $this->type;
	}

	public function set_type( ?int $type ): void {
		$this->type = $type;
	}

	public function get_access_roles(): ?array {
		return $this->access_roles;
	}

	public function set_access_roles( ?array $access_roles ): void {
		$this->access_roles = $access_roles;
	}

	public function get_config(): ?array {
		return $this->config;
	}

	public function set_config( ?array $config ): void {
		$this->config = $config;
	}

	public function get_chatbot_uuid(): ?string {
		return $this->chatbot_uuid;
	}

	public function set_chatbot_uuid( ?string $chatbot_uuid ): void {
		$this->chatbot_uuid = $chatbot_uuid;
	}

	public function get_chatbot(): ?Chatbot {
		$chatbot = property_exists( $this, 'chatbot_uuid' ) && ! empty( $this->chatbot_uuid ) ? Chatbot::find_by_uuid( $this->chatbot_uuid ) : null;
		if ( ! $chatbot instanceof Chatbot ) {
			$chatbot = Chatbot::make();
		}

		return $chatbot;
	}

	public function entries_count() {
		return Action_Submission::count( [ 'action_id' => $this->get_id() ] );
	}

	public function get_config_value( $key ) {
		$value = ! empty( $this->get_config()[ $key ] ) ? $this->get_config()[ $key ] : null;
		if ( ! is_string( $value ) ) {
			return $value;
		}
		if ( 'callback_text' !== (string) $key ) {
			return $value;
		}

		return apply_filters(
			'lbaic_translate_dynamic_string',
			$value,
			sprintf( 'action.%d.callback_text', (int) $this->get_id() ),
			[
				'scope'     => 'action_callback_text',
				'action_id' => (int) $this->get_id(),
			]
		);
	}
}