<?php
/**
 * Shared OAuth helpers for mail-sending providers.
 *
 * @package EmailSmtp
 */

namespace EmailSmtp\Includes\OAuth;

use EmailSmtp\Includes\AbstractApiProvider;
use EmailSmtp\Includes\ApiHttpClient;
use EmailSmtp\Includes\DeliveryResult;
use EmailSmtp\Includes\ErrorNormalizer;

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

abstract class AbstractOAuthMailProvider extends AbstractApiProvider {

	public function get_transport_label(): string {
		return __( 'OAuth', 'wp-powersuite' );
	}

	/**
	 * @return list<string>
	 */
	abstract public function get_scopes(): array;

	abstract public function get_authorize_url( array $settings, string $state, string $code_challenge ): string;

	/**
	 * Exchange authorization code for tokens.
	 *
	 * @return array{access_token:string, refresh_token:string, expires_in:int, email:string}|\WP_Error
	 */
	abstract public function exchange_code( array $settings, string $code, string $code_verifier );

	/**
	 * @return array{access_token:string, refresh_token:string, expires_in:int}|\WP_Error
	 */
	abstract public function refresh_access_token( array $settings, string $refresh_token );

	abstract public function fetch_account_email( string $access_token ): string;

	public function get_required_fields(): array {
		$prefix = OAuthTokenStore::prefixes()[ $this->get_id() ] ?? '';
		return $prefix !== '' ? [ $prefix . '_client_id', $prefix . '_client_secret' ] : [];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return true|\WP_Error
	 */
	public function validate_settings( array $settings ) {
		$data = OAuthTokenStore::read( $settings, $this->get_id() );
		if ( $data['client_id'] === '' ) {
			return new \WP_Error( 'missing_client_id', __( 'OAuth Client ID is required.', 'wp-powersuite' ) );
		}
		if ( $data['client_secret'] === '' ) {
			return new \WP_Error( 'missing_client_secret', __( 'OAuth Client Secret is required.', 'wp-powersuite' ) );
		}
		return true;
	}

	public function get_connection_status( array $settings ): string {
		$valid = $this->validate_settings( $settings );
		if ( is_wp_error( $valid ) ) {
			$data = OAuthTokenStore::read( $settings, $this->get_id() );
			return ( $data['client_id'] !== '' || $data['client_secret'] !== '' ) ? 'incomplete' : 'not_configured';
		}
		if ( ! OAuthTokenStore::is_connected( $settings, $this->get_id() ) ) {
			return 'incomplete';
		}
		return 'connected';
	}

	public function connect( array $settings ): DeliveryResult {
		$valid = $this->validate_settings( $settings );
		if ( is_wp_error( $valid ) ) {
			return ErrorNormalizer::from_mixed( $valid );
		}
		if ( ! OAuthTokenStore::is_connected( $settings, $this->get_id() ) ) {
			return DeliveryResult::failure(
				__( 'Account not connected. Click Connect to authorize.', 'wp-powersuite' ),
				'not_connected',
				'authentication',
				'',
				false,
				__( 'Use Connect to complete OAuth authorization.', 'wp-powersuite' )
			);
		}

		$token = $this->get_valid_access_token( $settings );
		if ( is_wp_error( $token ) ) {
			return ErrorNormalizer::from_mixed( $token );
		}

		$email = $this->fetch_account_email( $token );
		if ( is_email( $email ) ) {
			OAuthTokenStore::write(
				$this->get_id(),
				[ 'connected_email' => $email ],
				sanitize_key( (string) ( $settings['id'] ?? '' ) )
			);
		}

		return DeliveryResult::success(
			$email !== ''
				? sprintf(
					/* translators: %s: connected account email */
					__( 'Connected as %s.', 'wp-powersuite' ),
					$email
				)
				: __( 'OAuth access token is valid.', 'wp-powersuite' )
		);
	}

	public function disconnect( array $settings ): array {
		return OAuthTokenStore::clear_tokens( $settings, $this->get_id() );
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return string|\WP_Error
	 */
	public function get_valid_access_token( array $settings ) {
		$provider_id   = $this->get_id();
		$connection_id = sanitize_key( (string) ( $settings['id'] ?? '' ) );
		$data          = OAuthTokenStore::read( $settings, $provider_id );
		if ( $data['access_token'] !== '' && ! OAuthTokenStore::access_token_expired( $settings, $provider_id ) ) {
			return $data['access_token'];
		}

		if ( $data['refresh_token'] === '' ) {
			return new \WP_Error(
				'auth_expired',
				__( 'OAuth authorization expired. Disconnect and connect again.', 'wp-powersuite' )
			);
		}

		$lock_key = 'wpps_ed_oauth_rf_' . md5( $provider_id . '|' . $connection_id );
		$deadline = time() + 12;
		$have_lock = false;

		while ( time() <= $deadline ) {
			$data = OAuthTokenStore::read( $settings, $provider_id );
			if ( $data['access_token'] !== '' && ! OAuthTokenStore::access_token_expired( $settings, $provider_id ) ) {
				return $data['access_token'];
			}

			$have_lock = $this->acquire_token_refresh_lock( $lock_key );
			if ( $have_lock ) {
				break;
			}
			usleep( 200000 );
		}

		if ( ! $have_lock ) {
			$data = OAuthTokenStore::read( $settings, $provider_id );
			if ( $data['access_token'] !== '' && ! OAuthTokenStore::access_token_expired( $settings, $provider_id ) ) {
				return $data['access_token'];
			}
			return new \WP_Error(
				'token_refresh_busy',
				__( 'OAuth token refresh is already in progress. Try again in a moment.', 'wp-powersuite' )
			);
		}

		try {
			$data = OAuthTokenStore::read( $settings, $provider_id );
			if ( $data['access_token'] !== '' && ! OAuthTokenStore::access_token_expired( $settings, $provider_id ) ) {
				return $data['access_token'];
			}
			if ( $data['refresh_token'] === '' ) {
				return new \WP_Error(
					'auth_expired',
					__( 'OAuth authorization expired. Disconnect and connect again.', 'wp-powersuite' )
				);
			}

			$refreshed = $this->refresh_access_token( $settings, $data['refresh_token'] );
			if ( is_wp_error( $refreshed ) ) {
				return $refreshed;
			}

			$expires_in = max( 0, (int) ( $refreshed['expires_in'] ?? 3600 ) );
			OAuthTokenStore::write(
				$provider_id,
				[
					'access_token'  => (string) $refreshed['access_token'],
					'refresh_token' => (string) ( $refreshed['refresh_token'] ?? '' ),
					'expires_at'    => time() + $expires_in,
				],
				$connection_id
			);

			return (string) $refreshed['access_token'];
		} finally {
			delete_option( $lock_key );
		}
	}

	private function acquire_token_refresh_lock( string $lock_key ): bool {
		$now      = time();
		$existing = get_option( $lock_key, false );
		if ( is_numeric( $existing ) && ( $now - (int) $existing ) < 30 ) {
			return false;
		}
		if ( false !== $existing ) {
			delete_option( $lock_key );
		}
		return (bool) add_option( $lock_key, (string) $now, '', 'no' );
	}

	/**
	 * @param array<string, mixed> $form
	 * @return array{code:int, json:array<string,mixed>|null, body:string, duration_ms:int, error:string}
	 */
	protected function post_token_form( string $url, array $form ): array {
		return ApiHttpClient::request(
			'POST',
			$url,
			[ 'Content-Type' => 'application/x-www-form-urlencoded' ],
			http_build_query( $form )
		);
	}

	/**
	 * @param int $max_recipients Combined To+Cc+Bcc cap (0 = no cap).
	 */
	protected function build_rfc822( \EmailSmtp\Includes\EmailMessage $message, int $max_recipients = 0 ): string {
		$remaining = $max_recipients > 0 ? $max_recipients : PHP_INT_MAX;
		$to        = array_slice( $message->get_to(), 0, max( 0, $remaining ) );
		$remaining -= count( $to );
		$cc        = array_slice( $message->get_cc(), 0, max( 0, $remaining ) );
		$remaining -= count( $cc );
		$bcc       = array_slice( $message->get_bcc(), 0, max( 0, $remaining ) );

		$from    = $this->from_parts( $message );
		$charset = $message->get_charset() ?: 'UTF-8';
		$headers = [
			'From: ' . $this->format_address( $from['email'], $from['name'] ),
			'To: ' . implode( ', ', $to ),
			'Subject: ' . $this->encode_header( $message->get_subject() ),
			'MIME-Version: 1.0',
		];
		if ( $cc !== [] ) {
			$headers[] = 'Cc: ' . implode( ', ', $cc );
		}
		if ( $bcc !== [] ) {
			$headers[] = 'Bcc: ' . implode( ', ', $bcc );
		}
		if ( is_email( $message->get_reply_to_email() ) ) {
			$headers[] = 'Reply-To: ' . $this->format_address( $message->get_reply_to_email(), $message->get_reply_to_name() );
		}
		foreach ( $this->safe_custom_headers( $message ) as $name => $value ) {
			$headers[] = $name . ': ' . $value;
		}

		$attachments = $this->attachments_as_base64( $message );
		$body_mime   = $this->build_rfc822_body_mime( $message, $charset );

		if ( $attachments === [] ) {
			return implode( "\r\n", array_merge( $headers, $body_mime['headers'] ) ) . "\r\n\r\n" . $body_mime['body'];
		}

		$mixed     = 'wpps_m_' . wp_generate_password( 12, false, false );
		$headers[] = 'Content-Type: multipart/mixed; boundary="' . $mixed . '"';
		$raw       = implode( "\r\n", $headers ) . "\r\n\r\n";
		$raw      .= '--' . $mixed . "\r\n";
		foreach ( $body_mime['headers'] as $h ) {
			$raw .= $h . "\r\n";
		}
		$raw .= "\r\n" . $body_mime['body'] . "\r\n";
		foreach ( $attachments as $attachment ) {
			$type = $attachment['type'] !== '' ? $attachment['type'] : 'application/octet-stream';
			$name = str_replace( [ "\r", "\n", '"' ], '', $attachment['name'] );
			$raw .= '--' . $mixed . "\r\n";
			$raw .= 'Content-Type: ' . $type . '; name="' . $name . '"' . "\r\n";
			$raw .= "Content-Transfer-Encoding: base64\r\n";
			$raw .= 'Content-Disposition: attachment; filename="' . $name . '"' . "\r\n\r\n";
			$raw .= chunk_split( $attachment['content'] ) . "\r\n";
		}
		$raw .= '--' . $mixed . "--\r\n";
		return $raw;
	}

	/**
	 * @return array{headers:list<string>, body:string}
	 */
	private function build_rfc822_body_mime( \EmailSmtp\Includes\EmailMessage $message, string $charset ): array {
		if ( ! $message->is_html() ) {
			return [
				'headers' => [ 'Content-Type: text/plain; charset=' . $charset ],
				'body'    => $this->plain_text_body( $message ),
			];
		}

		$alt  = 'wpps_a_' . wp_generate_password( 12, false, false );
		$body = '--' . $alt . "\r\n";
		$body .= 'Content-Type: text/plain; charset=' . $charset . "\r\n\r\n";
		$body .= $this->plain_text_body( $message ) . "\r\n";
		$body .= '--' . $alt . "\r\n";
		$body .= 'Content-Type: text/html; charset=' . $charset . "\r\n\r\n";
		$html  = $message->get_body();
		$body .= ( $html !== '' ? $html : ' ' ) . "\r\n";
		$body .= '--' . $alt . '--';

		return [
			'headers' => [ 'Content-Type: multipart/alternative; boundary="' . $alt . '"' ],
			'body'    => $body,
		];
	}

	protected function base64url( string $raw ): string {
		return rtrim( strtr( base64_encode( $raw ), '+/', '-_' ), '=' );
	}

	private function encode_header( string $value ): string {
		if ( preg_match( '/[^\x20-\x7E]/', $value ) ) {
			return '=?UTF-8?B?' . base64_encode( $value ) . '?=';
		}
		return $value;
	}
}
