<?php

namespace Limb_Chatbot\Includes\Services\Notifications;

use Exception;
use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Chatbot_User;
use Limb_Chatbot\Includes\Data_Objects\Setting;
use Limb_Chatbot\Includes\Interfaces\Notification_Type_Interface;
use Limb_Chatbot\Includes\Services\Helper;

/**
 * Main entry point for the notification system.
 *
 * Queues notifications (via Action Scheduler), checks whether a notification should be sent
 * (type + role + user/global opt-in), and processes queued items. Notifications are always sent to Chatbot_User.
 * Supports instant (send immediately) vs queued delivery per type.
 *
 * @package Limb_Chatbot\Includes\Services\Notifications
 * @since 1.0.15
 */
class Notification_Service {

	/**
	 * Action Scheduler hook name for processing a single notification.
	 */
	const ACTION_PROCESS_NOTIFICATION = 'lbaic_process_notification';

	/**
	 * Action Scheduler group for notification jobs.
	 */
	const ACTION_GROUP = 'lbaic_notifications';

	/**
	 * Meta key prefix for per-user opt-in: notification_opted_in_{type_key}.
	 * If meta exists and value is false/0, do not send. If meta does not exist, use global setting.
	 */
	const USER_META_OPTED_IN_PREFIX = 'notification_opted_in_';

	/**
	 * Setting key prefix for global default per type: lbaic.notifications.notification_type.{type_key}.
	 * When user meta is not set, send based on this setting (1/enabled = send, 0/disabled = do not send).
	 * If setting does not exist, default is enabled (send).
	 */
	const SETTING_NOTIFICATION_TYPE_PREFIX = 'notifications.notification_type.';
	/**
	 * Key in payload for log row IDs per channel (set during queueing, used during processing).
	 */
	const PAYLOAD_LOG_IDS = 'log_ids';
	/**
	 * @var Notification_Type_Registry
	 * @since 1.0.15
	 */
	private Notification_Type_Registry $type_registry;
	/**
	 * @var Notification_Channel_Registry
	 * @since 1.0.15
	 */
	private Notification_Channel_Registry $channel_registry;
	/**
	 * @var Notification_Role_Resolver
	 * @since 1.0.15
	 */
	private Notification_Role_Resolver $role_resolver;
	/**
	 * @var Notification_Log_Repository|null
	 * @since 1.0.15
	 */
	private ?Notification_Log_Repository $log_repository;

	/**
	 * Constructor.
	 *
	 * @param  Notification_Type_Registry  $type_registry  Type registry.
	 * @param  Notification_Channel_Registry  $channel_registry  Channel registry.
	 * @param  Notification_Role_Resolver  $role_resolver  Role resolver.
	 * @param  Notification_Log_Repository|null  $log_repository  Optional log repository.
	 *
	 * @since 1.0.15
	 */
	public function __construct(
		Notification_Type_Registry $type_registry,
		Notification_Channel_Registry $channel_registry,
		Notification_Role_Resolver $role_resolver,
		?Notification_Log_Repository $log_repository = null
	) {
		$this->type_registry    = $type_registry;
		$this->channel_registry = $channel_registry;
		$this->role_resolver    = $role_resolver;
		$this->log_repository   = $log_repository ?? new Notification_Log_Repository();
	}

	/**
	 * Queue or send a notification for one or more Chatbot_Users.
	 *
	 * Accepts a single Chatbot_User or an array of Chatbot_User. For each recipient, checks: type exists,
	 * role allowed, and user/global opt-in enabled. If the type is instant, sends immediately;
	 * otherwise schedules via Action Scheduler. Payload must be serializable.
	 *
	 * @param  Chatbot_User|Chatbot_User[]  $recipients  Target chatbot user(s).
	 * @param  string  $type_key  Registered notification type key.
	 * @param  array  $payload  Context data (e.g. ['chat_uuid' => '...']). Must be serializable.
	 *
	 * @return bool True if at least one notification was sent or queued, false if all skipped.
	 * @since 1.0.15
	 */
	public function queue( Chatbot $chatbot, $recipients, string $type_key, array $payload = [] ): bool {
		$recipients = is_array( $recipients ) ? $recipients : [ $recipients ];
		$any        = false;

		foreach ( $recipients as $recipient ) {
			if ( ! $recipient instanceof Chatbot_User ) {
				continue;
			}
			if ( $this->queue_one( $chatbot, $recipient, $type_key, $payload ) ) {
				$any = true;
			}
		}

		return $any;
	}

	/**
	 * Queue or send a notification for a single Chatbot_User.
	 *
	 * is_notification_enabled_for_user is checked only here (queue time), so we never insert
	 * a pending row for opted-out users. Pending rows are created only when we will process/send.
	 *
	 * @param  Chatbot_User  $recipient  Target chatbot user.
	 * @param  string  $type_key  Notification type key.
	 * @param  array  $payload  Context payload (serializable).
	 *
	 * @return bool True if sent or queued, false if skipped.
	 * @since 1.0.15
	 */
	private function queue_one( ?Chatbot $chatbot, Chatbot_User $recipient, string $type_key, array $payload = [] ): bool {
		if ( ! $this->should_send( $recipient, $type_key ) || ! $chatbot instanceof Chatbot ) {
			return false;
		}

		if ( ! $this->is_notification_enabled_for_user( $chatbot, $recipient, $type_key ) ) {
			return false;
		}

		$type = $this->type_registry->get( $type_key );
		if ( ! $type instanceof Notification_Type_Interface ) {
			return false;
		}

		$channels = $type->get_channels();
		if ( empty( $channels ) ) {
			return false;
		}

		$chatbot_user_id = $recipient->get_id();
		if ( empty( $chatbot_user_id ) ) {
			return false;
		}

		$log_ids = [];
		foreach ( $channels as $channel_key ) {
			$channel = $this->channel_registry->get( $channel_key );
			if ( $channel && $channel->can_deliver_to( $recipient ) ) {
				$id = $this->log_repository->insert_pending( $chatbot_user_id, $type_key, $channel_key );
				if ( $id !== null ) {
					$log_ids[ $channel_key ] = $id;
				}
			}
		}
		if ( empty( $log_ids ) ) {
			return false;
		}
		$payload = array_merge( $payload, [ self::PAYLOAD_LOG_IDS => $log_ids ] );

		if ( $type->is_instant() ) {
			$this->process( $chatbot_user_id, $type_key, $payload );

			return true;
		}

		return $this->schedule_action( $chatbot_user_id, $type_key, $payload );
	}

	/**
	 * Check whether the notification should be sent to this user (type exists, role allowed).
	 *
	 * @param  Chatbot_User  $recipient  Target chatbot user.
	 * @param  string  $type_key  Notification type key.
	 *
	 * @return bool
	 * @since 1.0.15
	 */
	public function should_send( Chatbot_User $recipient, string $type_key ): bool {
		$type = $this->type_registry->get( $type_key );
		if ( ! $type instanceof Notification_Type_Interface ) {
			return false;
		}

		$role  = $this->role_resolver->resolve( $recipient );
		$roles = $type->get_target_roles();

		return in_array( $role, $roles, true );
	}

	/**
	 * Check whether the notification is enabled for this chatbot user.
	 *
	 * First checks user meta: notification_opted_in_{type_key}. If meta exists and is false/0, do not send.
	 * If meta does not exist, falls back to Setting::find('lbaic.notifications.notification_type.{type_key}').
	 * If that setting exists and is false/0, do not send. If setting does not exist, default is enabled (send).
	 *
	 * @param  Chatbot_User  $recipient  Target chatbot user.
	 * @param  string  $type_key  Notification type key.
	 *
	 * @return bool True if sending is enabled for this user, false otherwise.
	 * @since 1.0.15
	 */
	public function is_notification_enabled_for_user( ?Chatbot $chatbot, Chatbot_User $recipient, string $type_key ): bool {
		if ( ! $chatbot instanceof Chatbot ) {
			return false;
		}
		$meta_key = self::USER_META_OPTED_IN_PREFIX . $type_key;
		$user_val = $recipient->get_meta( $meta_key );

		// User has an explicit preference: respect it.
		if ( $user_val !== null && $user_val !== '' ) {
			return $this->value_is_enabled( $user_val );
		}

		// No user preference: use global setting for this notification type.
		$parameter = $chatbot->get_parameter( self::SETTING_NOTIFICATION_TYPE_PREFIX . $type_key );
		if ( $parameter === null ) {
			return true;
		}

		return $this->value_is_enabled( $parameter );
	}

	/**
	 * Interpret a stored value as enabled (true) or disabled (false).
	 *
	 * @param  mixed  $value  Stored value (e.g. 1, '1', true, 0, '0', false).
	 *
	 * @return bool
	 * @since 1.0.15
	 */
	private function value_is_enabled( $value ): bool {
		if ( $value === true || $value === 1 || $value === '1' ) {
			return true;
		}
		if ( $value === false || $value === 0 || $value === '0' ) {
			return false;
		}

		return (bool) $value;
	}

	/**
	 * Process a single queued notification (called by Action Scheduler).
	 *
	 * Opt-in is not re-checked here so pending rows are always updated to sent/failed (no stuck pending).
	 * Payload may contain log_ids (channel => id) from queue time; those rows are updated to completed.
	 *
	 * @param  int  $chatbot_user_id  Chatbot user ID.
	 * @param  string  $type_key  Notification type key.
	 * @param  array  $payload  Context payload (serializable); may include 'log_ids' from queue.
	 *
	 * @return void
	 * @since 1.0.15
	 */
	public function process( int $chatbot_user_id, string $type_key, array $payload = [] ): void {
		$recipient = Chatbot_User::find( $chatbot_user_id );
		if ( ! $recipient instanceof Chatbot_User ) {
			return;
		}

		$log_ids         = $payload[ self::PAYLOAD_LOG_IDS ] ?? [];
		$context_payload = $payload;
		unset( $context_payload[ self::PAYLOAD_LOG_IDS ] );

		$type = $this->type_registry->get( $type_key );
		if ( ! $type instanceof Notification_Type_Interface ) {
			return;
		}

		$channels = $type->get_channels();
		foreach ( $channels as $channel_key ) {
			$channel = $this->channel_registry->get( $channel_key );
			if ( ! $channel || ! $channel->can_deliver_to( $recipient ) ) {
				$this->mark_log_failed_if_pending( $channel_key, $log_ids, __( 'Channel cannot deliver to recipient.', 'limb-chatbot' ) );

				continue;
			}

			$envelope = $type->build_envelope( $recipient, $context_payload );
			if ( ! $envelope instanceof Notification_Envelope ) {
				$this->mark_log_failed_if_pending( $channel_key, $log_ids, __( 'Envelope could not be built.', 'limb-chatbot' ) );

				continue;
			}
			$reason = __( 'Delivery failed.', 'limb-chatbot' );
			try {
				$sent = $channel->send( $recipient, $envelope );
			} catch ( Exception $e ) {
				$sent   = false;
				$reason .= ': ' . Helper::get_wp_error( $e )->get_error_message();
			}

			if ( $this->log_repository ) {
				$log_id = $log_ids[ $channel_key ] ?? null;
				$content_type = $envelope->get_content_type();
				$content      = $envelope->get_body();
				if ( $log_id !== null ) {
					$this->log_repository->update_status(
						$log_id,
						$sent ? 'sent' : 'failed',
						null,
						$sent ? null : $reason,
						$content_type,
						$content
					);
				} else {
					$this->log_repository->log(
						$chatbot_user_id,
						$type_key,
						$channel_key,
						$sent ? 'sent' : 'failed',
						$sent ? null : $reason,
						$content_type,
						$content
					);
				}
			}
		}
	}

	/**
	 * Mark a pending log row as failed when we skip sending (e.g. can't deliver or no envelope).
	 *
	 * @param  string  $channel_key  Channel key.
	 * @param  array  $log_ids  Map of channel => log id.
	 * @param  string|null  $fail_reason  Reason for failure.
	 *
	 * @return void
	 * @since 1.0.15
	 */
	private function mark_log_failed_if_pending(
		string $channel_key,
		array $log_ids,
		?string $fail_reason = null
	): void {
		$log_id = $log_ids[ $channel_key ] ?? null;
		if ( $log_id !== null && $this->log_repository ) {
			$this->log_repository->update_status( $log_id, 'failed', null, $fail_reason );
		}
	}

	/**
	 * Schedule the notification via Action Scheduler.
	 *
	 * Uses a short delay (default 15 seconds) so actions are not all "due now". That lets
	 * WP-Cron process them in bulk (every minute) without triggering long async chains
	 * that sleep 5 seconds between batches and tie up PHP workers (which slows the site).
	 *
	 * @param  int  $chatbot_user_id  Chatbot user ID.
	 * @param  string  $type_key  Type key.
	 * @param  array  $payload  Payload.
	 *
	 * @return bool True if scheduled.
	 * @since 1.0.15
	 */
	private function schedule_action( int $chatbot_user_id, string $type_key, array $payload ): bool {
		if ( ! function_exists( 'as_schedule_single_action' ) ) {
			do_action( self::ACTION_PROCESS_NOTIFICATION, $chatbot_user_id, $type_key, $payload );

			return true;
		}

		$delay_seconds = (int) apply_filters( 'lbaic_notification_schedule_delay_seconds', 15 );
		$delay_seconds = max( 0, $delay_seconds );
		$when          = time() + $delay_seconds;

		$action_id = as_schedule_single_action(
			$when,
			self::ACTION_PROCESS_NOTIFICATION,
			[ $chatbot_user_id, $type_key, $payload ],
			self::ACTION_GROUP
		);

		return $action_id !== 0;
	}
}
