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

/**
 * Tumblr OAuth 1.0a integration with persistent local readiness evidence.
 */
class Calliope_SP_Tumblr {

    private const VALIDATION_OPTION = 'calliope_sp_tumblr_validation';
    private const VALIDATION_SCHEMA = 'tumblr-validation-v1';
    private const USER_INFO_URL = 'https://api.tumblr.com/v2/user/info';
    private const REQUEST_TOKEN_URL = 'https://www.tumblr.com/oauth/request_token';
    private const ACCESS_TOKEN_URL = 'https://www.tumblr.com/oauth/access_token';
    private const AUTHORIZE_URL = 'https://www.tumblr.com/oauth/authorize';
    private const VIDEO_MAX_BYTES = 500 * 1024 * 1024;

    /**
     * Build the local Tumblr connection state without HTTP or option writes.
     *
     * @return Calliope_SP_Connection_State|WP_Error
     */
    public function get_connection_state() {
        if (!class_exists('Calliope_SP_Connection_State')) {
            return new WP_Error(
                'tumblr_connection_contract_missing',
                __('Tumblr connection status is temporarily unavailable.', 'calliope-social-publisher')
            );
        }

        $consumer_key = trim((string) get_option('calliope_sp_tumblr_consumer_key', ''));
        $consumer_secret = trim((string) get_option('calliope_sp_tumblr_consumer_secret', ''));
        $token = trim((string) get_option('calliope_sp_tumblr_token', ''));
        $token_secret = trim((string) get_option('calliope_sp_tumblr_token_secret', ''));
        $raw_blog = get_option('calliope_sp_tumblr_blog_identifier', '');
        $blog = $this->normalize_blog_identifier($raw_blog);

        $has_any = $consumer_key !== '' || $consumer_secret !== '' || $token !== '' || $token_secret !== '' || trim((string) $raw_blog) !== '';
        $app_complete = $consumer_key !== '' && $consumer_secret !== '';
        $app_partial = (($consumer_key !== '') xor ($consumer_secret !== ''));
        $token_complete = $token !== '' && $token_secret !== '';
        $token_partial = (($token !== '') xor ($token_secret !== ''));

        if (!$has_any) {
            return $this->create_state([
                '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 Tumblr application credentials and select a blog.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'connection_method'     => 'oauth1',
            ]);
        }

        if ($app_partial || $token_partial || (!$app_complete && $token_complete)) {
            return $this->create_state([
                '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'               => __('Complete the Tumblr application and token pair configuration.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'connection_method'     => 'oauth1',
            ]);
        }

        if ($app_complete && !$token_complete) {
            return $this->create_state([
                'status'                => 'credentials_saved',
                'health'                => 'unknown',
                'is_configured'         => true,
                'is_authenticated'      => false,
                'is_identity_known'     => false,
                'has_target'            => false,
                'has_permission'        => false,
                'evidence_level'        => 'E1',
                'message'               => __('Tumblr application credentials are saved. Connect with Tumblr to authorize the account.', 'calliope-social-publisher'),
                'action'                => __('Connect with Tumblr', 'calliope-social-publisher'),
                'connection_method'     => 'oauth1',
            ]);
        }

        if (!$app_complete || !$token_complete) {
            return $this->create_state([
                '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'               => __('Complete the Tumblr application and token pair configuration.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'connection_method'     => 'oauth1',
            ]);
        }

        $application_fingerprint = $this->application_fingerprint($consumer_key, $consumer_secret);
        $credentials_fingerprint = $this->credentials_fingerprint($consumer_key, $consumer_secret, $token, $token_secret);

        if ($blog === '') {
            $credentials_marker = $this->get_validation_marker_for_credentials($application_fingerprint, $credentials_fingerprint);

            if (is_array($credentials_marker) && $credentials_marker['health'] === 'invalid') {
                if ($credentials_marker['failure_kind'] === 'application') {
                    return $this->create_state([
                        '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'               => __('The Tumblr application credentials are invalid or no longer match the saved tokens.', 'calliope-social-publisher'),
                        'action'                => __('Reconnect with Tumblr', 'calliope-social-publisher'),
                        'validated_at'          => (int) $credentials_marker['validated_at'],
                        'connection_method'     => 'oauth1',
                    ]);
                }

                if ($credentials_marker['failure_kind'] === 'auth') {
                    return $this->create_state([
                        'status'                => 'token_invalid',
                        'health'                => 'invalid',
                        'is_configured'         => true,
                        'is_authenticated'      => true,
                        'is_identity_known'     => false,
                        'has_target'            => false,
                        'has_permission'        => false,
                        'evidence_level'        => 'E2',
                        'message'               => __('The Tumblr OAuth tokens are invalid or were revoked.', 'calliope-social-publisher'),
                        'action'                => __('Reconnect with Tumblr', 'calliope-social-publisher'),
                        'validated_at'          => (int) $credentials_marker['validated_at'],
                        'connection_method'     => 'oauth1',
                    ]);
                }

                if ($credentials_marker['failure_kind'] === 'identity') {
                    return $this->create_state([
                        'status'                => 'identity_missing',
                        'health'                => 'invalid',
                        'is_configured'         => true,
                        'is_authenticated'      => true,
                        'is_identity_known'     => false,
                        'has_target'            => false,
                        'has_permission'        => false,
                        'evidence_level'        => 'E2',
                        'message'               => __('Tumblr could not confirm the connected account identity.', 'calliope-social-publisher'),
                        'action'                => __('Save and test', 'calliope-social-publisher'),
                        'validated_at'          => (int) $credentials_marker['validated_at'],
                        'connection_method'     => 'oauth1',
                    ]);
                }
            }

            $identity_known = is_array($credentials_marker)
                && $credentials_marker['identity_fingerprint'] !== ''
                && (int) $credentials_marker['validated_at'] > 0;

            if ($identity_known) {
                return $this->create_state([
                    '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'               => __('Select the Tumblr blog that should receive publications.', 'calliope-social-publisher'),
                    'action'                => __('Save and test', 'calliope-social-publisher'),
                    'validated_at'          => (int) $credentials_marker['validated_at'],
                    'connection_method'     => 'oauth1',
                ]);
            }

            return $this->create_state([
                'status'                => 'connected_unverified',
                'health'                => 'unknown',
                'is_configured'         => true,
                'is_authenticated'      => true,
                'is_identity_known'     => false,
                'has_target'            => false,
                'has_permission'        => false,
                'evidence_level'        => 'E2',
                'message'               => __('Select a valid Tumblr blog before testing the connection.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'validated_at'          => 0,
                'connection_method'     => 'oauth1',
            ]);
        }

        $target_fingerprint = $this->target_fingerprint($blog);
        $marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $target_fingerprint);

        if (!is_array($marker)) {
            return $this->create_state([
                'status'                => 'connected_unverified',
                'health'                => 'unknown',
                'is_configured'         => true,
                'is_authenticated'      => true,
                'is_identity_known'     => false,
                'has_target'            => false,
                'has_permission'        => false,
                'evidence_level'        => 'E2',
                'message'               => __('Tumblr credentials are saved but have not been verified for the selected blog.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'connection_method'     => 'oauth1',
            ]);
        }

        $identity_known = $marker['identity_fingerprint'] !== '';
        $target_known = $marker['target_identity_fingerprint'] !== '' && $marker['resolved_blog_identifier'] !== '';

        if ($marker['health'] === 'invalid') {
            $status_map = [
                'auth'        => 'token_invalid',
                'application' => 'configuration_error',
                'identity'    => 'identity_missing',
                'target'      => 'target_missing',
                'permission'  => 'permission_missing',
            ];
            $status = $status_map[$marker['failure_kind']] ?? 'connected_unverified';
            $message_map = [
                'token_invalid'       => __('The Tumblr OAuth tokens are invalid or were revoked.', 'calliope-social-publisher'),
                'configuration_error' => __('The Tumblr application credentials are invalid or no longer match the saved tokens.', 'calliope-social-publisher'),
                'identity_missing'    => __('Tumblr could not confirm the connected account identity.', 'calliope-social-publisher'),
                'target_missing'      => __('The selected Tumblr blog is unavailable.', 'calliope-social-publisher'),
                'permission_missing'  => __('The connected Tumblr account cannot publish to the selected blog.', 'calliope-social-publisher'),
                'connected_unverified'=> __('Tumblr must be verified again before publishing.', 'calliope-social-publisher'),
            ];

            if ($status === 'configuration_error') {
                return $this->create_state([
                    'status'                => $status,
                    'health'                => 'invalid',
                    'is_configured'         => true,
                    'is_authenticated'      => false,
                    'is_identity_known'     => false,
                    'has_target'            => false,
                    'has_permission'        => false,
                    'evidence_level'        => 'E1',
                    'message'               => $message_map[$status],
                    'action'                => __('Reconnect with Tumblr', 'calliope-social-publisher'),
                    'validated_at'          => $marker['validated_at'],
                    'connection_method'     => 'oauth1',
                ]);
            }

            if ($status === 'token_invalid') {
                return $this->create_state([
                    'status'                => $status,
                    'health'                => 'invalid',
                    'is_configured'         => true,
                    'is_authenticated'      => true,
                    'is_identity_known'     => false,
                    'has_target'            => false,
                    'has_permission'        => false,
                    'evidence_level'        => 'E2',
                    'message'               => $message_map[$status],
                    'action'                => __('Reconnect with Tumblr', 'calliope-social-publisher'),
                    'validated_at'          => $marker['validated_at'],
                    'connection_method'     => 'oauth1',
                ]);
            }

            if ($status === 'identity_missing') {
                return $this->create_state([
                    'status'                => $status,
                    'health'                => 'invalid',
                    'is_configured'         => true,
                    'is_authenticated'      => true,
                    'is_identity_known'     => false,
                    'has_target'            => false,
                    'has_permission'        => false,
                    'evidence_level'        => 'E2',
                    'message'               => $message_map[$status],
                    'action'                => __('Save and test', 'calliope-social-publisher'),
                    'validated_at'          => $marker['validated_at'],
                    'connection_method'     => 'oauth1',
                ]);
            }

            if ($status === 'target_missing') {
                return $this->create_state([
                    'status'                => $status,
                    'health'                => 'invalid',
                    'is_configured'         => true,
                    'is_authenticated'      => true,
                    'is_identity_known'     => true,
                    'has_target'            => false,
                    'has_permission'        => false,
                    'evidence_level'        => 'E3',
                    'message'               => $message_map[$status],
                    'action'                => __('Save and test', 'calliope-social-publisher'),
                    'validated_at'          => $marker['validated_at'],
                    'connection_method'     => 'oauth1',
                ]);
            }

            if ($status === 'permission_missing') {
                return $this->create_state([
                    'status'                => $status,
                    'health'                => 'invalid',
                    'is_configured'         => true,
                    'is_authenticated'      => true,
                    'is_identity_known'     => true,
                    'has_target'            => true,
                    'has_permission'        => false,
                    'evidence_level'        => 'E4',
                    'message'               => $message_map[$status],
                    'action'                => __('Select another blog or reconnect with Tumblr', 'calliope-social-publisher'),
                    'validated_at'          => $marker['validated_at'],
                    'connection_method'     => 'oauth1',
                ]);
            }
        }

        if (!$identity_known || !$target_known || $marker['validated_at'] <= 0) {
            return $this->create_state([
                'status'                => 'connected_unverified',
                'health'                => $marker['health'] === 'degraded' ? 'degraded' : 'unknown',
                'is_configured'         => true,
                'is_authenticated'      => true,
                'is_identity_known'     => false,
                'has_target'            => false,
                'has_permission'        => false,
                'evidence_level'        => 'E2',
                'message'               => __('Tumblr credentials are saved but have not been verified for the selected blog.', 'calliope-social-publisher'),
                'action'                => __('Save and test', 'calliope-social-publisher'),
                'connection_method'     => 'oauth1',
            ]);
        }

        $health = $marker['health'] === 'degraded' ? 'degraded' : 'healthy';
        return $this->create_state([
            'status'                => 'ready_to_publish',
            'health'                => $health,
            'tone'                  => $health === 'degraded' ? 'warning' : 'success',
            'is_configured'         => true,
            'is_authenticated'      => true,
            'is_identity_known'     => true,
            'has_target'            => true,
            'has_permission'        => true,
            'evidence_level'        => 'E5',
            'message'               => $health === 'degraded'
                ? __('Tumblr is ready, but the latest remote check failed temporarily.', 'calliope-social-publisher')
                : __('Tumblr is ready to publish.', 'calliope-social-publisher'),
            'action'                => $health === 'degraded' ? __('Review connection', 'calliope-social-publisher') : '',
            'validated_at'          => $marker['validated_at'],
            'connection_method'     => 'oauth1',
        ]);
    }

    /**
     * Explicitly validate the Tumblr account and selected publishable blog.
     *
     * @return true|WP_Error
     */
    public function test_connection() {
        return $this->validate_connection_remote();
    }

    /**
     * Explicit remote validation. No post is created.
     *
     * @return true|WP_Error
     */
    public function validate_connection_remote() {
        $consumer_key = trim((string) get_option('calliope_sp_tumblr_consumer_key', ''));
        $consumer_secret = trim((string) get_option('calliope_sp_tumblr_consumer_secret', ''));
        $token = trim((string) get_option('calliope_sp_tumblr_token', ''));
        $token_secret = trim((string) get_option('calliope_sp_tumblr_token_secret', ''));
        $blog = $this->normalize_blog_identifier(get_option('calliope_sp_tumblr_blog_identifier', ''));

        if ($consumer_key === '' || $consumer_secret === '' || $token === '' || $token_secret === '') {
            $this->sync_legacy_status($consumer_key !== '' && $consumer_secret !== '' ? 'credentials_saved' : 'configuration_error');
            return $this->safe_error(
                'tumblr_configuration',
                __('Complete the Tumblr application and token pair configuration.', 'calliope-social-publisher'),
                'configuration',
                0,
                false,
                'application'
            );
        }

        if ($blog === '') {
            $this->sync_legacy_status('target_missing');
            return $this->safe_error(
                'tumblr_target_missing',
                __('Select a valid Tumblr blog before testing the connection.', 'calliope-social-publisher'),
                'target',
                0,
                false,
                'target'
            );
        }

        $application_fingerprint = $this->application_fingerprint($consumer_key, $consumer_secret);
        $credentials_fingerprint = $this->credentials_fingerprint($consumer_key, $consumer_secret, $token, $token_secret);
        $target_fingerprint = $this->target_fingerprint($blog);
        $previous_marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $target_fingerprint);

        $response = $this->signed_request('GET', self::USER_INFO_URL, [], $consumer_key, $consumer_secret, $token, $token_secret, 'identity');
        if (is_wp_error($response)) {
            $this->apply_remote_failure_to_validation(
                $response,
                $application_fingerprint,
                $credentials_fingerprint,
                $target_fingerprint,
                $previous_marker
            );
            return $response;
        }

        $decoded = $this->decode_standard_response($response, 'identity');
        if (is_wp_error($decoded)) {
            $this->apply_remote_failure_to_validation(
                $decoded,
                $application_fingerprint,
                $credentials_fingerprint,
                $target_fingerprint,
                $previous_marker
            );
            return $decoded;
        }

        $user = $decoded['response']['user'] ?? null;
        if (!is_array($user)) {
            $error = $this->safe_error(
                'tumblr_identity_response',
                __('Tumblr returned an unexpected account response. Please try again.', 'calliope-social-publisher'),
                'identity',
                (int) $decoded['_http_code'],
                true,
                'temporary'
            );
            $this->apply_remote_failure_to_validation($error, $application_fingerprint, $credentials_fingerprint, $target_fingerprint, $previous_marker);
            return $error;
        }

        $identity_value = $this->normalize_identity_value($user['name'] ?? '');
        if ($identity_value === '') {
            $error = $this->safe_error(
                'tumblr_identity_missing',
                __('Tumblr could not confirm the connected account identity.', 'calliope-social-publisher'),
                'identity',
                (int) $decoded['_http_code'],
                false,
                'identity'
            );
            $this->apply_remote_failure_to_validation($error, $application_fingerprint, $credentials_fingerprint, $target_fingerprint, $previous_marker);
            return $error;
        }

        if (!isset($user['blogs']) || !is_array($user['blogs'])) {
            $error = $this->safe_error(
                'tumblr_blogs_response',
                __('Tumblr returned an unexpected blog list. Please try again.', 'calliope-social-publisher'),
                'target',
                (int) $decoded['_http_code'],
                true,
                'temporary'
            );
            $this->apply_remote_failure_to_validation($error, $application_fingerprint, $credentials_fingerprint, $target_fingerprint, $previous_marker);
            return $error;
        }

        $identity_fingerprint = $this->identity_fingerprint($identity_value);
        if (is_array($previous_marker)
            && $previous_marker['identity_fingerprint'] !== ''
            && !hash_equals($previous_marker['identity_fingerprint'], $identity_fingerprint)
        ) {
            $error = $this->safe_error(
                'tumblr_identity_changed',
                __('Tumblr returned a different account identity. Reconnect the account before publishing.', 'calliope-social-publisher'),
                'identity',
                (int) $decoded['_http_code'],
                false,
                'identity'
            );
            $this->save_invalid_marker('identity', $application_fingerprint, $credentials_fingerprint, $target_fingerprint, [
                'identity_fingerprint' => $previous_marker['identity_fingerprint'],
                'target_identity_fingerprint' => $previous_marker['target_identity_fingerprint'],
                'resolved_blog_identifier' => $previous_marker['resolved_blog_identifier'],
                'validated_at' => $previous_marker['validated_at'],
            ]);
            $this->sync_legacy_status('identity_missing');
            return $error;
        }

        $matched_blog = $this->find_publishable_blog($blog, $user['blogs']);
        if (is_wp_error($matched_blog)) {
            $this->apply_remote_failure_to_validation(
                $matched_blog,
                $application_fingerprint,
                $credentials_fingerprint,
                $target_fingerprint,
                $previous_marker,
                ['identity_fingerprint' => $identity_fingerprint]
            );
            return $matched_blog;
        }

        if (!is_array($matched_blog)) {
            $blog_info = $this->request_blog_info($blog, $consumer_key);
            if (is_wp_error($blog_info)) {
                $this->apply_remote_failure_to_validation(
                    $blog_info,
                    $application_fingerprint,
                    $credentials_fingerprint,
                    $target_fingerprint,
                    $previous_marker,
                    ['identity_fingerprint' => $identity_fingerprint]
                );
                return $blog_info;
            }

            $error = $this->safe_error(
                'tumblr_blog_permission_missing',
                __('The selected Tumblr blog exists, but the connected account cannot publish to it.', 'calliope-social-publisher'),
                'target',
                403,
                false,
                'permission'
            );
            $this->save_invalid_marker('permission', $application_fingerprint, $credentials_fingerprint, $target_fingerprint, [
                'identity_fingerprint' => $identity_fingerprint,
                'validated_at' => time(),
            ]);
            $this->sync_legacy_status('permission_missing');
            return $error;
        }

        $resolved = $this->resolve_remote_blog_identifier($matched_blog);
        if ($resolved === '') {
            $error = $this->safe_error(
                'tumblr_target_response',
                __('Tumblr returned an unexpected blog response. Please try again.', 'calliope-social-publisher'),
                'target',
                (int) $decoded['_http_code'],
                true,
                'temporary'
            );
            $this->apply_remote_failure_to_validation(
                $error,
                $application_fingerprint,
                $credentials_fingerprint,
                $target_fingerprint,
                $previous_marker,
                ['identity_fingerprint' => $identity_fingerprint]
            );
            return $error;
        }

        $target_identity_fingerprint = $this->target_identity_fingerprint($resolved);
        if (is_array($previous_marker)
            && $previous_marker['target_identity_fingerprint'] !== ''
            && !hash_equals($previous_marker['target_identity_fingerprint'], $target_identity_fingerprint)
        ) {
            $error = $this->safe_error(
                'tumblr_target_changed',
                __('Tumblr returned a different blog identity. Verify the selected blog again.', 'calliope-social-publisher'),
                'target',
                (int) $decoded['_http_code'],
                false,
                'target'
            );
            $this->save_invalid_marker('target', $application_fingerprint, $credentials_fingerprint, $target_fingerprint, [
                'identity_fingerprint' => $identity_fingerprint,
                'target_identity_fingerprint' => $previous_marker['target_identity_fingerprint'],
                'resolved_blog_identifier' => $previous_marker['resolved_blog_identifier'],
                'validated_at' => $previous_marker['validated_at'],
            ]);
            $this->sync_legacy_status('target_missing');
            return $error;
        }

        $this->save_validation_marker([
            'health'                      => 'healthy',
            'failure_kind'                => '',
            'validated_at'                => time(),
            'application_fingerprint'     => $application_fingerprint,
            'credentials_fingerprint'     => $credentials_fingerprint,
            'identity_fingerprint'        => $identity_fingerprint,
            'target_fingerprint'          => $target_fingerprint,
            'target_identity_fingerprint' => $target_identity_fingerprint,
            'resolved_blog_identifier'    => $resolved,
            'auth_method'                 => 'oauth1',
        ]);

        $state = $this->get_connection_state();
        if (!is_wp_error($state) && $state->is_ready()) {
            $this->sync_legacy_status('ready_to_publish');
            return true;
        }

        return $this->safe_error(
            'tumblr_validation_incomplete',
            __('Tumblr could not confirm publishing readiness.', 'calliope-social-publisher'),
            'target',
            0,
            false,
            'target'
        );
    }

    /**
     * Publish a legacy Tumblr text or photo post after local readiness.
     *
     * @param array $data Publication data.
     * @return true|WP_Error
     */
    public function publish($data) {
        $state = $this->get_connection_state();
        if (is_wp_error($state)) {
            return $state;
        }
        if (!$state->is_ready()) {
            return $this->safe_error(
                'tumblr_not_ready',
                __('Verify Tumblr from Connections before publishing.', 'calliope-social-publisher'),
                'precheck',
                0,
                false,
                'local'
            );
        }

        $consumer_key = trim((string) get_option('calliope_sp_tumblr_consumer_key', ''));
        $consumer_secret = trim((string) get_option('calliope_sp_tumblr_consumer_secret', ''));
        $token = trim((string) get_option('calliope_sp_tumblr_token', ''));
        $token_secret = trim((string) get_option('calliope_sp_tumblr_token_secret', ''));
        $blog = $this->normalize_blog_identifier(get_option('calliope_sp_tumblr_blog_identifier', ''));
        $application_fingerprint = $this->application_fingerprint($consumer_key, $consumer_secret);
        $credentials_fingerprint = $this->credentials_fingerprint($consumer_key, $consumer_secret, $token, $token_secret);
        $target_fingerprint = $this->target_fingerprint($blog);
        $marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $target_fingerprint);

        if (!is_array($marker) || $marker['resolved_blog_identifier'] === '') {
            return $this->safe_error(
                'tumblr_marker_missing',
                __('Verify Tumblr from Connections before publishing.', 'calliope-social-publisher'),
                'precheck',
                0,
                false,
                'local'
            );
        }

        $caption = $this->build_html_caption((array) $data);

        $tags_clean = '';
        if (!empty($data['hashtags'])) {
            $temp = str_replace('#', '', (string) $data['hashtags']);
            $temp = preg_replace('/\s+/', ' ', $temp);
            $tags_clean = str_replace(' ', ',', trim((string) $temp));
        }

        $post_params = [
            'type'   => 'text',
            'body'   => $caption,
            'format' => 'html',
            'tags'   => $tags_clean,
            'state'  => 'published',
        ];

        $media_mode = sanitize_key((string) ($data['media_mode'] ?? 'none'));
        $temporary_paths = [];
        $use_npf_json = false;
        $this->log_video_diagnostics('publish_start', [
            'media_mode' => $media_mode,
            'has_image_b64' => !empty($data['image_b64']),
            'media_count' => isset($data['media']) && is_array($data['media']) ? count($data['media']) : 0,
            'media' => $this->media_log_items(isset($data['media']) && is_array($data['media']) ? $data['media'] : []),
        ]);
        if ($media_mode === 'multiple_images') {
            $npf = $this->multi_image_npf_payload((array) $data, $tags_clean);
            if (is_wp_error($npf)) {
                return $npf;
            }
            $post_params = $npf['payload'];
            $temporary_paths = $npf['temporary_paths'];
            $use_npf_json = true;
        } elseif ($media_mode === 'video') {
            $this->log_video_diagnostics('blocked_text_only_risk', [
                'reason' => 'tumblr_embed_response_does_not_confirm_video_attachment',
                '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'] : []),
            ], 'error');
            return $this->safe_error(
                'tumblr_video_not_supported',
                __('Tumblr video publishing is disabled because Tumblr did not confirm embedded videos as real video posts. No text-only fallback was sent.', 'calliope-social-publisher'),
                'publish',
                0,
                false,
                'local'
            );
        } elseif (!empty($data['image_b64'])) {
            $parts = explode('base64,', (string) $data['image_b64'], 2);
            $encoded = trim((string) ($parts[1] ?? $parts[0]));
            if ($encoded === '' || base64_decode($encoded, true) === false) {
                return $this->safe_error(
                    'tumblr_image_invalid',
                    __('The selected image could not be prepared safely.', 'calliope-social-publisher'),
                    'publish',
                    0,
                    false,
                    'local'
                );
            }
            $post_params = [
                'type'    => 'photo',
                'caption' => $caption,
                'format'  => 'html',
                'tags'    => $tags_clean,
                'state'   => 'published',
                'data64'  => $encoded,
            ];
        }

        $url = 'https://api.tumblr.com/v2/blog/' . rawurlencode($marker['resolved_blog_identifier']) . ($use_npf_json ? '/posts' : '/post');
        $response = $use_npf_json
            ? $this->signed_json_request('POST', $url, $post_params, $consumer_key, $consumer_secret, $token, $token_secret, 'publish')
            : $this->signed_request('POST', $url, $post_params, $consumer_key, $consumer_secret, $token, $token_secret, 'publish');
        foreach ($temporary_paths as $temporary_path) {
            if (is_string($temporary_path) && $temporary_path !== '' && is_file($temporary_path)) {
                wp_delete_file($temporary_path);
            }
        }
        if (is_wp_error($response)) {
            $this->apply_operation_failure($response, $marker);
            return $response;
        }

        $decoded = $this->decode_standard_response($response, 'publish');
        if (is_wp_error($decoded)) {
            $this->apply_operation_failure($decoded, $marker);
            return $decoded;
        }

        $marker['health'] = 'healthy';
        $marker['failure_kind'] = '';
        $marker['validated_at'] = time();
        $this->save_validation_marker($marker);
        $this->sync_legacy_status('ready_to_publish');

        if ($media_mode === 'multiple_images') {
            return [
                'ok' => true,
                'media_type' => 'photoset',
                'attachments' => count((array) ($post_params['content'] ?? [])) - absint($npf['text_block_count'] ?? 0),
                'post_id' => isset($decoded['response']['id']) && is_scalar($decoded['response']['id']) ? sanitize_text_field((string) $decoded['response']['id']) : '',
            ];
        }
        if ($media_mode === 'video') {
            return [
                'ok' => true,
                'media_type' => 'video',
                'post_id' => isset($decoded['response']['id']) && is_scalar($decoded['response']['id']) ? sanitize_text_field((string) $decoded['response']['id']) : '',
            ];
        }

        return [
            'ok' => true,
            'post_id' => isset($decoded['response']['id']) && is_scalar($decoded['response']['id']) ? sanitize_text_field((string) $decoded['response']['id']) : '',
            'media_type' => !empty($data['image_b64']) ? 'image' : 'text',
        ];
    }

    /**
     * Build the legacy Tumblr caption as explicit, sanitized HTML.
     * Tumblr's legacy post endpoint accepts HTML in body/caption; keeping each
     * logical section in its own paragraph prevents the flattened wall of text.
     */
    private function build_html_caption(array $data): string {
        $html = '';
        $title = trim(wp_strip_all_tags((string) ($data['title'] ?? '')));
        if ($title !== '') {
            $html .= '<p><strong>' . esc_html($title) . '</strong></p>';
        }

        $content = trim(wp_strip_all_tags((string) ($data['content'] ?? '')));
        if ($content !== '') {
            $paragraphs = preg_split('/(?:\r\n|\r|\n){2,}/', $content);
            if (!is_array($paragraphs) || $paragraphs === []) { $paragraphs = [$content]; }
            foreach ($paragraphs as $paragraph) {
                $paragraph = trim((string) $paragraph);
                if ($paragraph === '') { continue; }
                $html .= '<p>' . nl2br(esc_html($paragraph), false) . '</p>';
            }
        }

        $link = esc_url((string) ($data['link'] ?? ''));
        if ($link !== '') {
            $html .= '<p><a href="' . $link . '">Read the full article →</a></p>';
        }

        $hashtags = trim(wp_strip_all_tags((string) ($data['hashtags'] ?? '')));
        if ($hashtags !== '') {
            $hashtags = preg_replace('/\s+/', ' ', $hashtags);
            $html .= '<p>' . esc_html(trim((string) $hashtags)) . '</p>';
        }

        return $html;
    }

    /** @return array{payload:array<string,mixed>,temporary_paths:array<int,string>,text_block_count:int}|WP_Error */
    private function multi_image_npf_payload(array $data, string $tags_clean) {
        if (!class_exists('Calliope_SPP_Media_Set') || !class_exists('Calliope_SPP_Media_Resolver')) {
            return $this->safe_error('tumblr_multi_image_requires_pro', __('Tumblr multiple-image publishing requires Social Publisher Pro media support.', 'calliope-social-publisher'), 'publish', 0, false, 'local');
        }
        $media = isset($data['media']) && is_array($data['media']) ? $data['media'] : [];
        $normalized = array_values(array_filter(
            Calliope_SPP_Media_Set::normalize($media),
            static fn(array $item): bool => (string) ($item['type'] ?? '') === 'image'
        ));
        if (count($normalized) < 2 || count($normalized) > 10) {
            return $this->safe_error('tumblr_multi_image_invalid', __('Tumblr multiple-image publishing requires 2 to 10 images.', 'calliope-social-publisher'), 'publish', 0, false, 'local');
        }
        $contract = Calliope_SPP_Media_Set::validate_contract('multiple_images', $normalized);
        if (is_wp_error($contract)) {
            return $this->safe_error('tumblr_multi_image_invalid', __('Tumblr multiple-image publishing requires 2 to 10 images.', 'calliope-social-publisher'), 'publish', 0, false, 'local');
        }
        $resolved = Calliope_SPP_Media_Resolver::resolve($normalized, [
            'allowed_video_mimes' => [],
            'log_context' => ['network' => 'tumblr', 'mode' => 'multiple_images'],
        ]);
        if (is_wp_error($resolved)) {
            return $this->safe_error('tumblr_multi_image_media_error', sprintf(
                /* translators: %s: media preparation error. */
                __('Tumblr could not prepare the selected images. %s', 'calliope-social-publisher'),
                $resolved->get_error_message()
            ), 'publish', 0, false, 'local');
        }

        $content = [];
        $display = [];
        $title = sanitize_text_field((string) ($data['title'] ?? ''));
        if ($title !== '') {
            $content[] = ['type' => 'text', 'subtype' => 'heading1', 'text' => $title];
            $display[] = ['blocks' => [count($content) - 1]];
        }
        $body_parts = [];
        $body = trim(wp_strip_all_tags((string) ($data['content'] ?? '')));
        if ($body !== '') { $body_parts[] = $body; }
        $link = esc_url_raw((string) ($data['link'] ?? ''));
        if ($link !== '') { $body_parts[] = $link; }
        if ($body_parts !== []) {
            $content[] = ['type' => 'text', 'text' => implode("\n\n", $body_parts)];
            $display[] = ['blocks' => [count($content) - 1]];
        }
        $text_block_count = count($content);
        $image_indexes = [];
        $temporary_paths = [];
        foreach ($resolved as $file) {
            $asset = $this->tumblr_public_image_asset((array) $file);
            if (is_wp_error($asset)) {
                foreach ($temporary_paths as $temporary_path) {
                    if ($temporary_path !== '' && is_file($temporary_path)) { wp_delete_file($temporary_path); }
                }
                return $asset;
            }
            if ($asset['temporary_path'] !== '') { $temporary_paths[] = $asset['temporary_path']; }
            $block = [
                'type' => 'image',
                'media' => [[
                    'type' => (string) $asset['mime_type'],
                    'url' => (string) $asset['url'],
                    'width' => (int) $asset['width'],
                    'height' => (int) $asset['height'],
                ]],
            ];
            $alt = sanitize_text_field((string) ($file['description'] ?? ''));
            if ($alt !== '') { $block['alt_text'] = $alt; }
            $content[] = $block;
            $image_indexes[] = count($content) - 1;
        }
        foreach (array_chunk($image_indexes, 3) as $row) {
            $display[] = ['blocks' => array_values($row)];
        }
        return [
            'payload' => [
                'content' => $content,
                'layout' => [['type' => 'rows', 'display' => $display]],
                'state' => 'published',
                'tags' => $tags_clean,
            ],
            'temporary_paths' => $temporary_paths,
            'text_block_count' => $text_block_count,
        ];
    }

    /** @return array{url:string,mime_type:string,width:int,height:int,temporary_path:string}|WP_Error */
    private function tumblr_public_image_asset(array $file) {
        $id = absint($file['id'] ?? 0);
        $url = $id > 0 ? wp_get_attachment_url($id) : '';
        $temporary_path = '';
        if (!is_string($url) || !$this->is_safe_public_image_url($url)) {
            $path = (string) ($file['path'] ?? '');
            $bytes = $path !== '' && is_file($path) && is_readable($path) ? file_get_contents($path) : false;
            if (!is_string($bytes) || $bytes === '' || !class_exists('Calliope_SP_Media_Helper')) {
                return $this->safe_error('tumblr_multi_image_read_failed', __('Tumblr could not prepare a public URL for one of the selected images.', 'calliope-social-publisher'), 'publish', 0, false, 'local');
            }
            $mime = sanitize_mime_type((string) ($file['mime_type'] ?? 'image/jpeg'));
            $created = Calliope_SP_Media_Helper::create_public_image_from_b64('data:' . $mime . ';base64,' . base64_encode($bytes), [
                'context' => 'tumblr', 'filename_prefix' => 'calliope-social-publisher-tumblr', 'network' => 'tumblr', 'max_attempts' => 6,
            ]);
            if (is_wp_error($created)) { return $created; }
            $url = esc_url_raw((string) ($created['url'] ?? ''));
            $temporary_path = (string) ($created['local_path'] ?? '');
        }
        $size = @getimagesize((string) ($file['path'] ?? ''));
        $width = is_array($size) ? absint($size[0] ?? 0) : 0;
        $height = is_array($size) ? absint($size[1] ?? 0) : 0;
        return [
            'url' => esc_url_raw((string) $url),
            'mime_type' => sanitize_mime_type((string) ($file['mime_type'] ?? 'image/jpeg')),
            'width' => max(1, $width),
            'height' => max(1, $height),
            'temporary_path' => $temporary_path,
        ];
    }

    private function is_safe_public_image_url($url) {
        $parts = wp_parse_url((string) $url);
        if (!is_array($parts) || !in_array(strtolower((string) ($parts['scheme'] ?? '')), ['http', 'https'], true) || empty($parts['host']) || isset($parts['user']) || isset($parts['pass'])) {
            return false;
        }
        return function_exists('wp_http_validate_url') ? (bool) wp_http_validate_url((string) $url) : true;
    }

    /** @return array{url:string,embed_html:string}|WP_Error */
    private function video_embed_from_payload(array $data) {
        if (!class_exists('Calliope_SPP_Media_Set') || !class_exists('Calliope_SPP_Media_Resolver')) {
            return $this->safe_error(
                'tumblr_video_requires_pro',
                __('Tumblr video publishing requires Social Publisher Pro media support.', 'calliope-social-publisher'),
                'publish',
                0,
                false,
                '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 $this->safe_error(
                'tumblr_video_invalid',
                __('Tumblr video publishing requires exactly one local video attachment.', 'calliope-social-publisher'),
                'publish',
                0,
                false,
                'local'
            );
        }

        $max = $this->tumblr_video_max_file_size();
        $resolved = Calliope_SPP_Media_Resolver::resolve($media, [
            'max_file_bytes' => $max,
            'max_request_bytes' => $max,
            'allowed_video_mimes' => $this->tumblr_video_mimes(),
            'log_context' => ['network' => 'tumblr', 'mode' => 'video'],
        ]);
        if (is_wp_error($resolved)) {
            return $this->safe_error(
                'tumblr_video_media_error',
                sprintf(
                    /* translators: %s: media preparation error. */
                    __('Tumblr could not prepare the selected video. %s', 'calliope-social-publisher'),
                    $resolved->get_error_message()
                ),
                'publish',
                0,
                false,
                'local'
            );
        }

        $file = $resolved[0] ?? null;
        if (!is_array($file) || empty($file['id']) || empty($file['mime_type'])) {
            return $this->safe_error('tumblr_video_media_error', __('Tumblr could not prepare the selected video.', 'calliope-social-publisher'), 'publish', 0, false, 'local');
        }
        if (!$this->video_extension_matches((string) ($file['filename'] ?? ''), (string) $file['mime_type'])) {
            return $this->safe_error('tumblr_video_media_error', __('Tumblr video publishing supports MP4 and QuickTime files in this version.', 'calliope-social-publisher'), 'publish', 0, false, 'local');
        }

        $url = function_exists('wp_get_attachment_url') ? wp_get_attachment_url(absint($file['id'])) : '';
        $url = is_string($url) ? esc_url_raw($url) : '';
        if ($url === '' || !preg_match('#^https?://#i', $url)) {
            return $this->safe_error(
                'tumblr_video_url_missing',
                __('Tumblr video embed publishing requires a public WordPress media URL.', 'calliope-social-publisher'),
                'publish',
                0,
                false,
                '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,
            'public_url_set' => true,
            'url_host' => $this->safe_url_host($url),
        ]);

        return [
            'url' => $url,
            'embed_html' => sprintf(
                '<video controls playsinline preload="metadata" style="max-width:100%%;height:auto;"><source src="%s" type="%s"></video>',
                esc_url($url),
                esc_attr((string) ($file['mime_type'] ?? 'video/mp4'))
            ),
        ];
    }

    /** @return string[] */
    private function tumblr_video_mimes() {
        $mimes = apply_filters('calliope_sp_tumblr_video_mimes', ['video/mp4', 'video/quicktime']);
        $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/quicktime'];
    }

    private function tumblr_video_max_file_size() {
        $limit = (int) apply_filters('calliope_sp_tumblr_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/quicktime' => ['mov', 'qt'],
        ];
        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;
    }

    /** @return string */
    private function safe_url_host($url) {
        $parts = wp_parse_url((string) $url);
        return is_array($parts) ? sanitize_text_field((string) ($parts['host'] ?? '')) : '';
    }

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

    /**
     * Request temporary OAuth 1.0a credentials.
     *
     * @return array|WP_Error
     */
    public function oauth_request_token($consumer_key, $consumer_secret, $callback_url) {
        $consumer_key = trim((string) $consumer_key);
        $consumer_secret = trim((string) $consumer_secret);
        $callback_url = esc_url_raw((string) $callback_url);
        if ($consumer_key === '' || $consumer_secret === '' || $callback_url === '') {
            return $this->safe_error('tumblr_oauth_configuration', __('Tumblr OAuth application credentials are incomplete.', 'calliope-social-publisher'), 'request_token', 0, false, 'application');
        }

        $auth = $this->build_oauth_header('POST', self::REQUEST_TOKEN_URL, [], $consumer_key, $consumer_secret, '', '', [
            'oauth_callback' => $callback_url,
        ]);
        if (is_wp_error($auth)) {
            return $auth;
        }

        $response = wp_remote_post(self::REQUEST_TOKEN_URL, $this->request_args([
            'headers' => [
                'Authorization' => $auth,
                'User-Agent'    => $this->get_user_agent(),
                'Accept'        => 'application/x-www-form-urlencoded',
            ],
        ], 30));
        if (is_wp_error($response)) {
            return $this->transport_error($response, 'request_token');
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        if ($code < 200 || $code >= 300) {
            return $this->build_http_error('request_token', $code, $raw);
        }

        parse_str($raw, $out);
        $callback_confirmed = is_array($out) ? ($out['oauth_callback_confirmed'] ?? null) : null;
        $callback_confirmed_valid = is_string($callback_confirmed)
            && !preg_match('/[\x00-\x1F\x7F]/', $callback_confirmed)
            && strtolower(trim($callback_confirmed)) === 'true';

        if (!is_array($out)
            || !$this->is_safe_oauth_value($out['oauth_token'] ?? '')
            || !$this->is_safe_oauth_value($out['oauth_token_secret'] ?? '')
            || !$callback_confirmed_valid
        ) {
            return $this->safe_error('tumblr_request_token_response', __('Tumblr returned an invalid temporary credential response.', 'calliope-social-publisher'), 'request_token', $code, true, 'temporary');
        }

        return [
            'oauth_token' => (string) $out['oauth_token'],
            'oauth_token_secret' => (string) $out['oauth_token_secret'],
            'oauth_callback_confirmed' => 'true',
        ];
    }

    /**
     * Exchange temporary OAuth credentials for access credentials.
     * Tumblr's current official docs advertise GET for this endpoint.
     *
     * @return array|WP_Error
     */
    public function oauth_access_token($consumer_key, $consumer_secret, $request_token, $verifier, $request_token_secret) {
        foreach ([$consumer_key, $consumer_secret, $request_token, $verifier, $request_token_secret] as $value) {
            if (!$this->is_safe_oauth_value($value)) {
                return $this->safe_error('tumblr_access_token_configuration', __('Tumblr OAuth callback data is invalid or incomplete.', 'calliope-social-publisher'), 'access_token', 0, false, 'application');
            }
        }

        $auth = $this->build_oauth_header('GET', self::ACCESS_TOKEN_URL, [], $consumer_key, $consumer_secret, $request_token, $request_token_secret, [
            'oauth_verifier' => $verifier,
        ]);
        if (is_wp_error($auth)) {
            return $auth;
        }

        $response = wp_remote_get(self::ACCESS_TOKEN_URL, $this->request_args([
            'headers' => [
                'Authorization' => $auth,
                'User-Agent'    => $this->get_user_agent(),
                'Accept'        => 'application/x-www-form-urlencoded',
            ],
        ], 30));
        if (is_wp_error($response)) {
            return $this->transport_error($response, 'access_token');
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        if ($code < 200 || $code >= 300) {
            return $this->build_http_error('access_token', $code, $raw);
        }

        parse_str($raw, $out);
        if (!is_array($out)
            || !$this->is_safe_oauth_value($out['oauth_token'] ?? '')
            || !$this->is_safe_oauth_value($out['oauth_token_secret'] ?? '')
        ) {
            return $this->safe_error('tumblr_access_token_response', __('Tumblr returned an invalid access credential response.', 'calliope-social-publisher'), 'access_token', $code, true, 'temporary');
        }

        return [
            'oauth_token' => (string) $out['oauth_token'],
            'oauth_token_secret' => (string) $out['oauth_token_secret'],
        ];
    }

    /**
     * Normalize a documented Tumblr blog identifier without making HTTP requests.
     *
     * @param mixed $value Candidate identifier.
     * @return string
     */
    public function normalize_blog_identifier($value) {
        if (!is_string($value)) {
            return '';
        }
        $value = trim($value);
        if ($value === '' || strlen($value) > 255 || preg_match('/[\x00-\x20\x7F]/', $value)) {
            return '';
        }

        if (preg_match('/^t:[A-Za-z0-9_-]{8,128}$/', $value)) {
            return $value;
        }

        if (preg_match('#^https://#i', $value)) {
            $parts = wp_parse_url($value);
            if (!is_array($parts)
                || strtolower((string) ($parts['scheme'] ?? '')) !== 'https'
                || !empty($parts['user'])
                || !empty($parts['pass'])
                || !empty($parts['port'])
                || isset($parts['query'])
                || isset($parts['fragment'])
            ) {
                return '';
            }

            $host = strtolower((string) ($parts['host'] ?? ''));
            $path = (string) ($parts['path'] ?? '');
            if ($host === 'www.tumblr.com') {
                if (!preg_match('#^/@?([A-Za-z0-9-]{1,64})/?$#', $path, $match)) {
                    return '';
                }
                return strtolower($match[1]);
            }

            if (substr($host, -11) === '.tumblr.com' && ($path === '' || $path === '/')) {
                return $this->normalize_hostname($host);
            }

            return '';
        }

        if (strpos($value, '://') !== false || strpbrk($value, '/?#\\') !== false || str_starts_with($value, '@')) {
            return '';
        }

        $value = strtolower($value);
        if ($value === 'localhost' || filter_var($value, FILTER_VALIDATE_IP)) {
            return '';
        }
        if (strpos($value, '.') === false) {
            return preg_match('/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/', $value) ? $value : '';
        }

        return $this->normalize_hostname($value);
    }

    /** @return string */
    public function get_user_agent() {
        return 'Calliope-Social-Publisher/' . (defined('CALLIOPE_SP_VERSION') ? CALLIOPE_SP_VERSION : '2.0.1') . '; WordPress';
    }

    /** @return string */
    public function get_application_fingerprint($consumer_key, $consumer_secret) {
        return $this->application_fingerprint((string) $consumer_key, (string) $consumer_secret);
    }

    /** @return string */
    public function get_oauth_authorize_url($request_token) {
        return self::AUTHORIZE_URL . '?oauth_token=' . rawurlencode((string) $request_token);
    }

    /**
     * Delete the persistent validation marker.
     */
    public function invalidate_validation() {
        delete_option(self::VALIDATION_OPTION);
    }

    /**
     * Build a signed Tumblr request to a fixed official endpoint.
     *
     * @return array|WP_Error
     */
    private function signed_request($method, $url, array $params, $consumer_key, $consumer_secret, $token, $token_secret, $phase) {
        if (!$this->is_allowed_api_url($url)) {
            return $this->safe_error('tumblr_endpoint_invalid', __('The Tumblr API request is invalid.', 'calliope-social-publisher'), $phase, 0, false, 'application');
        }

        $auth = $this->build_oauth_header($method, $url, $params, $consumer_key, $consumer_secret, $token, $token_secret);
        if (is_wp_error($auth)) {
            return $auth;
        }

        $args = $this->request_args([
            'headers' => [
                'Authorization' => $auth,
                'User-Agent'    => $this->get_user_agent(),
                'Accept'        => 'application/json',
            ],
        ], $phase === 'publish' ? 60 : 30);

        if (strtoupper($method) === 'POST') {
            $args['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
            $args['body'] = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
            $response = wp_remote_post($url, $args);
        } else {
            $response = wp_remote_get($url, $args);
        }

        if (is_wp_error($response)) {
            return $this->transport_error($response, $phase);
        }

        if ($phase === 'publish' && (string) ($params['type'] ?? '') === 'video') {
            $this->log_video_diagnostics('publish_response', [
                'http_code' => (int) wp_remote_retrieve_response_code($response),
                'response_bytes' => strlen((string) wp_remote_retrieve_body($response)),
                'type' => 'video',
                'has_embed' => !empty($params['embed']),
            ], ((int) wp_remote_retrieve_response_code($response) >= 200 && (int) wp_remote_retrieve_response_code($response) < 300) ? 'info' : 'error');
        }

        return $response;
    }

    /** @return array|WP_Error */
    private function signed_json_request($method, $url, array $payload, $consumer_key, $consumer_secret, $token, $token_secret, $phase) {
        if (!$this->is_allowed_api_url($url)) {
            return $this->safe_error('tumblr_endpoint_invalid', __('The Tumblr API request is invalid.', 'calliope-social-publisher'), $phase, 0, false, 'application');
        }
        $auth = $this->build_oauth_header($method, $url, [], $consumer_key, $consumer_secret, $token, $token_secret);
        if (is_wp_error($auth)) { return $auth; }
        $body = wp_json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($body) || $body === '') {
            return $this->safe_error('tumblr_npf_payload_invalid', __('Tumblr could not prepare the multiple-image post.', 'calliope-social-publisher'), $phase, 0, false, 'local');
        }
        $response = wp_remote_post($url, $this->request_args([
            'headers' => [
                'Authorization' => $auth,
                'User-Agent' => $this->get_user_agent(),
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
            'body' => $body,
        ], 60));
        return is_wp_error($response) ? $this->transport_error($response, $phase) : $response;
    }

    /**
     * Public blog info request used only to distinguish missing target from missing permission.
     *
     * @return array|WP_Error
     */
    private function request_blog_info($blog, $consumer_key) {
        $url = 'https://api.tumblr.com/v2/blog/' . rawurlencode($blog) . '/info?api_key=' . rawurlencode($consumer_key);
        $response = wp_remote_get($url, $this->request_args([
            'headers' => [
                'User-Agent' => $this->get_user_agent(),
                'Accept'     => 'application/json',
            ],
        ], 30));
        if (is_wp_error($response)) {
            return $this->transport_error($response, 'target');
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        if ($code === 404) {
            return $this->safe_error('tumblr_blog_not_found', __('The selected Tumblr blog does not exist or is unavailable.', 'calliope-social-publisher'), 'target', $code, false, 'target');
        }
        if ($code < 200 || $code >= 300) {
            return $this->build_http_error('target', $code, $raw);
        }

        $json = json_decode($raw, true);
        if (!is_array($json)
            || !isset($json['meta'])
            || !is_array($json['meta'])
            || (int) ($json['meta']['status'] ?? 0) < 200
            || (int) ($json['meta']['status'] ?? 0) >= 300
            || !isset($json['response']['blog'])
            || !is_array($json['response']['blog'])
        ) {
            return $this->safe_error('tumblr_blog_info_response', __('Tumblr returned an unexpected blog response. Please try again.', 'calliope-social-publisher'), 'target', $code, true, 'temporary');
        }

        return $json['response']['blog'];
    }

    /**
     * Decode Tumblr's standard response envelope.
     *
     * @return array|WP_Error
     */
    private function decode_standard_response($response, $phase) {
        $code = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        if ($code < 200 || $code >= 300) {
            return $this->build_http_error($phase, $code, $raw);
        }

        $json = json_decode($raw, true);
        if (!is_array($json) || !isset($json['meta']) || !is_array($json['meta'])) {
            return $this->safe_error('tumblr_response_invalid', __('Tumblr returned an unexpected response. Please try again.', 'calliope-social-publisher'), $phase, $code, true, 'temporary');
        }

        $meta_status = (int) ($json['meta']['status'] ?? 0);
        if ($meta_status < 200 || $meta_status >= 300) {
            return $this->build_http_error($phase, $meta_status > 0 ? $meta_status : $code, $raw);
        }

        if (!array_key_exists('response', $json)) {
            return $this->safe_error('tumblr_response_incomplete', __('Tumblr returned an incomplete response. Please try again.', 'calliope-social-publisher'), $phase, $code, true, 'temporary');
        }

        $json['_http_code'] = $code;
        return $json;
    }

    /**
     * Find a selected blog inside the authenticated user's publishable blogs.
     *
     * @return array|null|WP_Error
     */
    private function find_publishable_blog($configured, array $blogs) {
        $configured_aliases = $this->blog_identifier_aliases($configured);
        if (empty($configured_aliases)) {
            return $this->safe_error('tumblr_target_invalid', __('Select a valid Tumblr blog before testing the connection.', 'calliope-social-publisher'), 'target', 0, false, 'target');
        }

        foreach ($blogs as $blog) {
            if (!is_array($blog)) {
                continue;
            }
            $remote_aliases = $this->remote_blog_aliases($blog);
            if (!empty(array_intersect($configured_aliases, $remote_aliases))) {
                return $blog;
            }
        }

        return null;
    }

    /** @return string[] */
    private function blog_identifier_aliases($identifier) {
        $identifier = $this->normalize_blog_identifier($identifier);
        if ($identifier === '') {
            return [];
        }
        $aliases = [$identifier];
        if (str_starts_with($identifier, 't:')) {
            return $aliases;
        }
        if (substr($identifier, -11) === '.tumblr.com') {
            $name = substr($identifier, 0, -11);
            if ($this->normalize_blog_identifier($name) !== '') {
                $aliases[] = $name;
            }
        } elseif (strpos($identifier, '.') === false) {
            $aliases[] = $identifier . '.tumblr.com';
        }
        return array_values(array_unique($aliases));
    }

    /** @return string[] */
    private function remote_blog_aliases(array $blog) {
        $aliases = [];
        foreach (['uuid', 'name'] as $field) {
            $normalized = $this->normalize_blog_identifier($blog[$field] ?? '');
            if ($normalized !== '') {
                $aliases = array_merge($aliases, $this->blog_identifier_aliases($normalized));
            }
        }
        $url = isset($blog['url']) && is_string($blog['url']) ? trim($blog['url']) : '';
        if ($url !== '') {
            $normalized = $this->normalize_blog_identifier($url);
            if ($normalized !== '') {
                $aliases = array_merge($aliases, $this->blog_identifier_aliases($normalized));
            }
            $parts = wp_parse_url($url);
            if (is_array($parts) && !empty($parts['host'])) {
                $host = $this->normalize_blog_identifier((string) $parts['host']);
                if ($host !== '') {
                    $aliases = array_merge($aliases, $this->blog_identifier_aliases($host));
                }
            }
        }
        return array_values(array_unique($aliases));
    }

    /** @return string */
    private function resolve_remote_blog_identifier(array $blog) {
        $uuid = $this->normalize_blog_identifier($blog['uuid'] ?? '');
        if ($uuid !== '' && str_starts_with($uuid, 't:')) {
            return $uuid;
        }
        $name = $this->normalize_blog_identifier($blog['name'] ?? '');
        if ($name !== '') {
            return $name;
        }
        $url = isset($blog['url']) && is_string($blog['url']) ? $blog['url'] : '';
        $normalized_url = $this->normalize_blog_identifier($url);
        if ($normalized_url !== '') {
            return $normalized_url;
        }
        $parts = wp_parse_url($url);
        return is_array($parts) ? $this->normalize_blog_identifier((string) ($parts['host'] ?? '')) : '';
    }

    /** @return string */
    private function normalize_hostname($host) {
        $host = strtolower(trim((string) $host, '.'));
        if ($host === '' || strlen($host) > 253 || $host === 'localhost' || substr($host, -6) === '.local') {
            return '';
        }
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return '';
        }
        if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
            return '';
        }
        return $host;
    }

    /** @return string */
    private function normalize_identity_value($value) {
        if (!is_scalar($value)) {
            return '';
        }
        $value = trim((string) $value);
        if ($value === '' || strlen($value) > 190 || preg_match('/[\x00-\x1F\x7F]/', $value)) {
            return '';
        }
        return strtolower($value);
    }

    /**
     * RFC 5849 OAuth Authorization header using HMAC-SHA1.
     *
     * @return string|WP_Error
     */
    private function build_oauth_header($method, $url, $params, $consumer_key, $consumer_secret, $token = '', $token_secret = '', $oauth_extra = []) {
        if (!is_array($params) || !is_array($oauth_extra)) {
            return $this->safe_error('tumblr_oauth_parameters', __('The Tumblr OAuth request contains invalid parameters.', 'calliope-social-publisher'), 'configuration', 0, false, 'application');
        }
        foreach (array_merge($params, $oauth_extra) as $value) {
            if (!is_scalar($value)) {
                return $this->safe_error('tumblr_oauth_parameters', __('The Tumblr OAuth request contains invalid parameters.', 'calliope-social-publisher'), 'configuration', 0, false, 'application');
            }
        }

        $oauth = [
            'oauth_consumer_key'     => (string) $consumer_key,
            'oauth_nonce'            => $this->generate_oauth_nonce(),
            'oauth_signature_method' => 'HMAC-SHA1',
            'oauth_timestamp'        => (string) time(),
            'oauth_version'          => '1.0',
        ];
        if ((string) $token !== '') {
            $oauth['oauth_token'] = (string) $token;
        }
        foreach ($oauth_extra as $key => $value) {
            $oauth[(string) $key] = (string) $value;
        }

        $signature_params = [];
        $query = wp_parse_url($url, PHP_URL_QUERY);
        if (is_string($query) && $query !== '') {
            parse_str($query, $query_params);
            foreach ((array) $query_params as $key => $value) {
                if (!is_scalar($value)) {
                    return $this->safe_error('tumblr_oauth_parameters', __('The Tumblr OAuth request contains invalid parameters.', 'calliope-social-publisher'), 'configuration', 0, false, 'application');
                }
                $signature_params[] = [(string) $key, (string) $value];
            }
        }
        foreach ($oauth as $key => $value) {
            if ($key !== 'oauth_signature') {
                $signature_params[] = [$key, (string) $value];
            }
        }
        foreach ($params as $key => $value) {
            $signature_params[] = [(string) $key, (string) $value];
        }

        usort($signature_params, function ($left, $right) {
            $left_key = rawurlencode($left[0]);
            $right_key = rawurlencode($right[0]);
            if ($left_key === $right_key) {
                return strcmp(rawurlencode($left[1]), rawurlencode($right[1]));
            }
            return strcmp($left_key, $right_key);
        });

        $normalized = [];
        foreach ($signature_params as $pair) {
            $normalized[] = rawurlencode($pair[0]) . '=' . rawurlencode($pair[1]);
        }

        $base_url = $this->oauth_base_url($url);
        $base_string = strtoupper((string) $method) . '&' . rawurlencode($base_url) . '&' . rawurlencode(implode('&', $normalized));
        $signing_key = rawurlencode((string) $consumer_secret) . '&' . rawurlencode((string) $token_secret);
        $oauth['oauth_signature'] = base64_encode(hash_hmac('sha1', $base_string, $signing_key, true));

        ksort($oauth, SORT_STRING);
        $header = [];
        foreach ($oauth as $key => $value) {
            $header[] = rawurlencode($key) . '="' . rawurlencode((string) $value) . '"';
        }
        return 'OAuth ' . implode(', ', $header);
    }

    /** @return string */
    private function oauth_base_url($url) {
        $parts = wp_parse_url($url);
        if (!is_array($parts)) {
            return '';
        }
        $scheme = strtolower((string) ($parts['scheme'] ?? 'https'));
        $host = strtolower((string) ($parts['host'] ?? ''));
        $port = isset($parts['port']) ? (int) $parts['port'] : 0;
        $path = (string) ($parts['path'] ?? '/');
        $port_part = ($port > 0 && !(($scheme === 'https' && $port === 443) || ($scheme === 'http' && $port === 80))) ? ':' . $port : '';
        return $scheme . '://' . $host . $port_part . ($path === '' ? '/' : $path);
    }

    /** @return string */
    private function generate_oauth_nonce() {
        try {
            return bin2hex(random_bytes(24));
        } catch (Throwable $error) {
            return str_replace('-', '', wp_generate_uuid4()) . wp_rand(100000, 999999);
        }
    }

    /** @return array */
    private function request_args(array $args, $timeout) {
        $args['timeout'] = max(5, min(60, (int) $timeout));
        $args['redirection'] = 0;
        $args['reject_unsafe_urls'] = true;
        $args['sslverify'] = true;
        return $args;
    }

    /** @return bool */
    private function is_allowed_api_url($url) {
        $parts = wp_parse_url($url);
        if (!is_array($parts) || strtolower((string) ($parts['scheme'] ?? '')) !== 'https') {
            return false;
        }
        $host = strtolower((string) ($parts['host'] ?? ''));
        return in_array($host, ['api.tumblr.com', 'www.tumblr.com'], true);
    }

    /** @return bool */
    private function is_safe_oauth_value($value) {
        return is_string($value)
            && $value !== ''
            && strlen($value) <= 2048
            && !preg_match('/[\x00-\x1F\x7F]/', $value);
    }

    /** @return string */
    private function application_fingerprint($consumer_key, $consumer_secret) {
        return $this->hmac_fingerprint([
            'schema' => self::VALIDATION_SCHEMA,
            'consumer_key' => (string) $consumer_key,
            'consumer_secret' => (string) $consumer_secret,
        ]);
    }

    /** @return string */
    private function credentials_fingerprint($consumer_key, $consumer_secret, $token, $token_secret) {
        return $this->hmac_fingerprint([
            'schema' => self::VALIDATION_SCHEMA,
            'consumer_key' => (string) $consumer_key,
            'consumer_secret' => (string) $consumer_secret,
            'token' => (string) $token,
            'token_secret' => (string) $token_secret,
        ]);
    }

    /** @return string */
    private function identity_fingerprint($identity) {
        return $this->hmac_fingerprint(['schema' => self::VALIDATION_SCHEMA, 'identity' => (string) $identity]);
    }

    /** @return string */
    private function target_fingerprint($blog) {
        return $this->hmac_fingerprint(['schema' => self::VALIDATION_SCHEMA, 'blog' => (string) $blog]);
    }

    /** @return string */
    private function target_identity_fingerprint($resolved) {
        return $this->hmac_fingerprint(['schema' => self::VALIDATION_SCHEMA, 'resolved_blog' => (string) $resolved]);
    }

    /** @return string */
    private function hmac_fingerprint(array $value) {
        return hash_hmac('sha256', wp_json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), wp_salt('auth'));
    }

    /** @return array|null */
    private function get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $target_fingerprint) {
        $marker = $this->normalize_validation_marker(get_option(self::VALIDATION_OPTION, null));
        if (!is_array($marker)
            || !hash_equals($marker['application_fingerprint'], (string) $application_fingerprint)
            || !hash_equals($marker['credentials_fingerprint'], (string) $credentials_fingerprint)
            || !hash_equals($marker['target_fingerprint'], (string) $target_fingerprint)
        ) {
            return null;
        }
        return $marker;
    }

    /** @return array|null */
    private function get_validation_marker_for_credentials($application_fingerprint, $credentials_fingerprint) {
        $marker = $this->normalize_validation_marker(get_option(self::VALIDATION_OPTION, null));
        if (!is_array($marker)
            || !hash_equals($marker['application_fingerprint'], (string) $application_fingerprint)
            || !hash_equals($marker['credentials_fingerprint'], (string) $credentials_fingerprint)
        ) {
            return null;
        }
        return $marker;
    }

    /** @return array|null */
    private function normalize_validation_marker($marker) {
        if (!is_array($marker) || ($marker['schema'] ?? '') !== self::VALIDATION_SCHEMA) {
            return null;
        }
        $health = (string) ($marker['health'] ?? '');
        $failure = (string) ($marker['failure_kind'] ?? '');
        $allowed_health = ['healthy', 'degraded', 'invalid'];
        $allowed_failure = ['', 'auth', 'application', 'identity', 'target', 'permission'];
        if (!in_array($health, $allowed_health, true) || !in_array($failure, $allowed_failure, true)) {
            return null;
        }
        foreach (['application_fingerprint', 'credentials_fingerprint', 'target_fingerprint'] as $field) {
            if (!is_string($marker[$field] ?? null) || !preg_match('/^[a-f0-9]{64}$/', (string) $marker[$field])) {
                return null;
            }
        }
        foreach (['identity_fingerprint', 'target_identity_fingerprint'] as $field) {
            $value = (string) ($marker[$field] ?? '');
            if ($value !== '' && !preg_match('/^[a-f0-9]{64}$/', $value)) {
                return null;
            }
        }
        $resolved = $this->normalize_blog_identifier($marker['resolved_blog_identifier'] ?? '');
        $validated_at = isset($marker['validated_at']) && is_numeric($marker['validated_at']) ? max(0, (int) $marker['validated_at']) : 0;
        return [
            'schema'                      => self::VALIDATION_SCHEMA,
            'health'                      => $health,
            'failure_kind'                => $failure,
            'validated_at'                => $validated_at,
            'application_fingerprint'     => (string) $marker['application_fingerprint'],
            'credentials_fingerprint'     => (string) $marker['credentials_fingerprint'],
            'identity_fingerprint'        => (string) ($marker['identity_fingerprint'] ?? ''),
            'target_fingerprint'          => (string) $marker['target_fingerprint'],
            'target_identity_fingerprint' => (string) ($marker['target_identity_fingerprint'] ?? ''),
            'resolved_blog_identifier'    => $resolved,
            'auth_method'                 => 'oauth1',
        ];
    }

    private function save_validation_marker(array $marker) {
        $normalized = [
            'schema'                      => self::VALIDATION_SCHEMA,
            'health'                      => in_array(($marker['health'] ?? ''), ['healthy', 'degraded', 'invalid'], true) ? (string) $marker['health'] : 'degraded',
            'failure_kind'                => in_array(($marker['failure_kind'] ?? ''), ['', 'auth', 'application', 'identity', 'target', 'permission'], true) ? (string) $marker['failure_kind'] : '',
            'validated_at'                => max(0, (int) ($marker['validated_at'] ?? 0)),
            'application_fingerprint'     => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['application_fingerprint'] ?? '')) ? (string) $marker['application_fingerprint'] : '',
            'credentials_fingerprint'     => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['credentials_fingerprint'] ?? '')) ? (string) $marker['credentials_fingerprint'] : '',
            'identity_fingerprint'        => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['identity_fingerprint'] ?? '')) ? (string) $marker['identity_fingerprint'] : '',
            'target_fingerprint'          => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['target_fingerprint'] ?? '')) ? (string) $marker['target_fingerprint'] : '',
            'target_identity_fingerprint' => preg_match('/^[a-f0-9]{64}$/', (string) ($marker['target_identity_fingerprint'] ?? '')) ? (string) $marker['target_identity_fingerprint'] : '',
            'resolved_blog_identifier'    => $this->normalize_blog_identifier($marker['resolved_blog_identifier'] ?? ''),
            'auth_method'                 => 'oauth1',
        ];
        update_option(self::VALIDATION_OPTION, $normalized, false);
    }

    private function save_invalid_marker($failure_kind, $application_fingerprint, $credentials_fingerprint, $target_fingerprint, array $context = []) {
        $this->save_validation_marker([
            'health'                      => 'invalid',
            'failure_kind'                => $failure_kind,
            'validated_at'                => (int) ($context['validated_at'] ?? time()),
            'application_fingerprint'     => $application_fingerprint,
            'credentials_fingerprint'     => $credentials_fingerprint,
            'identity_fingerprint'        => (string) ($context['identity_fingerprint'] ?? ''),
            'target_fingerprint'          => $target_fingerprint,
            'target_identity_fingerprint' => (string) ($context['target_identity_fingerprint'] ?? ''),
            'resolved_blog_identifier'    => (string) ($context['resolved_blog_identifier'] ?? ''),
        ]);
    }

    private function apply_remote_failure_to_validation($error, $application_fingerprint, $credentials_fingerprint, $target_fingerprint, $previous_marker, array $context = []) {
        $failure_kind = $this->get_error_failure_kind($error);
        $data = is_wp_error($error) ? $error->get_error_data() : [];
        $retryable = is_array($data) && !empty($data['retryable']);

        if ($failure_kind === 'temporary' || $retryable) {
            if (is_array($previous_marker)
                && $previous_marker['validated_at'] > 0
                && $previous_marker['identity_fingerprint'] !== ''
                && $previous_marker['target_identity_fingerprint'] !== ''
                && $previous_marker['resolved_blog_identifier'] !== ''
            ) {
                $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,
                'application_fingerprint'     => $application_fingerprint,
                'credentials_fingerprint'     => $credentials_fingerprint,
                'identity_fingerprint'        => '',
                'target_fingerprint'          => $target_fingerprint,
                'target_identity_fingerprint' => '',
                'resolved_blog_identifier'    => '',
            ]);
            $this->sync_legacy_status('connected_unverified');
            return;
        }

        $base = [
            'identity_fingerprint' => (string) ($context['identity_fingerprint'] ?? (is_array($previous_marker) ? $previous_marker['identity_fingerprint'] : '')),
            'target_identity_fingerprint' => (string) ($context['target_identity_fingerprint'] ?? (is_array($previous_marker) ? $previous_marker['target_identity_fingerprint'] : '')),
            'resolved_blog_identifier' => (string) ($context['resolved_blog_identifier'] ?? (is_array($previous_marker) ? $previous_marker['resolved_blog_identifier'] : '')),
            'validated_at' => (int) (is_array($previous_marker) ? $previous_marker['validated_at'] : time()),
        ];
        $definitive = in_array($failure_kind, ['auth', 'application', 'identity', 'target', 'permission'], true) ? $failure_kind : 'identity';
        $this->save_invalid_marker($definitive, $application_fingerprint, $credentials_fingerprint, $target_fingerprint, $base);
        $status_map = [
            'auth'        => 'token_invalid',
            'application' => 'configuration_error',
            'identity'    => 'identity_missing',
            'target'      => 'target_missing',
            'permission'  => 'permission_missing',
        ];
        $this->sync_legacy_status($status_map[$definitive]);
    }

    private function apply_operation_failure($error, array $marker) {
        $failure_kind = $this->get_error_failure_kind($error);
        $data = is_wp_error($error) ? $error->get_error_data() : [];
        $retryable = is_array($data) && !empty($data['retryable']);
        if ($failure_kind === 'temporary' || $retryable) {
            $marker['health'] = 'degraded';
            $marker['failure_kind'] = '';
            $this->save_validation_marker($marker);
            $this->sync_legacy_status('ready_to_publish');
            return;
        }
        if ($failure_kind === 'local') {
            return;
        }
        if (in_array($failure_kind, ['auth', 'application', 'identity', 'target', 'permission'], true)) {
            $this->save_invalid_marker($failure_kind, $marker['application_fingerprint'], $marker['credentials_fingerprint'], $marker['target_fingerprint'], $marker);
            $status_map = [
                'auth' => 'token_invalid',
                'application' => 'configuration_error',
                'identity' => 'identity_missing',
                'target' => 'target_missing',
                'permission' => 'permission_missing',
            ];
            $this->sync_legacy_status($status_map[$failure_kind]);
        }
    }

    /** @return WP_Error */
    private function build_http_error($phase, $http_code, $raw) {
        $safe = strtolower($this->sanitize_remote_message($raw));
        $failure_kind = 'temporary';
        $retryable = $http_code === 408 || $http_code === 429 || ($http_code >= 500 && $http_code <= 599);

        if ($http_code === 401 || str_contains($safe, 'invalid token') || str_contains($safe, 'token rejected') || str_contains($safe, 'oauth token')) {
            $failure_kind = 'auth';
            $retryable = false;
        } elseif (str_contains($safe, 'consumer key') || str_contains($safe, 'consumer secret') || str_contains($safe, 'invalid signature')) {
            $failure_kind = 'application';
            $retryable = false;
        } elseif ($phase === 'target' && $http_code === 404) {
            $failure_kind = 'target';
            $retryable = false;
        } elseif ($phase === 'publish' && $http_code === 404) {
            $failure_kind = 'target';
            $retryable = false;
        } elseif ($phase === 'publish' && $http_code === 403 && (str_contains($safe, 'forbidden') || str_contains($safe, 'permission') || str_contains($safe, 'not allowed'))) {
            $failure_kind = 'permission';
            $retryable = false;
        } elseif ($phase === 'publish' && in_array($http_code, [400, 422], true)) {
            $failure_kind = 'local';
            $retryable = false;
        }

        $messages = [
            'auth'        => __('The Tumblr OAuth tokens are invalid or were revoked.', 'calliope-social-publisher'),
            'application' => __('Tumblr rejected the application credentials or OAuth signature.', 'calliope-social-publisher'),
            'target'      => __('The selected Tumblr blog is unavailable.', 'calliope-social-publisher'),
            'permission'  => __('The connected Tumblr account cannot publish to the selected blog.', 'calliope-social-publisher'),
            'local'       => __('Tumblr rejected the publication content.', 'calliope-social-publisher'),
            'temporary'   => __('Tumblr is temporarily unavailable. Please try again later.', 'calliope-social-publisher'),
        ];
        return $this->safe_error('tumblr_' . sanitize_key($phase) . '_http', $messages[$failure_kind], $phase, $http_code, $retryable, $failure_kind);
    }

    /** @return WP_Error */
    private function transport_error($error, $phase) {
        return $this->safe_error('tumblr_transport', __('Tumblr could not be reached. Please try again later.', 'calliope-social-publisher'), $phase, 0, true, 'temporary');
    }

    /** @return WP_Error */
    private function safe_error($code, $message, $phase, $http_code, $retryable, $failure_kind) {
        return new WP_Error($code, $message, [
            'phase'        => sanitize_key((string) $phase),
            'http_code'    => max(0, (int) $http_code),
            'retryable'    => (bool) $retryable,
            'failure_kind' => sanitize_key((string) $failure_kind),
        ]);
    }

    /** @return string */
    private function sanitize_remote_message($raw) {
        $raw = is_string($raw) ? $raw : '';
        $json = json_decode($raw, true);
        if (is_array($json)) {
            $raw = (string) ($json['meta']['msg'] ?? $json['errors'][0]['detail'] ?? $json['error'] ?? '');
        }
        $raw = wp_strip_all_tags($raw, true);
        $raw = preg_replace('/[\x00-\x1F\x7F]/', ' ', (string) $raw);
        $raw = preg_replace('/\s+/', ' ', (string) $raw);
        return substr(trim((string) $raw), 0, 180);
    }

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

    private function sync_legacy_status($status) {
        $status = sanitize_key((string) $status);
        if (get_option('calliope_sp_tumblr_connection_status', '') !== $status) {
            update_option('calliope_sp_tumblr_connection_status', $status, false);
        }
    }

    /** @return Calliope_SP_Connection_State|WP_Error */
    private function create_state(array $data) {
        $data['network'] = 'tumblr';
        return Calliope_SP_Connection_State::create($data, 'E5');
    }
}
