<?php

namespace SugarCalendarBookings\Integrations;

use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Integrations\ExternalBusyTimes\ExternalBusyTimeService;
use SugarCalendarBookings\Integrations\OAuthRelay\OAuthConnectionModel;
use SugarCalendarBookings\Integrations\Webhooks\IncomingWebhookHandler;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Bootable;
use WP_Error;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Meeting Manager.
 *
 * Provider-agnostic orchestrator that listens to appointment lifecycle hooks
 * and delegates meeting operations to the correct MeetingProviderInterface.
 *
 * Replaces the provider-specific ZoomMeetingManager with a generic version
 * that works with any registered meeting provider.
 *
 * @since 1.4.0
 */
class MeetingManager implements Bootable {

	/**
	 * Integration capability registry.
	 *
	 * @since 1.4.0
	 *
	 * @var IntegrationCapabilityRegistry
	 */
	private IntegrationCapabilityRegistry $registry;

	/**
	 * Cached OAuth connections by provider slug.
	 *
	 * @since 1.4.0
	 *
	 * @var array<string, OAuthConnectionModel|null>
	 */
	private array $connection_cache = [];

	/**
	 * Last meeting creation error, if any.
	 *
	 * Stored so callers (e.g. controllers) can check whether
	 * meeting creation succeeded after the appointment hook fires.
	 *
	 * @since 1.4.0
	 *
	 * @var WP_Error|null
	 */
	private ?WP_Error $last_error = null;

	/**
	 * Constructor.
	 *
	 * @since 1.4.0
	 *
	 * @param IntegrationCapabilityRegistry $registry Capability registry.
	 */
	public function __construct( IntegrationCapabilityRegistry $registry ) {

		$this->registry = $registry;
	}

	/**
	 * Get the last meeting creation error.
	 *
	 * @since 1.4.0
	 *
	 * @return WP_Error|null
	 */
	public function get_last_error() {

		return $this->last_error;
	}

	/**
	 * Check if the meeting manager should load.
	 *
	 * Only register hooks when at least one meeting provider is registered.
	 * Note: providers register during init(), before hooks() is called.
	 *
	 * @since 1.4.0
	 *
	 * @return bool
	 */
	public function allow_load(): bool {

		return count( $this->registry->get( MeetingProviderInterface::class ) ) > 0;
	}

	/**
	 * Register WordPress hook listeners.
	 *
	 * @since 1.4.0
	 */
	public function hooks(): void {

		// phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
		add_action( 'sugar_calendar_bookings_appointment_created', [ $this, 'maybe_create_meeting' ] );
		add_action( 'sugar_calendar_bookings_appointment_updated', [ $this, 'maybe_update_meeting' ] );
		add_action( 'sugar_calendar_bookings_appointment_cancelled', [ $this, 'maybe_delete_meeting' ] );
		add_action( 'sugar_calendar_bookings_appointment_deleted', [ $this, 'maybe_delete_meeting' ] );
		add_action( 'sugar_calendar_bookings_appointment_finalized', [ $this, 'maybe_create_meeting' ] );
		// phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Maybe create a meeting for the appointment.
	 *
	 * Creates a meeting only if:
	 * - Appointment status is approved.
	 * - Appointment has an online location with a registered meeting provider.
	 * - No meeting has been created yet (idempotent).
	 *
	 * @since 1.4.0
	 *
	 * @param AppointmentModel $appointment The appointment model.
	 */
	public function maybe_create_meeting( AppointmentModel $appointment ) {

		$this->last_error = null;

		// Only create meetings for approved appointments.
		if ( $appointment->status !== AppointmentModel::STATUS_APPROVED ) {
			return;
		}

		$online_location = $appointment->online_location;

		if ( ! is_array( $online_location ) ) {
			return;
		}

		// Skip if meeting already exists (idempotent).
		if ( ! empty( $online_location['meeting_id'] ) ) {
			return;
		}

		$provider = $this->resolve_provider( $appointment );

		if ( ! $provider ) {
			return;
		}

		$result = $provider->create_meeting( $appointment );

		if ( is_wp_error( $result ) ) {
			$this->last_error = $result;

			return;
		}

		// Merge provider-returned metadata into online_location.
		$appointment->online_location = array_merge(
			$online_location,
			$result
		);

		// Store meeting_id as a dedicated meta key for indexed lookups.
		if ( ! empty( $result['meeting_id'] ) ) {
			$appointment->provider_meeting_id = (string) $result['meeting_id'];
		}

		$appointment->save();

		$this->upsert_meeting_busy_time( $appointment, $result['provider'] ?? '' );
	}

	/**
	 * Maybe update a meeting when the appointment is updated.
	 *
	 * Delegates to the provider's update_meeting() which handles
	 * provider-specific logic (e.g., recreate on invitee change for Zoom).
	 *
	 * @since 1.4.0
	 *
	 * @param AppointmentModel $appointment The appointment model.
	 */
	public function maybe_update_meeting( AppointmentModel $appointment ) {

		$this->last_error = null;

		// Skip provider API calls when the change originated from a provider webhook.
		if ( IncomingWebhookHandler::is_processing() ) {
			return;
		}

		$online_location = $appointment->online_location;

		// Skip if no online location set.
		if ( ! is_array( $online_location ) ) {
			return;
		}

		// If status changed from pending to approved, create a meeting if none exists.
		if (
			$appointment->has_changed( 'status' ) &&
			$appointment->get_previous( 'status' ) === AppointmentModel::STATUS_PENDING &&
			$appointment->status === AppointmentModel::STATUS_APPROVED &&
			empty( $online_location['meeting_id'] )
		) {
			$this->maybe_create_meeting( $appointment );

			return;
		}

		// Skip if no meeting exists (nothing to update/delete).
		if ( empty( $online_location['meeting_id'] ) ) {
			return;
		}

		// If status changed from approved to pending, delete the meeting.
		if (
			$appointment->has_changed( 'status' ) &&
			$appointment->get_previous( 'status' ) === AppointmentModel::STATUS_APPROVED &&
			$appointment->status === AppointmentModel::STATUS_PENDING
		) {
			$this->maybe_delete_meeting( $appointment );

			return;
		}

		$provider = $this->resolve_provider( $appointment );

		if ( ! $provider ) {
			return;
		}

		$has_relevant_changes = $appointment->has_changed( 'start_at' )
			|| $appointment->has_changed( 'end_at' )
			|| $appointment->has_changed( 'employee_id' )
			|| $appointment->has_changed( 'customer_id' )
			|| $appointment->has_changed( 'service_id' );

		if ( ! $has_relevant_changes ) {
			return;
		}

		$result = $provider->update_meeting( $appointment );

		if ( is_wp_error( $result ) ) {
			$this->last_error = $result;

			return;
		}

		// If update returned new metadata (e.g., recreated meeting), save it.
		if ( is_array( $result ) ) {
			$appointment->online_location = array_merge(
				$appointment->online_location,
				$result
			);

			// Update the dedicated meeting_id meta key.
			if ( ! empty( $result['meeting_id'] ) ) {
				$appointment->provider_meeting_id = (string) $result['meeting_id'];
			}

			$appointment->save();
		}

		$this->upsert_meeting_busy_time( $appointment, $appointment->online_location['provider'] ?? '' );
	}

	/**
	 * Maybe delete a meeting for the appointment.
	 *
	 * @since 1.4.0
	 *
	 * @param AppointmentModel $appointment The appointment model.
	 */
	public function maybe_delete_meeting( AppointmentModel $appointment ): void {

		// Skip provider API calls when the change originated from a provider webhook.
		if ( IncomingWebhookHandler::is_processing() ) {
			return;
		}

		$online_location = $appointment->online_location;

		// Skip if no meeting exists.
		if ( ! is_array( $online_location ) || empty( $online_location['meeting_id'] ) ) {
			return;
		}

		$provider = $this->resolve_provider( $appointment );

		if ( ! $provider ) {
			return;
		}

		$provider->delete_meeting( $appointment );

		Plugin::get( ExternalBusyTimeService::class )->delete_by_provider_event(
			$online_location['provider'],
			$online_location['meeting_id']
		);

		// Clear meeting metadata, keep provider slug.
		$meta_keys = $provider->get_meeting_meta_keys();

		foreach ( $meta_keys as $key ) {
			unset( $online_location[ $key ] );
		}

		$appointment->online_location     = $online_location;
		$appointment->provider_meeting_id = null;

		// Only persist when the appointment is being cancelled, not deleted.
		if ( current_action() !== 'sugar_calendar_bookings_appointment_deleted' ) {
			$appointment->save();
		}
	}

	/**
	 * Upsert a busy time entry for a meeting-linked appointment.
	 *
	 * @since 1.4.0
	 *
	 * @param AppointmentModel $appointment   The appointment.
	 * @param string           $provider_slug Provider slug.
	 */
	private function upsert_meeting_busy_time( AppointmentModel $appointment, string $provider_slug ): void {

		$online_location = $appointment->online_location;

		if ( empty( $online_location['meeting_id'] ) || empty( $provider_slug ) ) {
			return;
		}

		if ( ! $appointment->start_at || ! $appointment->end_at ) {
			return;
		}

		$connection = $this->get_active_connection( $provider_slug );

		if ( ! $connection ) {
			return;
		}

		Plugin::get( ExternalBusyTimeService::class )->upsert_meeting_busy_time(
			$connection,
			$provider_slug,
			$online_location['meeting_id'],
			$appointment->service ? $appointment->service->name : '',
			$appointment->start_at->format( 'Y-m-d H:i:s' ),
			$appointment->end_at->format( 'Y-m-d H:i:s' )
		);
	}

	/**
	 * Get the active OAuth connection for a provider (cached per request).
	 *
	 * @since 1.4.0
	 *
	 * @param string $provider_slug Provider slug.
	 *
	 * @return OAuthConnectionModel|null The connection or null.
	 */
	private function get_active_connection( string $provider_slug ): ?OAuthConnectionModel {

		if ( ! array_key_exists( $provider_slug, $this->connection_cache ) ) {
			$this->connection_cache[ $provider_slug ] = OAuthConnectionModel::query()
				->where( 'provider', '=', $provider_slug, '%s' )
				->where( 'status', '=', 'active', '%s' )
				->first();
		}

		return $this->connection_cache[ $provider_slug ];
	}

	/**
	 * Resolve the meeting provider for an appointment.
	 *
	 * Looks up the provider slug from the appointment's online_location
	 * and finds the matching MeetingProviderInterface in the registry.
	 *
	 * @since 1.4.0
	 *
	 * @param AppointmentModel $appointment The appointment model.
	 *
	 * @return MeetingProviderInterface|null The meeting provider or null if not found.
	 */
	private function resolve_provider( AppointmentModel $appointment ): ?MeetingProviderInterface {

		$online_location = $appointment->online_location;

		if ( ! is_array( $online_location ) || empty( $online_location['provider'] ) ) {
			return null;
		}

		$provider = $this->registry->find(
			MeetingProviderInterface::class,
			$online_location['provider']
		);

		if ( ! $provider instanceof MeetingProviderInterface ) {
			return null;
		}

		return $provider;
	}
}
