<?php
if (!defined('ABSPATH')) exit;

class Calliope_SP_Bluesky {
    /** @var string */
    private $current_session_did = '';

    /** @var string */
    private $current_session_pds_url = '';

    /** @var string */
    private $current_session_pds_did = '';

    private const VALIDATION_OPTION = 'calliope_sp_bsky_validation';
    private const VALIDATION_SCHEMA = 'bluesky-validation-v1';
    private const MINIMUM_EVIDENCE = 'E4';
    private const VIDEO_MAX_BYTES = 50 * 1024 * 1024;
    private const EXTERNAL_THUMB_MAX_BYTES = 1024 * 1024;
    // Bluesky/ATProto rejects image blobs above 2,000,000 bytes (decimal).
    // Keep a little headroom so records do not fail at the provider boundary.
    private const IMAGE_BLOB_MAX_BYTES = 2000000;
    private const IMAGE_BLOB_TARGET_BYTES = 1900000;
    private const IMAGE_EMBED_MAX_COUNT = 4;
    private const GALLERY_MAX_COUNT = 10;

    /**
     * Build the local Bluesky connection snapshot without performing HTTP.
     *
     * @return Calliope_SP_Connection_State|WP_Error
     */
    public function get_connection_state() {
        if (!class_exists('Calliope_SP_Connection_State')) {
            return new WP_Error(
                'bsky_connection_contract_missing',
                __('Bluesky connection status is temporarily unavailable.', 'calliope-social-publisher')
            );
        }

        $raw_handle = (string) get_option('calliope_sp_bsky_handle', '');
        $raw_password = (string) get_option('calliope_sp_bsky_password', '');
        $has_handle = trim($raw_handle) !== '';
        $has_password = trim($raw_password) !== '';

        if (!$has_handle && !$has_password) {
            return Calliope_SP_Connection_State::create([
                'network'               => 'bluesky',
                'status'                => 'not_configured',
                'health'                => 'unknown',
                'is_configured'         => false,
                'is_authenticated'      => false,
                'is_identity_known'     => false,
                'has_target'            => false,
                'has_permission'        => false,
                'evidence_level'        => 'E0',
                'message'               => __('Enter your Bluesky handle and App Password.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'expires_at'            => 0,
                'validated_at'          => 0,
                'connection_method'     => 'app_password',
            ], self::MINIMUM_EVIDENCE);
        }

        $handle = $this->normalize_handle($raw_handle);
        $password = $this->normalize_app_password($raw_password);
        if (!$has_handle || !$has_password || is_wp_error($handle) || is_wp_error($password)) {
            return Calliope_SP_Connection_State::create([
                'network'               => 'bluesky',
                'status'                => 'configuration_error',
                'health'                => 'invalid',
                'is_configured'         => true,
                'is_authenticated'      => false,
                'is_identity_known'     => false,
                'has_target'            => false,
                'has_permission'        => false,
                'evidence_level'        => 'E1',
                'message'               => __('Bluesky configuration is incomplete or invalid.', 'calliope-social-publisher'),
                'action'                => __('Review credentials', 'calliope-social-publisher'),
                'expires_at'            => 0,
                'validated_at'          => 0,
                'connection_method'     => 'app_password',
            ], self::MINIMUM_EVIDENCE);
        }

        $credentials_fingerprint = $this->build_credentials_fingerprint($handle, $password);
        $marker = $this->get_matching_validation_marker($credentials_fingerprint);

        if (is_array($marker) && $marker['health'] === 'invalid') {
            return Calliope_SP_Connection_State::create([
                'network'               => 'bluesky',
                'status'                => 'token_invalid',
                'health'                => 'invalid',
                'is_configured'         => true,
                'is_authenticated'      => false,
                'is_identity_known'     => false,
                'has_target'            => false,
                'has_permission'        => false,
                'evidence_level'        => 'E1',
                'message'               => __('The saved Bluesky credentials were rejected. Create a new App Password and try again.', 'calliope-social-publisher'),
                'action'                => __('Create a new App Password', 'calliope-social-publisher'),
                'expires_at'            => 0,
                'validated_at'          => isset($marker['validated_at']) ? (int) $marker['validated_at'] : 0,
                'connection_method'     => 'app_password',
            ], self::MINIMUM_EVIDENCE);
        }

        $has_valid_identity = is_array($marker)
            && in_array($marker['health'], ['healthy', 'degraded'], true)
            && !empty($marker['identity_fingerprint'])
            && !empty($marker['validated_at']);

        if ($has_valid_identity) {
            $health = $marker['health'];
            return Calliope_SP_Connection_State::create([
                'network'               => 'bluesky',
                'status'                => 'ready_to_publish',
                'health'                => $health,
                'is_configured'         => true,
                'is_authenticated'      => true,
                'is_identity_known'     => true,
                'has_target'            => true,
                'has_permission'        => true,
                'evidence_level'        => 'E4',
                'message'               => $health === 'degraded'
                    ? __('Bluesky was verified previously, but the latest check failed temporarily.', 'calliope-social-publisher')
                    : '',
                'action'                => $health === 'degraded' ? __('Save and test', 'calliope-social-publisher') : '',
                'expires_at'            => 0,
                'validated_at'          => (int) $marker['validated_at'],
                'connection_method'     => 'app_password',
            ], self::MINIMUM_EVIDENCE);
        }

        $health = is_array($marker) && $marker['health'] === 'degraded' ? 'degraded' : 'unknown';
        return Calliope_SP_Connection_State::create([
            'network'               => 'bluesky',
            'status'                => 'connected_unverified',
            'health'                => $health,
            'is_configured'         => true,
            'is_authenticated'      => true,
            'is_identity_known'     => false,
            'has_target'            => false,
            'has_permission'        => false,
            'evidence_level'        => 'E2',
            'message'               => $health === 'degraded'
                ? __('Bluesky could not be verified because of a temporary connection problem.', 'calliope-social-publisher')
                : __('Bluesky credentials are saved but have not been verified for the current account.', 'calliope-social-publisher'),
            'action'                => __('Save and test', 'calliope-social-publisher'),
            'expires_at'            => 0,
            'validated_at'          => 0,
            'connection_method'     => 'app_password',
        ], self::MINIMUM_EVIDENCE);
    }

    /**
     * Normalize a Bluesky handle consistently for storage, login and fingerprints.
     *
     * @param mixed $handle Candidate handle.
     * @return string|WP_Error
     */
    public function normalize_handle($handle) {
        if (!is_scalar($handle)) {
            return new WP_Error('bsky_handle_invalid', __('Enter a valid Bluesky handle.', 'calliope-social-publisher'));
        }

        $handle = trim((string) $handle);
        if (str_starts_with($handle, '@')) {
            $handle = substr($handle, 1);
        }
        $handle = strtolower($handle);

        if ($handle === ''
            || strlen($handle) > 253
            || strpos($handle, "\0") !== false
            || preg_match('/[\s\x00-\x1F\x7F]/u', $handle)
            || strpos($handle, '://') !== false
            || strpos($handle, '/') !== false
            || strpos($handle, '\\') !== false
            || !preg_match('/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/', $handle)
            || strpos($handle, '..') !== false
        ) {
            return new WP_Error('bsky_handle_invalid', __('Enter a valid Bluesky handle.', 'calliope-social-publisher'));
        }

        foreach (explode('.', $handle) as $label) {
            if ($label === '' || strlen($label) > 63 || $label[0] === '-' || substr($label, -1) === '-') {
                return new WP_Error('bsky_handle_invalid', __('Enter a valid Bluesky handle.', 'calliope-social-publisher'));
            }
        }

        return $handle;
    }

    /**
     * Explicitly validate the currently saved Bluesky credentials.
     *
     * @return true|WP_Error
     */
    public function test_connection() {
        $credentials = $this->get_current_credentials();
        if (is_wp_error($credentials)) {
            return $credentials;
        }

        $credentials_fingerprint = $this->build_credentials_fingerprint($credentials['handle'], $credentials['password']);
        $previous_marker = $this->get_matching_validation_marker($credentials_fingerprint);
        $session = $this->request_create_session($credentials['handle'], $credentials['password']);

        if (is_wp_error($session)) {
            $this->apply_remote_failure_to_validation($session, $credentials_fingerprint, $previous_marker);
            return $session;
        }

        $validated_session = $this->validate_session_response($session);
        if (is_wp_error($validated_session)) {
            $this->degrade_or_create_unverified_marker($credentials_fingerprint, $previous_marker);
            return $validated_session;
        }

        $this->save_validation_marker(
            'healthy',
            $credentials_fingerprint,
            $this->build_identity_fingerprint($validated_session['did']),
            time()
        );
        $this->sync_legacy_status('ready_to_publish');

        $state = $this->get_connection_state();
        if (is_wp_error($state) || !$state->is_ready()) {
            return is_wp_error($state)
                ? $state
                : new WP_Error('bsky_connection_unverified', __('Bluesky could not be verified for publishing.', 'calliope-social-publisher'));
        }

        return true;
    }

    /**
     * Publish after a local readiness precheck. No network request is made when the
     * current credentials have not been explicitly validated.
     *
     * @param array<string,mixed> $data Publication data.
     * @return true|WP_Error
     */
    public function publish($data) {
        $state = $this->get_connection_state();
        if (is_wp_error($state)) {
            return $state;
        }
        if (!$state->is_ready()) {
            return new WP_Error(
                'bsky_connection_not_ready',
                __('Bluesky is not verified for publishing. Open Connections and use Save and test.', 'calliope-social-publisher'),
                [
                    'phase'      => 'precheck',
                    'status'     => $state->get_status(),
                    'health'     => $state->get_health(),
                    'retryable'  => false,
                    'auth_error' => $state->get_status() === 'token_invalid',
                ]
            );
        }

        $credentials = $this->get_current_credentials();
        if (is_wp_error($credentials)) {
            return $credentials;
        }
        $credentials_fingerprint = $this->build_credentials_fingerprint($credentials['handle'], $credentials['password']);
        $marker = $this->get_matching_validation_marker($credentials_fingerprint);

        $media_mode = sanitize_key((string) ($data['media_mode'] ?? 'none'));
        $this->log_video_diagnostics('publish_start', [
            'media_mode' => $media_mode,
            'has_image_b64' => !empty($data['image_b64']),
            'media_count' => isset($data['media']) && is_array($data['media']) ? count($data['media']) : 0,
            'media' => $this->media_log_items(isset($data['media']) && is_array($data['media']) ? $data['media'] : []),
        ]);
        $prepared_video = null;
        if ($media_mode === 'video') {
            $prepared_video = $this->prepare_blob_video((array) $data);
            if (is_wp_error($prepared_video)) {
                return $prepared_video;
            }
        }

        $youtube_link_source = $this->is_youtube_link_source_payload((array) $data);
        $youtube_external_url = $youtube_link_source ? $this->youtube_external_url_from_payload((array) $data) : '';
        if ($youtube_link_source && $youtube_external_url === '') {
            return new WP_Error(
                'bsky_youtube_link_invalid',
                __('Bluesky requires a valid YouTube URL to publish a YouTube link embed. No text-only fallback was sent.', 'calliope-social-publisher'),
                ['phase' => 'precheck', 'retryable' => false, 'auth_error' => false]
            );
        }

        if ($media_mode === 'multiple_images' && isset($data['media']) && is_array($data['media']) && count($data['media']) > self::IMAGE_EMBED_MAX_COUNT) {
            return $this->publish_limited_images((array) $data);
        }

        $prepared_images = [];
        if (!is_array($prepared_video)) {
            $prepared_images = $this->prepare_blob_images((array) $data);
            if (is_wp_error($prepared_images)) {
                return $prepared_images;
            }
        }

        $session = $this->request_create_session($credentials['handle'], $credentials['password']);
        if (is_wp_error($session)) {
            $this->apply_remote_failure_to_validation($session, $credentials_fingerprint, $marker);
            return $session;
        }

        $validated_session = $this->validate_session_response($session);
        if (is_wp_error($validated_session)) {
            $this->degrade_or_create_unverified_marker($credentials_fingerprint, $marker);
            return $validated_session;
        }

        $jwt = $validated_session['accessJwt'];
        $did = $validated_session['did'];
        $this->current_session_did = sanitize_text_field((string) $did);
        $this->current_session_pds_url = esc_url_raw((string) ($validated_session['pds_url'] ?? ''));
        $this->current_session_pds_did = sanitize_text_field((string) ($validated_session['pds_did'] ?? ''));
        $identity_fingerprint = $this->build_identity_fingerprint($did);
        if (!is_array($marker)
            || empty($marker['identity_fingerprint'])
            || !hash_equals((string) $marker['identity_fingerprint'], $identity_fingerprint)
        ) {
            $this->delete_validation_marker();
            $this->sync_legacy_status('connected_unverified');
            return new WP_Error(
                'bsky_identity_changed',
                __('The Bluesky account identity changed. Use Save and test before publishing again.', 'calliope-social-publisher'),
                [
                    'phase'      => 'authentication',
                    'retryable'  => false,
                    'auth_error' => false,
                ]
            );
        }

        $text_data = (array) $data;
        if ($youtube_external_url !== '') {
            $text_data['link'] = '';
        }

        $text = $this->format_text_clean_tags($text_data, 290);
        $facets = $this->gen_facets($text);
        $record = [
            '$type'     => 'app.bsky.feed.post',
            'text'      => $text,
            'createdAt' => gmdate('c'),
        ];
        if (!empty($facets)) {
            $record['facets'] = $facets;
        }

        // Media publishing remains atomic. A selected media item must be prepared
        // and uploaded successfully before createRecord is called.
        if ($youtube_external_url !== '') {
            $external_embed = $this->prepare_youtube_external_embed($jwt, (array) $data, $youtube_external_url);
            if (is_wp_error($external_embed)) {
                return $external_embed;
            }
            $record['embed'] = $external_embed;
            $this->log_video_diagnostics('youtube_link_external_embed_prepared', [
                'embed_type' => 'app.bsky.embed.external',
                'has_thumb' => !empty($external_embed['external']['thumb']),
            ]);
        } elseif (is_array($prepared_video)) {
            $upload = $this->upload_video_blob($jwt, $prepared_video);
            if (is_wp_error($upload)) {
                $this->apply_remote_failure_to_validation($upload, $credentials_fingerprint, $marker);
                return $upload;
            }

            $alt_text = isset($data['alt_text']) ? sanitize_text_field(wp_strip_all_tags((string) $data['alt_text'])) : '';
            if ($alt_text === '') {
                $alt_text = sanitize_text_field((string) ($prepared_video['description'] ?? ''));
            }
            $record['embed'] = [
                '$type' => 'app.bsky.embed.video',
                'video' => $upload['blob'],
                'alt'   => $alt_text !== '' ? $alt_text : 'Video',
            ];
            if (!empty($upload['aspectRatio']) && is_array($upload['aspectRatio'])) {
                $record['embed']['aspectRatio'] = $upload['aspectRatio'];
            }
            $this->log_video_diagnostics('record_embed_prepared', [
                'embed_type' => 'app.bsky.embed.video',
                'has_blob' => !empty($upload['blob']),
                'has_aspect_ratio' => !empty($upload['aspectRatio']),
                'alt_set' => $alt_text !== '',
            ]);
        } elseif ($prepared_images !== []) {
            $images = [];
            foreach ($prepared_images as $prepared_image) {
                $upload = $this->upload_blob($jwt, $prepared_image);
                if (is_wp_error($upload)) {
                    $this->apply_remote_failure_to_validation($upload, $credentials_fingerprint, $marker);
                    return $upload;
                }

                $alt_text = sanitize_text_field(wp_strip_all_tags((string) ($prepared_image['description'] ?? '')));
                if ($alt_text === '') {
                    $alt_text = isset($data['alt_text']) ? sanitize_text_field(wp_strip_all_tags((string) $data['alt_text'])) : '';
                }
                $image = [
                    'alt'   => $alt_text !== '' ? $alt_text : 'Image',
                    'image' => $upload['blob'],
                ];
                if (!empty($upload['aspectRatio']) && is_array($upload['aspectRatio'])) {
                    $image['aspectRatio'] = $upload['aspectRatio'];
                }
                $images[] = $image;
            }
            $record['embed'] = [
                '$type'  => 'app.bsky.embed.images',
                'images' => $images,
            ];
            $this->log_video_diagnostics('record_embed_prepared', [
                'embed_type' => (string) $record['embed']['$type'],
                'image_count' => count($images),
            ]);
        }

        $created = $this->create_record($jwt, $did, $record);
        if (is_wp_error($created)) {
            $this->apply_remote_failure_to_validation($created, $credentials_fingerprint, $marker);
            return $created;
        }

        $this->save_validation_marker('healthy', $credentials_fingerprint, $identity_fingerprint, time());
        $this->sync_legacy_status('ready_to_publish');

        $created = is_array($created) ? $created : [];
        if (is_array($prepared_video)) {
            return [
                'ok' => true,
                'media_type' => 'video',
                'post_uri' => sanitize_text_field((string) ($created['post_uri'] ?? '')),
                'post_cid' => sanitize_text_field((string) ($created['post_cid'] ?? '')),
            ];
        }

        return [
            'ok' => true,
            'post_uri' => sanitize_text_field((string) ($created['post_uri'] ?? '')),
            'post_cid' => sanitize_text_field((string) ($created['post_cid'] ?? '')),
        ];
    }

    /** @return array<string,mixed>|WP_Error */
    private function publish_limited_images(array $data) {
        $media = isset($data['media']) && is_array($data['media']) ? array_values($data['media']) : [];
        $count = count($media);
        if ($count < 5 || $count > self::GALLERY_MAX_COUNT) {
            return new WP_Error('bsky_images_invalid', __('Bluesky multiple-image publishing requires 2 to 10 images.', 'calliope-social-publisher'), ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]);
        }
        $limited = $data;
        $limited['media'] = array_slice($media, 0, self::IMAGE_EMBED_MAX_COUNT);
        $limited['media_mode'] = 'multiple_images';
        $limited['image_b64'] = '';
        $result = $this->publish($limited);
        if (is_wp_error($result)) { return $result; }
        $result = is_array($result) ? $result : ['ok' => true];
        $result['ok'] = true;
        $result['warning'] = true;
        $result['warning_code'] = 'bsky_images_limited';
        $result['ui_status'] = 'warning';
        $result['media_type'] = 'gallery';
        $result['attachments'] = self::IMAGE_EMBED_MAX_COUNT;
        $result['requested_attachments'] = $count;
        $result['omitted_attachments'] = $count - self::IMAGE_EMBED_MAX_COUNT;
        $result['user_message'] = sprintf(
            /* translators: 1: published image count, 2: requested image count. */
            __('Bluesky published one post with the first %1$d of %2$d selected images, which is the network maximum.', 'calliope-social-publisher'),
            self::IMAGE_EMBED_MAX_COUNT,
            $count
        );
        return $result;
    }

    /** @return array{handle:string,password:string}|WP_Error */
    private function get_current_credentials() {
        $handle = $this->normalize_handle(get_option('calliope_sp_bsky_handle', ''));
        $password = $this->normalize_app_password(get_option('calliope_sp_bsky_password', ''));
        if (is_wp_error($handle) || is_wp_error($password)) {
            return new WP_Error(
                'bsky_configuration_invalid',
                __('Bluesky configuration is incomplete or invalid.', 'calliope-social-publisher'),
                ['phase' => 'configuration', 'retryable' => false, 'auth_error' => false]
            );
        }

        return ['handle' => $handle, 'password' => $password];
    }

    /** @return string|WP_Error */
    private function normalize_app_password($password) {
        if (!is_scalar($password)) {
            return new WP_Error('bsky_password_invalid', __('Enter a Bluesky App Password.', 'calliope-social-publisher'));
        }
        $password = trim((string) $password);
        if ($password === '' || strpos($password, "\0") !== false || preg_match('/[\r\n]/', $password)) {
            return new WP_Error('bsky_password_invalid', __('Enter a Bluesky App Password.', 'calliope-social-publisher'));
        }
        return $password;
    }

    /** @return string */
    private function build_credentials_fingerprint($handle, $password) {
        $canonical = wp_json_encode([
            'schema'   => self::VALIDATION_SCHEMA,
            'handle'   => (string) $handle,
            'password' => (string) $password,
        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return hash_hmac('sha256', (string) $canonical, wp_salt('auth'));
    }

    /** @return string */
    private function build_identity_fingerprint($did) {
        return hash_hmac('sha256', 'bluesky-did:' . (string) $did, wp_salt('auth'));
    }

    /** @return array<string,mixed>|null */
    private function get_matching_validation_marker($credentials_fingerprint) {
        $marker = get_option(self::VALIDATION_OPTION, []);
        if (!is_array($marker)
            || ($marker['schema'] ?? '') !== self::VALIDATION_SCHEMA
            || !in_array(($marker['health'] ?? ''), ['healthy', 'degraded', 'invalid'], true)
            || !is_string($marker['credentials_fingerprint'] ?? null)
            || !preg_match('/^[a-f0-9]{64}$/', (string) $marker['credentials_fingerprint'])
            || !hash_equals((string) $marker['credentials_fingerprint'], (string) $credentials_fingerprint)
        ) {
            return null;
        }

        $identity = $marker['identity_fingerprint'] ?? '';
        if (!is_string($identity) || ($identity !== '' && !preg_match('/^[a-f0-9]{64}$/', $identity))) {
            return null;
        }
        $validated_at = $marker['validated_at'] ?? 0;
        if (!is_int($validated_at) && !(is_string($validated_at) && preg_match('/^\d+$/', $validated_at))) {
            return null;
        }
        $validated_at = (int) $validated_at;
        if ($validated_at < 0) {
            return null;
        }
        if (in_array($marker['health'], ['healthy', 'degraded'], true)
            && (($identity === '') !== ($validated_at === 0))
        ) {
            return null;
        }

        return [
            'schema'                  => self::VALIDATION_SCHEMA,
            'health'                  => (string) $marker['health'],
            'validated_at'            => $validated_at,
            'credentials_fingerprint' => (string) $marker['credentials_fingerprint'],
            'identity_fingerprint'    => $identity,
        ];
    }

    /** @return void */
    private function save_validation_marker($health, $credentials_fingerprint, $identity_fingerprint = '', $validated_at = 0) {
        $marker = [
            'schema'                  => self::VALIDATION_SCHEMA,
            'health'                  => in_array($health, ['healthy', 'degraded', 'invalid'], true) ? $health : 'invalid',
            'validated_at'            => max(0, (int) $validated_at),
            'credentials_fingerprint' => (string) $credentials_fingerprint,
            'identity_fingerprint'    => (string) $identity_fingerprint,
        ];

        if (get_option(self::VALIDATION_OPTION, false) === false) {
            add_option(self::VALIDATION_OPTION, $marker, '', 'no');
            return;
        }
        update_option(self::VALIDATION_OPTION, $marker, false);
    }

    /** @return void */
    private function delete_validation_marker() {
        delete_option(self::VALIDATION_OPTION);
    }

    /** @return void */
    private function degrade_or_create_unverified_marker($credentials_fingerprint, $previous_marker) {
        if (is_array($previous_marker)
            && !empty($previous_marker['identity_fingerprint'])
            && !empty($previous_marker['validated_at'])
            && in_array($previous_marker['health'], ['healthy', 'degraded'], true)
        ) {
            $this->save_validation_marker(
                'degraded',
                $credentials_fingerprint,
                $previous_marker['identity_fingerprint'],
                (int) $previous_marker['validated_at']
            );
            $this->sync_legacy_status('ready_to_publish');
            return;
        }

        $this->save_validation_marker('degraded', $credentials_fingerprint, '', 0);
        $this->sync_legacy_status('connected_unverified');
    }

    /** @return void */
    private function apply_remote_failure_to_validation($error, $credentials_fingerprint, $previous_marker) {
        if (!is_wp_error($error)) {
            return;
        }
        $data = $error->get_error_data();
        $data = is_array($data) ? $data : [];

        if (!empty($data['auth_error'])) {
            $identity = is_array($previous_marker) ? (string) ($previous_marker['identity_fingerprint'] ?? '') : '';
            $validated_at = is_array($previous_marker) ? (int) ($previous_marker['validated_at'] ?? 0) : 0;
            $this->save_validation_marker('invalid', $credentials_fingerprint, $identity, $validated_at);
            $this->sync_legacy_status('token_invalid');
            return;
        }

        if (!empty($data['retryable'])) {
            $this->degrade_or_create_unverified_marker($credentials_fingerprint, $previous_marker);
        }
    }

    /** @return void */
    private function sync_legacy_status($status) {
        $status = sanitize_key((string) $status);
        if (get_option('calliope_sp_bsky_connection_status', '') !== $status) {
            update_option('calliope_sp_bsky_connection_status', $status, false);
        }
    }

    /** @return array<string,mixed>|WP_Error */
    private function request_create_session($handle, $password) {
        return $this->bsky_req(
            'com.atproto.server.createSession',
            'POST',
            ['identifier' => $handle, 'password' => $password],
            'authentication'
        );
    }

    /** @return array{accessJwt:string,did:string,pds_url:string,pds_did:string}|WP_Error */
    private function validate_session_response($session) {
        $jwt = is_array($session) && isset($session['accessJwt']) && is_string($session['accessJwt'])
            ? trim($session['accessJwt'])
            : '';
        $did = is_array($session) && isset($session['did']) && is_string($session['did'])
            ? trim($session['did'])
            : '';

        if (!$this->is_valid_jwt($jwt) || !$this->is_valid_did($did)) {
            return new WP_Error(
                'bsky_auth_invalid_response',
                __('Bluesky did not return a valid authenticated session.', 'calliope-social-publisher'),
                ['phase' => 'authentication', 'retryable' => true, 'auth_error' => false]
            );
        }

        $pds = isset($session['didDoc']) && is_array($session['didDoc'])
            ? $this->extract_pds_service($session['didDoc'], $did)
            : null;

        return [
            'accessJwt' => $jwt,
            'did'       => $did,
            'pds_url'   => is_array($pds) ? (string) $pds['url'] : '',
            'pds_did'   => is_array($pds) ? (string) $pds['did'] : '',
        ];
    }

    /**
     * Resolve the account's authoritative PDS for Bluesky video service auth.
     *
     * @return array{url:string,did:string}|WP_Error
     */
    private function current_pds_service($account_did) {
        $account_did = trim((string) $account_did);
        if ($this->current_session_pds_url !== '' && $this->current_session_pds_did !== '') {
            return [
                'url' => $this->current_session_pds_url,
                'did' => $this->current_session_pds_did,
            ];
        }

        $document = $this->resolve_did_document($account_did);
        if (is_wp_error($document)) {
            return $document;
        }
        $pds = $this->extract_pds_service($document, $account_did);
        if (is_wp_error($pds)) {
            return $pds;
        }

        $this->current_session_pds_url = (string) $pds['url'];
        $this->current_session_pds_did = (string) $pds['did'];
        return $pds;
    }

    /** @return array<string,mixed>|WP_Error */
    private function resolve_did_document($did) {
        $did = trim((string) $did);
        if (!$this->is_valid_did($did)) {
            return $this->pds_resolution_error('invalid_account_did');
        }

        if (strpos($did, 'did:plc:') === 0) {
            if (preg_match('/^did:plc:[a-z2-7]{24}$/D', $did) !== 1) {
                return $this->pds_resolution_error('invalid_plc_did');
            }
            $url = 'https://plc.directory/' . rawurlencode($did);
        } elseif (strpos($did, 'did:web:') === 0) {
            $host = substr($did, strlen('did:web:'));
            if (!$this->is_valid_public_hostname($host)) {
                return $this->pds_resolution_error('invalid_web_did');
            }
            $url = 'https://' . strtolower($host) . '/.well-known/did.json';
        } else {
            return $this->pds_resolution_error('unsupported_did_method');
        }

        $response = wp_safe_remote_get($url, [
            'headers' => ['Accept' => 'application/json'],
            'timeout' => 20,
            'redirection' => 0,
            'sslverify' => true,
        ]);
        if (is_wp_error($response)) {
            return $this->pds_resolution_error('did_document_transport', true);
        }
        $status = (int) wp_remote_retrieve_response_code($response);
        $document = json_decode((string) wp_remote_retrieve_body($response), true);
        if ($status < 200 || $status >= 300 || !is_array($document)) {
            return $this->pds_resolution_error('did_document_invalid', $this->is_retryable_http_status($status), $status);
        }
        return $document;
    }

    /**
     * @param array<string,mixed> $document
     * @return array{url:string,did:string}|WP_Error
     */
    private function extract_pds_service(array $document, $expected_did) {
        $expected_did = trim((string) $expected_did);
        $document_did = isset($document['id']) && is_string($document['id']) ? trim($document['id']) : '';
        if ($document_did !== '' && !hash_equals($expected_did, $document_did)) {
            return $this->pds_resolution_error('did_document_identity_mismatch');
        }

        $services = isset($document['service']) && is_array($document['service']) ? $document['service'] : [];
        foreach ($services as $service) {
            if (!is_array($service)) {
                continue;
            }
            $id = isset($service['id']) && is_string($service['id']) ? trim($service['id']) : '';
            $type = isset($service['type']) && is_string($service['type']) ? trim($service['type']) : '';
            $endpoint = isset($service['serviceEndpoint']) && is_string($service['serviceEndpoint'])
                ? trim($service['serviceEndpoint'])
                : '';
            if (substr($id, -strlen('#atproto_pds')) !== '#atproto_pds'
                || $type !== 'AtprotoPersonalDataServer'
                || $endpoint === ''
            ) {
                continue;
            }

            $validated = function_exists('wp_http_validate_url') ? wp_http_validate_url($endpoint) : $endpoint;
            $parts = is_string($validated) && $validated !== '' ? wp_parse_url($validated) : false;
            if (!is_array($parts)
                || strtolower((string) ($parts['scheme'] ?? '')) !== 'https'
                || !empty($parts['user'])
                || !empty($parts['pass'])
                || !empty($parts['port'])
                || !empty($parts['query'])
                || !empty($parts['fragment'])
                || !in_array((string) ($parts['path'] ?? ''), ['', '/'], true)
            ) {
                continue;
            }
            $host = strtolower((string) ($parts['host'] ?? ''));
            if (!$this->is_valid_public_hostname($host)) {
                continue;
            }

            return [
                'url' => 'https://' . $host,
                'did' => 'did:web:' . $host,
            ];
        }

        return $this->pds_resolution_error('pds_service_missing');
    }

    /** @return bool */
    private function is_valid_public_hostname($host) {
        $host = strtolower(trim((string) $host));
        return $host !== ''
            && strlen($host) <= 253
            && strpos($host, ':') === false
            && filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false
            && !in_array($host, ['localhost', 'localhost.localdomain'], true);
    }

    /** @return WP_Error */
    private function pds_resolution_error($reason, $retryable = false, $http_code = 0) {
        return new WP_Error(
            'bsky_video_pds_resolution_failed',
            __('Bluesky could not determine the account hosting server required for video processing.', 'calliope-social-publisher'),
            [
                'phase' => 'upload',
                'retryable' => (bool) $retryable,
                'auth_error' => false,
                'http_code' => max(0, (int) $http_code),
                'response_reason' => sanitize_key((string) $reason),
            ]
        );
    }

    /** @return bool */
    private function is_valid_jwt($jwt) {
        return is_string($jwt)
            && strlen($jwt) >= 20
            && strlen($jwt) <= 8192
            && preg_match('/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/', $jwt) === 1;
    }

    /** @return bool */
    private function is_valid_did($did) {
        return is_string($did)
            && strlen($did) >= 7
            && strlen($did) <= 2048
            && preg_match('/^did:[a-z0-9]+:[^\s\x00-\x1F\x7F]+$/', $did) === 1;
    }

    /** @return true|WP_Error */
    private function create_record($jwt, $did, array $record) {
        $embed_type = isset($record['embed']['$type']) && is_scalar($record['embed']['$type'])
            ? (string) $record['embed']['$type']
            : '';
        $res = wp_remote_post('https://bsky.social/xrpc/com.atproto.repo.createRecord', [
            'headers' => [
                'Authorization' => 'Bearer ' . $jwt,
                'Content-Type'  => 'application/json',
            ],
            'body' => wp_json_encode([
                'repo'       => $did,
                'collection' => 'app.bsky.feed.post',
                'record'     => $record,
            ]),
            'timeout' => 45,
        ]);

        if (is_wp_error($res)) {
            if ($embed_type === 'app.bsky.embed.video') {
                $this->log_video_diagnostics('create_record_transport', [
                    'embed_type' => $embed_type,
                    'transport_error' => true,
                ], 'error');
            }
            return $this->build_transport_error($res, 'create_record');
        }

        $status = (int) wp_remote_retrieve_response_code($res);
        if ($embed_type === 'app.bsky.embed.video') {
            $this->log_video_diagnostics('create_record_response', [
                'embed_type' => $embed_type,
                'http_code' => $status,
                'response_bytes' => strlen((string) wp_remote_retrieve_body($res)),
            ], ($status >= 200 && $status < 300) ? 'info' : 'error');
        }
        if ($status >= 200 && $status < 300) {
            $created = json_decode((string) wp_remote_retrieve_body($res), true);
            $uri_set = is_array($created) && !empty($created['uri']);
            $cid_set = is_array($created) && !empty($created['cid']);
            if ($embed_type === 'app.bsky.embed.video') {
                $this->log_video_diagnostics('create_record_validated', [
                    'embed_type' => $embed_type,
                    'record_uri_set' => $uri_set,
                    'record_cid_set' => $cid_set,
                ], ($uri_set && $cid_set) ? 'info' : 'warning');
                if (!$uri_set || !$cid_set) {
                    return new WP_Error(
                        'bsky_video_create_invalid_response',
                        __('Bluesky accepted the video post request but did not return a valid post record.', 'calliope-social-publisher'),
                        [
                            'phase' => 'create_record',
                            'retryable' => true,
                            'auth_error' => false,
                        ]
                    );
                }
            }
            return [
                'post_uri' => $uri_set ? sanitize_text_field((string) $created['uri']) : '',
                'post_cid' => $cid_set ? sanitize_text_field((string) $created['cid']) : '',
            ];
        }

        return $this->build_http_error($status, (string) wp_remote_retrieve_body($res), 'create_record');
    }

    /**
     * Prepare one or more images for a Bluesky images embed.
     *
     * @param array<string,mixed> $data Publication data.
     * @return array<int,array<string,mixed>>|WP_Error
     */
    private function prepare_blob_images(array $data) {
        $mode = sanitize_key((string) ($data['media_mode'] ?? 'none'));
        if ($mode === 'video') {
            return [];
        }

        if (in_array($mode, ['multiple_images', 'single_image'], true) && !empty($data['media']) && is_array($data['media'])) {
            if (!class_exists('Calliope_SPP_Media_Set') || !class_exists('Calliope_SPP_Media_Resolver')) {
                if (!empty($data['image_b64'])) {
                    $image = $this->prepare_blob_image($data['image_b64']);
                    if (is_wp_error($image)) {
                        return $image;
                    }
                    $image['description'] = isset($data['alt_text']) ? sanitize_text_field((string) $data['alt_text']) : '';
                    return [$image];
                }
                return new WP_Error(
                    'bsky_images_require_pro',
                    __('Bluesky multiple-image publishing requires Social Publisher Pro media support.', 'calliope-social-publisher'),
                    ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
                );
            }

            $media = array_values(array_filter(
                Calliope_SPP_Media_Set::normalize((array) $data['media']),
                static fn(array $item): bool => (string) ($item['type'] ?? '') === 'image'
            ));
            if ($media === []) {
                return new WP_Error(
                    'bsky_images_missing',
                    __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                    ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
                );
            }

            $requested_count = count($media);
            if ($requested_count > self::GALLERY_MAX_COUNT) {
                $media = array_slice($media, 0, self::GALLERY_MAX_COUNT);
                $this->log_video_diagnostics('images_limited', [
                    'requested_count' => $requested_count,
                    'published_count' => count($media),
                    'limit' => self::GALLERY_MAX_COUNT,
                ], 'warning');
            }

            $resolved = Calliope_SPP_Media_Resolver::resolve($media, [
                'allowed_image_mimes' => ['image/jpeg', 'image/png', 'image/webp'],
                'allowed_video_mimes' => [],
                'log_context' => ['network' => 'bluesky', 'mode' => $mode],
            ]);
            if (is_wp_error($resolved)) {
                return new WP_Error(
                    'bsky_images_media_error',
                    sprintf(
                        /* translators: %s: media preparation error. */
                        __('Bluesky could not prepare the selected images. %s', 'calliope-social-publisher'),
                        $resolved->get_error_message()
                    ),
                    ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
                );
            }

            $prepared = [];
            foreach ($resolved as $file) {
                if (!is_array($file) || empty($file['path']) || empty($file['mime_type'])) {
                    return new WP_Error(
                        'bsky_images_media_error',
                        __('Bluesky could not prepare the selected images.', 'calliope-social-publisher'),
                        ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
                    );
                }
                $binary = $this->read_local_file((string) $file['path']);
                if ($binary === false || $binary === '') {
                    return new WP_Error(
                        'bsky_images_read_failed',
                        __('Bluesky could not read a selected image file.', 'calliope-social-publisher'),
                        ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
                    );
                }
                $image_data = 'data:' . sanitize_mime_type((string) $file['mime_type']) . ';base64,' . base64_encode($binary);
                $image = $this->prepare_blob_image($image_data);
                if (is_wp_error($image)) {
                    return $image;
                }
                $image['description'] = sanitize_text_field((string) ($file['description'] ?? ''));
                $image['attachment_id'] = absint($file['id'] ?? 0);
                $prepared[] = $image;
            }

            return $prepared;
        }

        if (!empty($data['image_b64'])) {
            $image = $this->prepare_blob_image($data['image_b64']);
            if (is_wp_error($image)) {
                return $image;
            }
            $image['description'] = isset($data['alt_text']) ? sanitize_text_field((string) $data['alt_text']) : '';
            return [$image];
        }

        return [];
    }

    /**
     * Decode and inspect an image before sending it to Bluesky.
     *
     * @param string $image_data Sanitized Data URL or raw base64.
     * @return array{bytes:string,mime:string,size:int,aspect_ratio:?array{width:int,height:int}}|WP_Error
     */
    private function prepare_blob_image($image_data) {
        $image_data = is_string($image_data) ? trim($image_data) : '';
        if ($image_data === '') {
            return new WP_Error(
                'bsky_image_invalid_base64',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                ['phase' => 'prepare']
            );
        }

        $declared_mime = '';
        $payload = $image_data;

        if (stripos($image_data, 'data:') === 0) {
            if (!preg_match('#^data:([^;,]+);base64,(.*)$#is', $image_data, $matches)) {
                return new WP_Error(
                    'bsky_image_invalid_base64',
                    __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                    ['phase' => 'prepare']
                );
            }

            $declared_mime = $this->normalize_image_mime((string) $matches[1]);
            $payload = (string) $matches[2];
        }

        $payload = preg_replace('/\s+/', '', (string) $payload);
        if (!is_string($payload) || $payload === '') {
            return new WP_Error(
                'bsky_image_invalid_base64',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                ['phase' => 'prepare']
            );
        }

        $binary = base64_decode($payload, true);
        if (!is_string($binary) || $binary === '') {
            return new WP_Error(
                'bsky_image_invalid_base64',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                ['phase' => 'prepare']
            );
        }

        $image_info = false;
        if (function_exists('getimagesizefromstring')) {
            $image_info = @getimagesizefromstring($binary);
        }

        $detected_mime = is_array($image_info) && !empty($image_info['mime'])
            ? $this->normalize_image_mime((string) $image_info['mime'])
            : '';

        if ($detected_mime === '' && function_exists('finfo_open') && function_exists('finfo_buffer')) {
            $finfo = @finfo_open(FILEINFO_MIME_TYPE);
            if ($finfo) {
                $finfo_mime = @finfo_buffer($finfo, $binary);
                finfo_close($finfo);
                if (is_string($finfo_mime)) {
                    $detected_mime = $this->normalize_image_mime($finfo_mime);
                }
            }
        }

        $supported_mimes = ['image/jpeg', 'image/png', 'image/webp'];
        if ($detected_mime === '') {
            return new WP_Error(
                'bsky_image_invalid',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                [
                    'phase' => 'prepare',
                    'size'  => strlen($binary),
                ]
            );
        }

        if (!in_array($detected_mime, $supported_mimes, true)) {
            $is_image_mime = str_starts_with($detected_mime, 'image/');
            return new WP_Error(
                $is_image_mime ? 'bsky_image_unsupported_mime' : 'bsky_image_invalid',
                $is_image_mime
                    ? __('Bluesky supports JPEG, PNG, and WebP images only.', 'calliope-social-publisher')
                    : __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                [
                    'phase' => 'prepare',
                    'mime'  => $detected_mime,
                    'size'  => strlen($binary),
                ]
            );
        }

        // Some PHP builds can identify WebP with fileinfo even when
        // getimagesizefromstring() cannot return dimensions. In that fallback path,
        // verify the format signature before accepting the bytes.
        if (!is_array($image_info) && !$this->has_valid_image_signature($binary, $detected_mime)) {
            return new WP_Error(
                'bsky_image_invalid',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                [
                    'phase' => 'prepare',
                    'mime'  => $detected_mime,
                    'size'  => strlen($binary),
                ]
            );
        }

        $aspect_ratio = null;
        if (is_array($image_info)) {
            $width = isset($image_info[0]) ? (int) $image_info[0] : 0;
            $height = isset($image_info[1]) ? (int) $image_info[1] : 0;
            if ($width > 0 && $height > 0) {
                $aspect_ratio = [
                    'width'  => $width,
                    'height' => $height,
                ];
            }
        }

        if ($declared_mime !== '' && $declared_mime !== $detected_mime && class_exists('Calliope_SP_Logger')) {
            Calliope_SP_Logger::log('bluesky', 'warning', wp_json_encode([
                'phase'         => 'prepare',
                'code'          => 'bsky_image_mime_mismatch',
                'declared_mime' => $declared_mime,
                'detected_mime' => $detected_mime,
                'size'          => strlen($binary),
                'width'         => is_array($aspect_ratio) ? (int) $aspect_ratio['width'] : 0,
                'height'        => is_array($aspect_ratio) ? (int) $aspect_ratio['height'] : 0,
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
        }

        $prepared = [
            'bytes'        => $binary,
            'mime'         => $detected_mime,
            'size'         => strlen($binary),
            'aspect_ratio' => $aspect_ratio,
        ];

        if ($prepared['size'] > self::IMAGE_BLOB_MAX_BYTES) {
            $prepared = $this->fit_image_to_blob_limit($prepared);
            if (is_wp_error($prepared)) {
                return $prepared;
            }
        }

        return $prepared;
    }

    /**
     * Reduce an image only for the Bluesky payload when its blob would exceed
     * ATProto's 2,000,000-byte limit. The Media Library original is never
     * modified. WebP is preferred for PNG/WebP sources because it preserves
     * transparency; JPEG is the compatibility fallback.
     *
     * @param array{bytes:string,mime:string,size:int,aspect_ratio:?array{width:int,height:int}} $prepared
     * @return array{bytes:string,mime:string,size:int,aspect_ratio:?array{width:int,height:int}}|WP_Error
     */
    private function fit_image_to_blob_limit(array $prepared) {
        $binary = isset($prepared['bytes']) && is_string($prepared['bytes']) ? $prepared['bytes'] : '';
        $original_mime = $this->normalize_image_mime((string) ($prepared['mime'] ?? ''));
        $original_size = strlen($binary);
        if ($binary === '' || $original_size <= self::IMAGE_BLOB_MAX_BYTES) {
            return $prepared;
        }

        if (!function_exists('wp_tempnam') && defined('ABSPATH')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (!function_exists('wp_tempnam') || !function_exists('wp_get_image_editor')) {
            return new WP_Error(
                'bsky_image_too_large',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                [
                    'phase' => 'prepare',
                    'reason' => 'image_editor_unavailable',
                    'size' => $original_size,
                    'max_size' => self::IMAGE_BLOB_MAX_BYTES,
                    'retryable' => false,
                    'auth_error' => false,
                ]
            );
        }

        $extension_map = [
            'image/jpeg' => 'jpg',
            'image/png' => 'png',
            'image/webp' => 'webp',
        ];
        $source_ext = $extension_map[$original_mime] ?? 'img';
        $source_path = wp_tempnam('calliope-bsky-source.' . $source_ext);
        if (!is_string($source_path) || $source_path === '' || false === @file_put_contents($source_path, $binary, LOCK_EX)) {
            if (is_string($source_path) && $source_path !== '') { @unlink($source_path); }
            return new WP_Error(
                'bsky_image_too_large',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                ['phase' => 'prepare', 'reason' => 'temporary_file_failed', 'size' => $original_size, 'max_size' => self::IMAGE_BLOB_MAX_BYTES]
            );
        }

        $editor = wp_get_image_editor($source_path);
        if (is_wp_error($editor)) {
            @unlink($source_path);
            return new WP_Error(
                'bsky_image_too_large',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                ['phase' => 'prepare', 'reason' => 'image_editor_failed', 'size' => $original_size, 'max_size' => self::IMAGE_BLOB_MAX_BYTES]
            );
        }

        $dimensions = $editor->get_size();
        $original_width = is_array($dimensions) ? absint($dimensions['width'] ?? 0) : 0;
        $original_height = is_array($dimensions) ? absint($dimensions['height'] ?? 0) : 0;
        $formats = $original_mime === 'image/jpeg'
            ? ['image/jpeg', 'image/webp']
            : ['image/webp', 'image/jpeg'];
        $qualities = [88, 82, 76, 70, 64];
        $scales = [1.0, 0.88, 0.76, 0.64, 0.52];
        $best = null;
        $temporary_paths = [];

        foreach ($scales as $scale) {
            if ($scale < 1.0 && $original_width > 0 && $original_height > 0) {
                $target_width = max(320, (int) floor($original_width * $scale));
                $target_height = max(320, (int) floor($original_height * $scale));
                $resized = $editor->resize($target_width, $target_height, false);
                if (is_wp_error($resized)) {
                    continue;
                }
            }

            foreach ($formats as $output_mime) {
                foreach ($qualities as $quality) {
                    $editor->set_quality($quality);
                    $output_ext = $extension_map[$output_mime] ?? 'jpg';
                    $output_path = wp_tempnam('calliope-bsky-output.' . $output_ext);
                    if (!is_string($output_path) || $output_path === '') {
                        continue;
                    }
                    $temporary_paths[] = $output_path;
                    $saved = $editor->save($output_path, $output_mime);
                    if (is_wp_error($saved)) {
                        continue;
                    }
                    $saved_path = is_array($saved) && !empty($saved['path']) ? (string) $saved['path'] : $output_path;
                    if ($saved_path !== $output_path) { $temporary_paths[] = $saved_path; }
                    if (!is_file($saved_path) || !is_readable($saved_path)) {
                        continue;
                    }
                    $candidate_size = @filesize($saved_path);
                    if (!is_int($candidate_size) || $candidate_size <= 0 || $candidate_size > self::IMAGE_BLOB_MAX_BYTES) {
                        continue;
                    }
                    $candidate_bytes = @file_get_contents($saved_path);
                    if (!is_string($candidate_bytes) || strlen($candidate_bytes) !== $candidate_size) {
                        continue;
                    }
                    $candidate_info = function_exists('getimagesizefromstring') ? @getimagesizefromstring($candidate_bytes) : false;
                    $candidate_mime = is_array($candidate_info) && !empty($candidate_info['mime'])
                        ? $this->normalize_image_mime((string) $candidate_info['mime'])
                        : $this->normalize_image_mime((string) (is_array($saved) ? ($saved['mime-type'] ?? '') : ''));
                    if (!in_array($candidate_mime, ['image/jpeg', 'image/webp', 'image/png'], true)) {
                        continue;
                    }
                    $candidate_ratio = null;
                    if (is_array($candidate_info) && !empty($candidate_info[0]) && !empty($candidate_info[1])) {
                        $candidate_ratio = ['width' => (int) $candidate_info[0], 'height' => (int) $candidate_info[1]];
                    }
                    $candidate = [
                        'bytes' => $candidate_bytes,
                        'mime' => $candidate_mime,
                        'size' => $candidate_size,
                        'aspect_ratio' => $candidate_ratio,
                    ];
                    if ($best === null || $candidate_size < (int) $best['size']) {
                        $best = $candidate;
                    }
                    if ($candidate_size <= self::IMAGE_BLOB_TARGET_BYTES) {
                        break 3;
                    }
                }
            }
        }

        @unlink($source_path);
        foreach (array_unique($temporary_paths) as $temporary_path) {
            if (is_string($temporary_path) && $temporary_path !== '') { @unlink($temporary_path); }
        }

        if (!is_array($best)) {
            return new WP_Error(
                'bsky_image_too_large',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                [
                    'phase' => 'prepare',
                    'reason' => 'could_not_reduce_below_blob_limit',
                    'size' => $original_size,
                    'max_size' => self::IMAGE_BLOB_MAX_BYTES,
                    'retryable' => false,
                    'auth_error' => false,
                ]
            );
        }

        if (class_exists('Calliope_SP_Logger')) {
            Calliope_SP_Logger::log('bluesky', 'info', wp_json_encode([
                'event' => 'bsky_image_optimized_for_blob_limit',
                'original_mime' => $original_mime,
                'original_size' => $original_size,
                'final_mime' => (string) $best['mime'],
                'final_size' => (int) $best['size'],
                'max_size' => self::IMAGE_BLOB_MAX_BYTES,
                'width' => is_array($best['aspect_ratio']) ? (int) ($best['aspect_ratio']['width'] ?? 0) : 0,
                'height' => is_array($best['aspect_ratio']) ? (int) ($best['aspect_ratio']['height'] ?? 0) : 0,
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
        }

        return $best;
    }

    /**
     * Prepare a local Pro video attachment for Bluesky blob upload.
     *
     * @param array<string,mixed> $data
     * @return array{bytes:string,mime:string,size:int,aspect_ratio:?array{width:int,height:int},description:string}|WP_Error
     */
    private function prepare_blob_video(array $data) {
        if (!class_exists('Calliope_SPP_Media_Set') || !class_exists('Calliope_SPP_Media_Resolver')) {
            return new WP_Error(
                'bsky_video_requires_pro',
                __('Bluesky video publishing requires Social Publisher Pro media support.', 'calliope-social-publisher'),
                ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
            );
        }

        $media = isset($data['media']) && is_array($data['media']) ? $data['media'] : [];
        $contract = Calliope_SPP_Media_Set::validate_contract('video', $media, [
            'image' => false,
            'max_images' => 0,
            'video' => true,
        ]);
        if (is_wp_error($contract)) {
            return new WP_Error(
                'bsky_video_invalid',
                __('Bluesky video publishing requires exactly one local MP4 video attachment.', 'calliope-social-publisher'),
                ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
            );
        }

        $max = $this->bluesky_video_max_file_size();
        $resolved = Calliope_SPP_Media_Resolver::resolve($media, [
            'max_file_bytes' => $max,
            'max_request_bytes' => $max,
            'allowed_video_mimes' => $this->bluesky_video_mimes(),
            'log_context' => ['network' => 'bluesky', 'mode' => 'video'],
        ]);
        if (is_wp_error($resolved)) {
            return new WP_Error(
                'bsky_video_media_error',
                sprintf(
                    /* translators: %s: media preparation error. */
                    __('Bluesky could not prepare the selected video. %s', 'calliope-social-publisher'),
                    $resolved->get_error_message()
                ),
                ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
            );
        }

        $file = $resolved[0] ?? null;
        if (!is_array($file) || empty($file['path']) || empty($file['mime_type'])) {
            return new WP_Error(
                'bsky_video_media_error',
                __('Bluesky could not prepare the selected video.', 'calliope-social-publisher'),
                ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
            );
        }
        if (!$this->video_extension_matches((string) ($file['filename'] ?? ''), (string) $file['mime_type'])) {
            return new WP_Error(
                'bsky_video_media_error',
                __('Bluesky video publishing supports MP4 files in this version.', 'calliope-social-publisher'),
                ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
            );
        }

        $binary = $this->read_local_file((string) $file['path']);
        if ($binary === false || $binary === '') {
            return new WP_Error(
                'bsky_video_read_failed',
                __('Bluesky could not read the selected video file.', 'calliope-social-publisher'),
                ['phase' => 'prepare', 'retryable' => false, 'auth_error' => false]
            );
        }

        $this->log_video_diagnostics('video_resolved', [
            'attachment_id' => absint($file['id'] ?? 0),
            'mime_type' => sanitize_mime_type((string) ($file['mime_type'] ?? '')),
            'size' => absint($file['size'] ?? 0),
            'filename' => sanitize_file_name((string) ($file['filename'] ?? '')),
            'limit_bytes' => $max,
            'has_aspect_ratio' => is_array($this->video_aspect_ratio((int) ($file['id'] ?? 0), (string) $file['path'])),
        ]);

        return [
            'bytes'        => $binary,
            'mime'         => (string) $file['mime_type'],
            'size'         => (int) $file['size'],
            'aspect_ratio' => $this->video_aspect_ratio((int) ($file['id'] ?? 0), (string) $file['path']),
            'description'  => sanitize_text_field((string) ($file['description'] ?? '')),
        ];
    }

    /**
     * Upload a prepared image to Bluesky.
     *
     * @param string $jwt Access JWT.
     * @param array{bytes:string,mime:string,size:int,aspect_ratio:?array{width:int,height:int}} $prepared_image
     * @return array{blob:array,aspectRatio:?array{width:int,height:int}}|WP_Error
     */
    private function upload_blob($jwt, array $prepared_image) {
        $binary = isset($prepared_image['bytes']) && is_string($prepared_image['bytes'])
            ? $prepared_image['bytes']
            : '';
        $mime = isset($prepared_image['mime'])
            ? $this->normalize_image_mime((string) $prepared_image['mime'])
            : '';
        $size = isset($prepared_image['size']) ? (int) $prepared_image['size'] : strlen($binary);
        $aspect_ratio = isset($prepared_image['aspect_ratio']) && is_array($prepared_image['aspect_ratio'])
            ? $prepared_image['aspect_ratio']
            : null;

        if ($binary === '' || !in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
            return new WP_Error(
                'bsky_image_invalid',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                ['phase' => 'upload', 'retryable' => false, 'auth_error' => false]
            );
        }
        if ($size > self::IMAGE_BLOB_MAX_BYTES || strlen($binary) > self::IMAGE_BLOB_MAX_BYTES) {
            return new WP_Error(
                'bsky_image_too_large',
                __('The selected image could not be prepared for Bluesky.', 'calliope-social-publisher'),
                [
                    'phase' => 'upload',
                    'reason' => 'blob_limit_guard',
                    'size' => max($size, strlen($binary)),
                    'max_size' => self::IMAGE_BLOB_MAX_BYTES,
                    'retryable' => false,
                    'auth_error' => false,
                ]
            );
        }

        $res = wp_remote_post('https://bsky.social/xrpc/com.atproto.repo.uploadBlob', [
            'headers' => [
                'Authorization' => 'Bearer ' . $jwt,
                'Content-Type'  => $mime,
            ],
            'body'    => $binary,
            'timeout' => 45,
        ]);

        $safe_context = [
            'phase'      => 'upload',
            'mime'       => $mime,
            'size'       => $size,
            'width'      => is_array($aspect_ratio) ? (int) ($aspect_ratio['width'] ?? 0) : 0,
            'height'     => is_array($aspect_ratio) ? (int) ($aspect_ratio['height'] ?? 0) : 0,
            'retryable'  => false,
            'auth_error' => false,
        ];

        if (is_wp_error($res)) {
            $safe_context['upstream_error_code'] = sanitize_key((string) $res->get_error_code());
            $safe_context['retryable'] = true;
            return new WP_Error(
                'bsky_image_upload_transport',
                __('Bluesky could not upload the selected image.', 'calliope-social-publisher'),
                $safe_context
            );
        }

        $status = (int) wp_remote_retrieve_response_code($res);
        $raw_body = (string) wp_remote_retrieve_body($res);
        if ($status < 200 || $status >= 300) {
            $error_details = $this->extract_xrpc_error_details($raw_body);
            $safe_context['http_code'] = $status;
            $safe_context['retryable'] = $this->is_retryable_http_status($status);
            $safe_context['auth_error'] = $this->is_definitive_auth_failure(
                $status,
                $error_details['error'],
                $error_details['message']
            );
            if ($error_details['error'] !== '') {
                $safe_context['xrpc_error'] = $error_details['error'];
            }
            if ($error_details['message'] !== '') {
                $safe_context['upstream_message'] = $error_details['message'];
            }

            return new WP_Error(
                'bsky_image_upload_http',
                $safe_context['auth_error']
                    ? __('Bluesky rejected the saved credentials.', 'calliope-social-publisher')
                    : __('Bluesky rejected the selected image.', 'calliope-social-publisher'),
                $safe_context
            );
        }

        $body = json_decode($raw_body, true);
        if (!is_array($body)) {
            $safe_context['http_code'] = $status;
            $safe_context['response_reason'] = 'invalid_json';
            return new WP_Error(
                'bsky_image_upload_invalid_response',
                __('Bluesky did not return a valid uploaded image.', 'calliope-social-publisher'),
                $safe_context
            );
        }

        $blob = isset($body['blob']) && is_array($body['blob']) ? $body['blob'] : [];
        if (!$this->is_valid_uploaded_blob($blob)) {
            $safe_context['http_code'] = $status;
            $safe_context['response_reason'] = 'invalid_blob';
            return new WP_Error(
                'bsky_image_upload_invalid_response',
                __('Bluesky did not return a valid uploaded image.', 'calliope-social-publisher'),
                $safe_context
            );
        }

        return [
            'blob'        => $blob,
            'aspectRatio' => $aspect_ratio,
        ];
    }

    /**
     * Upload a prepared video to Bluesky.
     *
     * @param string $jwt Access JWT.
     * @param array{bytes:string,mime:string,size:int,aspect_ratio:?array{width:int,height:int},description:string} $prepared_video
     * @return array{blob:array,aspectRatio:?array{width:int,height:int}}|WP_Error
     */
    private function upload_video_blob($jwt, array $prepared_video) {
        $binary = isset($prepared_video['bytes']) && is_string($prepared_video['bytes']) ? $prepared_video['bytes'] : '';
        $mime = isset($prepared_video['mime']) ? $this->normalize_video_mime((string) $prepared_video['mime']) : '';
        $size = isset($prepared_video['size']) ? (int) $prepared_video['size'] : strlen($binary);
        $aspect_ratio = isset($prepared_video['aspect_ratio']) && is_array($prepared_video['aspect_ratio']) ? $prepared_video['aspect_ratio'] : null;

        if ($binary === '' || !in_array($mime, $this->bluesky_video_mimes(), true)) {
            return new WP_Error('bsky_video_invalid', __('The selected video could not be prepared for Bluesky.', 'calliope-social-publisher'), [
                'phase' => 'upload', 'retryable' => false, 'auth_error' => false,
            ]);
        }

        $session_did = $this->current_session_did;
        if ($session_did === '') {
            return new WP_Error('bsky_video_identity_missing', __('Bluesky could not determine the connected account for video processing.', 'calliope-social-publisher'), [
                'phase' => 'upload', 'retryable' => false, 'auth_error' => false,
            ]);
        }

        $pds = $this->current_pds_service($session_did);
        if (is_wp_error($pds)) {
            return $pds;
        }

        $this->log_video_diagnostics('video_service_auth_prepare', [
            'mime_type' => $mime,
            'size' => $size,
            'limit_bytes' => $this->bluesky_video_max_file_size(),
            'pds_host' => (string) wp_parse_url((string) $pds['url'], PHP_URL_HOST),
        ]);

        $auth_url = add_query_arg([
            'aud' => (string) $pds['did'],
            'lxm' => 'com.atproto.repo.uploadBlob',
            'exp' => time() + 1800,
        ], rtrim((string) $pds['url'], '/') . '/xrpc/com.atproto.server.getServiceAuth');
        $auth_res = wp_safe_remote_get($auth_url, [
            'headers' => ['Authorization' => 'Bearer ' . $jwt, 'Accept' => 'application/json'],
            'timeout' => 30, 'redirection' => 0, 'sslverify' => true,
        ]);
        if (is_wp_error($auth_res)) {
            return new WP_Error('bsky_video_service_auth_transport', __('Bluesky could not authorize video processing.', 'calliope-social-publisher'), [
                'phase' => 'upload', 'retryable' => true, 'auth_error' => false,
            ]);
        }
        $auth_http = (int) wp_remote_retrieve_response_code($auth_res);
        $auth_json = json_decode((string) wp_remote_retrieve_body($auth_res), true);
        $service_token = is_array($auth_json) && isset($auth_json['token']) ? trim((string) $auth_json['token']) : '';
        if ($auth_http < 200 || $auth_http >= 300 || $service_token === '') {
            return new WP_Error('bsky_video_service_auth_failed', __('Bluesky rejected video processing authorization.', 'calliope-social-publisher'), [
                'phase' => 'upload', 'http_code' => $auth_http, 'retryable' => $this->is_retryable_http_status($auth_http), 'auth_error' => in_array($auth_http, [401,403], true),
            ]);
        }

        $filename = 'calliope-video.mp4';
        $upload_url = add_query_arg(['did' => $session_did, 'name' => $filename], 'https://video.bsky.app/xrpc/app.bsky.video.uploadVideo');
        $upload_res = wp_safe_remote_post($upload_url, [
            'headers' => [
                'Authorization' => 'Bearer ' . $service_token,
                'Content-Type' => $mime,
                'Content-Length' => (string) $size,
                'Accept' => 'application/json',
            ],
            'body' => $binary, 'timeout' => 180, 'redirection' => 0, 'sslverify' => true,
        ]);
        if (is_wp_error($upload_res)) {
            return new WP_Error('bsky_video_upload_transport', __('Bluesky could not upload the selected video.', 'calliope-social-publisher'), [
                'phase' => 'upload', 'retryable' => true, 'auth_error' => false,
            ]);
        }
        $upload_http = (int) wp_remote_retrieve_response_code($upload_res);
        $upload_raw_body = (string) wp_remote_retrieve_body($upload_res);
        $upload_json = json_decode($upload_raw_body, true);
        $this->log_video_diagnostics('video_service_upload_response', [
            'http_code' => $upload_http, 'has_job_id' => !empty($upload_json['jobId']), 'has_blob' => !empty($upload_json['blob']),
        ], ($upload_http >= 200 && $upload_http < 300) ? 'info' : 'error');
        if ($upload_http < 200 || $upload_http >= 300 || !is_array($upload_json)) {
            $error_details = $this->extract_xrpc_error_details($upload_raw_body);
            $auth_error = $this->is_definitive_auth_failure($upload_http, $error_details['error'], $error_details['message']);
            $safe_context = [
                'phase' => 'upload', 'http_code' => $upload_http, 'retryable' => $this->is_retryable_http_status($upload_http), 'auth_error' => $auth_error,
            ];
            if ($error_details['error'] !== '') {
                $safe_context['xrpc_error'] = $error_details['error'];
            }
            if ($error_details['message'] !== '') {
                $safe_context['upstream_message'] = $error_details['message'];
            }
            return new WP_Error(
                'bsky_video_upload_failed',
                $auth_error
                    ? __('Bluesky rejected the saved credentials. Reconnect Bluesky or create a new App Password, then try again.', 'calliope-social-publisher')
                    : __('Bluesky could not process the selected video.', 'calliope-social-publisher'),
                $safe_context
            );
        }

        $blob = isset($upload_json['blob']) && is_array($upload_json['blob']) ? $upload_json['blob'] : null;
        $job_id = isset($upload_json['jobId']) ? sanitize_text_field((string) $upload_json['jobId']) : '';
        for ($attempt = 1; !is_array($blob) && $job_id !== '' && $attempt <= 90; $attempt++) {
            usleep(1000000);
            $status_url = add_query_arg(['jobId' => $job_id], 'https://video.bsky.app/xrpc/app.bsky.video.getJobStatus');
            $status_res = wp_safe_remote_get($status_url, ['headers' => ['Accept' => 'application/json'], 'timeout' => 30, 'redirection' => 0, 'sslverify' => true]);
            if (is_wp_error($status_res)) { continue; }
            $status_http = (int) wp_remote_retrieve_response_code($status_res);
            $status_json = json_decode((string) wp_remote_retrieve_body($status_res), true);
            $job = is_array($status_json) && isset($status_json['jobStatus']) && is_array($status_json['jobStatus']) ? $status_json['jobStatus'] : [];
            if (isset($job['blob']) && is_array($job['blob'])) { $blob = $job['blob']; break; }
            $state = sanitize_key((string) ($job['state'] ?? ''));
            if (in_array($state, ['job_failed', 'failed', 'error'], true)) {
                return new WP_Error('bsky_video_processing_failed', __('Bluesky could not finish processing the selected video.', 'calliope-social-publisher'), [
                    'phase' => 'upload', 'http_code' => $status_http, 'retryable' => false, 'auth_error' => false,
                ]);
            }
        }
        if (!is_array($blob)) {
            return new WP_Error('bsky_video_processing_timeout', __('Bluesky is still processing the video. Please try again in a moment.', 'calliope-social-publisher'), [
                'phase' => 'upload', 'retryable' => true, 'auth_error' => false,
            ]);
        }
        $this->log_video_diagnostics('video_service_processing_finished', ['has_blob' => true, 'job_id_set' => $job_id !== '']);
        return ['blob' => $blob, 'aspectRatio' => $aspect_ratio];
    }

    /** @return string[] */
    private function bluesky_video_mimes() {
        $mimes = apply_filters('calliope_sp_bluesky_video_mimes', ['video/mp4']);
        $clean = [];
        foreach ((array) $mimes as $mime) {
            $mime = $this->normalize_video_mime((string) $mime);
            if ($mime !== '' && strpos($mime, 'video/') === 0) {
                $clean[] = $mime;
            }
        }
        return $clean !== [] ? array_values(array_unique($clean)) : ['video/mp4'];
    }

    private function bluesky_video_max_file_size() {
        $limit = (int) apply_filters('calliope_sp_bluesky_video_max_file_size', self::VIDEO_MAX_BYTES);
        return max(1024, min(self::VIDEO_MAX_BYTES, $limit));
    }

    /** @return string|false */
    private function read_local_file($path) {
        $path = (string) $path;
        if ($path === '' || !is_file($path) || !is_readable($path)) {
            return false;
        }
        $contents = file_get_contents($path);
        return is_string($contents) ? $contents : false;
    }

    /** @return array{width:int,height:int}|null */
    private function video_aspect_ratio($attachment_id, $path) {
        $metadata = [];
        if (function_exists('wp_read_video_metadata')) {
            $read = wp_read_video_metadata((string) $path);
            if (is_array($read)) {
                $metadata = $read;
            }
        }
        if ($metadata === [] && function_exists('wp_get_attachment_metadata')) {
            $read = wp_get_attachment_metadata(absint($attachment_id));
            if (is_array($read)) {
                $metadata = $read;
            }
        }

        $width = absint($metadata['width'] ?? 0);
        $height = absint($metadata['height'] ?? 0);
        return $width > 0 && $height > 0 ? ['width' => $width, 'height' => $height] : null;
    }

    private function video_extension_matches($filename, $mime) {
        $extension = strtolower((string) pathinfo((string) $filename, PATHINFO_EXTENSION));
        $mime = $this->normalize_video_mime($mime);
        return $mime === 'video/mp4' && in_array($extension, ['mp4', 'm4v'], true);
    }

    private function normalize_video_mime($mime) {
        return strtolower(sanitize_mime_type((string) $mime));
    }

    private function is_youtube_link_source_payload(array $data) {
        $metadata = isset($data['metadata']) && is_array($data['metadata']) ? $data['metadata'] : [];
        return sanitize_key((string) ($metadata['video_source'] ?? '')) === 'youtube_link';
    }

    private function youtube_external_url_from_payload(array $data) {
        if (!$this->is_youtube_link_source_payload($data)) {
            return '';
        }
        $metadata = isset($data['metadata']) && is_array($data['metadata']) ? $data['metadata'] : [];
        $raw = isset($metadata['youtube_link_url']) ? (string) $metadata['youtube_link_url'] : (string) ($data['link'] ?? '');
        return $this->normalize_youtube_link_url($raw);
    }

    private function normalize_youtube_link_url($raw) {
        $raw = trim((string) $raw);
        if ($raw === '') {
            return '';
        }

        if (preg_match('/^(?:https?:\/\/)?(?:www\.|m\.)?(?:youtube\.com\/watch\?(?:[^#\s]*&)?v=|youtube\.com\/shorts\/|youtube\.com\/embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,32})/i', $raw, $matches) !== 1) {
            return '';
        }

        return 'https://www.youtube.com/watch?v=' . $matches[1];
    }

    private function youtube_video_id_from_url($url) {
        $url = $this->normalize_youtube_link_url($url);
        if ($url === '') {
            return '';
        }

        $parts = wp_parse_url($url);
        if (!is_array($parts) || empty($parts['query'])) {
            return '';
        }

        parse_str((string) $parts['query'], $query);
        $id = isset($query['v']) && is_scalar($query['v']) ? (string) $query['v'] : '';
        return preg_match('/^[A-Za-z0-9_-]{6,32}$/', $id) === 1 ? $id : '';
    }

    private function prepare_youtube_external_embed($jwt, array $data, $url) {
        $url = $this->normalize_youtube_link_url($url);
        $video_id = $this->youtube_video_id_from_url($url);
        if ($url === '' || $video_id === '') {
            return new WP_Error(
                'bsky_youtube_link_invalid',
                __('Bluesky requires a valid YouTube URL to publish a YouTube link embed. No text-only fallback was sent.', 'calliope-social-publisher'),
                ['phase' => 'external_embed', 'retryable' => false, 'auth_error' => false]
            );
        }

        $title = $this->external_embed_text((string) ($data['title'] ?? ''), 300);
        if ($title === '') {
            $title = __('YouTube video', 'calliope-social-publisher');
        }
        $description = $this->external_embed_text((string) ($data['content'] ?? ''), 1000);

        $embed = [
            '$type' => 'app.bsky.embed.external',
            'external' => [
                'uri' => $url,
                'title' => $title,
                'description' => $description,
            ],
        ];

        $thumbnail = $this->fetch_youtube_thumbnail_image($video_id);
        if (is_array($thumbnail)) {
            $uploaded = $this->upload_blob($jwt, $thumbnail);
            if (!is_wp_error($uploaded) && !empty($uploaded['blob']) && is_array($uploaded['blob'])) {
                $embed['external']['thumb'] = $uploaded['blob'];
            } else {
                $this->log_video_diagnostics('youtube_link_thumb_upload_skipped', [
                    'reason' => is_wp_error($uploaded) ? sanitize_key((string) $uploaded->get_error_code()) : 'invalid_upload',
                ], 'warning');
            }
        }

        return $embed;
    }

    private function external_embed_text($value, $limit) {
        $value = wp_strip_all_tags((string) $value);
        $value = preg_replace('/[\x00-\x1F\x7F]/u', ' ', (string) $value);
        $value = preg_replace('/\s+/u', ' ', (string) $value);
        $value = trim((string) $value);
        $limit = max(0, (int) $limit);
        if ($limit > 0 && function_exists('mb_strlen') && mb_strlen($value) > $limit) {
            return mb_substr($value, 0, max(0, $limit - 3)) . '...';
        }
        if ($limit > 0 && !function_exists('mb_strlen') && strlen($value) > $limit) {
            return substr($value, 0, max(0, $limit - 3)) . '...';
        }
        return $value;
    }

    private function fetch_youtube_thumbnail_image($video_id) {
        $video_id = preg_match('/^[A-Za-z0-9_-]{6,32}$/', (string) $video_id) === 1 ? (string) $video_id : '';
        if ($video_id === '') {
            return null;
        }

        foreach (['hqdefault.jpg', 'mqdefault.jpg'] as $filename) {
            $url = 'https://i.ytimg.com/vi/' . rawurlencode($video_id) . '/' . $filename;
            $res = wp_remote_get($url, [
                'timeout' => 20,
                'redirection' => 2,
                'limit_response_size' => self::EXTERNAL_THUMB_MAX_BYTES + 1,
            ]);
            if (is_wp_error($res)) {
                $this->log_video_diagnostics('youtube_link_thumb_fetch_failed', [
                    'reason' => sanitize_key((string) $res->get_error_code()),
                ], 'warning');
                continue;
            }

            $status = (int) wp_remote_retrieve_response_code($res);
            $body = (string) wp_remote_retrieve_body($res);
            if ($status < 200 || $status >= 300 || $body === '' || strlen($body) > self::EXTERNAL_THUMB_MAX_BYTES) {
                $this->log_video_diagnostics('youtube_link_thumb_fetch_skipped', [
                    'http_code' => $status,
                    'bytes' => strlen($body),
                ], 'warning');
                continue;
            }

            $content_type = wp_remote_retrieve_header($res, 'content-type');
            if (is_array($content_type)) {
                $content_type = reset($content_type);
            }
            $mime = $this->thumbnail_mime_from_binary($body, (string) $content_type);
            if ($mime === '') {
                $this->log_video_diagnostics('youtube_link_thumb_fetch_skipped', [
                    'reason' => 'unsupported_thumbnail_mime',
                ], 'warning');
                continue;
            }

            return [
                'bytes' => $body,
                'mime' => $mime,
                'size' => strlen($body),
                'aspect_ratio' => $this->image_aspect_ratio_from_binary($body),
            ];
        }

        return null;
    }

    private function thumbnail_mime_from_binary($binary, $content_type) {
        $mime = strtolower(trim((string) preg_replace('/;.*$/', '', (string) $content_type)));
        $mime = $this->normalize_image_mime($mime);
        if (in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true) && $this->has_valid_image_signature($binary, $mime)) {
            return $mime;
        }

        foreach (['image/jpeg', 'image/png', 'image/webp'] as $candidate) {
            if ($this->has_valid_image_signature($binary, $candidate)) {
                return $candidate;
            }
        }

        return '';
    }

    private function image_aspect_ratio_from_binary($binary) {
        if (!function_exists('getimagesizefromstring')) {
            return null;
        }
        $size = @getimagesizefromstring((string) $binary);
        if (!is_array($size) || empty($size[0]) || empty($size[1])) {
            return null;
        }
        return [
            'width' => max(1, (int) $size[0]),
            'height' => max(1, (int) $size[1]),
        ];
    }

    /** @param array<int,mixed> $media */
    private function media_log_items(array $media) {
        $items = [];
        foreach ($media as $item) {
            if (!is_array($item)) {
                continue;
            }
            $items[] = [
                'id' => absint($item['id'] ?? 0),
                'type' => sanitize_key((string) ($item['type'] ?? '')),
                'mime_type' => sanitize_mime_type((string) ($item['mime_type'] ?? '')),
            ];
        }
        return $items;
    }

    /** @param array<string,mixed> $entry */
    private function log_video_diagnostics($event, array $entry, $status = 'info') {
        if (!class_exists('Calliope_SP_Logger')) {
            return;
        }
        $entry = array_merge([
            'event' => 'bsky_video_' . sanitize_key((string) $event),
        ], $entry);
        Calliope_SP_Logger::log('bluesky', sanitize_key((string) $status), wp_json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    private function has_valid_image_signature($binary, $mime) {
        $binary = is_string($binary) ? $binary : '';
        $mime = $this->normalize_image_mime($mime);

        if ($mime === 'image/jpeg') {
            return strlen($binary) >= 4 && substr($binary, 0, 3) === "\xFF\xD8\xFF";
        }

        if ($mime === 'image/png') {
            return strlen($binary) >= 8 && substr($binary, 0, 8) === "\x89PNG\r\n\x1A\n";
        }

        if ($mime === 'image/webp') {
            return strlen($binary) >= 12
                && substr($binary, 0, 4) === 'RIFF'
                && substr($binary, 8, 4) === 'WEBP';
        }

        return false;
    }

    private function normalize_image_mime($mime) {
        $mime = strtolower(trim((string) $mime));
        if ($mime === 'image/jpg' || $mime === 'image/pjpeg') {
            return 'image/jpeg';
        }

        return $mime;
    }

    private function is_valid_uploaded_blob(array $blob) {
        if (empty($blob)) {
            return false;
        }

        if (!isset($blob['$type']) || !is_string($blob['$type']) || $blob['$type'] !== 'blob') {
            return false;
        }

        if (!isset($blob['ref']) || !is_array($blob['ref']) || empty($blob['ref']['$link']) || !is_string($blob['ref']['$link'])) {
            return false;
        }

        if (empty($blob['mimeType']) || !is_string($blob['mimeType'])) {
            return false;
        }
        $blob_mime = $this->normalize_image_mime($blob['mimeType']);
        if (!in_array($blob_mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
            return false;
        }

        if (!isset($blob['size']) || !is_numeric($blob['size']) || (int) $blob['size'] <= 0) {
            return false;
        }

        return true;
    }

    private function is_valid_uploaded_video_blob(array $blob) {
        if (empty($blob)) {
            return false;
        }

        if (!isset($blob['$type']) || !is_string($blob['$type']) || $blob['$type'] !== 'blob') {
            return false;
        }

        if (!isset($blob['ref']) || !is_array($blob['ref']) || empty($blob['ref']['$link']) || !is_string($blob['ref']['$link'])) {
            return false;
        }

        if (empty($blob['mimeType']) || !is_string($blob['mimeType'])) {
            return false;
        }
        if (!in_array($this->normalize_video_mime($blob['mimeType']), $this->bluesky_video_mimes(), true)) {
            return false;
        }

        if (!isset($blob['size']) || !is_numeric($blob['size']) || (int) $blob['size'] <= 0) {
            return false;
        }

        return true;
    }

    /** @return array{error:string,message:string} */
    private function extract_xrpc_error_details($raw_body) {
        $decoded = json_decode((string) $raw_body, true);
        if (!is_array($decoded)) {
            return ['error' => '', 'message' => ''];
        }

        $error = isset($decoded['error']) && is_scalar($decoded['error'])
            ? preg_replace('/[^A-Za-z0-9_-]/', '', (string) $decoded['error'])
            : '';
        $message = isset($decoded['message']) && is_scalar($decoded['message'])
            ? $this->sanitize_remote_message((string) $decoded['message'])
            : '';

        return ['error' => (string) $error, 'message' => $message];
    }

    /** @return string */
    private function sanitize_remote_message($message) {
        $message = wp_strip_all_tags((string) $message);
        $message = preg_replace('/[\x00-\x1F\x7F]/u', ' ', (string) $message);
        $message = preg_replace('/\s+/u', ' ', (string) $message);
        $message = preg_replace('/\b(?:Bearer\s+)?[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,}\b/u', '[redacted]', (string) $message);
        $message = preg_replace('/((?:accessJwt|refreshJwt|authorization|password|token)\s*[=:]\s*)([^\s,;]+)/iu', '$1[redacted]', (string) $message);
        $message = trim((string) $message);
        return function_exists('mb_substr') ? mb_substr($message, 0, 300) : substr($message, 0, 300);
    }

    /** @return WP_Error */
    private function build_transport_error($response, $phase) {
        return new WP_Error(
            'bsky_transport_error',
            __('Bluesky could not be reached. Please try again later.', 'calliope-social-publisher'),
            [
                'phase'          => sanitize_key((string) $phase),
                'retryable'      => true,
                'auth_error'     => false,
                'transport_code' => is_wp_error($response) ? sanitize_key((string) $response->get_error_code()) : '',
            ]
        );
    }

    /** @return WP_Error */
    private function build_http_error($status, $raw_body, $phase) {
        $details = $this->extract_xrpc_error_details($raw_body);
        $auth_error = $this->is_definitive_auth_failure($status, $details['error'], $details['message']);
        $retryable = $this->is_retryable_http_status($status);
        $data = [
            'phase'      => sanitize_key((string) $phase),
            'http_code'  => (int) $status,
            'retryable'  => $retryable,
            'auth_error' => $auth_error,
        ];
        if ($details['error'] !== '') {
            $data['xrpc_error'] = $details['error'];
        }
        if ($details['message'] !== '') {
            $data['upstream_message'] = $details['message'];
        }

        if ($auth_error) {
            $message = __('Bluesky rejected the saved handle or App Password.', 'calliope-social-publisher');
            $code = 'bsky_auth_invalid';
        } elseif ($retryable) {
            $message = __('Bluesky is temporarily unavailable. Please try again later.', 'calliope-social-publisher');
            $code = 'bsky_temporary_error';
        } else {
            $message = __('Bluesky could not complete the request.', 'calliope-social-publisher');
            $code = 'bsky_api_error';
        }

        return new WP_Error($code, $message, $data);
    }

    /** @return bool */
    private function is_retryable_http_status($status) {
        $status = (int) $status;
        return $status === 408 || $status === 429 || ($status >= 500 && $status <= 599);
    }

    /** @return bool */
    private function is_definitive_auth_failure($status, $xrpc_error, $message) {
        $status = (int) $status;
        $error_key = strtolower(preg_replace('/[^A-Za-z0-9]/', '', (string) $xrpc_error));
        $definitive = [
            'authenticationrequired',
            'invalididentifier',
            'invalidpassword',
            'invalidtoken',
            'expiredtoken',
            'accounttakedown',
            'accountdeactivated',
        ];
        if ($status === 401 || in_array($error_key, $definitive, true)) {
            return true;
        }

        $message = strtolower((string) $message);
        $auth_message = preg_match('/invalid (?:identifier|password|credentials)|authentication required|app password.{0,40}(?:invalid|revoked)|(?:invalid|revoked).{0,40}app password|account.{0,30}(?:taken down|deactivated)/i', $message) === 1;
        if ($status === 400 && $auth_message) {
            return true;
        }
        if ($status === 403 && preg_match('/auth|token|credential|password|unauthor/i', $message)) {
            return true;
        }

        return false;
    }

    /** @return array<string,mixed>|WP_Error */
    private function bsky_req($endpoint, $method, $body, $phase = 'request') {
        $res = wp_remote_request('https://bsky.social/xrpc/' . $endpoint, [
            'method'  => $method,
            'headers' => ['Content-Type' => 'application/json'],
            'body'    => wp_json_encode($body),
            'timeout' => 30,
        ]);

        if (is_wp_error($res)) {
            return $this->build_transport_error($res, $phase);
        }

        $status = (int) wp_remote_retrieve_response_code($res);
        $raw_body = (string) wp_remote_retrieve_body($res);
        $json = json_decode($raw_body, true);
        if ($status >= 200 && $status < 300 && is_array($json)) {
            return $json;
        }
        if ($status >= 200 && $status < 300) {
            return new WP_Error(
                'bsky_invalid_response',
                __('Bluesky returned an invalid response.', 'calliope-social-publisher'),
                ['phase' => sanitize_key((string) $phase), 'http_code' => $status, 'retryable' => true, 'auth_error' => false]
            );
        }

        return $this->build_http_error($status, $raw_body, $phase);
    }

    /**
     * Format title, content, hashtags, and the link while respecting the limit.
     */
    private function format_text_clean_tags($data, $limit) {
        $title = trim((string) ($data['title'] ?? ''));
        $content = trim((string) ($data['content'] ?? ''));
        $link = trim((string) ($data['link'] ?? ''));
        $tags = !empty($data['hashtags']) ? preg_split('/\s+/', trim((string) $data['hashtags'])) : [];

        $tags_clean = [];
        foreach ($tags as $tag) {
            $tag = trim((string) $tag);
            if ($tag === '') {
                continue;
            }
            $tags_clean[] = $tag;
        }
        $hashtags_line = implode(' ', $tags_clean);

        $fixed_blocks = [];
        if ($title !== '') {
            $fixed_blocks[] = $title;
        }
        if ($hashtags_line !== '') {
            $fixed_blocks[] = $hashtags_line;
        }
        if ($link !== '') {
            $fixed_blocks[] = $link;
        }

        $reserved_length = 0;
        if (!empty($fixed_blocks)) {
            $reserved_length = array_sum(array_map('mb_strlen', $fixed_blocks));
            $reserved_length += count($fixed_blocks);
        }

        $content_limit = max(0, $limit - $reserved_length);
        if ($content !== '' && mb_strlen($content) > $content_limit) {
            $content = $content_limit > 3 ? mb_substr($content, 0, $content_limit - 3) . '...' : '';
        }

        $blocks = [];
        foreach ([$title, $content, $hashtags_line, $link] as $block) {
            if ($block !== '') {
                $blocks[] = $block;
            }
        }

        $text = implode("\n", $blocks);
        if (mb_strlen($text) <= $limit) {
            return $text;
        }

        if ($hashtags_line !== '') {
            $tags_final = [];
            foreach ($tags_clean as $tag) {
                $candidate = implode(' ', array_merge($tags_final, [$tag]));
                $candidate_blocks = [];
                foreach ([$title, $content, $candidate, $link] as $block) {
                    if ($block !== '') {
                        $candidate_blocks[] = $block;
                    }
                }
                $candidate_text = implode("\n", $candidate_blocks);
                if (mb_strlen($candidate_text) <= $limit) {
                    $tags_final[] = $tag;
                } else {
                    break;
                }
            }
            $hashtags_line = implode(' ', $tags_final);
        }

        $blocks = [];
        foreach ([$title, $content, $hashtags_line, $link] as $block) {
            if ($block !== '') {
                $blocks[] = $block;
            }
        }

        return implode("\n", $blocks);
    }

    private function gen_facets($text) {
        $facets = [];

        // Links
        preg_match_all('/(https?:\/\/[^\s]+)/', $text, $m, PREG_OFFSET_CAPTURE);
        foreach ($m[0] as $x) {
            $facets[] = [
                'index' => [
                    'byteStart' => $x[1],
                    'byteEnd' => $x[1] + strlen($x[0]),
                ],
                'features' => [[
                    '$type' => 'app.bsky.richtext.facet#link',
                    'uri' => $x[0],
                ]],
            ];
        }

        // Tags
        preg_match_all('/#([\p{L}\p{M}\p{N}_]+)/u', $text, $m, PREG_OFFSET_CAPTURE);
        foreach ($m[0] as $k => $x) {
            $tag = $m[1][$k][0] ?? '';
            if ($tag === '') continue;

            $facet_tag = $this->normalize_facet_tag($tag);
            if ($facet_tag === '') {
                continue;
            }

            $facets[] = [
                'index' => [
                    'byteStart' => $x[1],
                    'byteEnd' => $x[1] + strlen($x[0]),
                ],
                'features' => [[
                    '$type' => 'app.bsky.richtext.facet#tag',
                    'tag' => $facet_tag,
                ]],
            ];
        }

        return $facets;
    }

    private function normalize_facet_tag($tag) {
        $tag = (string) $tag;

        if (function_exists('remove_accents')) {
            $tag = remove_accents($tag);
        }

        $tag = preg_replace('/[^A-Za-z0-9_]+/', '', $tag);

        return trim((string) $tag);
    }
}
