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

/**
 * Telegram Bot API integration for groups, supergroups, channels and forum topics.
 */
class Calliope_SP_Telegram {

    private const VALIDATION_OPTION = 'calliope_sp_telegram_validation';
    private const VALIDATION_SCHEMA = 'telegram-validation-v1';
    private const MINIMUM_EVIDENCE = 'E5';
    private const VIDEO_MAX_BYTES = 50 * 1024 * 1024;
    private const MEDIA_GROUP_MAX_ITEMS = 10;

    /**
     * Build the local Telegram 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(
                'telegram_connection_contract_missing',
                __('Telegram connection status is temporarily unavailable.', 'calliope-social-publisher')
            );
        }

        $raw_token = (string) get_option('calliope_sp_telegram_bot_token', '');
        $raw_chat = (string) get_option('calliope_sp_telegram_chat_id', '');
        $raw_thread = get_option('calliope_sp_telegram_message_thread_id', 0);
        $has_token = trim($raw_token) !== '';
        $has_chat = trim($raw_chat) !== '';

        if (!$has_token && !$has_chat && $this->is_empty_thread_value($raw_thread)) {
            return Calliope_SP_Connection_State::create([
                'network'               => 'telegram',
                '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 a Telegram Bot Token and destination.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'expires_at'            => 0,
                'validated_at'          => 0,
                'connection_method'     => 'bot_token',
            ], self::MINIMUM_EVIDENCE);
        }

        $token = $this->validate_bot_token_value($raw_token);
        $destination = $this->get_validated_destination($raw_chat, $raw_thread);
        if (!$has_token || !$has_chat || is_wp_error($token) || is_wp_error($destination)) {
            return Calliope_SP_Connection_State::create([
                'network'               => 'telegram',
                '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'               => __('Telegram configuration is incomplete or invalid.', 'calliope-social-publisher'),
                'action'                => __('Review Telegram configuration', 'calliope-social-publisher'),
                'expires_at'            => 0,
                'validated_at'          => 0,
                'connection_method'     => 'bot_token',
            ], self::MINIMUM_EVIDENCE);
        }

        $credentials_fingerprint = $this->build_credentials_fingerprint($token);
        $target_fingerprint = $this->build_target_fingerprint(
            $destination['configured_chat_id'],
            $destination['thread_id']
        );
        $marker = $this->get_matching_validation_marker($credentials_fingerprint, $target_fingerprint);

        if (is_array($marker) && $marker['health'] === 'invalid') {
            $failure_kind = (string) $marker['failure_kind'];
            if ($failure_kind === 'auth') {
                return Calliope_SP_Connection_State::create([
                    'network'               => 'telegram',
                    '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 Telegram Bot Token was rejected. Create a new token with BotFather and try again.', 'calliope-social-publisher'),
                    'action'                => __('Create a new Bot Token', 'calliope-social-publisher'),
                    'expires_at'            => 0,
                    'validated_at'          => (int) $marker['validated_at'],
                    'connection_method'     => 'bot_token',
                ], self::MINIMUM_EVIDENCE);
            }

            if ($failure_kind === 'target') {
                return Calliope_SP_Connection_State::create([
                    'network'               => 'telegram',
                    'status'                => 'target_missing',
                    'health'                => 'invalid',
                    'is_configured'         => true,
                    'is_authenticated'      => true,
                    'is_identity_known'     => true,
                    'has_target'            => false,
                    'has_permission'        => false,
                    'evidence_level'        => 'E3',
                    'message'               => __('The selected Telegram destination is unavailable or no longer matches the validated chat.', 'calliope-social-publisher'),
                    'action'                => __('Review destination', 'calliope-social-publisher'),
                    'expires_at'            => 0,
                    'validated_at'          => (int) $marker['validated_at'],
                    'connection_method'     => 'bot_token',
                ], self::MINIMUM_EVIDENCE);
            }

            if ($failure_kind === 'permission') {
                return Calliope_SP_Connection_State::create([
                    'network'               => 'telegram',
                    'status'                => 'permission_missing',
                    'health'                => 'invalid',
                    'is_configured'         => true,
                    'is_authenticated'      => true,
                    'is_identity_known'     => true,
                    'has_target'            => true,
                    'has_permission'        => false,
                    'evidence_level'        => 'E4',
                    'message'               => __('The bot cannot publish messages to the selected Telegram destination.', 'calliope-social-publisher'),
                    'action'                => __('Review bot permissions', 'calliope-social-publisher'),
                    'expires_at'            => 0,
                    'validated_at'          => (int) $marker['validated_at'],
                    'connection_method'     => 'bot_token',
                ], self::MINIMUM_EVIDENCE);
            }

            return Calliope_SP_Connection_State::create([
                'network'               => 'telegram',
                'status'                => 'configuration_error',
                'health'                => 'invalid',
                'is_configured'         => true,
                'is_authenticated'      => true,
                'is_identity_known'     => true,
                'has_target'            => true,
                'has_permission'        => false,
                'evidence_level'        => 'E4',
                'message'               => __('The selected Telegram Topic ID is invalid for the validated destination.', 'calliope-social-publisher'),
                'action'                => __('Review Topic ID', 'calliope-social-publisher'),
                'expires_at'            => 0,
                'validated_at'          => (int) $marker['validated_at'],
                'connection_method'     => 'bot_token',
            ], self::MINIMUM_EVIDENCE);
        }

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

        if ($has_valid_evidence) {
            $health = (string) $marker['health'];
            $message = '';
            $action = '';
            if ($marker['topic_state'] === 'deferred') {
                $health = 'degraded';
                $message = __('The bot and forum were verified. The specific Topic ID will be confirmed on the first publication.', 'calliope-social-publisher');
                $action = __('Publish once to confirm the Topic ID', 'calliope-social-publisher');
            } elseif ($marker['can_send_photo'] !== true) {
                $health = 'degraded';
                $message = __('Telegram is ready for text, but this bot may not send photos to the selected destination.', 'calliope-social-publisher');
                $action = __('Review photo permissions', 'calliope-social-publisher');
            } elseif ($health === 'degraded') {
                $message = __('Telegram was verified previously, but the latest check failed temporarily.', 'calliope-social-publisher');
                $action = __('Save and test', 'calliope-social-publisher');
            }

            return Calliope_SP_Connection_State::create([
                'network'               => 'telegram',
                'status'                => 'ready_to_publish',
                'health'                => $health,
                'is_configured'         => true,
                'is_authenticated'      => true,
                'is_identity_known'     => true,
                'has_target'            => true,
                'has_permission'        => true,
                'evidence_level'        => 'E5',
                'message'               => $message,
                'action'                => $action,
                'expires_at'            => 0,
                'validated_at'          => (int) $marker['validated_at'],
                'connection_method'     => 'bot_token',
            ], self::MINIMUM_EVIDENCE);
        }

        $health = is_array($marker) && $marker['health'] === 'degraded' ? 'degraded' : 'unknown';
        return Calliope_SP_Connection_State::create([
            'network'               => 'telegram',
            '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'
                ? __('Telegram could not be verified because of a temporary connection problem.', 'calliope-social-publisher')
                : __('Telegram credentials are saved but have not been verified for the current destination.', 'calliope-social-publisher'),
            'action'                => __('Save and test', 'calliope-social-publisher'),
            'expires_at'            => 0,
            'validated_at'          => 0,
            'connection_method'     => 'bot_token',
        ], self::MINIMUM_EVIDENCE);
    }

    /**
     * Normalize a Telegram chat destination.
     *
     * @param mixed $chat_id Raw chat id, username or public t.me link.
     * @return string Empty string means invalid.
     */
    public function normalize_chat_id($chat_id) {
        if (!is_scalar($chat_id)) {
            return '';
        }

        $chat_id = trim((string) $chat_id);
        if ($chat_id === ''
            || strpos($chat_id, "\0") !== false
            || preg_match('/[\x00-\x20\x7F]/', $chat_id)
        ) {
            return '';
        }

        if (preg_match('#^https://(?:www\.)?t\.me/(?:s/)?([A-Za-z][A-Za-z0-9_]{4,31})/?$#i', $chat_id, $matches)) {
            $chat_id = '@' . $matches[1];
        } elseif (strpos($chat_id, '://') !== false) {
            return '';
        }

        if (preg_match('/^@[A-Za-z][A-Za-z0-9_]{4,31}$/', $chat_id)) {
            return '@' . strtolower(substr($chat_id, 1));
        }

        return $this->normalize_decimal_identifier($chat_id, true, false);
    }

    /**
     * Return true when a numeric Telegram destination is actually the bot's own user ID.
     * BotFather tokens begin with the bot user ID, so this can be rejected locally before
     * any publish request is attempted. Public @destinations are verified remotely.
     *
     * @param mixed $chat_id Destination candidate.
     * @param mixed $token Bot token candidate.
     */
    public function destination_is_bot_identity($chat_id, $token = null) {
        $normalized_chat = $this->normalize_chat_id($chat_id);
        if ($normalized_chat === '' || strpos($normalized_chat, '@') === 0) {
            return false;
        }
        if ($token === null) {
            $token = $this->get_bot_token();
        }
        $bot_id = $this->extract_bot_id_from_token($token);
        return $bot_id !== '' && hash_equals($bot_id, $normalized_chat);
    }

    /**
     * Validate a Bot Token value without storing or changing it.
     *
     * @param mixed $token Token candidate.
     * @return string|WP_Error
     */
    public function validate_bot_token_value($token) {
        if (!is_scalar($token)) {
            return new WP_Error('telegram_invalid_token', __('Invalid Telegram Bot Token.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        $token = trim((string) $token);
        if ($token === '') {
            return new WP_Error('telegram_missing_token', __('Missing Telegram Bot Token.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        if (
            strpos($token, "\0") !== false
            || preg_match('/[\x00-\x20\x7F]/', $token)
            || !preg_match('/\A[A-Za-z0-9_-]+:[A-Za-z0-9_-]+\z/', $token)
        ) {
            return new WP_Error('telegram_invalid_token', __('Invalid Telegram Bot Token.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        return $token;
    }

    /**
     * Validate and normalize the destination and optional Topic ID.
     *
     * @param mixed $chat_id Raw chat destination.
     * @param mixed $thread_id Raw message thread ID.
     * @return array{configured_chat_id:string,thread_id:int}|WP_Error
     */
    public function get_validated_destination($chat_id = null, $thread_id = null) {
        if ($chat_id === null) {
            $chat_id = get_option('calliope_sp_telegram_chat_id', '');
        }
        if ($thread_id === null) {
            $thread_id = get_option('calliope_sp_telegram_message_thread_id', 0);
        }

        $normalized_chat = $this->normalize_chat_id($chat_id);
        if ($normalized_chat === '') {
            return new WP_Error('telegram_invalid_destination', __('Enter a valid Telegram destination.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        $normalized_thread = $this->normalize_message_thread_id($thread_id);
        if (is_wp_error($normalized_thread)) {
            return $normalized_thread;
        }

        return [
            'configured_chat_id' => $normalized_chat,
            'thread_id'          => $normalized_thread,
        ];
    }

    /**
     * @param mixed $thread_id Raw Topic ID.
     * @return int|WP_Error
     */
    public function normalize_message_thread_id($thread_id) {
        if (!is_scalar($thread_id)) {
            return new WP_Error('telegram_invalid_topic', __('Enter a valid Telegram Topic ID.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        $thread_id = trim((string) $thread_id);
        if ($thread_id === '' || $thread_id === '0') {
            return 0;
        }
        if (!preg_match('/^[0-9]+$/', $thread_id)) {
            return new WP_Error('telegram_invalid_topic', __('Enter a valid Telegram Topic ID.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        $canonical = ltrim($thread_id, '0');
        if ($canonical === '') {
            return 0;
        }
        if (strlen($canonical) > 10 || (strlen($canonical) === 10 && strcmp($canonical, '2147483647') > 0)) {
            return new WP_Error('telegram_invalid_topic', __('Enter a valid Telegram Topic ID.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        return (int) $canonical;
    }

    /**
     * Explicitly validate the saved bot, destination and permissions without posting.
     *
     * @return true|WP_Error
     */
    public function test_connection() {
        $token = $this->get_validated_bot_token();
        $destination = $this->get_validated_destination();
        if (is_wp_error($token) || is_wp_error($destination)) {
            return is_wp_error($token) ? $token : $destination;
        }

        $credentials_fingerprint = $this->build_credentials_fingerprint($token);
        $target_fingerprint = $this->build_target_fingerprint(
            $destination['configured_chat_id'],
            $destination['thread_id']
        );
        $previous_marker = $this->get_matching_validation_marker($credentials_fingerprint, $target_fingerprint);

        $bot_response = $this->request('getMe');
        if (is_wp_error($bot_response)) {
            $this->apply_remote_failure_to_validation(
                $bot_response,
                $credentials_fingerprint,
                $target_fingerprint,
                $previous_marker
            );
            return $bot_response;
        }

        $bot = $this->validate_get_me_response($bot_response);
        if (is_wp_error($bot)) {
            $this->degrade_or_create_unverified_marker($credentials_fingerprint, $target_fingerprint, $previous_marker);
            return $bot;
        }
        $identity_fingerprint = $this->build_identity_fingerprint($bot['id']);

        $chat_response = $this->request('getChat', [
            'chat_id' => $destination['configured_chat_id'],
        ]);
        if (is_wp_error($chat_response)) {
            $this->apply_remote_failure_to_validation(
                $chat_response,
                $credentials_fingerprint,
                $target_fingerprint,
                $previous_marker,
                [
                    'identity_fingerprint' => $identity_fingerprint,
                ]
            );
            return $chat_response;
        }

        $chat = $this->validate_get_chat_response($chat_response, $destination['configured_chat_id']);
        if (is_wp_error($chat)) {
            $this->apply_remote_failure_to_validation(
                $chat,
                $credentials_fingerprint,
                $target_fingerprint,
                $previous_marker,
                [
                    'identity_fingerprint' => $identity_fingerprint,
                    'resolved_chat_id'     => '',
                    'chat_type'            => '',
                    'can_send_text'        => false,
                    'can_send_photo'       => false,
                    'topic_state'           => 'none',
                ]
            );
            return $chat;
        }

        if (hash_equals((string) $bot['id'], (string) $chat['id'])) {
            $error = $this->bot_destination_error();
            $this->save_invalid_marker(
                'target',
                $credentials_fingerprint,
                $target_fingerprint,
                [
                    'identity_fingerprint' => $identity_fingerprint,
                    'resolved_chat_id'     => $chat['id'],
                    'chat_type'            => $chat['type'],
                    'can_send_text'        => false,
                    'can_send_photo'       => false,
                    'topic_state'          => 'none',
                ]
            );
            $this->sync_legacy_status('target_missing');
            return $error;
        }

        $permission = [
            'can_send_text'  => true,
            'can_send_photo' => true,
        ];
        if ($chat['type'] !== 'private') {
            $member_response = $this->request('getChatMember', [
                'chat_id' => $chat['id'],
                'user_id' => $bot['id'],
            ]);
            if (is_wp_error($member_response)) {
                $this->apply_remote_failure_to_validation(
                    $member_response,
                    $credentials_fingerprint,
                    $target_fingerprint,
                    $previous_marker,
                    [
                        'identity_fingerprint' => $identity_fingerprint,
                        'resolved_chat_id'     => $chat['id'],
                        'chat_type'            => $chat['type'],
                    ]
                );
                return $member_response;
            }

            $permission = $this->evaluate_chat_member_permissions($member_response, $bot['id'], $chat);
            if (is_wp_error($permission)) {
                $this->apply_remote_failure_to_validation(
                    $permission,
                    $credentials_fingerprint,
                    $target_fingerprint,
                    $previous_marker,
                    [
                        'identity_fingerprint' => $identity_fingerprint,
                        'resolved_chat_id'     => $chat['id'],
                        'chat_type'            => $chat['type'],
                    ]
                );
                return $permission;
            }
        }

        if (!$permission['can_send_text']) {
            $error = new WP_Error(
                'telegram_permission_missing',
                __('The bot cannot publish messages to the selected Telegram destination.', 'calliope-social-publisher'),
                [
                    'phase'        => 'permissions',
                    'retryable'    => false,
                    'failure_kind' => 'permission',
                ]
            );
            $this->save_invalid_marker(
                'permission',
                $credentials_fingerprint,
                $target_fingerprint,
                [
                    'identity_fingerprint' => $identity_fingerprint,
                    'resolved_chat_id'     => $chat['id'],
                    'chat_type'            => $chat['type'],
                    'can_send_text'        => false,
                    'can_send_photo'       => false,
                ]
            );
            $this->sync_legacy_status('permission_missing');
            return $error;
        }

        $topic_state = 'none';
        $health = $permission['can_send_photo'] ? 'healthy' : 'degraded';
        if ($destination['thread_id'] > 0) {
            if ($chat['type'] !== 'supergroup' || !$chat['is_forum']) {
                $error = new WP_Error(
                    'telegram_topic_not_supported',
                    __('The selected Telegram Topic ID can only be used with a forum supergroup.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'topic',
                        'retryable'    => false,
                        'failure_kind' => 'topic',
                    ]
                );
                $this->save_invalid_marker(
                    'topic',
                    $credentials_fingerprint,
                    $target_fingerprint,
                    [
                        'identity_fingerprint' => $identity_fingerprint,
                        'resolved_chat_id'     => $chat['id'],
                        'chat_type'            => $chat['type'],
                        'can_send_text'        => true,
                        'can_send_photo'       => (bool) $permission['can_send_photo'],
                        'topic_state'          => 'invalid',
                    ]
                );
                $this->sync_legacy_status('configuration_error');
                return $error;
            }
            $topic_state = 'deferred';
            $health = 'degraded';
        }

        $this->save_validation_marker([
            'health'                  => $health,
            'failure_kind'            => '',
            'validated_at'            => time(),
            'credentials_fingerprint' => $credentials_fingerprint,
            'target_fingerprint'      => $target_fingerprint,
            'identity_fingerprint'    => $identity_fingerprint,
            'resolved_chat_id'        => $chat['id'],
            'chat_type'               => $chat['type'],
            'can_send_text'           => true,
            'can_send_photo'          => (bool) $permission['can_send_photo'],
            'topic_state'             => $topic_state,
        ]);
        $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('telegram_validation_incomplete', __('Telegram could not confirm publishing readiness.', 'calliope-social-publisher'));
        }

        return true;
    }

    /**
     * Publish a post to Telegram.
     *
     * @param array<string,mixed> $data Normalized publish payload.
     * @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(
                'telegram_connection_not_ready',
                __('Telegram must be verified with Save and test before publishing.', 'calliope-social-publisher'),
                [
                    'phase'        => 'precheck',
                    'retryable'    => false,
                    'failure_kind' => 'local',
                    'status'       => $state->get_status(),
                ]
            );
        }

        $token = $this->get_validated_bot_token();
        $destination = $this->get_validated_destination();
        if (is_wp_error($token) || is_wp_error($destination)) {
            return is_wp_error($token) ? $token : $destination;
        }
        $credentials_fingerprint = $this->build_credentials_fingerprint($token);
        $target_fingerprint = $this->build_target_fingerprint(
            $destination['configured_chat_id'],
            $destination['thread_id']
        );
        $marker = $this->get_matching_validation_marker($credentials_fingerprint, $target_fingerprint);
        if (!is_array($marker)
            || empty($marker['resolved_chat_id'])
            || empty($marker['can_send_text'])
            || !in_array($marker['health'], ['healthy', 'degraded'], true)
        ) {
            return new WP_Error(
                'telegram_connection_not_ready',
                __('Telegram must be verified with Save and test before publishing.', 'calliope-social-publisher'),
                [
                    'phase'        => 'precheck',
                    'retryable'    => false,
                    'failure_kind' => 'local',
                ]
            );
        }

        $token_bot_id = $this->extract_bot_id_from_token($token);
        if ($token_bot_id !== '' && hash_equals($token_bot_id, (string) $marker['resolved_chat_id'])) {
            $error = $this->bot_destination_error();
            $this->save_invalid_marker('target', $credentials_fingerprint, $target_fingerprint, $marker);
            $this->sync_legacy_status('target_missing');
            return $error;
        }

        $media_mode = sanitize_key((string) ($data['media_mode'] ?? 'none'));
        $has_video = $media_mode === 'video';
        $this->log_video_diagnostics('publish_start', [
            'media_mode' => $media_mode,
            'has_video' => $has_video,
            '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'] : []),
        ]);
        $video_asset = null;
        if ($has_video) {
            $video_asset = $this->video_asset_from_payload((array) $data);
            if (is_wp_error($video_asset)) {
                return $video_asset;
            }
        }

        $image_assets = [];
        if (!$has_video && $media_mode === 'multiple_images') {
            $image_assets = $this->multi_image_assets_from_payload((array) $data);
            if (is_wp_error($image_assets)) {
                return $image_assets;
            }
        }

        $has_media_group = count($image_assets) > 1;
        $has_image = !$has_video && (!empty($data['image_b64']) || count($image_assets) === 1 || $has_media_group);
        $message_limit = ($has_image || $has_video) ? 1000 : 3900;
        $message = $this->format_message((array) $data, $message_limit);
        if ($message === '' && !$has_image && !$has_video) {
            return new WP_Error('telegram_empty_message', __('Telegram needs text, a link, hashtags, an image, or a video to publish.', 'calliope-social-publisher'));
        }

        if ($has_image && !$marker['can_send_photo']) {
            return new WP_Error(
                'telegram_photo_permission_missing',
                __('This Telegram bot cannot send photos to the selected destination.', 'calliope-social-publisher'),
                [
                    'phase'        => 'precheck',
                    'retryable'    => false,
                    'failure_kind' => 'permission',
                ]
            );
        }

        $params = [
            'chat_id' => $marker['resolved_chat_id'],
        ];
        if ($destination['thread_id'] > 0) {
            $params['message_thread_id'] = $destination['thread_id'];
        }

        if ($has_video && is_array($video_asset)) {
            if ($message !== '') {
                $params['caption'] = $this->truncate_text($message, 1000);
            }
            $params['supports_streaming'] = true;

            $result = $this->request_multipart(
                'sendVideo',
                $params,
                'video',
                (string) $video_asset['path'],
                (string) ($video_asset['mime_type'] ?? 'video/mp4'),
                180
            );
        } elseif ($has_media_group) {
            $media_group = [];
            $files = [];
            foreach ($image_assets as $index => $image_asset) {
                $field = 'photo' . absint($index);
                $item = [
                    'type'  => 'photo',
                    'media' => 'attach://' . $field,
                ];
                if ($index === 0 && $message !== '') {
                    $item['caption'] = $this->truncate_text($message, 1000);
                }
                $media_group[] = $item;
                $files[] = [
                    'field' => $field,
                    'path'  => (string) $image_asset['path'],
                    'mime'  => (string) ($image_asset['mime_type'] ?? 'image/jpeg'),
                ];
            }

            $encoded_media = wp_json_encode($media_group, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if (!is_string($encoded_media) || $encoded_media === '') {
                return new WP_Error('telegram_media_group_invalid', __('Telegram could not prepare the selected images.', 'calliope-social-publisher'), [
                    'phase' => 'prepare', 'retryable' => false, 'failure_kind' => 'local',
                ]);
            }
            $params['media'] = $encoded_media;
            $result = $this->request_multipart_files('sendMediaGroup', $params, $files, 120);
        } elseif ($has_image) {
            $image_asset = count($image_assets) === 1 ? $image_assets[0] : null;
            if (!is_array($image_asset)) {
                $image_asset = $this->image_url_from_b64((string) $data['image_b64']);
                if (is_wp_error($image_asset)) {
                    return $image_asset;
                }
            }
            if ($message !== '') {
                $params['caption'] = $this->truncate_text($message, 1000);
            }

            $result = $this->request_multipart(
                'sendPhoto',
                $params,
                'photo',
                (string) $image_asset['path'],
                (string) ($image_asset['mime_type'] ?? $image_asset['mime'] ?? 'image/jpeg')
            );
            if (!empty($image_asset['temporary'])) {
                $this->delete_temp_image((string) $image_asset['path']);
            }
        } else {
            $params['text'] = $this->truncate_text($message, 3900);
            $result = $this->request('sendMessage', $params);
        }

        if (is_wp_error($result)) {
            $this->apply_remote_failure_to_validation(
                $result,
                $credentials_fingerprint,
                $target_fingerprint,
                $marker
            );
            return $result;
        }

        $validated_result = $this->validate_publish_response(
            $result,
            $marker['resolved_chat_id'],
            $destination['thread_id'],
            $has_video ? 'video' : ($has_media_group ? 'media_group' : ($has_image ? 'photo' : 'text'))
        );
        if (is_wp_error($validated_result)) {
            $failure_kind = $this->get_error_failure_kind($validated_result);
            $this->save_invalid_marker(
                $failure_kind === 'topic' ? 'topic' : 'target',
                $credentials_fingerprint,
                $target_fingerprint,
                $marker
            );
            $this->sync_legacy_status($failure_kind === 'topic' ? 'configuration_error' : 'target_missing');
            return $validated_result;
        }

        $marker['health'] = $marker['can_send_photo'] ? 'healthy' : 'degraded';
        $marker['failure_kind'] = '';
        $marker['validated_at'] = time();
        if ($destination['thread_id'] > 0 && $validated_result['topic_confirmed']) {
            $marker['topic_state'] = 'verified';
        }
        if ($marker['topic_state'] === 'deferred') {
            $marker['health'] = 'degraded';
        }
        $this->save_validation_marker($marker);
        $this->sync_legacy_status('ready_to_publish');

        if ($has_video) {
            return [
                'ok' => true,
                'media_type' => 'video',
                'message_id' => sanitize_text_field((string) ($validated_result['message_id'] ?? '')),
                'video_file_id' => sanitize_text_field((string) ($validated_result['video_file_id'] ?? '')),
            ];
        }
        if ($has_media_group) {
            return [
                'ok' => true,
                'media_type' => 'media_group',
                'attachments' => absint($validated_result['attachments'] ?? count($image_assets)),
                'message_id' => sanitize_text_field((string) ($validated_result['message_id'] ?? '')),
            ];
        }

        return [
            'ok' => true,
            'media_type' => $has_image ? 'image' : 'text',
            'message_id' => sanitize_text_field((string) ($validated_result['message_id'] ?? '')),
        ];
    }

    /** @return string */
    private function extract_bot_id_from_token($token) {
        if (!is_scalar($token)) {
            return '';
        }
        $token = trim((string) $token);
        if (!preg_match('/\A([0-9]+):[A-Za-z0-9_-]+\z/', $token, $matches)) {
            return '';
        }
        return $this->normalize_decimal_identifier($matches[1], false, false);
    }

    /** @return WP_Error */
    private function bot_destination_error() {
        return new WP_Error(
            'telegram_destination_is_bot',
            __('The Telegram destination is the bot itself. Enter the channel or group @username, t.me link, or numeric chat ID instead of the Bot ID.', 'calliope-social-publisher'),
            [
                'phase'        => 'target',
                'retryable'    => false,
                'failure_kind' => 'target',
            ]
        );
    }

    /** @return string */
    private function get_bot_token() {
        return trim((string) get_option('calliope_sp_telegram_bot_token', ''));
    }

    /** @return string|WP_Error */
    private function get_validated_bot_token() {
        return $this->validate_bot_token_value($this->get_bot_token());
    }

    /**
     * Build the Telegram Bot API endpoint locally for the outgoing request.
     * The returned URL must never be stored, logged, or included in errors.
     *
     * @param string $token Validated Bot Token.
     * @param string $method Telegram Bot API method.
     * @return string|WP_Error
     */
    private function build_api_endpoint($token, $method) {
        $token = (string) $token;
        $method = (string) $method;

        if (!preg_match('/\A[A-Za-z0-9_-]+:[A-Za-z0-9_-]+\z/', $token)) {
            return new WP_Error('telegram_invalid_token', __('Invalid Telegram Bot Token.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'configuration',
                'failure_kind' => 'local',
            ]);
        }

        if ($method === '' || !preg_match('/\A[A-Za-z0-9_]+\z/', $method)) {
            return new WP_Error('telegram_invalid_method', __('Invalid Telegram API method.', 'calliope-social-publisher'), [
                'retryable' => false,
                'phase'     => 'request',
                'failure_kind' => 'local',
            ]);
        }

        return 'https://api.telegram.org/bot' . $token . '/' . $method;
    }

    /**
     * Redact Telegram Bot API URLs, path fragments, and the known token.
     *
     * @param mixed  $value Value to redact.
     * @param string $token Validated token when available.
     * @return string
     */
    private function redact_telegram_secrets($value, $token = '') {
        if (!is_scalar($value) && $value !== null) {
            return '';
        }

        $message = (string) $value;
        $message = preg_replace_callback(
            '#(https?://api\.telegram\.org/bot)([A-Za-z0-9_-]+(?::|%3A)[A-Za-z0-9_-]+)(/[A-Za-z0-9_]+)?#iu',
            static function ($matches) {
                return $matches[1] . '[redacted]' . ($matches[3] ?? '');
            },
            $message
        );
        $message = preg_replace_callback(
            '#\bbot([A-Za-z0-9_-]+(?::|%3A)[A-Za-z0-9_-]+)(/[A-Za-z0-9_]+)#iu',
            static function ($matches) {
                return 'bot[redacted]' . $matches[2];
            },
            $message
        );

        $token = (string) $token;
        if ($token !== '') {
            $message = str_replace($token, '[redacted]', $message);
            $encoded_token = rawurlencode($token);
            if ($encoded_token !== $token) {
                $message = str_ireplace($encoded_token, '[redacted]', $message);
            }
        }

        return $message;
    }

    /** @return string */
    private function sanitize_api_detail($detail, $token = '') {
        $detail = wp_strip_all_tags((string) $detail);
        $detail = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $detail);
        $detail = preg_replace('/\s+/u', ' ', (string) $detail);
        $detail = $this->redact_telegram_secrets($detail, $token);
        $detail = preg_replace_callback(
            '/((?:"?(?:chat_id|caption|text|message_thread_id)"?\s*[:=]\s*"?))([^",&}]+)/iu',
            static function ($matches) {
                return $matches[1] . '[redacted]';
            },
            (string) $detail
        );

        $detail = trim((string) $detail);
        if (function_exists('mb_substr')) {
            return mb_substr($detail, 0, 400, 'UTF-8');
        }
        return substr($detail, 0, 400);
    }

    /** @return WP_Error */
    private function build_transport_error($transport_error, $method) {
        $transport_code = is_wp_error($transport_error)
            ? sanitize_key((string) $transport_error->get_error_code())
            : '';
        $method = preg_match('/\A[A-Za-z0-9_]+\z/', (string) $method) ? (string) $method : '';

        return new WP_Error(
            'telegram_transport_error',
            __('Telegram could not be reached. Please try again later.', 'calliope-social-publisher'),
            [
                'method'         => $method,
                'transport_code' => $transport_code,
                'retryable'      => true,
                'phase'          => 'transport',
                'failure_kind'   => 'temporary',
            ]
        );
    }

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

    /**
     * @param string $method Telegram API method.
     * @param array<string,mixed> $params Request params.
     * @return array<string,mixed>|WP_Error
     */
    private function request($method, array $params = []) {
        $token = $this->get_validated_bot_token();
        if (is_wp_error($token)) {
            return $token;
        }
        $url = $this->build_api_endpoint($token, $method);
        if (is_wp_error($url)) {
            return $url;
        }

        $method = (string) $method;
        $response = wp_remote_post($url, [
            'headers' => [
                'Accept'       => 'application/json',
                'Content-Type' => 'application/json; charset=utf-8',
            ],
            'body'    => wp_json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'timeout' => 45,
        ]);

        if (is_wp_error($response)) {
            return $this->build_transport_error($response, $method);
        }

        return $this->parse_api_response($response, $method, $token);
    }

    /**
     * Send a Telegram API request as multipart/form-data.
     *
     * @return array<string,mixed>|WP_Error
     */
    private function request_multipart($method, array $params = [], $file_field = '', $file_path = '', $mime = 'image/jpeg', $timeout = 60) {
        $token = $this->get_validated_bot_token();
        if (is_wp_error($token)) {
            return $token;
        }
        $url = $this->build_api_endpoint($token, $method);
        if (is_wp_error($url)) {
            return $url;
        }

        $method = (string) $method;
        $file_field = sanitize_key((string) $file_field);
        $file_path = (string) $file_path;
        if ($file_field === '' || $file_path === '' || !file_exists($file_path) || !is_file($file_path) || !is_readable($file_path)) {
            return new WP_Error('telegram_file_upload_failed', __('Telegram could not read the selected media file.', 'calliope-social-publisher'), [
                'method'        => $method,
                'retryable'     => false,
                'phase'         => 'prepare',
                'failure_kind'  => 'local',
            ]);
        }

        $fields = [];
        foreach ($params as $key => $value) {
            if ($key === $file_field) {
                continue;
            }
            if (is_bool($value)) {
                $fields[$key] = $value ? '1' : '0';
            } elseif (is_scalar($value)) {
                $fields[$key] = (string) $value;
            }
        }

        $file_size = filesize($file_path);
        if ($method === 'sendVideo') {
            $this->log_video_diagnostics('multipart_prepare', [
                'method' => $method,
                'file_field' => $file_field,
                'mime_type' => sanitize_mime_type((string) $mime),
                'file_size' => is_int($file_size) ? $file_size : 0,
                'caption_set' => isset($fields['caption']) && trim((string) $fields['caption']) !== '',
                'supports_streaming' => !empty($fields['supports_streaming']),
                'timeout' => max(30, min(300, (int) $timeout)),
            ]);
        }

        $file_contents = $this->get_local_file_contents($file_path);
        if ($file_contents === false) {
            return new WP_Error('telegram_file_upload_failed', __('Telegram could not read the selected media file.', 'calliope-social-publisher'), [
                'method'        => $method,
                'retryable'     => false,
                'phase'         => 'prepare',
                'failure_kind'  => 'local',
            ]);
        }

        $boundary = '----CalliopeSP' . wp_generate_password(24, false, false);
        $body = $this->build_multipart_body(
            $boundary,
            $fields,
            $file_field,
            basename($file_path),
            (string) $mime,
            $file_contents
        );

        $response = wp_remote_post($url, [
            'headers' => [
                'Accept'       => 'application/json',
                'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
            ],
            'body'    => $body,
            'timeout' => max(30, min(300, (int) $timeout)),
        ]);

        if (is_wp_error($response)) {
            return $this->build_transport_error($response, $method);
        }

        if ($method === 'sendVideo') {
            $this->log_video_diagnostics('multipart_response', [
                'method' => $method,
                'http_code' => (int) wp_remote_retrieve_response_code($response),
                'response_bytes' => strlen((string) wp_remote_retrieve_body($response)),
            ], ((int) wp_remote_retrieve_response_code($response) >= 200 && (int) wp_remote_retrieve_response_code($response) < 300) ? 'info' : 'error');
        }

        return $this->parse_api_response($response, $method, $token);
    }

    /**
     * Send a Telegram API request as multipart/form-data with multiple files.
     *
     * @param array<int,array{field:string,path:string,mime:string}> $files
     * @return array<string,mixed>|WP_Error
     */
    private function request_multipart_files($method, array $params = [], array $files = [], $timeout = 60) {
        $token = $this->get_validated_bot_token();
        if (is_wp_error($token)) {
            return $token;
        }
        $url = $this->build_api_endpoint($token, $method);
        if (is_wp_error($url)) {
            return $url;
        }

        $method = (string) $method;
        $prepared_files = [];
        foreach ($files as $file) {
            if (!is_array($file)) {
                continue;
            }
            $field = sanitize_key((string) ($file['field'] ?? ''));
            $path = (string) ($file['path'] ?? '');
            if ($field === '' || $path === '' || !file_exists($path) || !is_file($path) || !is_readable($path)) {
                return new WP_Error('telegram_file_upload_failed', __('Telegram could not read the selected media file.', 'calliope-social-publisher'), [
                    'method'        => $method,
                    'retryable'     => false,
                    'phase'         => 'prepare',
                    'failure_kind'  => 'local',
                ]);
            }
            $contents = $this->get_local_file_contents($path);
            if ($contents === false) {
                return new WP_Error('telegram_file_upload_failed', __('Telegram could not read the selected media file.', 'calliope-social-publisher'), [
                    'method'        => $method,
                    'retryable'     => false,
                    'phase'         => 'prepare',
                    'failure_kind'  => 'local',
                ]);
            }
            $prepared_files[] = [
                'field' => $field,
                'filename' => basename($path),
                'mime' => (string) ($file['mime'] ?? 'image/jpeg'),
                'contents' => $contents,
            ];
        }

        if ($prepared_files === []) {
            return new WP_Error('telegram_file_upload_failed', __('Telegram could not read the selected media file.', 'calliope-social-publisher'), [
                'method'        => $method,
                'retryable'     => false,
                'phase'         => 'prepare',
                'failure_kind'  => 'local',
            ]);
        }

        $fields = [];
        foreach ($params as $key => $value) {
            if (is_bool($value)) {
                $fields[$key] = $value ? '1' : '0';
            } elseif (is_scalar($value)) {
                $fields[$key] = (string) $value;
            }
        }

        $this->log_video_diagnostics('multipart_prepare', [
            'method' => $method,
            'file_count' => count($prepared_files),
            'timeout' => max(30, min(300, (int) $timeout)),
        ]);

        $boundary = '----CalliopeSP' . wp_generate_password(24, false, false);
        $body = $this->build_multipart_body_files($boundary, $fields, $prepared_files);

        $response = wp_remote_post($url, [
            'headers' => [
                'Accept'       => 'application/json',
                'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
            ],
            'body'    => $body,
            'timeout' => max(30, min(300, (int) $timeout)),
        ]);

        if (is_wp_error($response)) {
            return $this->build_transport_error($response, $method);
        }

        $this->log_video_diagnostics('multipart_response', [
            'method' => $method,
            'http_code' => (int) wp_remote_retrieve_response_code($response),
            'response_bytes' => strlen((string) wp_remote_retrieve_body($response)),
        ], ((int) wp_remote_retrieve_response_code($response) >= 200 && (int) wp_remote_retrieve_response_code($response) < 300) ? 'info' : 'error');

        return $this->parse_api_response($response, $method, $token);
    }

    /** @return array<string,mixed>|WP_Error */
    private function parse_api_response($response, $method, $token) {
        $code = (int) wp_remote_retrieve_response_code($response);
        $raw  = (string) wp_remote_retrieve_body($response);
        $flags = defined('JSON_BIGINT_AS_STRING') ? JSON_BIGINT_AS_STRING : 0;
        $json = json_decode($raw, true, 512, $flags);

        if ($code < 200 || $code >= 300 || !is_array($json) || empty($json['ok'])) {
            $detail = $this->extract_api_error($json, $raw, $code, $token);
            $message = $this->format_api_error_message($detail, $code);
            $failure_kind = $this->classify_remote_failure($method, $code, $detail);
            return new WP_Error('telegram_api_error', $message, [
                'http_code'   => $code,
                'method'      => preg_match('/\A[A-Za-z0-9_]+\z/', (string) $method) ? (string) $method : '',
                'retryable'   => $this->is_retryable_http_status($code),
                'phase'       => 'api',
                'failure_kind'=> $failure_kind,
            ]);
        }

        return $json;
    }


    /**
     * Extract a bounded, redacted Telegram API detail without exposing raw data.
     *
     * @param mixed  $json Parsed response.
     * @param string $raw Raw response body.
     * @param int    $code HTTP status code.
     * @param string $token Validated token for literal redaction.
     * @return string
     */
    private function extract_api_error($json, $raw, $code, $token = '') {
        unset($code); // Status is carried separately in controlled WP_Error data.
        if (is_array($json) && isset($json['description']) && is_string($json['description'])) {
            return $this->sanitize_api_detail($json['description'], $token);
        }
        if (!is_array($json)) {
            return $this->sanitize_api_detail($raw, $token);
        }
        return '';
    }

    /** @return string */
    private function format_api_error_message($detail, $code) {
        $normalized_detail = strtolower((string) $detail);
        if (strpos($normalized_detail, "bot can't send messages to the bot") !== false
            || strpos($normalized_detail, "bot can't send messages to bots") !== false
        ) {
            return $this->bot_destination_error()->get_error_message();
        }
        if ($detail !== '') {
            return sprintf(
                /* translators: 1: HTTP status code, 2: Telegram API error message or response summary. */
                __('Telegram error (%1$d): %2$s', 'calliope-social-publisher'),
                (int) $code,
                $detail
            );
        }
        return sprintf(
            /* translators: %d: HTTP status code. */
            __('Telegram error (%d): the API request failed.', 'calliope-social-publisher'),
            (int) $code
        );
    }

    /** @return string */
    private function classify_remote_failure($method, $code, $detail) {
        $method = strtolower((string) $method);
        $code = (int) $code;
        $detail = strtolower((string) $detail);

        if ($this->is_retryable_http_status($code)) {
            return 'temporary';
        }
        if ($code === 401 || strpos($detail, 'unauthorized') !== false || strpos($detail, 'invalid token') !== false) {
            return 'auth';
        }
        if (strpos($detail, 'message thread not found') !== false || strpos($detail, 'topic') !== false && strpos($detail, 'not found') !== false) {
            return 'topic';
        }
        if (strpos($detail, 'chat not found') !== false
            || strpos($detail, 'bot is not a member') !== false
            || strpos($detail, 'user not found') !== false
            || strpos($detail, 'chat_id is empty') !== false
            || strpos($detail, "bot can't send messages to the bot") !== false
            || strpos($detail, "bot can't send messages to bots") !== false
        ) {
            return 'target';
        }
        if (strpos($detail, 'not enough rights') !== false
            || strpos($detail, 'have no rights') !== false
            || strpos($detail, 'forbidden: bot was kicked') !== false
            || strpos($detail, 'bot was blocked') !== false
            || strpos($detail, 'chat_write_forbidden') !== false
            || strpos($detail, 'not allowed to send') !== false
            || strpos($detail, 'can\'t post') !== false
        ) {
            return 'permission';
        }
        // A generic 400/403 is ambiguous. Only explicit authentication evidence
        // may invalidate the Bot Token; otherwise keep the previous validation.
        return '';
    }

    /** @return array{id:string}|WP_Error */
    private function validate_get_me_response($response) {
        $result = is_array($response) && isset($response['result']) && is_array($response['result'])
            ? $response['result']
            : null;
        $id = is_array($result) && array_key_exists('id', $result)
            ? $this->normalize_decimal_identifier($result['id'], false, false)
            : '';
        if (!is_array($result) || $id === '' || !array_key_exists('is_bot', $result) || $result['is_bot'] !== true) {
            return new WP_Error(
                'telegram_invalid_bot_identity',
                __('Telegram did not return a valid bot identity.', 'calliope-social-publisher'),
                [
                    'phase'        => 'identity',
                    'retryable'    => true,
                    'failure_kind' => 'temporary',
                ]
            );
        }
        return ['id' => $id];
    }

    /** @return array{id:string,type:string,username:string,is_forum:bool,permissions:array<string,mixed>}|WP_Error */
    private function validate_get_chat_response($response, $configured_chat_id) {
        $result = is_array($response) && isset($response['result']) && is_array($response['result'])
            ? $response['result']
            : null;
        $id = is_array($result) && array_key_exists('id', $result)
            ? $this->normalize_decimal_identifier($result['id'], true, false)
            : '';
        $type = is_array($result) && isset($result['type']) && is_string($result['type'])
            ? sanitize_key($result['type'])
            : '';
        if (!is_array($result) || $id === '' || !in_array($type, ['private', 'group', 'supergroup', 'channel'], true)) {
            return new WP_Error(
                'telegram_invalid_chat_response',
                __('Telegram did not return a valid destination.', 'calliope-social-publisher'),
                [
                    'phase'        => 'target',
                    'retryable'    => true,
                    'failure_kind' => 'temporary',
                ]
            );
        }

        if (strpos((string) $configured_chat_id, '@') === 0) {
            $username = isset($result['username']) && is_string($result['username'])
                ? strtolower(trim($result['username']))
                : '';
            if ($username === '' || !hash_equals(strtolower(substr((string) $configured_chat_id, 1)), $username)) {
                return new WP_Error(
                    'telegram_destination_mismatch',
                    __('Telegram returned a different destination than the one configured.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'target',
                        'retryable'    => false,
                        'failure_kind' => 'target',
                    ]
                );
            }
        } elseif (!hash_equals((string) $configured_chat_id, $id)) {
            return new WP_Error(
                'telegram_destination_mismatch',
                __('Telegram returned a different destination than the one configured.', 'calliope-social-publisher'),
                [
                    'phase'        => 'target',
                    'retryable'    => false,
                    'failure_kind' => 'target',
                ]
            );
        }

        return [
            'id'          => $id,
            'type'        => $type,
            'username'    => isset($result['username']) && is_string($result['username']) ? strtolower(trim($result['username'])) : '',
            'is_forum'    => !empty($result['is_forum']),
            'permissions' => isset($result['permissions']) && is_array($result['permissions']) ? $result['permissions'] : [],
        ];
    }

    /** @return array{can_send_text:bool,can_send_photo:bool}|WP_Error */
    private function evaluate_chat_member_permissions($response, $bot_id, array $chat) {
        $member = is_array($response) && isset($response['result']) && is_array($response['result'])
            ? $response['result']
            : null;
        $member_user = is_array($member) && isset($member['user']) && is_array($member['user'])
            ? $member['user']
            : null;
        $member_user_id = is_array($member_user) && array_key_exists('id', $member_user)
            ? $this->normalize_decimal_identifier($member_user['id'], false, false)
            : '';
        $status = is_array($member) && isset($member['status']) && is_string($member['status'])
            ? sanitize_key($member['status'])
            : '';

        if (!is_array($member) || $member_user_id === '' || !hash_equals((string) $bot_id, $member_user_id)) {
            return new WP_Error(
                'telegram_member_identity_mismatch',
                __('Telegram could not confirm the bot membership in the selected destination.', 'calliope-social-publisher'),
                [
                    'phase'        => 'permissions',
                    'retryable'    => false,
                    'failure_kind' => 'target',
                ]
            );
        }

        if ($status === 'creator') {
            return ['can_send_text' => true, 'can_send_photo' => true];
        }
        if ($status === 'administrator') {
            if ($chat['type'] === 'channel' && empty($member['can_post_messages'])) {
                return new WP_Error(
                    'telegram_permission_missing',
                    __('The bot cannot publish messages to the selected Telegram destination.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'permissions',
                        'retryable'    => false,
                        'failure_kind' => 'permission',
                    ]
                );
            }
            return ['can_send_text' => true, 'can_send_photo' => true];
        }
        if ($status === 'member') {
            if ($chat['type'] === 'channel') {
                return new WP_Error(
                    'telegram_permission_missing',
                    __('The bot cannot publish messages to the selected Telegram destination.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'permissions',
                        'retryable'    => false,
                        'failure_kind' => 'permission',
                    ]
                );
            }
            $permissions = $chat['permissions'];
            $text = !array_key_exists('can_send_messages', $permissions) || $permissions['can_send_messages'] !== false;
            $photo = !array_key_exists('can_send_photos', $permissions) || $permissions['can_send_photos'] !== false;
            return ['can_send_text' => $text, 'can_send_photo' => $photo];
        }
        if ($status === 'restricted') {
            $text = !empty($member['is_member']) && !empty($member['can_send_messages']);
            $photo = $text && !empty($member['can_send_photos']);
            if (!$text) {
                return new WP_Error(
                    'telegram_permission_missing',
                    __('The bot cannot publish messages to the selected Telegram destination.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'permissions',
                        'retryable'    => false,
                        'failure_kind' => 'permission',
                    ]
                );
            }
            return ['can_send_text' => true, 'can_send_photo' => $photo];
        }
        if ($status === 'left') {
            return new WP_Error(
                'telegram_target_missing',
                __('The bot is not a member of the selected Telegram destination.', 'calliope-social-publisher'),
                [
                    'phase'        => 'permissions',
                    'retryable'    => false,
                    'failure_kind' => 'target',
                ]
            );
        }
        if (in_array($status, ['kicked', 'banned'], true)) {
            return new WP_Error(
                'telegram_permission_missing',
                __('The bot cannot publish messages to the selected Telegram destination.', 'calliope-social-publisher'),
                [
                    'phase'        => 'permissions',
                    'retryable'    => false,
                    'failure_kind' => 'permission',
                ]
            );
        }

        return new WP_Error(
            'telegram_invalid_member_response',
            __('Telegram could not confirm the bot permissions for the selected destination.', 'calliope-social-publisher'),
            [
                'phase'        => 'permissions',
                'retryable'    => true,
                'failure_kind' => 'temporary',
            ]
        );
    }

    /** @return array{topic_confirmed:bool}|WP_Error */
    private function validate_publish_response($response, $resolved_chat_id, $thread_id, $expected_media = 'text') {
        $expected_media = sanitize_key((string) $expected_media);
        if ($expected_media === 'media_group') {
            return $this->validate_media_group_publish_response($response, $resolved_chat_id, $thread_id);
        }

        $result = is_array($response) && isset($response['result']) && is_array($response['result'])
            ? $response['result']
            : null;
        $chat = is_array($result) && isset($result['chat']) && is_array($result['chat'])
            ? $result['chat']
            : null;
        $response_chat_id = is_array($chat) && array_key_exists('id', $chat)
            ? $this->normalize_decimal_identifier($chat['id'], true, false)
            : '';
        if ($response_chat_id === '' || !hash_equals((string) $resolved_chat_id, $response_chat_id)) {
            return new WP_Error(
                'telegram_publish_destination_mismatch',
                __('Telegram returned a different destination than the validated chat.', 'calliope-social-publisher'),
                [
                    'phase'        => 'publish_response',
                    'retryable'    => false,
                    'failure_kind' => 'target',
                ]
            );
        }

        $topic_confirmed = false;
        if ((int) $thread_id > 0 && array_key_exists('message_thread_id', (array) $result)) {
            $response_thread = $this->normalize_message_thread_id($result['message_thread_id']);
            if (is_wp_error($response_thread) || (int) $response_thread !== (int) $thread_id) {
                return new WP_Error(
                    'telegram_publish_topic_mismatch',
                    __('Telegram returned a different Topic ID than the configured topic.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'publish_response',
                        'retryable'    => false,
                        'failure_kind' => 'topic',
                    ]
                );
            }
            $topic_confirmed = true;
        }

        if ($expected_media === 'video' && (!is_array($result) || !isset($result['video']) || !is_array($result['video']))) {
            $this->log_video_diagnostics('publish_response_validated', [
                'expected_media' => 'video',
                'has_video' => false,
                'has_photo' => is_array($result) && !empty($result['photo']),
                'message_id_set' => is_array($result) && !empty($result['message_id']),
            ], 'warning');
            return new WP_Error(
                'telegram_video_response_missing',
                __('Telegram accepted the request but did not return a video message.', 'calliope-social-publisher'),
                [
                    'phase'        => 'publish_response',
                    'retryable'    => true,
                    'failure_kind' => 'temporary',
                ]
            );
        }
        $video_file_id = '';
        if ($expected_media === 'video') {
            $video_file_id = is_array($result) && isset($result['video']['file_id']) && is_scalar($result['video']['file_id'])
                ? sanitize_text_field((string) $result['video']['file_id'])
                : '';
            $this->log_video_diagnostics('publish_response_validated', [
                'expected_media' => 'video',
                'has_video' => true,
                'message_id_set' => is_array($result) && !empty($result['message_id']),
                'file_id_set' => $video_file_id !== '',
            ]);
        }
        if ($expected_media === 'photo' && (!is_array($result) || empty($result['photo']) || !is_array($result['photo']))) {
            return new WP_Error(
                'telegram_photo_response_missing',
                __('Telegram accepted the request but did not return a photo message.', 'calliope-social-publisher'),
                [
                    'phase'        => 'publish_response',
                    'retryable'    => true,
                    'failure_kind' => 'temporary',
                ]
            );
        }

        return [
            'topic_confirmed' => $topic_confirmed,
            'media_type' => $expected_media === 'video' ? 'video' : ($expected_media === 'photo' ? 'photo' : 'text'),
            'message_id' => is_array($result) && isset($result['message_id']) && is_scalar($result['message_id']) ? sanitize_text_field((string) $result['message_id']) : '',
            'video_file_id' => $video_file_id,
        ];
    }

    /** @return array{topic_confirmed:bool,media_type:string,message_id:string,attachments:int}|WP_Error */
    private function validate_media_group_publish_response($response, $resolved_chat_id, $thread_id) {
        $messages = is_array($response) && isset($response['result']) && is_array($response['result'])
            ? array_values($response['result'])
            : [];
        if (count($messages) < 2) {
            return new WP_Error(
                'telegram_media_group_response_missing',
                __('Telegram accepted the request but did not return an album response.', 'calliope-social-publisher'),
                [
                    'phase'        => 'publish_response',
                    'retryable'    => true,
                    'failure_kind' => 'temporary',
                ]
            );
        }

        $topic_confirmed = false;
        $first_message_id = '';
        foreach ($messages as $index => $message) {
            if (!is_array($message)) {
                return new WP_Error(
                    'telegram_media_group_response_missing',
                    __('Telegram accepted the request but did not return an album response.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'publish_response',
                        'retryable'    => true,
                        'failure_kind' => 'temporary',
                    ]
                );
            }
            $chat = isset($message['chat']) && is_array($message['chat']) ? $message['chat'] : null;
            $response_chat_id = is_array($chat) && array_key_exists('id', $chat)
                ? $this->normalize_decimal_identifier($chat['id'], true, false)
                : '';
            if ($response_chat_id === '' || !hash_equals((string) $resolved_chat_id, $response_chat_id)) {
                return new WP_Error(
                    'telegram_publish_destination_mismatch',
                    __('Telegram returned a different destination than the validated chat.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'publish_response',
                        'retryable'    => false,
                        'failure_kind' => 'target',
                    ]
                );
            }
            if (empty($message['photo']) || !is_array($message['photo'])) {
                return new WP_Error(
                    'telegram_photo_response_missing',
                    __('Telegram accepted the request but did not return a photo message.', 'calliope-social-publisher'),
                    [
                        'phase'        => 'publish_response',
                        'retryable'    => true,
                        'failure_kind' => 'temporary',
                    ]
                );
            }
            if ($index === 0 && isset($message['message_id']) && is_scalar($message['message_id'])) {
                $first_message_id = sanitize_text_field((string) $message['message_id']);
            }
            if ((int) $thread_id > 0 && array_key_exists('message_thread_id', $message)) {
                $response_thread = $this->normalize_message_thread_id($message['message_thread_id']);
                if (is_wp_error($response_thread) || (int) $response_thread !== (int) $thread_id) {
                    return new WP_Error(
                        'telegram_publish_topic_mismatch',
                        __('Telegram returned a different Topic ID than the configured topic.', 'calliope-social-publisher'),
                        [
                            'phase'        => 'publish_response',
                            'retryable'    => false,
                            'failure_kind' => 'topic',
                        ]
                    );
                }
                $topic_confirmed = true;
            }
        }

        return [
            'topic_confirmed' => $topic_confirmed,
            'media_type' => 'media_group',
            'message_id' => $first_message_id,
            'attachments' => count($messages),
        ];
    }

    /** @return string */
    private function build_credentials_fingerprint($token) {
        return hash_hmac(
            'sha256',
            self::VALIDATION_SCHEMA . ':token:' . (string) $token,
            wp_salt('auth')
        );
    }

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

    /** @return string */
    private function build_identity_fingerprint($bot_id) {
        return hash_hmac('sha256', self::VALIDATION_SCHEMA . ':bot:' . (string) $bot_id, wp_salt('auth'));
    }

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

        $identity = $marker['identity_fingerprint'] ?? '';
        if (!is_string($identity) || ($identity !== '' && !preg_match('/^[a-f0-9]{64}$/', $identity))) {
            return null;
        }
        $resolved_chat_id = isset($marker['resolved_chat_id']) && is_scalar($marker['resolved_chat_id'])
            ? $this->normalize_decimal_identifier($marker['resolved_chat_id'], true, false)
            : '';
        $chat_type = isset($marker['chat_type']) && is_string($marker['chat_type'])
            ? sanitize_key($marker['chat_type'])
            : '';
        if ($chat_type !== '' && !in_array($chat_type, ['private', 'group', 'supergroup', 'channel'], true)) {
            return null;
        }
        $topic_state = isset($marker['topic_state']) && is_string($marker['topic_state'])
            ? sanitize_key($marker['topic_state'])
            : 'none';
        if (!in_array($topic_state, ['none', 'deferred', 'verified', 'invalid'], true)) {
            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;
        }
        $can_send_text = $this->normalize_marker_boolean($marker['can_send_text'] ?? false);
        $can_send_photo = $this->normalize_marker_boolean($marker['can_send_photo'] ?? false);
        if ($can_send_text === null || $can_send_photo === null) {
            return null;
        }

        return [
            'schema'                  => self::VALIDATION_SCHEMA,
            'health'                  => (string) $marker['health'],
            'failure_kind'            => (string) $marker['failure_kind'],
            'validated_at'            => $validated_at,
            'credentials_fingerprint' => (string) $marker['credentials_fingerprint'],
            'target_fingerprint'      => (string) $marker['target_fingerprint'],
            'identity_fingerprint'    => $identity,
            'resolved_chat_id'        => $resolved_chat_id,
            'chat_type'               => $chat_type,
            'can_send_text'           => $can_send_text,
            'can_send_photo'          => $can_send_photo,
            'topic_state'             => $topic_state,
        ];
    }

    /** @return void */
    private function save_validation_marker(array $marker) {
        $safe = [
            'schema'                  => self::VALIDATION_SCHEMA,
            'health'                  => in_array(($marker['health'] ?? ''), ['healthy', 'degraded', 'invalid'], true) ? (string) $marker['health'] : 'invalid',
            'failure_kind'            => in_array(($marker['failure_kind'] ?? ''), ['', 'auth', 'target', 'permission', 'topic'], true) ? (string) $marker['failure_kind'] : '',
            'validated_at'            => max(0, (int) ($marker['validated_at'] ?? 0)),
            'credentials_fingerprint' => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['credentials_fingerprint'] ?? '')) ? (string) $marker['credentials_fingerprint'] : '',
            'target_fingerprint'      => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['target_fingerprint'] ?? '')) ? (string) $marker['target_fingerprint'] : '',
            'identity_fingerprint'    => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['identity_fingerprint'] ?? '')) ? (string) $marker['identity_fingerprint'] : '',
            'resolved_chat_id'        => $this->normalize_decimal_identifier($marker['resolved_chat_id'] ?? '', true, false),
            'chat_type'               => in_array(($marker['chat_type'] ?? ''), ['private', 'group', 'supergroup', 'channel'], true) ? (string) $marker['chat_type'] : '',
            'can_send_text'           => !empty($marker['can_send_text']),
            'can_send_photo'          => !empty($marker['can_send_photo']),
            'topic_state'             => in_array(($marker['topic_state'] ?? ''), ['none', 'deferred', 'verified', 'invalid'], true) ? (string) $marker['topic_state'] : 'none',
        ];

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

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

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

        $this->save_validation_marker([
            'health'                  => 'degraded',
            'failure_kind'            => '',
            'validated_at'            => 0,
            'credentials_fingerprint' => $credentials_fingerprint,
            'target_fingerprint'      => $target_fingerprint,
            'identity_fingerprint'    => '',
            'resolved_chat_id'        => '',
            'chat_type'               => '',
            'can_send_text'           => false,
            'can_send_photo'          => false,
            'topic_state'             => 'none',
        ]);
        $this->sync_legacy_status('connected_unverified');
    }

    /** @return void */
    private function save_invalid_marker($failure_kind, $credentials_fingerprint, $target_fingerprint, array $context = []) {
        $this->save_validation_marker([
            'health'                  => 'invalid',
            'failure_kind'            => in_array($failure_kind, ['auth', 'target', 'permission', 'topic'], true) ? $failure_kind : 'target',
            'validated_at'            => max(0, (int) ($context['validated_at'] ?? time())),
            'credentials_fingerprint' => $credentials_fingerprint,
            'target_fingerprint'      => $target_fingerprint,
            'identity_fingerprint'    => (string) ($context['identity_fingerprint'] ?? ''),
            'resolved_chat_id'        => (string) ($context['resolved_chat_id'] ?? ''),
            'chat_type'               => (string) ($context['chat_type'] ?? ''),
            'can_send_text'           => !empty($context['can_send_text']),
            'can_send_photo'          => !empty($context['can_send_photo']),
            'topic_state'             => $failure_kind === 'topic' ? 'invalid' : (string) ($context['topic_state'] ?? 'none'),
        ]);
    }

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

        if (!empty($data['retryable']) || $failure_kind === 'temporary') {
            $this->degrade_or_create_unverified_marker($credentials_fingerprint, $target_fingerprint, $previous_marker);
            return;
        }

        if (in_array($failure_kind, ['auth', 'target', 'permission', 'topic'], true)) {
            $base = is_array($previous_marker) ? array_merge($previous_marker, $context) : $context;
            $this->save_invalid_marker($failure_kind, $credentials_fingerprint, $target_fingerprint, $base);
            $status_map = [
                'auth'       => 'token_invalid',
                'target'     => 'target_missing',
                'permission' => 'permission_missing',
                'topic'      => 'configuration_error',
            ];
            $this->sync_legacy_status($status_map[$failure_kind]);
        }
    }

    /** @return string */
    private function get_error_failure_kind($error) {
        if (!is_wp_error($error)) {
            return '';
        }
        $data = $error->get_error_data();
        return is_array($data) && isset($data['failure_kind']) ? (string) $data['failure_kind'] : '';
    }

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

    /** @return string */
    private function normalize_decimal_identifier($value, $allow_negative, $allow_zero) {
        if (!is_scalar($value) || is_float($value)) {
            return '';
        }
        $value = trim((string) $value);
        $pattern = $allow_negative ? '/^-?[0-9]+$/' : '/^[0-9]+$/';
        if ($value === '' || !preg_match($pattern, $value)) {
            return '';
        }

        $negative = $value[0] === '-';
        $digits = $negative ? substr($value, 1) : $value;
        $digits = ltrim($digits, '0');
        if ($digits === '') {
            return $allow_zero ? '0' : '';
        }
        // Telegram documents user/chat identifiers with at most 52 significant bits.
        if (strlen($digits) > 16 || (strlen($digits) === 16 && strcmp($digits, '4503599627370495') > 0)) {
            return '';
        }
        return ($negative ? '-' : '') . $digits;
    }

    /** @return bool */
    private function is_empty_thread_value($value) {
        return $value === null || trim((string) $value) === '' || trim((string) $value) === '0';
    }

    /** @return bool|null */
    private function normalize_marker_boolean($value) {
        if (is_bool($value)) {
            return $value;
        }
        if ($value === 0 || $value === '0') {
            return false;
        }
        if ($value === 1 || $value === '1') {
            return true;
        }
        return null;
    }

    /**
     * @param string               $boundary Multipart boundary.
     * @param array<string,string> $fields Scalar fields.
     * @return string
     */
    private function build_multipart_body($boundary, array $fields, $file_field, $filename, $mime, $file_contents) {
        return $this->build_multipart_body_files($boundary, $fields, [[
            'field' => $file_field,
            'filename' => $filename,
            'mime' => $mime,
            'contents' => $file_contents,
        ]]);
    }

    /**
     * @param string               $boundary Multipart boundary.
     * @param array<string,string> $fields Scalar fields.
     * @param array<int,array{field:string,filename:string,mime:string,contents:string}> $files File fields.
     * @return string
     */
    private function build_multipart_body_files($boundary, array $fields, array $files) {
        $body = '';
        foreach ($fields as $key => $value) {
            $name = preg_replace('/[^A-Za-z0-9_-]/', '', (string) $key);
            if ($name === '') {
                continue;
            }
            $body .= '--' . $boundary . "\r\n";
            $body .= 'Content-Disposition: form-data; name="' . $name . '"' . "\r\n\r\n";
            $body .= (string) $value . "\r\n";
        }

        foreach ($files as $file) {
            $file_field = preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($file['field'] ?? ''));
            if ($file_field === '') {
                continue;
            }
            $filename = sanitize_file_name((string) ($file['filename'] ?? ''));
            $mime = sanitize_mime_type((string) ($file['mime'] ?? ''));
            $body .= '--' . $boundary . "\r\n";
            $body .= 'Content-Disposition: form-data; name="' . $file_field . '"; filename="' . $filename . '"' . "\r\n";
            $body .= 'Content-Type: ' . ($mime !== '' ? $mime : 'application/octet-stream') . "\r\n\r\n";
            $body .= (string) ($file['contents'] ?? '') . "\r\n";
        }
        $body .= '--' . $boundary . "--\r\n";
        return $body;
    }

    /** @return string|false */
    private function get_local_file_contents($path) {
        global $wp_filesystem;
        if (!function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (!WP_Filesystem()) {
            return false;
        }
        return $wp_filesystem->get_contents((string) $path);
    }

    /**
     * Resolve Pro multi-image media items into local files Telegram can upload as an album.
     *
     * @param array<string,mixed> $data
     * @return array<int,array<string,mixed>>|WP_Error
     */
    private function multi_image_assets_from_payload(array $data) {
        if (!class_exists('Calliope_SPP_Media_Set') || !class_exists('Calliope_SPP_Media_Resolver')) {
            return new WP_Error('telegram_multi_image_requires_pro', __('Telegram multiple-image publishing requires Social Publisher Pro media support.', 'calliope-social-publisher'), [
                'phase' => 'prepare', 'retryable' => false, 'failure_kind' => 'local',
            ]);
        }

        $media = isset($data['media']) && is_array($data['media']) ? $data['media'] : [];
        $contract = Calliope_SPP_Media_Set::validate_contract('multiple_images', $media);
        if (is_wp_error($contract)) {
            return new WP_Error('telegram_multi_image_invalid', __('Telegram multiple-image publishing requires 2 to 10 local image attachments.', 'calliope-social-publisher'), [
                'phase' => 'prepare', 'retryable' => false, 'failure_kind' => 'local',
            ]);
        }

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

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

        $by_id = [];
        foreach ($normalized as $item) {
            $by_id[(int) ($item['id'] ?? 0)] = $item;
        }

        $assets = [];
        foreach ($resolved as $index => $file) {
            if (!is_array($file) || empty($file['path']) || empty($file['mime_type'])) {
                return new WP_Error('telegram_multi_image_media_error', __('Telegram could not prepare one of the selected images.', 'calliope-social-publisher'), [
                    'phase' => 'prepare', 'retryable' => false, 'failure_kind' => 'local',
                ]);
            }
            $id = absint($file['id'] ?? 0);
            $source = $by_id[$id] ?? [];
            $description = trim((string) ($source['alt'] ?? ''));
            if ($description === '') {
                $description = trim((string) ($source['caption'] ?? ''));
            }
            if ($description === '') {
                $description = trim((string) ($file['description'] ?? ''));
            }
            $assets[] = [
                'id' => $id,
                'path' => (string) $file['path'],
                'mime_type' => (string) $file['mime_type'],
                'description' => sanitize_text_field($description),
                'position' => absint($source['position'] ?? $index),
                'temporary' => false,
            ];
        }

        usort($assets, static function ($left, $right) {
            return ((int) ($left['position'] ?? 0)) <=> ((int) ($right['position'] ?? 0));
        });

        return array_values($assets);
    }

    /** @return array<string,mixed>|WP_Error */
    private function video_asset_from_payload(array $data) {
        if (!class_exists('Calliope_SPP_Media_Set') || !class_exists('Calliope_SPP_Media_Resolver')) {
            return new WP_Error('telegram_video_requires_pro', __('Telegram video publishing requires Social Publisher Pro media support.', 'calliope-social-publisher'), [
                'phase' => 'prepare', 'retryable' => false, 'failure_kind' => 'local',
            ]);
        }

        $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('telegram_video_invalid', __('Telegram video publishing requires exactly one local video attachment.', 'calliope-social-publisher'), [
                'phase' => 'prepare', 'retryable' => false, 'failure_kind' => 'local',
            ]);
        }

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

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

        $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,
        ]);

        return $file;
    }

    /** @return string[] */
    private function telegram_video_mimes() {
        $mimes = apply_filters('calliope_sp_telegram_video_mimes', ['video/mp4', 'video/mpeg']);
        $clean = [];
        foreach ((array) $mimes as $mime) {
            $mime = strtolower(sanitize_mime_type((string) $mime));
            if ($mime !== '' && strpos($mime, 'video/') === 0) {
                $clean[] = $mime;
            }
        }
        return $clean !== [] ? array_values(array_unique($clean)) : ['video/mp4', 'video/mpeg'];
    }

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

    private function video_extension_matches($filename, $mime) {
        $extension = strtolower((string) pathinfo((string) $filename, PATHINFO_EXTENSION));
        $mime = strtolower(sanitize_mime_type((string) $mime));
        $map = [
            'video/mp4'  => ['mp4', 'm4v'],
            'video/mpeg' => ['mpeg', 'mpg', 'mp4'],
        ];
        return isset($map[$mime]) && in_array($extension, $map[$mime], true);
    }

    /** @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' => 'telegram_video_' . sanitize_key((string) $event),
        ], $entry);
        Calliope_SP_Logger::log('telegram', sanitize_key((string) $status), wp_json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    /** @return array{url:string,path:string,mime:string}|WP_Error */
    private function image_url_from_b64($b64) {
        $asset = class_exists('Calliope_SP_Media_Helper')
            ? Calliope_SP_Media_Helper::create_public_image_from_b64($b64, [
                'context'         => 'telegram',
                'filename_prefix' => 'calliope-social-publisher-telegram',
                'preferred_mime'  => 'image/jpeg',
                'network'         => 'telegram',
                'max_attempts'    => 1,
                'url_strategy'    => 'static_upload',
                'skip_public_validation' => true,
            ])
            : new WP_Error('telegram_image_prepare_failed', __('Telegram could not prepare a public image URL.', 'calliope-social-publisher'));

        if (is_wp_error($asset)) {
            return $asset;
        }
        return [
            'url'  => (string) $asset['url'],
            'path' => (string) $asset['local_path'],
            'mime' => (string) ($asset['mime'] ?? 'image/jpeg'),
            'mime_type' => (string) ($asset['mime'] ?? 'image/jpeg'),
            'temporary' => true,
        ];
    }

    /** @return void */
    private function delete_temp_image($path) {
        $path = (string) $path;
        if ($path !== '' && file_exists($path) && is_file($path)) {
            wp_delete_file($path);
        }
    }

    /** @return string */
    private function format_message(array $data, $max_length = 3900) {
        $max_length = max(1, (int) $max_length);
        $title = trim(wp_strip_all_tags((string) ($data['title'] ?? '')));
        $content = trim(wp_strip_all_tags((string) ($data['content'] ?? '')));
        $link = esc_url_raw((string) ($data['link'] ?? ''));
        $hashtags = trim(wp_strip_all_tags((string) ($data['hashtags'] ?? '')));

        if (isset($data['hashtags_list']) && is_array($data['hashtags_list'])) {
            $tags = array_values(array_filter(array_map(static function ($tag) {
                $tag = trim(wp_strip_all_tags((string) $tag));
                if ($tag === '') {
                    return '';
                }
                return strpos($tag, '#') === 0 ? $tag : '#' . ltrim($tag, '#');
            }, $data['hashtags_list'])));
            if (!empty($tags)) {
                $hashtags = implode(' ', $tags);
            }
        }

        // AI-generated content can already contain the destination URL. Adding the
        // dedicated Link field unconditionally produced two visible copies in
        // Telegram, so append it only when it is not already present.
        $link_to_append = ($link !== '' && !$this->text_contains_link($title . "\n" . $content, $link))
            ? $link
            : '';

        $message = $this->build_limited_message(
            $title,
            $content,
            $hashtags,
            $link_to_append,
            $max_length
        );

        // If the only copy of the URL was located in content removed by
        // truncation, rebuild while reserving one final link block.
        if ($link !== '' && !$this->text_contains_link($message, $link)) {
            $message = $this->build_limited_message(
                $title,
                $content,
                $hashtags,
                $link,
                $max_length
            );
        }

        return $message;
    }

    /** @return string */
    private function build_limited_message($title, $content, $hashtags, $link, $max_length) {
        $title = trim((string) $title);
        $content = trim((string) $content);
        $hashtags = trim((string) $hashtags);
        $link = trim((string) $link);
        $max_length = max(1, (int) $max_length);

        // Reserve the hashtags and final link before shortening the body. This is
        // especially important for Telegram photo captions, whose safe limit is
        // lower than a regular text message.
        $fixed_blocks = array_values(array_filter([$title, $hashtags, $link], static function ($block) {
            return $block !== '';
        }));
        $fixed_length = 0;
        foreach ($fixed_blocks as $block) {
            $fixed_length += function_exists('mb_strlen') ? mb_strlen($block, 'UTF-8') : strlen($block);
        }
        $separator_length = $content !== ''
            ? 2 * count($fixed_blocks)
            : 2 * max(0, count($fixed_blocks) - 1);
        $content_limit = max(0, $max_length - $fixed_length - $separator_length);

        $content_length = function_exists('mb_strlen') ? mb_strlen($content, 'UTF-8') : strlen($content);
        if ($content !== '' && $content_length > $content_limit) {
            $suffix = '…';
            $suffix_length = function_exists('mb_strlen') ? mb_strlen($suffix, 'UTF-8') : strlen($suffix);
            if ($content_limit > $suffix_length) {
                $slice_length = max(1, $content_limit - $suffix_length);
                $slice = function_exists('mb_substr')
                    ? mb_substr($content, 0, $slice_length, 'UTF-8')
                    : substr($content, 0, $slice_length);
                $content = rtrim((string) $slice) . $suffix;
            } elseif ($content_limit >= $suffix_length) {
                $content = $suffix;
            } else {
                $content = '';
            }
        }

        $blocks = array_values(array_filter([$title, $content, $hashtags, $link], static function ($block) {
            return $block !== '';
        }));
        $message = trim(preg_replace("/\n{3,}/", "\n\n", implode("\n\n", $blocks)));

        return $this->truncate_text($message, $max_length);
    }

    /** @return bool */
    private function text_contains_link($text, $link) {
        $needle = $this->normalize_link_for_text_match($link);
        if ($needle === '') {
            return false;
        }

        $haystack = html_entity_decode((string) $text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $haystack = preg_replace('#https?://#iu', '', $haystack);
        $haystack = preg_replace('#\bwww\.#iu', '', (string) $haystack);
        $haystack = function_exists('mb_strtolower')
            ? mb_strtolower((string) $haystack, 'UTF-8')
            : strtolower((string) $haystack);

        return strpos((string) $haystack, $needle) !== false;
    }

    /** @return string */
    private function normalize_link_for_text_match($link) {
        $link = html_entity_decode(trim((string) $link), ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $link = preg_replace('#^https?://#iu', '', $link);
        $link = preg_replace('#^www\.#iu', '', (string) $link);
        $link = rtrim((string) $link, "/ \t\n\r\0\x0B");

        return function_exists('mb_strtolower')
            ? mb_strtolower($link, 'UTF-8')
            : strtolower($link);
    }

    /** @return string */
    private function truncate_text($text, $max_length) {
        $text = trim((string) $text);
        $max_length = max(1, (int) $max_length);
        $length = function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text);
        if ($length <= $max_length) {
            return $text;
        }
        $suffix = '…';
        $suffix_length = function_exists('mb_strlen') ? mb_strlen($suffix, 'UTF-8') : strlen($suffix);
        $cut_at = max(0, $max_length - $suffix_length);
        $slice = function_exists('mb_substr')
            ? mb_substr($text, 0, $cut_at, 'UTF-8')
            : substr($text, 0, $cut_at);
        return rtrim((string) $slice) . ($cut_at > 0 ? $suffix : '');
    }
}
