<?php
/**
 * Google Calendar OAuth, booking sync, and external busy-time checks.
 *
 * Google application credentials remain on the Niwa-owned relay. Each plugin
 * installation signs relay requests with its own local P-256 identity.
 *
 * @package Niwa_AI
 */

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

class Niwa_AI_Calendar_Integration_Service {

	const OPTION_KEY          = 'niwa_ai_calendar_integrations';
	const BUSY_CACHE_PREFIX   = 'niwa_ai_calendar_busy_';
	const SYNC_HOOK           = 'niwa_ai_sync_calendar_appointment';
	const INVITE_LIFETIME     = 604800;
	const PERMISSIONS_VERSION = 3;

	/** @var Niwa_AI_Appointment_Service|null */
	private $appointment_service;

	public function __construct( $appointment_service = null ) {
		$this->appointment_service = $appointment_service;
	}

	/** Register booking, OAuth, invitation, and availability hooks. */
	public function register_hooks() {
		add_action( 'niwa_ai_appointment_booked', array( $this, 'queue_new_booking' ), 20, 1 );
		add_action( 'niwa_ai_appointment_changed', array( $this, 'queue_changed_booking' ), 20, 1 );
		add_action( self::SYNC_HOOK, array( $this, 'process_sync' ), 10, 3 );
		add_filter( 'niwa_ai_external_calendar_busy_periods', array( $this, 'external_busy_periods' ), 10, 5 );

		foreach ( array( 'niwa_ai_calendar_google_start', 'niwa_ai_calendar_google_callback', 'niwa_ai_calendar_portal' ) as $action ) {
			$handler = array( $this, 'handle_' . substr( $action, strlen( 'niwa_ai_calendar_' ) ) );
			add_action( 'admin_post_' . $action, $handler );
			add_action( 'admin_post_nopriv_' . $action, $handler );
		}
		add_action( 'admin_post_niwa_ai_calendar_send_invite', array( $this, 'handle_send_invite' ) );
		add_action( 'admin_post_niwa_ai_calendar_disconnect', array( $this, 'handle_disconnect' ) );
	}

	/** HTTPS callback on this installation; Google itself returns to the relay. */
	public static function google_redirect_uri() {
		return set_url_scheme( admin_url( 'admin-post.php?action=niwa_ai_calendar_google_callback' ), 'https' );
	}

	/** Scheduler admin page URL. */
	public static function scheduler_url() {
		return admin_url( 'admin.php?page=niwa-ai-lead-capture&tab=scheduler' );
	}

	/** Read only persisted user connections; obsolete app credentials/feeds are discarded. */
	public function get_data() {
		$data = get_option( self::OPTION_KEY, array() );
		$data = is_array( $data ) ? $data : array();
		$connections = is_array( $data['connections'] ?? null ) ? $data['connections'] : array();
		$cleaned     = false;
		foreach ( $connections as &$connection ) {
			if ( is_array( $connection ) && array_key_exists( 'refresh_token', $connection ) ) {
				unset( $connection['refresh_token'] );
				$cleaned = true;
			}
		}
		unset( $connection );
		if ( $cleaned ) {
			update_option( self::OPTION_KEY, array( 'connections' => $connections ), false );
		}
		return array(
			'connections' => $connections,
		);
	}

	/** Whether this server supports the signed Niwa relay connection. */
	public function is_google_available() {
		return $this->relay_client()->is_available();
	}

	/** Return safe connection metadata without exposing connector credentials. */
	public function get_connection( $owner ) {
		$owner = self::normalize_owner( $owner );
		$data  = $this->get_data();
		$row   = is_array( $data['connections'][ $owner ] ?? null ) ? $data['connections'][ $owner ] : array();
		$has_connector = ! empty( $row['connector_token'] );
		unset( $row['refresh_token'], $row['connector_token'] );
		if ( ! empty( $row ) ) {
			$row['needs_reconnect'] = ! $has_connector || (int) ( $row['permissions_version'] ?? 0 ) < self::PERMISSIONS_VERSION;
		}
		return $row;
	}

	/** Create a seven-day bearer invitation for one saved team member. */
	public function member_portal_url( $member_id ) {
		$token = Niwa_AI_Calendar_Credentials::create_invite_token( $member_id, time() + self::INVITE_LIFETIME );
		return add_query_arg(
			array( 'action' => 'niwa_ai_calendar_portal', 'token' => $token ),
			admin_url( 'admin-post.php' )
		);
	}

	/** Queue a newly committed booking without slowing its response. */
	public function queue_new_booking( $appointment ) {
		$this->queue_sync( $appointment, true, 0 );
	}

	/** Queue a booking status/details change. */
	public function queue_changed_booking( $appointment ) {
		$this->queue_sync( $appointment, false, 0 );
	}

	/** Process one asynchronous Google Calendar sync attempt. */
	public function process_sync( $appointment_id, $create_first = false, $attempt = 0 ) {
		$service = $this->appointment_service ?: new Niwa_AI_Appointment_Service();
		$row     = $service->get_appointment( (int) $appointment_id );
		if ( ! is_array( $row ) ) {
			return;
		}

		$client = $this->google_client();
		if ( is_wp_error( $client ) ) {
			return;
		}

		$data       = $this->get_data();
		$owner_keys = array( 'admin' );
		if ( ! empty( $row['team_member_id'] ) ) {
			$owner_keys[] = 'member:' . sanitize_key( (string) $row['team_member_id'] );
		}

		$failed = false;
		foreach ( array_unique( $owner_keys ) as $owner ) {
			$connection = is_array( $data['connections'][ $owner ] ?? null ) ? $data['connections'][ $owner ] : array();
			if ( 'google' !== (string) ( $connection['provider'] ?? '' ) || empty( $connection['connector_token'] ) ) {
				continue;
			}
			$connector_token = Niwa_AI_Calendar_Credentials::decrypt( $connection['connector_token'] );
			if ( is_wp_error( $connector_token ) ) {
				$failed = true;
				continue;
			}
			$result = $client->sync_appointment( $connector_token, $row, (bool) $create_first );
			if ( is_wp_error( $result ) ) {
				$failed = true;
				error_log( '[Niwa AI] Google Calendar sync failed for booking ' . (int) $appointment_id . ': ' . $result->get_error_message() );
			}
		}

		if ( $failed && (int) $attempt < 3 ) {
			$this->queue_sync( $row, false, (int) $attempt + 1 );
		}
	}

	/** Begin admin or signed-member Google OAuth. */
	public function handle_google_start() {
		$token = sanitize_text_field( wp_unslash( $_GET['token'] ?? '' ) );
		if ( '' !== $token ) {
			$member_id = $this->validated_member_from_token( $token );
			if ( is_wp_error( $member_id ) ) {
				$this->render_portal_error( $member_id->get_error_message() );
			}
			$owner      = 'member:' . $member_id;
			$return_url = $this->portal_url_from_token( $token );
		} else {
			$this->require_admin( 'niwa_ai_calendar_google_start' );
			$owner      = 'admin';
			$return_url = self::scheduler_url();
		}

		$relay = $this->relay_client();
		if ( ! $relay->is_available() ) {
			$this->redirect_url( $return_url, 'calendar_error', __( 'Secure Google Calendar connection is unavailable on this server.', 'niwa-ai' ) );
		}

		$state = Niwa_AI_Calendar_OAuth_State::create( array( 'owner' => $owner, 'return_url' => $return_url ) );
		if ( is_wp_error( $state ) ) {
			$this->redirect_url( $return_url, 'calendar_error', $state->get_error_message() );
		}
		$authorization_url = $relay->authorization_url( self::google_redirect_uri(), $state );
		if ( is_wp_error( $authorization_url ) ) {
			Niwa_AI_Calendar_OAuth_State::delete( $state );
			$this->redirect_url( $return_url, 'calendar_error', $authorization_url->get_error_message() );
		}
		wp_redirect( $authorization_url );
		exit;
	}

	/** Redeem the relay callback code and persist only its encrypted site-bound connector. */
	public function handle_google_callback() {
		header( 'Referrer-Policy: no-referrer' );
		$state = sanitize_text_field( (string) wp_unslash( $_GET['state'] ?? '' ) );
		$flow  = Niwa_AI_Calendar_OAuth_State::consume( $state );
		if ( ! is_array( $flow ) || empty( $flow['owner'] ) || empty( $flow['return_url'] ) ) {
			$this->render_portal_error( __( 'The Google Calendar connection expired. Start again from Scheduler.', 'niwa-ai' ) );
		}

		$return_url = (string) $flow['return_url'];
		if ( ! empty( $_GET['error'] ) ) {
			$this->redirect_url( $return_url, 'calendar_error', sanitize_text_field( (string) wp_unslash( $_GET['error'] ) ) );
		}
		$exchange_code = sanitize_text_field( (string) wp_unslash( $_GET['exchange_code'] ?? '' ) );
		$exchange      = '' !== $exchange_code ? $this->relay_client()->exchange( $exchange_code ) : new WP_Error( 'calendar_exchange_missing', __( 'Niwa did not receive a valid Google Calendar connection.', 'niwa-ai' ) );
		if ( is_wp_error( $exchange ) ) {
			$this->redirect_url( $return_url, 'calendar_error', $exchange->get_error_message() );
		}
		$connector_token = (string) ( $exchange['connector_token'] ?? '' );
		if ( ! Niwa_AI_Calendar_OAuth_Relay_Client::is_connector_token( $connector_token ) ) {
			$this->redirect_url( $return_url, 'calendar_error', __( 'Niwa did not receive a valid Google Calendar connection.', 'niwa-ai' ) );
		}
		$encrypted = Niwa_AI_Calendar_Credentials::encrypt( $connector_token );
		if ( is_wp_error( $encrypted ) ) {
			$this->redirect_url( $return_url, 'calendar_error', $encrypted->get_error_message() );
		}

		$owner = self::normalize_owner( $flow['owner'] );
		if ( '' === $owner ) {
			$this->redirect_url( $return_url, 'calendar_error', __( 'The calendar connection owner is invalid.', 'niwa-ai' ) );
		}
		if ( 0 === strpos( $owner, 'member:' ) && ! $this->find_member( substr( $owner, 7 ) ) ) {
			$this->redirect_url( $return_url, 'calendar_error', __( 'This team member no longer exists in Scheduler.', 'niwa-ai' ) );
		}

		$data = $this->get_data();
		$data['connections'][ $owner ] = array(
			'provider'            => 'google',
			'account_email'       => sanitize_email( (string) ( $exchange['account_email'] ?? '' ) ),
			'connector_token'     => $encrypted,
			'permissions_version' => self::PERMISSIONS_VERSION,
			'connected_at'        => current_time( 'mysql' ),
		);
		$this->save_data( $data );
		$this->queue_existing_bookings( $owner );
		$this->redirect_url( $return_url, 'calendar_connected', 'google' );
	}

	/** Email a team member a short-lived connection page. */
	public function handle_send_invite() {
		$this->require_admin( 'niwa_ai_calendar_send_invite' );
		$member_id = sanitize_key( wp_unslash( $_GET['member_id'] ?? '' ) );
		$member    = $this->find_member( $member_id );
		$email     = is_array( $member ) ? sanitize_email( (string) ( $member['email'] ?? '' ) ) : '';
		if ( ! $member || '' === $email ) {
			$this->redirect_admin( 'calendar_error', __( 'Save the team member and a valid email address before sending a calendar link.', 'niwa-ai' ) );
		}

		$subject = sprintf( __( 'Connect your calendar to %s', 'niwa-ai' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
		$message = sprintf(
			__( "Hello %1\$s,\n\nUse this private link to connect your calendar to Scheduler:\n%2\$s\n\nThe link expires in seven days.", 'niwa-ai' ),
			(string) ( $member['name'] ?? '' ),
			$this->member_portal_url( $member_id )
		);
		if ( ! wp_mail( $email, $subject, $message ) ) {
			$this->redirect_admin( 'calendar_error', __( 'The calendar connection email could not be sent.', 'niwa-ai' ) );
		}
		$this->redirect_admin( 'calendar_invite_sent', '1' );
	}

	/** Render the no-login team-member connection page. */
	public function handle_portal() {
		$token     = sanitize_text_field( wp_unslash( $_GET['token'] ?? '' ) );
		$member_id = $this->validated_member_from_token( $token );
		if ( is_wp_error( $member_id ) ) {
			$this->render_portal_error( $member_id->get_error_message() );
		}
		$member     = $this->find_member( $member_id );
		$connection = $this->get_connection( 'member:' . $member_id );
		$google_url = add_query_arg(
			array( 'action' => 'niwa_ai_calendar_google_start', 'token' => $token ),
			admin_url( 'admin-post.php' )
		);
		$this->render_portal_page( $member, $connection, $google_url );
	}

	/** Disconnect one Google Calendar connection. */
	public function handle_disconnect() {
		$this->require_admin( 'niwa_ai_calendar_disconnect' );
		$owner = self::normalize_owner( wp_unslash( $_GET['owner'] ?? '' ) );
		if ( '' === $owner ) {
			$this->redirect_admin( 'calendar_error', __( 'The calendar connection owner is invalid.', 'niwa-ai' ) );
		}
		$data = $this->get_data();
		$connection = is_array( $data['connections'][ $owner ] ?? null ) ? $data['connections'][ $owner ] : array();
		if ( ! empty( $connection['connector_token'] ) ) {
			$connector_token = Niwa_AI_Calendar_Credentials::decrypt( $connection['connector_token'] );
			if ( ! is_wp_error( $connector_token ) ) {
				$result = $this->relay_client()->revoke( $connector_token );
				if ( is_wp_error( $result ) ) {
					error_log( '[Niwa AI] Google Calendar revoke failed: ' . $result->get_error_message() );
				}
			}
		}
		unset( $data['connections'][ $owner ] );
		$this->save_data( $data );
		$this->redirect_admin( 'calendar_disconnected', 'google' );
	}

	/**
	 * Add a connected team member's Google busy periods to availability checks.
	 * A connected calendar that cannot be read fails closed for the requested
	 * range, preventing a double booking while Google is unavailable.
	 */
	public function external_busy_periods( $periods, $member_id, $range_start, $range_end, $fresh = false ) {
		$periods   = is_array( $periods ) ? $periods : array();
		$member_id = sanitize_key( (string) $member_id );
		$owner     = 'member:' . $member_id;
		$data      = $this->get_data();
		$connection = is_array( $data['connections'][ $owner ] ?? null ) ? $data['connections'][ $owner ] : array();
		if ( 'google' !== (string) ( $connection['provider'] ?? '' ) ) {
			return $periods;
		}

		try {
			$start = $range_start instanceof DateTimeInterface ? DateTimeImmutable::createFromInterface( $range_start ) : new DateTimeImmutable( (string) $range_start, new DateTimeZone( 'UTC' ) );
			$end   = $range_end instanceof DateTimeInterface ? DateTimeImmutable::createFromInterface( $range_end ) : new DateTimeImmutable( (string) $range_end, new DateTimeZone( 'UTC' ) );
		} catch ( Exception $error ) {
			return $periods;
		}
		if ( $end <= $start ) {
			return $periods;
		}

		if ( empty( $connection['connector_token'] ) || (int) ( $connection['permissions_version'] ?? 0 ) < self::PERMISSIONS_VERSION ) {
			return array_merge( $periods, array( self::closed_period( $start, $end ) ) );
		}
		$cache_key = self::BUSY_CACHE_PREFIX . hash( 'sha256', $owner . '|' . $start->format( 'c' ) . '|' . $end->format( 'c' ) );
		if ( ! $fresh ) {
			$cached = get_transient( $cache_key );
			if ( is_array( $cached ) ) {
				return array_merge( $periods, $cached );
			}
		}

		$connector_token = Niwa_AI_Calendar_Credentials::decrypt( $connection['connector_token'] );
		$client          = $this->google_client();
		$result          = is_wp_error( $connector_token ) || is_wp_error( $client )
			? ( is_wp_error( $connector_token ) ? $connector_token : $client )
			: $client->busy_periods( $connector_token, $start, $end );
		if ( is_wp_error( $result ) ) {
			error_log( '[Niwa AI] Google Calendar busy lookup failed for ' . $owner . ': ' . $result->get_error_message() );
			$result = array( self::closed_period( $start, $end ) );
		}
		set_transient( $cache_key, $result, MINUTE_IN_SECONDS );
		return array_merge( $periods, $result );
	}

	private static function closed_period( DateTimeInterface $start, DateTimeInterface $end ) {
		return array(
			'start' => $start->getTimestamp(),
			'end'   => $end->getTimestamp(),
		);
	}

	private function queue_sync( $appointment, $create_first, $attempt ) {
		$appointment_id = is_array( $appointment ) ? (int) ( $appointment['id'] ?? 0 ) : (int) $appointment;
		$data           = $this->get_data();
		$member_id      = is_array( $appointment ) ? sanitize_key( (string) ( $appointment['team_member_id'] ?? '' ) ) : '';
		$has_destination = ! empty( $data['connections']['admin'] ) || ( '' !== $member_id && ! empty( $data['connections'][ 'member:' . $member_id ] ) );
		if ( $appointment_id <= 0 || ! $has_destination ) {
			return;
		}
		$args = array( $appointment_id, (bool) $create_first, (int) $attempt );
		if ( function_exists( 'as_enqueue_async_action' ) && 0 === (int) $attempt ) {
			as_enqueue_async_action( self::SYNC_HOOK, $args, 'niwa-ai' );
			return;
		}
		$delays = array( 0, 300, 1800, 7200 );
		$when   = time() + ( $delays[ min( 3, max( 0, (int) $attempt ) ) ] ?? 0 );
		if ( function_exists( 'as_schedule_single_action' ) ) {
			as_schedule_single_action( $when, self::SYNC_HOOK, $args, 'niwa-ai' );
		} else {
			wp_schedule_single_event( $when, self::SYNC_HOOK, $args );
		}
	}

	private function queue_existing_bookings( $owner ) {
		$service  = $this->appointment_service ?: new Niwa_AI_Appointment_Service();
		$timezone = wp_timezone();
		$start    = new DateTimeImmutable( 'today', $timezone );
		$filters  = array();
		if ( 0 === strpos( $owner, 'member:' ) ) {
			$filters['team_member_id'] = substr( $owner, 7 );
		}
		$rows = $service->get_appointments_for_range( $start->format( 'Y-m-d' ), $start->modify( '+18 months' )->format( 'Y-m-d' ), $filters, 5000, 0 );
		foreach ( $rows as $row ) {
			if ( is_array( $row ) && ! in_array( (string) ( $row['status'] ?? '' ), array( 'cancelled', 'no_show' ), true ) ) {
				$this->queue_sync( $row, true, 0 );
			}
		}
	}

	private function google_client() {
		$relay = $this->relay_client();
		if ( ! $relay->is_available() ) {
			return new WP_Error( 'google_not_available', __( 'Secure Google Calendar connection is unavailable on this server.', 'niwa-ai' ) );
		}
		return new Niwa_AI_Google_Calendar_Client( $relay );
	}

	private function relay_client() {
		return new Niwa_AI_Calendar_OAuth_Relay_Client();
	}

	private function save_data( array $data ) {
		update_option(
			self::OPTION_KEY,
			array( 'connections' => is_array( $data['connections'] ?? null ) ? $data['connections'] : array() ),
			false
		);
	}

	private function validated_member_from_token( $token ) {
		$member_id = Niwa_AI_Calendar_Credentials::validate_invite_token( $token );
		if ( is_wp_error( $member_id ) ) {
			return $member_id;
		}
		return $this->find_member( $member_id ) ? $member_id : new WP_Error( 'calendar_member_missing', __( 'This team member no longer exists in Scheduler.', 'niwa-ai' ) );
	}

	private function find_member( $member_id ) {
		$settings = Niwa_AI_Appointment_Service::get_settings();
		foreach ( (array) ( $settings['team_members'] ?? array() ) as $member ) {
			if ( is_array( $member ) && (string) ( $member['id'] ?? '' ) === (string) $member_id ) {
				return $member;
			}
		}
		return null;
	}

	private static function normalize_owner( $owner ) {
		$owner = (string) $owner;
		if ( 'admin' === $owner ) {
			return 'admin';
		}
		if ( 0 === strpos( $owner, 'member:' ) ) {
			$member = sanitize_key( substr( $owner, 7 ) );
			return '' !== $member ? 'member:' . $member : '';
		}
		return '';
	}

	private function portal_url_from_token( $token ) {
		return add_query_arg( array( 'action' => 'niwa_ai_calendar_portal', 'token' => $token ), admin_url( 'admin-post.php' ) );
	}

	private function require_admin( $nonce_action ) {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to manage calendar integrations.', 'niwa-ai' ), '', array( 'response' => 403 ) );
		}
		check_admin_referer( $nonce_action );
	}

	private function redirect_admin( $key, $value ) {
		$this->redirect_url( self::scheduler_url(), $key, $value );
	}

	private function redirect_url( $url, $key, $value ) {
		wp_safe_redirect( add_query_arg( $key, (string) $value, (string) $url ) );
		exit;
	}

	private function render_portal_page( array $member, array $connection, $google_url ) {
		$title   = sprintf( __( 'Calendar connection for %s', 'niwa-ai' ), (string) $member['name'] );
		$message = sanitize_text_field( wp_unslash( $_GET['calendar_error'] ?? '' ) );
		header( 'Content-Type: text/html; charset=UTF-8' );
		echo '<!doctype html><html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . esc_html( $title ) . '</title>';
		echo '<style>body{font-family:system-ui,sans-serif;background:#f6f7fb;color:#18181b;margin:0;padding:32px}.card{max-width:620px;margin:8vh auto;background:#fff;border:1px solid #e4e4e7;border-radius:18px;padding:28px;box-shadow:0 18px 60px #18181b12}.actions{display:grid;gap:12px;margin-top:24px}.button{display:block;text-align:center;padding:13px 18px;border-radius:10px;background:#18181b;color:#fff;text-decoration:none;font-weight:650}.button.disabled{cursor:not-allowed;background:#e4e4e7;color:#71717a}.ok{color:#166534}.error{color:#b91c1c}.hint{color:#52525b;font-size:14px;line-height:1.55}</style></head><body><main class="card">';
		echo '<h1>' . esc_html( $title ) . '</h1><p class="hint">' . esc_html__( 'Authorize a live calendar connection. Niwa bookings will appear in the connected calendar, and existing busy times will be removed from online availability.', 'niwa-ai' ) . '</p>';
		if ( '' !== $message ) {
			echo '<p class="error">' . esc_html( $message ) . '</p>';
		} elseif ( ! empty( $_GET['calendar_connected'] ) || ! empty( $connection ) ) {
			echo '<p class="ok">' . esc_html__( 'Google Calendar is connected.', 'niwa-ai' ) . '</p>';
		}
		echo '<div class="actions">';
		if ( $this->is_google_available() ) {
			echo '<a class="button" href="' . esc_url( $google_url ) . '">' . esc_html__( 'Connect with Google', 'niwa-ai' ) . '</a>';
		} else {
			echo '<span class="button disabled" aria-disabled="true">' . esc_html__( 'Connect with Google', 'niwa-ai' ) . '</span>';
		}
		echo '<span class="button disabled" aria-disabled="true">' . esc_html__( 'Connect with Apple', 'niwa-ai' ) . '</span></div>';
		echo '<p class="hint">' . esc_html__( 'Apple live calendar access is not enabled yet. The previous download/subscription link was removed because it was not a real two-way connection.', 'niwa-ai' ) . '</p></main></body></html>';
		exit;
	}

	private function render_portal_error( $message ) {
		status_header( 400 );
		header( 'Content-Type: text/html; charset=UTF-8' );
		echo '<!doctype html><html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' . esc_html__( 'Calendar connection', 'niwa-ai' ) . '</title></head><body><p>' . esc_html( $message ) . '</p></body></html>';
		exit;
	}
}
