<?php

namespace SugarCalendarBookings\Integrations\OAuthRelay;

use SugarCalendarBookings\Admin\Settings\IntegrationsSettingsTab;
use SugarCalendarBookings\Integrations\CalendarConflictProviderInterface;
use SugarCalendarBookings\Integrations\ExternalBusyTimes\ExternalBusyTimeService;
use SugarCalendarBookings\Integrations\IntegrationCapabilityRegistry;
use SugarCalendarBookings\Integrations\Webhooks\IncomingWebhookHandler;
use SugarCalendarBookings\Integrations\Webhooks\WebhookUrlMonitor;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Bootable;
use SugarCalendarBookings\Support\Helpers\Date;
use SugarCalendarBookings\Support\Helpers\WP;
use WP_Error;

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

/**
 * Abstract OAuth Callback Handler.
 *
 * Base class for handling OAuth callbacks from different providers.
 *
 * @since 1.4.0
 */
abstract class AbstractOAuthCallbackHandler implements Bootable {

	/**
	 * OAuth Relay Client.
	 *
	 * @since 1.4.0
	 *
	 * @var OAuthRelayClient
	 */
	protected OAuthRelayClient $oauth_client;

	/**
	 * Constructor.
	 *
	 * @since 1.4.0
	 *
	 * @param OAuthRelayClient $oauth_client OAuth Relay Client.
	 */
	public function __construct( OAuthRelayClient $oauth_client ) {

		$this->oauth_client = $oauth_client;
	}

	/**
	 * Get the provider identifier (e.g., 'google', 'zoom').
	 *
	 * @since 1.4.0
	 *
	 * @return string
	 */
	abstract protected function get_provider(): string;

	/**
	 * Get the default redirect URL if redirect_to is not provided.
	 *
	 * @since 1.4.0
	 *
	 * @return string
	 */
	protected function get_default_redirect_url(): string {

		return IntegrationsSettingsTab::get_url();
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.4.0
	 */
	public function hooks() {

		add_action( 'admin_init', [ $this, 'handle_callback' ] );
	}

	/**
	 * Handle the OAuth callback.
	 *
	 * @since 1.4.0
	 */
	public function handle_callback(): void {   // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded

		// Check if this is our callback.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'scbookings_oauth_callback' ) {
			return;
		}

		// Verify user has permission (admin or employee).
		if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'scb_employee_access' ) ) {
			$this->redirect_with_error( 'local:callback:unauthorized' );

			return;
		}

		// Check if this callback is for our provider.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['provider'] ) || $_GET['provider'] !== $this->get_provider() ) {
			return;
		}

		// Verify nonce.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['nonce'] ), 'scbookings_oauth_connect' ) ) {
			$this->redirect_with_error( 'local:callback:invalid_nonce' );

			return;
		}

		// Check for errors from OAuth provider.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['error'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$this->redirect_with_error( sanitize_text_field( wp_unslash( $_GET['error'] ) ) );

			return;
		}

		// Get exchange code.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['exchange_code'] ) ) {
			$this->redirect_with_error( 'local:callback:missing_exchange_code' );

			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$exchange_code = sanitize_text_field( wp_unslash( $_GET['exchange_code'] ) );

		// Exchange code for tokens.
		$result = $this->oauth_client->exchange_code( $exchange_code );

		if ( is_wp_error( $result ) ) {
			$this->redirect_with_error( $result->get_error_code() );

			return;
		}

		// Validate exchange result.
		$tokens = $result['tokens'] ?? [];
		$user   = $result['user'] ?? [];
		$app_id = $result['app_id'] ?? 0;

		if ( empty( $tokens['access_token'] ) || empty( $tokens['refresh_token'] ) || empty( $tokens['expires_in'] ) ) {
			$this->redirect_with_error( 'local:callback:missing_tokens' );

			return;
		}

		if ( empty( $app_id ) ) {
			$this->redirect_with_error( 'local:callback:missing_app_id' );

			return;
		}

		if ( empty( $user['id'] ) ) {
			$this->redirect_with_error( 'local:callback:missing_account_id' );

			return;
		}

		// Store the connection.
		$stored = $this->store_connection( $tokens, $user, $app_id );

		if ( is_wp_error( $stored ) ) {
			$this->redirect_with_error( $stored->get_error_code() );

			return;
		}

		// Fetch busy times from the provider if it supports conflict checking.
		$this->maybe_fetch_busy_times( $stored );

		// Register webhook for real-time event updates.
		$this->maybe_register_webhook( $stored );

		// Redirect with success.
		$this->redirect_with_success();
	}

	/**
	 * Maybe fetch busy times from the provider after connection.
	 *
	 * If the provider implements CalendarConflictProviderInterface,
	 * fetches busy times for the next 90 days and stores them.
	 *
	 * @since 1.4.0
	 *
	 * @param OAuthConnectionModel $connection The stored connection.
	 */
	protected function maybe_fetch_busy_times( OAuthConnectionModel $connection ): void {

		$provider = $this->find_conflict_provider();

		if ( ! $provider ) {
			return;
		}

		$now  = Date::create();
		$from = $now->format( 'Y-m-d H:i:s' );
		$to   = $now->modify( '+90 days' )->format( 'Y-m-d H:i:s' );

		$events      = $provider->fetch_busy_times( $connection, $from, $to );
		$calendar_id = $provider->get_calendar_id( $connection );

		if ( ! empty( $events ) ) {
			Plugin::get( ExternalBusyTimeService::class )->store_busy_times(
				$connection,
				$this->get_provider(),
				$calendar_id,
				$events
			);
		}
	}

	/**
	 * Register a webhook URL with the Product API for real-time event updates.
	 *
	 * Only registers if the provider supports calendar conflict checking.
	 *
	 * @since 1.4.0
	 *
	 * @param OAuthConnectionModel $connection The stored connection.
	 */
	protected function maybe_register_webhook( OAuthConnectionModel $connection ): void {

		$provider = $this->find_conflict_provider();

		if ( ! $provider ) {
			return;
		}

		$webhook_url = IncomingWebhookHandler::get_webhook_url( $this->get_provider() );

		$response = $this->oauth_client->register_webhook(
			$this->get_provider(),
			$connection->account_id,
			$webhook_url
		);

		$this->maybe_store_subscription_id( $connection, $response );

		// Store the REST base URL so the WebhookUrlMonitor can detect changes.
		update_option( WebhookUrlMonitor::OPTION_REST_BASE_URL, rest_url(), true );
	}

	/**
	 * Persist the relay-issued subscription identifier on the connection row.
	 *
	 * The relay creates the provider-side subscription (e.g. Microsoft Graph)
	 * and returns its identifier in the registration response. Storing it in
	 * the indexed `subscription_id` column lets the webhook handler resolve
	 * the originating connection with an equality lookup instead of a LIKE
	 * scan against the JSON settings blob.
	 *
	 * @since 1.5.0
	 *
	 * @param OAuthConnectionModel $connection The connection that owns the subscription.
	 * @param mixed                $response   register_webhook() response (array on success, WP_Error on failure).
	 */
	private function maybe_store_subscription_id( OAuthConnectionModel $connection, $response ): void {

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

		// Accept the common shapes the relay may return.
		$candidates = [
			$response['subscription_id'] ?? null,
			$response['id'] ?? null,
			$response['subscription']['id'] ?? null,
			$response['data']['subscription_id'] ?? null,
		];

		foreach ( $candidates as $candidate ) {

			if ( ! is_string( $candidate ) || $candidate === '' ) {
				continue;
			}

			$connection->subscription_id = $candidate;
			$connection->save();

			return;
		}
	}

	/**
	 * Find the calendar conflict provider for this OAuth provider.
	 *
	 * @since 1.4.0
	 *
	 * @return CalendarConflictProviderInterface|null
	 */
	private function find_conflict_provider(): ?CalendarConflictProviderInterface {

		$provider = Plugin::get( IntegrationCapabilityRegistry::class )
			->find( CalendarConflictProviderInterface::class, $this->get_provider() );

		return $provider instanceof CalendarConflictProviderInterface ? $provider : null;
	}

	/**
	 * Store OAuth connection data.
	 *
	 * Creates a new connection or updates an existing one for the same user/provider/app/account combination.
	 *
	 * @since 1.4.0
	 *
	 * @param array $tokens Token data (access_token, refresh_token, expires_in, token_type, scope).
	 * @param array $user   User data (id, email, name, avatar).
	 * @param int   $app_id OAuth application ID.
	 *
	 * @return OAuthConnectionModel|\WP_Error
	 */
	protected function store_connection( array $tokens, array $user, int $app_id ) {    // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		// Calculate expires_at from expires_in. Date::create() returns UTC time.
		$expires_at = Date::create()->modify( '+' . $tokens['expires_in'] . ' seconds' );

		$account_id = $user['id'];

		// Check for existing connection (unique constraint: user_id + provider + app_id + account_id).
		$existing = OAuthConnectionModel::query()
			->where( 'user_id', '=', get_current_user_id(), '%d' )
			->where( 'provider', '=', $this->get_provider(), '%s' )
			->where( 'app_id', '=', $app_id, '%d' )
			->where( 'account_id', '=', $account_id, '%s' )
			->first();

		$connection_data = [
			'status'         => 'active',
			'access_token'   => $tokens['access_token'],
			'refresh_token'  => $tokens['refresh_token'],
			'expires_at'     => $expires_at->format( 'Y-m-d H:i:s' ),
			'token_type'     => $tokens['token_type'] ?? 'bearer',
			'scope'          => $tokens['scope'] ?? '',
			'account_email'  => $user['email'] ?? '',
			'account_name'   => $user['name'] ?? '',
			'account_avatar' => $user['avatar'] ?? '',
		];

		if ( $existing ) {
			// Update existing connection with new tokens.
			foreach ( $connection_data as $key => $value ) {
				$existing->$key = $value;
			}

			$existing->save();

			$connection = $existing;
		} else {
			// Create new connection.
			$connection_data['user_id']      = get_current_user_id();
			$connection_data['provider']     = $this->get_provider();
			$connection_data['app_id']       = $app_id;
			$connection_data['account_id']   = $account_id;
			$connection_data['connected_at'] = Date::create()->format( 'Y-m-d H:i:s' );

			$connection = OAuthConnectionModel::create( $connection_data );
		}

		if ( empty( $connection->id ) ) {
			return new WP_Error( 'local:storage:save_failed', esc_html__( 'Failed to save connection.', 'sugar-calendar-bookings-scheduling-appointments-lite' ) );
		}

		return $connection;
	}

	/**
	 * Get the redirect URL from request or default.
	 *
	 * @since 1.4.0
	 *
	 * @return string
	 */
	protected function get_redirect_url(): string {

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$redirect_to = isset( $_GET['redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) : '';

		return wp_validate_redirect( $redirect_to, $this->get_default_redirect_url() );
	}

	/**
	 * Map error codes to user-friendly messages.
	 *
	 * @since 1.4.0
	 *
	 * @param string $error_code Error code.
	 *
	 * @return string
	 */
	protected function get_error_message( string $error_code ): string {

		$messages = [
			// Local callback errors.
			'local:callback:unauthorized'          => esc_html__( 'You do not have permission to perform this action.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'local:callback:invalid_nonce'         => esc_html__( 'Invalid security token. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'local:callback:missing_exchange_code' => esc_html__( 'Missing authorization code. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'local:callback:missing_tokens'        => esc_html__( 'Token data is missing. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'local:callback:missing_app_id'        => esc_html__( 'Application information is missing. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'local:callback:missing_account_id'    => esc_html__( 'Account information is missing. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),

			// Local storage errors.
			'local:storage:save_failed'            => esc_html__( 'Failed to save connection. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),

			// OAuth Relay start errors.
			'start:site_not_registered'            => esc_html__( 'Your site is not registered. Please contact support.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'start:domain_mismatch'                => esc_html__( 'Authorization failed due to domain mismatch. Reactivate your license for the new domain and try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'start:invalid_provider'               => esc_html__( 'Invalid OAuth provider.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'start:no_active_app'                  => esc_html__( 'The provider app is unavailable. Please contact support.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),

			// OAuth Relay callback errors.
			'callback:session_expired'             => esc_html__( 'The authorization session expired. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'callback:missing_code'                => esc_html__( 'Missing authorization code from provider. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'callback:invalid_provider'            => esc_html__( 'Invalid OAuth provider.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'callback:app_not_found'               => esc_html__( 'OAuth application not found. Please contact support.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'callback:token_exchange_failed'       => esc_html__( 'Failed to exchange authorization code. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'callback:user_info_failed'            => esc_html__( 'Failed to retrieve account information. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'callback:scopes_missing'              => esc_html__( 'One or more required permissions were not granted. Please reconnect and accept all requested permissions.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),

			// OAuth provider errors (passed through from provider).
			'provider:access_denied'               => esc_html__( 'You denied access. Please try again and allow access.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),

			// Token exchange errors.
			'exchange:invalid_code'                => esc_html__( 'The exchange code is invalid or expired. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),

			// License validation errors.
			'license_key_missing'                  => esc_html__( 'License key is required.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'fake_license_key'                     => esc_html__( 'License key is not valid.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'license_not_found'                    => esc_html__( 'License not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'license_not_valid'                    => esc_html__( 'License is not valid.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'license_not_activated_for_site'       => esc_html__( 'License is not active for this site.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),

			// Site registration and authentication errors.
			'missing_headers'                      => esc_html__( 'Required authentication headers are missing.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'invalid_installation_id_format'       => esc_html__( 'Invalid installation ID format.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'invalid_verification_key_format'      => esc_html__( 'Invalid verification key format.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'invalid_verification_key'             => esc_html__( 'Invalid verification key.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'timestamp_skew'                       => esc_html__( 'Request timestamp is too old. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'site_not_found'                       => esc_html__( 'Site not found.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'site_blocked'                         => esc_html__( 'Site is blocked.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'site_url_mismatch'                    => esc_html__( 'Site URL does not match.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
			'invalid_signature'                    => esc_html__( 'Invalid request signature.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
		];

		return $messages[ $error_code ] ?? esc_html__( 'Authorization failed. Please try again.', 'sugar-calendar-bookings-scheduling-appointments-lite' );
	}

	/**
	 * Redirect with error message.
	 *
	 * @since 1.4.0
	 *
	 * @param string $error_code Error code.
	 */
	protected function redirect_with_error( string $error_code ): void {

		WP::redirect_with_notice( $this->get_redirect_url(), 'oauth_error', $this->get_error_message( $error_code ) );
	}

	/**
	 * Redirect with success message.
	 *
	 * @since 1.4.0
	 */
	protected function redirect_with_success(): void {

		wp_safe_redirect(
			add_query_arg(
				[
					'scbookings-notice' => 'oauth_connected',
					'integration'       => $this->get_provider(),
				],
				$this->get_redirect_url()
			)
		);
		exit;
	}
}
