<?php

namespace SugarCalendarBookings\Integrations;

use SugarCalendarBookings\Employees\EmployeeModel;
use SugarCalendarBookings\Integrations\Credits\CreditsService;
use SugarCalendarBookings\Integrations\OAuthRelay\OAuthConnectionInterface;
use SugarCalendarBookings\Integrations\OAuthRelay\OAuthConnectionModel;
use SugarCalendarBookings\Integrations\OAuthRelay\OAuthRelayClient;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Helpers\Helpers;
use SugarCalendarBookings\Support\Helpers\UI;

/**
 * Abstract OAuth integration.
 *
 * Provides shared OAuth-based integration logic: connection management,
 * authorization URL generation, and settings rendering.
 * Concrete integrations supply provider-specific settings and connection wrappers.
 *
 * @since 1.4.0
 */
abstract class AbstractOAuthIntegration extends AbstractIntegration {

	/**
	 * OAuth relay client.
	 *
	 * @since 1.4.0
	 *
	 * @var OAuthRelayClient
	 */
	protected OAuthRelayClient $oauth_relay_client;

	/**
	 * Cached admin connections for the current request.
	 *
	 * @since 1.4.0
	 *
	 * @var array|null
	 */
	protected ?array $admin_connections_cache = null;

	/**
	 * Cached active connections keyed by provider and employee scope.
	 *
	 * @since 1.4.0
	 *
	 * @var array<string, OAuthConnectionInterface|null>
	 */
	protected array $active_connection_cache = [];

	/**
	 * Constructor.
	 *
	 * @since 1.4.0
	 *
	 * @param OAuthRelayClient $oauth_relay_client OAuth relay client.
	 */
	public function __construct( OAuthRelayClient $oauth_relay_client ) {

		$this->oauth_relay_client = $oauth_relay_client;
	}

	/**
	 * Render provider-specific settings below the shared OAuth UI.
	 *
	 * @since 1.4.0
	 *
	 * @return void
	 */
	abstract protected function render_provider_settings(): void;

	/**
	 * Wrap an OAuthConnectionModel in a provider-specific connection object.
	 *
	 * @since 1.4.0
	 *
	 * @param OAuthConnectionModel $model The OAuth connection model.
	 *
	 * @return OAuthConnectionInterface
	 */
	abstract protected function wrap_connection( OAuthConnectionModel $model ): OAuthConnectionInterface;

	/**
	 * Get the label used in the "Connect with {label}" button.
	 *
	 * Distinguishes the OAuth account provider (e.g. "Microsoft", "Google")
	 * from the integration's display name (e.g. "Outlook Calendar", "Google Calendar").
	 * For most integrations they're the same, so this defaults to get_name(); providers
	 * whose OAuth account brand differs from the integration name override this.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	public function get_connect_label(): string {

		return $this->get_name();
	}

	/**
	 * Get the URL of the icon shown inside the "Connect with {label}" button.
	 *
	 * Defaults to the integration's main icon. Providers whose OAuth account
	 * brand differs from the integration (e.g. Outlook ↔ Microsoft) override
	 * this so the button shows the account-provider mark while the sidebar
	 * still shows the integration's own icon.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	public function get_connect_icon_url(): string {

		return $this->get_icon_url();
	}

	/**
	 * Get the integration connection status.
	 *
	 * @since 1.4.0
	 *
	 * @return string
	 */
	public function get_status(): string {

		$connections = $this->get_admin_connections();

		if ( empty( $connections ) ) {
			return 'not_connected';
		}

		$all_auth_error = empty( array_filter( $connections, fn( $c ) => $c->status !== 'auth_error' ) );

		if ( $all_auth_error ) {
			return 'error';
		}

		if ( ! $this->is_enabled() ) {
			return 'disabled';
		}

		$credits = Plugin::get( CreditsService::class )->get_credits();

		if ( $credits && $credits['is_maxed'] ) {
			return 'out_of_credits';
		}

		return 'connected';
	}

	/**
	 * Check if the integration is available for use.
	 *
	 * Returns true only when the integration is enabled, has an active connection,
	 * and has remaining credits.
	 *
	 * @since 1.4.0
	 *
	 * @return bool
	 */
	public function is_available(): bool {

		if ( ! $this->is_enabled() || ! $this->has_active_connection() ) {
			return false;
		}

		$credits = Plugin::get( CreditsService::class )->get_credits();

		if ( $credits && $credits['is_maxed'] ) {
			return false;
		}

		return true;
	}

	/**
	 * Render the integration content panel.
	 *
	 * Template method that renders description, enable toggle, connected accounts,
	 * employee connections education, and provider-specific settings.
	 *
	 * @since 1.4.0
	 */
	public function render_content(): void {

		$connections = $this->get_admin_connections();
		$auth_url    = $this->get_authorization_url();
		?>

		<div class="scb-integration-description">
			<p>
				<?php
				echo wp_kses(
					$this->get_description(),
					[
						'a' => [
							'href'   => [],
							'target' => [],
							'rel'    => [],
						],
					]
				);
				?>
			</p>
		</div>

		<?php
		$this->render_enable_toggle();
		$this->render_pre_accounts_settings();
		?>

		<?php if ( ! empty( $connections ) || $auth_url ) : ?>
		<div class="sugar-calendar-bookings-setting-row">
			<span class="sugar-calendar-bookings-setting-label">
				<label><?php esc_html_e( 'Your Accounts', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?></label>
			</span>
			<div class="sugar-calendar-bookings-setting-field">
				<div class="scb-integration-accounts">
					<?php if ( ! empty( $connections ) ) : ?>
						<div class="scb-integration-accounts__list">
							<?php foreach ( $connections as $connection ) : ?>
								<div class="scb-integration-accounts__item">
									<div class="scb-integration-accounts__item-main">
										<div class="scb-integration-accounts__item-info">
											<img
												class="scb-integration-accounts__item-icon"
												src="<?php echo esc_url( $this->get_icon_url() ); ?>"
												alt="<?php echo esc_attr( $this->get_name() ); ?>"
												width="40"
												height="40"
											/>
											<div class="scb-integration-accounts__item-details">
												<span class="scb-integration-accounts__item-email">
													<?php echo esc_html( $connection->account_email ); ?>
												</span>
												<span class="scb-integration-accounts__item-date">
													<?php
													echo esc_html(
														sprintf(
															/* translators: %1$s - date string. */
															esc_html__( 'Connected on %1$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
															wp_date( get_option( 'date_format' ), $connection->connected_at->getTimestamp() )
														)
													);
													?>
												</span>
												<?php if ( $connection->status === 'auth_error' ) : ?>
													<button type="button" class="scb-btn-text scb-btn-text--refresh" data-refresh-token="<?php echo esc_attr( $connection->id ); ?>">
														<?php esc_html_e( 'Refresh Token', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?>
													</button>
												<?php endif; ?>
											</div>
										</div>
										<div class="scb-integration-accounts__item-actions">
											<button type="button" class="scb-btn-text scb-btn-text--danger" data-remove-connection="<?php echo esc_attr( $connection->id ); ?>">
												<?php esc_html_e( 'Remove', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?>
											</button>
										</div>
										<?php if ( $connection->status === 'auth_error' ) : ?>
											<div class="scb-integration-accounts__item-error">
												<span class="scb-integration-accounts__item-error-icon">
													<?php UI::svg_icon( 'times-circle' ); ?>
												</span>
												<span>
													<?php esc_html_e( 'Authorization expired. Use "Refresh Token" to restore it, or remove this account and reconnect.', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?>
												</span>
											</div>
										<?php endif; ?>
									</div>
									<?php $this->render_account_item_extras( $connection ); ?>
								</div>
							<?php endforeach; ?>
						</div>
					<?php endif; ?>

					<?php if ( $auth_url && empty( $connections ) ) : ?>
						<a href="<?php echo esc_url( $auth_url ); ?>" class="scb-integration-accounts__connect-btn">
							<span class="scb-integration-accounts__connect-btn-icon">
								<img
									src="<?php echo esc_url( $this->get_connect_icon_url() ); ?>"
									alt=""
									width="20"
									height="20"
								/>
							</span>
							<?php
							printf(
								/* translators: %1$s - OAuth account provider name (e.g. "Microsoft", "Google", "Zoom"). */
								esc_html__( 'Connect with %1$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
								esc_html( $this->get_connect_label() )
							);
							?>
						</a>
					<?php endif; ?>
				</div>
			</div>
		</div>
		<?php endif; ?>

		<?php
		if ( current_user_can( 'manage_options' ) ) {
			$this->render_employee_connections_section();
			$this->render_provider_settings();
		}
	}

	/**
	 * Render provider-specific settings shown above the "Your Accounts" block.
	 *
	 * Default no-op. Providers can override this to surface settings — such as
	 * per-connection calendar selection — that belong logically above the
	 * connected accounts list.
	 *
	 * @since 1.5.0
	 */
	protected function render_pre_accounts_settings(): void {} // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedFunction

	/**
	 * Render extra content attached to the bottom of an account card.
	 *
	 * Default no-op. Providers can override this to attach per-connection
	 * controls — such as Outlook's "Calendars to check for conflicts" panel —
	 * inside the same rounded card as the account info row.
	 *
	 * @since 1.5.0
	 *
	 * @param OAuthConnectionModel $connection Connection whose card is being rendered.
	 */
	protected function render_account_item_extras( OAuthConnectionModel $connection ): void { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedFunction, Generic.CodeAnalysis.UnusedFunctionParameter.Found

		unset( $connection );
	}

	/**
	 * Render the enable/disable toggle control.
	 *
	 * Pro overrides this to handle employee-specific toggle states.
	 *
	 * @since 1.4.0
	 */
	protected function render_enable_toggle(): void {

		UI::toggle_control(
			[
				'id'    => $this->get_slug() . '-enabled',
				'name'  => 'integrations.' . $this->get_slug() . '.enabled',
				'label' => sprintf( /* translators: %1$s - integration name (e.g., "Zoom", "Outlook Calendar"). */
					esc_html__( 'Enable %1$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					$this->get_name()
				),
				'value' => $this->is_enabled(),
			]
		);
	}

	/**
	 * Render the employee connections section.
	 *
	 * Lite version shows a product education upsell.
	 * Pro overrides this to show the actual connected employee accounts.
	 *
	 * @since 1.4.0
	 */
	protected function render_employee_connections_section(): void {

		$upgrade_url = Helpers::get_utm_url(
			'https://sugarcalendar.com/pricing/bookings/',
			[
				'medium'  => 'integrations',
				'content' => 'Employee Connections',
			]
		);

		?>
		<hr class="sugar-calendar-bookings-setting-row-divider" />
		<div class="sugar-calendar-bookings-setting-row">
			<span class="sugar-calendar-bookings-setting-label">
				<label>
					<?php esc_html_e( 'Connected Employee Accounts', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?>
					<?php UI::pro_badge( true ); ?>
				</label>
			</span>
			<div class="sugar-calendar-bookings-setting-field">
				<div class="scb-employee-connections-education">
					<p class="scb-employee-connections-education__title">
						<?php esc_html_e( 'No Accounts Connected', 'sugar-calendar-bookings-scheduling-appointments-lite' ); ?>
					</p>
					<p class="scb-employee-connections-education__description">
						<?php
						printf(
							wp_kses( /* translators: %1$s - upgrade to Pro link. */
								__( '<a href="%1$s" target="_blank" rel="noopener noreferrer">Upgrade to Pro</a> to add and connect your employee accounts to Sugar Calendar Bookings', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
								[
									'a' => [
										'href'   => [],
										'target' => [],
										'rel'    => [],
									],
								]
							),
							esc_url( $upgrade_url )
						);
						?>
					</p>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Handle POST data for this integration's settings.
	 *
	 * Base implementation is a no-op. Pro overrides to process employee
	 * connection enable/disable toggles and other side effects.
	 *
	 * `$post_data` is taken by reference so overrides can drop or rewrite
	 * fields before the calling settings tab persists the result via
	 * `Options::update()`.
	 *
	 * @since 1.4.0
	 *
	 * @param array $post_data Processed POST data.
	 */
	public function handle_post_data( array &$post_data ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found

		unset( $post_data );
	}

	/**
	 * Check if the integration has an active connection.
	 *
	 * @since 1.4.0
	 *
	 * @return bool
	 */
	protected function has_active_connection(): bool {

		return $this->get_active_connection() !== null;
	}

	/**
	 * Filter a list of WP user IDs to those that can deliver this provider.
	 *
	 * Default behavior: if a global (admin) connection exists, every user can
	 * deliver via fallback; otherwise only users with their own active
	 * connection are eligible.
	 *
	 * @since 1.4.0
	 *
	 * @param int[] $user_ids Candidate WP user IDs.
	 *
	 * @return int[] Subset of $user_ids that can deliver this provider.
	 */
	public function get_deliverable_user_ids( array $user_ids ): array {

		if ( empty( $user_ids ) ) {
			return [];
		}

		if ( $this->has_global_active_connection() ) {
			return array_values( array_unique( array_map( 'intval', $user_ids ) ) );
		}

		$rows = OAuthConnectionModel::query()
			->where( 'provider', '=', $this->get_slug(), '%s' )
			->where( 'status', '=', 'active', '%s' )
			->where( 'user_id', 'IN', array_map( 'intval', $user_ids ), '%d' )
			->get();

		return array_values(
			array_unique(
				array_map(
					static fn( $row ) => (int) $row->user_id,
					$rows
				)
			)
		);
	}

	/**
	 * Check if the global (admin) account has an active connection for this provider.
	 *
	 * Uses {@see self::get_global_account_user_ids()} so the notion of "global"
	 * stays consistent with the rest of the OAuth stack — pure admins first,
	 * with a fallback to the earliest admin connection if none exist.
	 *
	 * @since 1.4.0
	 *
	 * @return bool
	 */
	protected function has_global_active_connection(): bool {

		$global_user_ids = $this->get_global_account_user_ids();

		if ( empty( $global_user_ids ) ) {
			return false;
		}

		return OAuthConnectionModel::query()
			->where( 'provider', '=', $this->get_slug(), '%s' )
			->where( 'status', '=', 'active', '%s' )
			->where( 'user_id', 'IN', $global_user_ids, '%d' )
			->count() > 0;
	}

	/**
	 * Get the active OAuth connection.
	 *
	 * Returns the first active connection for this provider.
	 * Pro overrides to resolve employee-specific connections first.
	 *
	 * @since 1.4.0
	 *
	 * @param int|null $employee_id Optional employee ID (used by Pro override).
	 *
	 * @return OAuthConnectionInterface|null The OAuth connection or null if none found.
	 */
	protected function get_active_connection( ?int $employee_id = null ): ?OAuthConnectionInterface {

		$cache_key = $this->get_slug() . '_global';

		if ( array_key_exists( $cache_key, $this->active_connection_cache ) ) {
			return $this->active_connection_cache[ $cache_key ];
		}

		$model = OAuthConnectionModel::query()
			->where( 'provider', '=', $this->get_slug(), '%s' )
			->where( 'status', '=', 'active', '%s' )
			->first();

		$connection = $model ? $this->wrap_connection( $model ) : null;

		$this->active_connection_cache[ $cache_key ] = $connection;

		return $connection;
	}

	/**
	 * Get admin connections.
	 *
	 * For admins, returns only the global connection — from admin users
	 * who are NOT linked to employee records. If no such "pure" admins
	 * exist, the earliest admin connection is treated as the global one.
	 *
	 * For employees, returns connections scoped to the current user.
	 *
	 * @since 1.4.0
	 *
	 * @return OAuthConnectionModel[]
	 */
	protected function get_admin_connections(): array {

		if ( $this->admin_connections_cache === null ) {
			$query = OAuthConnectionModel::query()
				->where( 'provider', '=', $this->get_slug(), '%s' );

			if ( current_user_can( 'manage_options' ) ) {
				$global_admin_ids = $this->get_global_account_user_ids();

				if ( ! empty( $global_admin_ids ) ) {
					$query->where( 'user_id', 'IN', $global_admin_ids, '%d' );
				} else {
					// No global account owner determined yet.
					$this->admin_connections_cache = [];

					return $this->admin_connections_cache;
				}
			} else {
				// Employees see only their own connections.
				$query->where( 'user_id', '=', get_current_user_id(), '%d' );
			}

			$this->admin_connections_cache = $query->get();
		}

		return $this->admin_connections_cache;
	}

	/**
	 * Get user IDs whose connections represent the global account.
	 *
	 * Priority:
	 * 1. "Pure" admins (admins NOT linked to any employee record) — but only
	 *    once at least one of them has actually connected an account.
	 * 2. Otherwise the owner of the earliest admin connection (covers the case
	 *    where the connected admin is also an employee, or every admin is).
	 * 3. When no admin has connected yet, the pure admins are returned as the
	 *    eligible owners (empty when every admin is also an employee).
	 *
	 * @since 1.4.0
	 * @since 1.5.0 Pure admins only claim the global account once one of
	 *                  them has connected, so an unconnected pure admin no
	 *                  longer orphans a connected admin-employee's global
	 *                  account (it stayed hidden from "Your Accounts" and was
	 *                  misfiled under "Connected Employee Accounts").
	 *
	 * @return int[]
	 */
	public function get_global_account_user_ids(): array {

		static $cache = [];

		$slug = $this->get_slug();

		if ( isset( $cache[ $slug ] ) ) {
			return $cache[ $slug ];
		}

		$admin_ids = get_users(
			[
				'capability' => 'manage_options',
				'fields'     => 'ID',
			]
		);

		if ( empty( $admin_ids ) ) {
			$cache[ $slug ] = [];

			return $cache[ $slug ];
		}

		$admin_ids = array_map( 'intval', $admin_ids );

		// Find which admins are linked to employee records.
		$admin_employees = EmployeeModel::query()
			->where( 'user_id', 'IN', $admin_ids, '%d' )
			->get();

		$admin_employee_user_ids = array_map(
			fn( $employee ) => (int) $employee->user_id,
			$admin_employees
		);

		$pure_admin_ids = array_values(
			array_diff( $admin_ids, $admin_employee_user_ids )
		);

		// A pure admin owns the global account only once one of them has actually
		// connected. Returning pure admins unconditionally would let a pure admin
		// who never connected hijack ownership from the admin who did — orphaning
		// the real global connection (it vanishes from "Your Accounts" and is
		// misfiled under "Connected Employee Accounts").
		if ( ! empty( $pure_admin_ids ) ) {
			$pure_admin_connected = OAuthConnectionModel::query()
				->where( 'provider', '=', $slug, '%s' )
				->where( 'user_id', 'IN', $pure_admin_ids, '%d' )
				->count() > 0;

			if ( $pure_admin_connected ) {
				$cache[ $slug ] = $pure_admin_ids;

				return $cache[ $slug ];
			}
		}

		// No pure admin has connected: the earliest admin connection is the
		// global account — even when its owner is also an employee.
		$first_connection = OAuthConnectionModel::query()
			->where( 'provider', '=', $slug, '%s' )
			->where( 'user_id', 'IN', $admin_ids, '%d' )
			->order_by( 'id', 'ASC' )
			->first();

		if ( $first_connection ) {
			$cache[ $slug ] = [ (int) $first_connection->user_id ];

			return $cache[ $slug ];
		}

		// No admin connection exists yet — fall back to the eligible owners
		// (pure admins, or empty when every admin is also an employee).
		$cache[ $slug ] = $pure_admin_ids;

		return $cache[ $slug ];
	}

	/**
	 * Whether a connection belongs to the global account owner.
	 *
	 * Global connections represent the business-wide account (the one listed
	 * in the admin "Your Accounts" section) as opposed to a personal employee
	 * connection. Resolution matches {@see self::get_global_account_user_ids()}:
	 * pure admins own the global account; when every admin is also an employee,
	 * the owner of the earliest admin connection does.
	 *
	 * @since 1.5.0
	 *
	 * @param OAuthConnectionModel $connection Connection to check.
	 *
	 * @return bool
	 */
	public function is_global_connection( OAuthConnectionModel $connection ): bool {

		return in_array( (int) $connection->user_id, $this->get_global_account_user_ids(), true );
	}

	/**
	 * Get the OAuth authorization URL.
	 *
	 * @since 1.4.0
	 *
	 * @return string
	 */
	protected function get_authorization_url(): string {

		return add_query_arg(
			[
				'action'   => 'scb_integration_connect',
				'provider' => $this->get_slug(),
				'_wpnonce' => wp_create_nonce( 'scb_integration_connect' ),
			],
			admin_url( 'admin.php' )
		);
	}
}
