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

/**
 * Pinterest API v5 integration with local, persistent connection evidence.
 */
class Calliope_SP_Pinterest {

    private const VALIDATION_OPTION = 'calliope_sp_pinterest_validation';
    private const VALIDATION_SCHEMA = 'pinterest-validation-v2';
    private const TOKEN_ENVIRONMENT_OPTION = 'calliope_sp_pinterest_token_environment';
    private const PUBLISH_DENIAL_OPTION = 'calliope_sp_pinterest_publish_access_denial';
    private const REQUIRED_SCOPES = [
        'pins:read',
        'pins:write',
        'boards:read',
        'boards:write',
        'user_accounts:read',
    ];
    private const API_BASES = [
        'production' => 'https://api.pinterest.com/v5',
        'sandbox' => 'https://api-sandbox.pinterest.com/v5',
    ];
    private const TOKEN_ENDPOINTS = [
        'production' => 'https://api.pinterest.com/v5/oauth/token',
        'sandbox' => 'https://api-sandbox.pinterest.com/v5/oauth/token',
    ];

    /** @var bool */
    private static $refresh_attempted = false;
    private const MAX_BOARD_ID_LENGTH = 32;
    private const MAX_LIST_PAGES = 3;
    private const MAX_LIST_ITEMS = 150;

    /** @var string */
    private $environment = 'production';

    /** @var string */
    private $api_base = 'https://api.pinterest.com/v5';

    public function __construct() {
        $this->environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        if ($this->environment === '') {
            $this->environment = 'production';
        }
        $this->api_base = $this->get_api_base($this->environment);
    }

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

        $app_id = trim((string) get_option('calliope_sp_pinterest_app_id', ''));
        $app_secret = trim((string) get_option('calliope_sp_pinterest_app_secret', ''));
        $raw_token = get_option('calliope_sp_pinterest_token', '');
        $raw_refresh_token = get_option('calliope_sp_pinterest_refresh_token', '');
        $token = ($raw_token === '' || $raw_token === null) ? '' : $this->normalize_opaque_token($raw_token);
        $refresh_token = ($raw_refresh_token === '' || $raw_refresh_token === null) ? '' : $this->normalize_opaque_token($raw_refresh_token, 'pinterest_refresh_token_invalid');
        $token_invalid = is_wp_error($token);
        $refresh_token_invalid = is_wp_error($refresh_token);
        if ($token_invalid) $token = '';
        if ($refresh_token_invalid) $refresh_token = '';

        $environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        $token_environment = $this->get_token_environment();
        $board_id = $this->get_board_id_for_environment($environment);
        $has_board_value = $board_id !== '';
        $access_expires_at = $this->normalize_timestamp_option(get_option('calliope_sp_pinterest_access_token_expires_at', 0));
        $refresh_expires_at = $this->normalize_timestamp_option(get_option('calliope_sp_pinterest_refresh_token_expires_at', 0));
        $granted_scopes = $this->normalize_scopes(get_option('calliope_sp_pinterest_granted_scopes', []));
        $auth_method = $this->get_effective_auth_method();

        $has_app_id = $app_id !== '';
        $has_app_secret = $app_secret !== '';
        $has_token = $token !== '';

        if ($token_invalid || $refresh_token_invalid) {
            return $this->create_state([
                'status' => 'token_invalid', 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => false, 'is_identity_known' => false,
                'has_target' => false, 'has_permission' => false, 'evidence_level' => 'E1',
                'message' => __('The saved Pinterest authorization is invalid.', 'calliope-social-publisher'),
                'action' => __('Reconnect Pinterest', 'calliope-social-publisher'), 'connection_method' => 'unknown',
            ], 'E4');
        }

        if (!$has_app_id && !$has_app_secret && !$has_token && !$has_board_value) {
            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 Pinterest application credentials and connect an account.', 'calliope-social-publisher'),
                'action' => __('Save and test', 'calliope-social-publisher'), 'connection_method' => 'unknown',
            ], 'E4');
        }

        if ($environment === '' || ($has_app_id xor $has_app_secret) || ($has_token && (!$has_app_id || !$has_app_secret))) {
            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 Pinterest application and environment configuration.', 'calliope-social-publisher'),
                'action' => __('Save and test', 'calliope-social-publisher'), 'connection_method' => 'unknown',
            ], 'E4');
        }

        if ($has_app_id && $has_app_secret && !$has_token) {
            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' => __('Pinterest application credentials are saved. Connect with Pinterest or enter an access token.', 'calliope-social-publisher'),
                'action' => __('Connect with Pinterest', 'calliope-social-publisher'), 'connection_method' => 'unknown',
            ], 'E4');
        }

        $method = $auth_method === 'oauth2' ? 'oauth2' : 'token';
        if ($auth_method === 'oauth2' && !$this->has_required_scopes($granted_scopes)) {
            return $this->create_state([
                'status' => 'permission_missing', 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => false,
                'has_target' => false, 'has_permission' => false, 'evidence_level' => 'E2',
                'message' => __('Pinterest needs to be reconnected to grant the complete publishing permissions.', 'calliope-social-publisher'),
                'action' => __('Reconnect Pinterest', 'calliope-social-publisher'),
                'expires_at' => $access_expires_at, 'connection_method' => 'oauth2',
            ], 'E4');
        }

        if ($token_environment === '' || !hash_equals($environment, $token_environment)) {
            return $this->create_state([
                'status' => 'environment_mismatch', 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => false,
                'has_target' => false, 'has_permission' => false, 'evidence_level' => 'E2',
                'message' => __('Pinterest was connected for a different environment. Reconnect Pinterest.', 'calliope-social-publisher'),
                'action' => __('Reconnect Pinterest', 'calliope-social-publisher'),
                'expires_at' => $access_expires_at, 'connection_method' => $method,
            ], 'E4');
        }

        $application_fingerprint = $this->application_fingerprint($environment, $app_id, $app_secret);
        $credentials_fingerprint = $this->credentials_fingerprint($environment, $app_id, $app_secret, $token);
        $target_fingerprint = $board_id !== '' ? $this->target_fingerprint($environment, $board_id) : '';
        $auth_evidence_marker = $this->get_validation_marker_for_credentials($application_fingerprint, $credentials_fingerprint, $environment);
        $marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint);
        $denial = $this->get_matching_publish_denial($application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint);

        if ($access_expires_at > 0 && $access_expires_at <= time()) {
            return $this->create_state([
                'status' => 'token_expired', 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => false,
                'has_target' => false, 'has_permission' => false, 'evidence_level' => 'E2',
                'message' => __('The Pinterest access token has expired.', 'calliope-social-publisher'),
                'action' => __('Reconnect Pinterest', 'calliope-social-publisher'),
                'expires_at' => $access_expires_at, 'connection_method' => $method,
            ], 'E4');
        }

        if ($auth_method === 'oauth2' && ($access_expires_at <= 0 || $refresh_token === '' || $refresh_expires_at <= 0)) {
            return $this->create_state([
                'status' => 'refresh_required', 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => false,
                'has_target' => false, 'has_permission' => false, 'evidence_level' => 'E2',
                'message' => __('Pinterest must be verified again before publishing.', 'calliope-social-publisher'),
                'action' => __('Reconnect Pinterest', 'calliope-social-publisher'),
                'expires_at' => $access_expires_at, 'connection_method' => 'oauth2',
            ], 'E4');
        }

        if (is_array($denial)) {
            $is_access_tier = ($denial['failure_kind'] ?? '') === 'access_tier';
            $message = $is_access_tier && $environment === 'production'
                ? __('Pinterest accepted the account connection but denied Pin publishing in Production. Verify that the app has Production publishing access or use Sandbox for Trial testing.', 'calliope-social-publisher')
                : __('Pinterest accepted the account connection but denied Pin publishing. Reconnect Pinterest and verify the granted permissions for the selected environment.', 'calliope-social-publisher');
            return $this->create_state([
                'status' => 'publish_access_denied', 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => true,
                'is_identity_known' => is_array($marker) && $marker['identity_fingerprint'] !== '',
                'has_target' => $board_id !== '', 'has_permission' => false,
                'evidence_level' => $board_id !== '' ? 'E4' : 'E2',
                'message' => $message,
                'action' => $environment === 'production' ? __('Review Production access', 'calliope-social-publisher') : __('Reconnect Pinterest', 'calliope-social-publisher'),
                'validated_at' => (int) ($denial['recorded_at'] ?? 0), 'expires_at' => $access_expires_at,
                'connection_method' => $method,
            ], 'E5');
        }

        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' => $board_id === ''
                    ? __('Pinterest is connected. Select a board before publishing.', 'calliope-social-publisher')
                    : __('Pinterest credentials are saved but have not been verified for this board.', 'calliope-social-publisher'),
                'action' => $board_id === '' ? __('Select a board', 'calliope-social-publisher') : __('Save and test', 'calliope-social-publisher'),
                'expires_at' => $access_expires_at, 'connection_method' => $method,
            ], 'E4');
        }

        $identity_known = $marker['identity_fingerprint'] !== '' && $marker['validated_at'] > 0;
        $failure_kind = $marker['failure_kind'];
        if ($marker['health'] === 'invalid') {
            $status_map = [
                'auth' => 'token_invalid', 'application' => 'configuration_error', 'environment' => 'environment_mismatch',
                'identity' => 'identity_missing', 'target' => 'target_missing', 'permission' => 'permission_missing',
                'access_tier' => 'publish_access_denied',
            ];
            $status = $status_map[$failure_kind] ?? 'connected_unverified';
            $messages = [
                'token_invalid' => __('The Pinterest access token is invalid or was revoked.', 'calliope-social-publisher'),
                'configuration_error' => __('The Pinterest application is incompatible with the saved connection.', 'calliope-social-publisher'),
                'environment_mismatch' => __('Pinterest was connected for a different environment. Reconnect Pinterest.', 'calliope-social-publisher'),
                'identity_missing' => __('Pinterest could not confirm the connected account identity.', 'calliope-social-publisher'),
                'target_missing' => __('The selected Pinterest board is unavailable or no longer matches the validated board.', 'calliope-social-publisher'),
                'permission_missing' => __('Pinterest needs to be reconnected to grant the complete publishing permissions.', 'calliope-social-publisher'),
                'publish_access_denied' => __('Pinterest accepted the connection but denied Pin publishing for this application environment.', 'calliope-social-publisher'),
                'connected_unverified' => __('Pinterest must be verified again before publishing.', 'calliope-social-publisher'),
            ];
            $identity_for_state = $identity_known && !in_array($status, ['identity_missing', 'configuration_error', 'environment_mismatch', 'token_invalid'], true);
            $target_for_state = $identity_for_state && $board_id !== '';
            return $this->create_state([
                'status' => $status, 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => !in_array($status, ['token_invalid', 'configuration_error'], true),
                'is_identity_known' => $identity_for_state, 'has_target' => $target_for_state,
                'has_permission' => false, 'evidence_level' => $target_for_state ? 'E4' : ($identity_for_state ? 'E3' : 'E2'),
                'message' => $messages[$status],
                'action' => in_array($status, ['target_missing'], true) ? __('Select a board', 'calliope-social-publisher') : __('Reconnect Pinterest', 'calliope-social-publisher'),
                'validated_at' => $marker['validated_at'], 'expires_at' => $access_expires_at,
                'connection_method' => $method,
            ], 'E5');
        }

        if (!$identity_known) {
            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' => __('Pinterest account identity has not been verified.', 'calliope-social-publisher'),
                'action' => __('Save and test', 'calliope-social-publisher'),
                'validated_at' => $marker['validated_at'], 'expires_at' => $access_expires_at,
                'connection_method' => $method,
            ], 'E4');
        }

        if ($board_id === '') {
            return $this->create_state([
                'status' => 'target_missing', 'health' => $marker['health'] === 'degraded' ? 'degraded' : 'unknown',
                'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => true,
                'has_target' => false, 'has_permission' => false, 'evidence_level' => 'E3',
                'message' => __('Pinterest is connected. Select a board before publishing.', 'calliope-social-publisher'),
                'action' => __('Select a board', 'calliope-social-publisher'),
                'validated_at' => $marker['validated_at'], 'expires_at' => $access_expires_at,
                'connection_method' => $method,
            ], 'E4');
        }

        $target_known = $marker['resolved_board_id'] !== '' && $marker['target_identity_fingerprint'] !== '';
        if (!$target_known) {
            return $this->create_state([
                'status' => 'connected_unverified', 'health' => $marker['health'] === 'degraded' ? 'degraded' : 'unknown',
                'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => true,
                'has_target' => false, 'has_permission' => false, 'evidence_level' => 'E3',
                'message' => __('The selected Pinterest board has not been verified.', 'calliope-social-publisher'),
                'action' => __('Save and test', 'calliope-social-publisher'),
                'validated_at' => $marker['validated_at'], 'expires_at' => $access_expires_at,
                'connection_method' => $method,
            ], 'E4');
        }

        $permission_confirmed = $this->has_required_scopes($granted_scopes)
            && $marker['user_account_scope_state'] === 'granted'
            && $marker['boards_scope_state'] === 'granted'
            && $marker['pins_scope_state'] === 'granted'
            && hash_equals($this->get_scope_fingerprint(), $marker['scope_fingerprint']);
        if (!$permission_confirmed) {
            return $this->create_state([
                'status' => 'permission_missing', 'health' => 'invalid',
                'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => true,
                'has_target' => true, 'has_permission' => false, 'evidence_level' => 'E4',
                'message' => __('Pinterest needs to be reconnected to grant the complete publishing permissions.', 'calliope-social-publisher'),
                'action' => __('Reconnect Pinterest', 'calliope-social-publisher'),
                'validated_at' => $marker['validated_at'], 'expires_at' => $access_expires_at,
                'connection_method' => $method,
            ], 'E5');
        }

        $health = $marker['health'];
        if ($refresh_expires_at > 0 && $refresh_expires_at <= time()) $health = 'degraded';
        $message = $health === 'healthy'
            ? __('Pinterest is ready to publish.', 'calliope-social-publisher')
            : __('Pinterest is connected, but the latest verification needs attention.', 'calliope-social-publisher');
        return $this->create_state([
            'status' => 'ready_to_publish', 'health' => $health,
            'is_configured' => true, 'is_authenticated' => true, 'is_identity_known' => true,
            'has_target' => true, 'has_permission' => true, 'evidence_level' => 'E5',
            'message' => $message,
            'action' => $health === 'degraded' ? __('Reconnect Pinterest', 'calliope-social-publisher') : '',
            'validated_at' => $marker['validated_at'], 'expires_at' => $access_expires_at,
            'connection_method' => $method,
        ], 'E5');
    }

    /** @return string[] */
    public function get_required_scopes() {
        return self::REQUIRED_SCOPES;
    }

    /** @return string */
    public function get_scope_fingerprint() {
        return hash('sha256', implode(' ', self::REQUIRED_SCOPES));
    }

    /** @return bool */
    public function has_required_scopes($scopes) {
        $scopes = $this->normalize_scopes($scopes);
        foreach (self::REQUIRED_SCOPES as $required_scope) {
            if (!in_array($required_scope, $scopes, true)) return false;
        }
        return true;
    }

    /** @return string */
    public function get_api_base($environment) {
        $environment = $this->normalize_environment($environment);
        return $environment !== '' && isset(self::API_BASES[$environment]) ? self::API_BASES[$environment] : '';
    }

    /** @return string */
    public function get_token_endpoint($environment) {
        $environment = $this->normalize_environment($environment);
        return $environment !== '' && isset(self::TOKEN_ENDPOINTS[$environment]) ? self::TOKEN_ENDPOINTS[$environment] : '';
    }

    /** @return string */
    public function get_token_environment() {
        return $this->normalize_environment(get_option(self::TOKEN_ENVIRONMENT_OPTION, ''));
    }

    /** @return string */
    public function get_board_option_name($environment) {
        $environment = $this->normalize_environment($environment);
        return $environment === '' ? '' : 'calliope_sp_pinterest_board_id_' . $environment;
    }

    /** @return string */
    public function get_boards_option_name($environment) {
        $environment = $this->normalize_environment($environment);
        return $environment === '' ? '' : 'calliope_sp_pinterest_boards_' . $environment;
    }

    /** @return string */
    public function get_board_id_for_environment($environment) {
        $environment = $this->normalize_environment($environment);
        if ($environment === '') return '';
        $name = $this->get_board_option_name($environment);
        $board = $name !== '' ? $this->normalize_board_id(get_option($name, '')) : '';
        if ($board !== '') return $board;
        $selected = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        if ($selected === $environment) return $this->normalize_board_id(get_option('calliope_sp_pinterest_board_id', ''));
        return '';
    }

    /** @return array */
    public function get_boards_for_environment($environment) {
        $environment = $this->normalize_environment($environment);
        if ($environment === '') return [];
        $name = $this->get_boards_option_name($environment);
        $boards = $name !== '' ? get_option($name, []) : [];
        if (!is_array($boards) || !$boards) {
            $selected = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
            if ($selected === $environment) $boards = get_option('calliope_sp_pinterest_boards_last', []);
        }
        return is_array($boards) ? $boards : [];
    }

    /** @return void */
    public function save_boards_for_environment($environment, array $boards) {
        $environment = $this->normalize_environment($environment);
        $name = $this->get_boards_option_name($environment);
        if ($name === '') return;
        update_option($name, $boards, false);
        if ($this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production')) === $environment) {
            update_option('calliope_sp_pinterest_boards_last', $boards, false);
        }
    }

    /** @return void */
    public function save_board_for_environment($environment, $board_id) {
        $environment = $this->normalize_environment($environment);
        $board_id = $this->normalize_board_id($board_id);
        $name = $this->get_board_option_name($environment);
        if ($name === '') return;
        if ($board_id === '') delete_option($name); else update_option($name, $board_id, false);
        if ($this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production')) === $environment) {
            if ($board_id === '') delete_option('calliope_sp_pinterest_board_id'); else update_option('calliope_sp_pinterest_board_id', $board_id, false);
        }
    }

    /** @return void */
    public function sync_legacy_environment_options($environment) {
        $environment = $this->normalize_environment($environment);
        if ($environment === '') return;
        $board = $this->get_board_id_for_environment($environment);
        $boards = $this->get_boards_for_environment($environment);
        if ($board === '') delete_option('calliope_sp_pinterest_board_id'); else update_option('calliope_sp_pinterest_board_id', $board, false);
        if (!$boards) delete_option('calliope_sp_pinterest_boards_last'); else update_option('calliope_sp_pinterest_boards_last', $boards, false);
    }

    /** @return string */
    public function normalize_environment($value) {
        if (!is_scalar($value)) return '';
        $value = strtolower(trim((string) $value));
        return in_array($value, ['production', 'sandbox'], true) ? $value : '';
    }

    /** @return string */
    public function normalize_board_id($value) {
        if (!is_scalar($value)) return '';
        $value = (string) $value;
        if (preg_match('/[\x00-\x1F\x7F]/', $value)) return '';
        $value = trim($value);
        if ($value === '') return '';
        if (!preg_match('/^\d{1,' . self::MAX_BOARD_ID_LENGTH . '}$/D', $value)) return '';
        return $value;
    }

    /** @return string */
    public function get_application_fingerprint($environment, $app_id, $app_secret) {
        return $this->application_fingerprint($this->normalize_environment($environment), (string) $app_id, (string) $app_secret);
    }

    /**
     * Normalize a Pinterest opaque secret without text-field sanitization.
     *
     * @return string|WP_Error
     */
    public function normalize_opaque_token($value, $error_code = 'pinterest_token_invalid') {
        if (!is_scalar($value)) {
            return $this->safe_error($error_code, __('Pinterest returned an invalid token.', 'calliope-social-publisher'), 'token', 0, false, 'auth');
        }
        $value = (string) $value;
        if ($value === '' || strlen($value) > 16384 || preg_match('/[\x00-\x1F\x7F]/', $value)) {
            return $this->safe_error($error_code, __('Pinterest returned an invalid token.', 'calliope-social-publisher'), 'token', 0, false, 'auth');
        }
        $value = preg_replace('/^\h+|\h+$/', '', $value);
        if (!is_string($value) || $value === '') {
            return $this->safe_error($error_code, __('Pinterest returned an invalid token.', 'calliope-social-publisher'), 'token', 0, false, 'auth');
        }
        return $value;
    }


    public function normalize_scopes($value) {
        if (is_array($value)) {
            $parts = $value;
        } elseif (is_scalar($value)) {
            $parts = preg_split('/[\s,]+/', strtolower(trim((string) $value)), -1, PREG_SPLIT_NO_EMPTY);
        } else {
            $parts = [];
        }
        $allowed = array_fill_keys(self::REQUIRED_SCOPES, true);
        $safe = [];
        foreach ((array) $parts as $scope) {
            if (!is_scalar($scope)) continue;
            $scope = strtolower(trim((string) $scope));
            if (isset($allowed[$scope])) $safe[$scope] = true;
        }
        $ordered = [];
        foreach (self::REQUIRED_SCOPES as $scope) {
            if (isset($safe[$scope])) $ordered[] = $scope;
        }
        return $ordered;
    }

    /** @return array */
    public function get_scope_states($auth_method, $scopes) {
        $auth_method = $this->normalize_auth_method($auth_method);
        $scopes = $this->normalize_scopes($scopes);
        if ($auth_method !== 'oauth2') {
            return ['user_account' => 'unknown', 'boards' => 'unknown', 'pins' => 'unknown'];
        }
        return [
            'user_account' => in_array('user_accounts:read', $scopes, true) ? 'granted' : 'denied',
            'boards' => in_array('boards:read', $scopes, true) && in_array('boards:write', $scopes, true) ? 'granted' : 'denied',
            'pins' => in_array('pins:read', $scopes, true) && in_array('pins:write', $scopes, true) ? 'granted' : 'denied',
        ];
    }

    /** @return true|WP_Error */
    public function test_connection($auth_method = 'manual', $granted_scopes = null) {
        $lifecycle = $this->maybe_refresh_access_token();
        if (is_wp_error($lifecycle)) return $lifecycle;
        return $this->validate_connection_remote($auth_method, $granted_scopes);
    }

    /**
     * Renew a Pinterest OAuth2 session using its continuous refresh token.
     * Manual access tokens intentionally bypass this lifecycle.
     *
     * @return true|WP_Error
     */
    public function maybe_refresh_access_token($threshold_seconds = null, $force = false) {
        $selected_environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        $token_environment = $this->get_token_environment();
        $app_id = trim((string) get_option('calliope_sp_pinterest_app_id', ''));
        $app_secret = trim((string) get_option('calliope_sp_pinterest_app_secret', ''));
        $token = $this->normalize_opaque_token(get_option('calliope_sp_pinterest_token', ''));
        $refresh_token = $this->normalize_opaque_token(get_option('calliope_sp_pinterest_refresh_token', ''), 'pinterest_refresh_token_invalid');
        $access_expires_at = $this->normalize_timestamp_option(get_option('calliope_sp_pinterest_access_token_expires_at', 0));
        $refresh_expires_at = $this->normalize_timestamp_option(get_option('calliope_sp_pinterest_refresh_token_expires_at', 0));
        $auth_method = $this->normalize_auth_method(get_option('calliope_sp_pinterest_auth_method', ''));
        $granted_scopes = $this->normalize_scopes(get_option('calliope_sp_pinterest_granted_scopes', []));

        if ($auth_method !== 'oauth2') return true;
        if (!$this->has_required_scopes($granted_scopes)) {
            return $this->safe_error('pinterest_scope_missing', __('Pinterest needs to be reconnected to grant the complete publishing permissions.', 'calliope-social-publisher'), 'refresh', 0, false, 'permission');
        }
        if ($selected_environment === '' || $token_environment === '' || !hash_equals($selected_environment, $token_environment)) {
            return $this->safe_error('pinterest_environment_mismatch', __('Pinterest was connected for a different environment. Reconnect Pinterest.', 'calliope-social-publisher'), 'refresh', 0, false, 'environment');
        }
        if (is_wp_error($token) || is_wp_error($refresh_token) || $app_id === '' || $app_secret === '' || $access_expires_at <= 0 || $refresh_expires_at <= 0) {
            return $this->safe_error('pinterest_refresh_required', __('Reconnect Pinterest to restore the complete OAuth session.', 'calliope-social-publisher'), 'refresh', 0, false, 'auth');
        }

        $threshold_seconds = $threshold_seconds === null
            ? max(60, min(3600, (int) apply_filters('calliope_sp_pinterest_refresh_margin', 600)))
            : max(0, (int) $threshold_seconds);
        $now = time();
        if ($refresh_expires_at <= $now) {
            $this->clear_rejected_oauth_tokens();
            return $this->safe_error('pinterest_refresh_failed', __('The Pinterest refresh token expired. Reconnect Pinterest.', 'calliope-social-publisher'), 'refresh', 0, false, 'auth');
        }
        if (!$force && $access_expires_at > ($now + $threshold_seconds)) return true;
        if (self::$refresh_attempted) {
            return $this->safe_error('pinterest_refresh_already_attempted', __('Pinterest already used the single token renewal allowed for this operation.', 'calliope-social-publisher'), 'refresh', 0, false, 'local');
        }
        self::$refresh_attempted = true;

        $endpoint = $this->get_token_endpoint($token_environment);
        if ($endpoint === '') {
            return $this->safe_error('pinterest_environment_mismatch', __('Pinterest token environment is invalid.', 'calliope-social-publisher'), 'refresh', 0, false, 'environment');
        }
        $lock = class_exists('Calliope_SP_Master') ? Calliope_SP_Master::acquire_lifecycle_refresh_lock('pinterest') : true;
        if (is_wp_error($lock)) return $lock;
        try {
            $response = wp_safe_remote_post($endpoint, [
                'timeout' => 30, 'redirection' => 0, 'reject_unsafe_urls' => true, 'sslverify' => true,
                'headers' => [
                    'Authorization' => 'Basic ' . base64_encode($app_id . ':' . $app_secret),
                    'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                'body' => ['grant_type' => 'refresh_token', 'refresh_token' => $refresh_token],
            ]);
            if (is_wp_error($response)) {
                return $this->safe_error('pinterest_refresh_failed', __('Pinterest could not renew the token right now.', 'calliope-social-publisher'), 'refresh', 0, true, 'temporary');
            }
            $http = (int) wp_remote_retrieve_response_code($response);
            $json = json_decode((string) wp_remote_retrieve_body($response), true);
            if ($http === 429 || $http >= 500) {
                return $this->safe_error('pinterest_refresh_failed', __('Pinterest could not renew the token right now.', 'calliope-social-publisher'), 'refresh', $http, true, 'temporary');
            }
            $remote_error = is_array($json) && isset($json['error']) && is_scalar($json['error']) ? strtolower(trim((string) $json['error'])) : '';
            if ($http < 200 || $http >= 300 || !is_array($json)) {
                if ($remote_error === 'invalid_grant' || in_array($http, [400, 401], true)) {
                    $this->clear_rejected_oauth_tokens();
                    return $this->safe_error('pinterest_refresh_failed', __('Pinterest rejected the saved refresh token. Reconnect Pinterest.', 'calliope-social-publisher'), 'refresh', $http, false, 'auth');
                }
                if ($http === 403) {
                    return $this->safe_error('pinterest_scope_missing', __('Pinterest denied the permissions required to renew this connection.', 'calliope-social-publisher'), 'refresh', $http, false, 'permission');
                }
                return $this->safe_error('pinterest_refresh_failed', __('Pinterest could not renew the token right now.', 'calliope-social-publisher'), 'refresh', $http, true, 'temporary');
            }

            $new_token = $this->normalize_opaque_token($json['access_token'] ?? '', 'pinterest_refresh_failed');
            $new_refresh_source = array_key_exists('refresh_token', $json)
                ? $json['refresh_token']
                : (array_key_exists('continuous_refresh_token', $json) ? $json['continuous_refresh_token'] : $refresh_token);
            $new_refresh = $this->normalize_opaque_token($new_refresh_source, 'pinterest_refresh_failed');
            $token_type = isset($json['token_type']) && is_scalar($json['token_type']) ? strtolower(trim((string) $json['token_type'])) : '';
            $expires_in = isset($json['expires_in']) && is_numeric($json['expires_in']) ? (int) $json['expires_in'] : 0;
            $scope_was_returned = array_key_exists('scope', $json) || array_key_exists('scopes', $json);
            $new_scopes = $scope_was_returned
                ? $this->normalize_scopes($json['scope'] ?? $json['scopes'])
                : $granted_scopes;
            $refresh_in = isset($json['refresh_token_expires_in']) && is_numeric($json['refresh_token_expires_in']) ? (int) $json['refresh_token_expires_in'] : 0;
            $new_refresh_expires = isset($json['refresh_token_expires_at']) ? $this->normalize_timestamp_option($json['refresh_token_expires_at']) : 0;
            if ($new_refresh_expires <= 0 && $refresh_in > 0) $new_refresh_expires = $now + $refresh_in;
            if ($new_refresh_expires <= 0) $new_refresh_expires = $refresh_expires_at;
            if (is_wp_error($new_token) || is_wp_error($new_refresh) || $token_type !== 'bearer' || $expires_in <= 0 || $new_refresh_expires <= $now) {
                return $this->safe_error('pinterest_refresh_failed', __('Pinterest returned an incomplete token renewal response.', 'calliope-social-publisher'), 'refresh', $http, false, 'auth');
            }
            if (!$this->has_required_scopes($new_scopes)) {
                $this->mark_permission_missing();
                return $this->safe_error('pinterest_scope_missing', __('Pinterest reduced the granted permissions. Reconnect Pinterest.', 'calliope-social-publisher'), 'refresh', $http, false, 'permission');
            }
            $token_data = [
                'access_token' => $new_token, 'refresh_token' => $new_refresh,
                'access_token_expires_at' => $now + $expires_in,
                'refresh_token_expires_at' => $new_refresh_expires,
                'scopes' => $new_scopes, 'environment' => $token_environment,
            ];
            $candidate = $this->validate_oauth_candidate($token_data);
            if (is_wp_error($candidate)) return $candidate;
            return $this->commit_oauth_candidate($token_data, $candidate);
        } finally {
            if (class_exists('Calliope_SP_Master') && is_string($lock)) Calliope_SP_Master::release_lifecycle_refresh_lock('pinterest', $lock);
        }
    }

    /**
     * Validate a new OAuth token set without replacing the current connection.
     *
     * @return array|WP_Error
     */
    public function validate_oauth_candidate(array $token_data) {
        $app_id = trim((string) get_option('calliope_sp_pinterest_app_id', ''));
        $app_secret = trim((string) get_option('calliope_sp_pinterest_app_secret', ''));
        $environment = $this->normalize_environment($token_data['environment'] ?? '');
        $selected_environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        $token = $this->normalize_opaque_token($token_data['access_token'] ?? '', 'pinterest_token_validation_failed');
        $refresh = $this->normalize_opaque_token($token_data['refresh_token'] ?? '', 'pinterest_refresh_token_missing');
        $scopes = $this->normalize_scopes($token_data['scopes'] ?? []);
        if ($app_id === '' || $app_secret === '' || $environment === '' || !hash_equals($selected_environment, $environment) || is_wp_error($token) || is_wp_error($refresh)) {
            return $this->safe_error('pinterest_token_validation_failed', __('Pinterest returned an incomplete authorization or the environment changed during OAuth.', 'calliope-social-publisher'), 'oauth_validation', 0, false, 'environment');
        }
        if (!$this->has_required_scopes($scopes)) {
            return $this->safe_error('pinterest_scope_missing', __('Pinterest was connected, but the authorization does not include all required permissions. Reconnect Pinterest.', 'calliope-social-publisher'), 'oauth_validation', 0, false, 'permission');
        }

        $old_environment = $this->environment;
        $old_api_base = $this->api_base;
        $this->environment = $environment;
        $this->api_base = $this->get_api_base($environment);
        try {
            $account_response = $this->request('GET', '/user_account', $token, null, 'identity');
            if (is_wp_error($account_response)) return $account_response;
            $identity = $this->validate_user_account_response($account_response);
            if (is_wp_error($identity)) return $identity;
            $boards = $this->list_boards($token);
            if (is_wp_error($boards)) return $boards;

            $selected_board = $this->get_board_id_for_environment($environment);
            $resolved_board = '';
            foreach ($boards as $board) {
                if (!is_array($board)) continue;
                $candidate_id = $this->normalize_board_id($board['id'] ?? '');
                if ($selected_board !== '' && $candidate_id !== '' && hash_equals($selected_board, $candidate_id)) {
                    $board_response = $this->request('GET', '/boards/' . rawurlencode($candidate_id), $token, null, 'target');
                    if (is_wp_error($board_response)) return $board_response;
                    $resolved_board = $this->validate_board_response($board_response, $candidate_id);
                    if (is_wp_error($resolved_board)) return $resolved_board;
                    break;
                }
            }
        } finally {
            $this->environment = $old_environment;
            $this->api_base = $old_api_base;
        }

        $application_fingerprint = $this->application_fingerprint($environment, $app_id, $app_secret);
        $credentials_fingerprint = $this->credentials_fingerprint($environment, $app_id, $app_secret, $token);
        $identity_fingerprint = $this->identity_fingerprint($environment, $identity);
        $target_fingerprint = $resolved_board !== '' ? $this->target_fingerprint($environment, $resolved_board) : '';
        return [
            'boards' => $boards, 'identity' => $identity, 'environment' => $environment,
            'resolved_board_id' => $resolved_board,
            'marker' => [
                'health' => 'healthy', 'failure_kind' => '', 'validated_at' => time(),
                'environment' => $environment, 'scope_fingerprint' => $this->get_scope_fingerprint(),
                'application_fingerprint' => $application_fingerprint,
                'credentials_fingerprint' => $credentials_fingerprint,
                'identity_fingerprint' => $identity_fingerprint,
                'target_fingerprint' => $target_fingerprint,
                'target_identity_fingerprint' => $resolved_board !== '' ? $this->target_identity_fingerprint($environment, $resolved_board) : '',
                'resolved_board_id' => $resolved_board, 'auth_method' => 'oauth2',
                'user_account_scope_state' => 'granted', 'boards_scope_state' => 'granted', 'pins_scope_state' => 'granted',
                'access_token_expires_at' => (int) ($token_data['access_token_expires_at'] ?? 0),
                'refresh_token_expires_at' => (int) ($token_data['refresh_token_expires_at'] ?? 0),
            ],
        ];
    }

    /** @return true|WP_Error */
    public function commit_oauth_candidate(array $token_data, array $candidate) {
        $environment = $this->normalize_environment($token_data['environment'] ?? ($candidate['environment'] ?? ''));
        $selected_environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        $token = $this->normalize_opaque_token($token_data['access_token'] ?? '', 'pinterest_token_validation_failed');
        $refresh = $this->normalize_opaque_token($token_data['refresh_token'] ?? '', 'pinterest_refresh_token_missing');
        $scopes = $this->normalize_scopes($token_data['scopes'] ?? []);
        $marker = isset($candidate['marker']) && is_array($candidate['marker']) ? $candidate['marker'] : [];
        if ($environment === '' || !hash_equals($selected_environment, $environment) || is_wp_error($token) || is_wp_error($refresh) || !$this->has_required_scopes($scopes) || !$marker) {
            return $this->safe_error('pinterest_token_validation_failed', __('Pinterest authorization could not be saved safely.', 'calliope-social-publisher'), 'oauth_commit', 0, false, 'auth');
        }
        $snapshot = $this->capture_lifecycle_snapshot();
        try {
            update_option(self::TOKEN_ENVIRONMENT_OPTION, $environment, false);
            update_option('calliope_sp_pinterest_token', $token, false);
            update_option('calliope_sp_pinterest_refresh_token', $refresh, false);
            update_option('calliope_sp_pinterest_access_token_expires_at', (int) ($token_data['access_token_expires_at'] ?? 0), false);
            update_option('calliope_sp_pinterest_refresh_token_expires_at', (int) ($token_data['refresh_token_expires_at'] ?? 0), false);
            update_option('calliope_sp_pinterest_granted_scopes', $scopes, false);
            update_option('calliope_sp_pinterest_auth_method', 'oauth2', false);
            $boards = isset($candidate['boards']) && is_array($candidate['boards']) ? $candidate['boards'] : [];
            $this->save_boards_for_environment($environment, $boards);
            $resolved = $this->normalize_board_id($candidate['resolved_board_id'] ?? '');
            $this->save_board_for_environment($environment, $resolved);
            $this->clear_publish_access_denial();
            $this->save_validation_marker($marker);
            $status = $resolved !== '' ? 'ready_to_publish' : 'target_missing';
            update_option('calliope_sp_pinterest_connection_status', $status, false);
            $this->environment = $environment;
            $this->api_base = $this->get_api_base($environment);
            $state = $this->get_connection_state();
            if (is_wp_error($state) || !($state instanceof Calliope_SP_Connection_State) || $state->get_status() !== $status) {
                throw new RuntimeException('Pinterest OAuth state verification failed.');
            }
            return true;
        } catch (Throwable $e) {
            $this->restore_lifecycle_snapshot($snapshot);
            return $this->safe_error('pinterest_token_validation_failed', __('Pinterest authorization could not be saved safely.', 'calliope-social-publisher'), 'oauth_commit', 0, false, 'auth');
        }
    }

    /** @return true|WP_Error */
    public function validate_connection_remote($auth_method = 'manual', $granted_scopes = null) {
        $environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        $app_id = trim((string) get_option('calliope_sp_pinterest_app_id', ''));
        $app_secret = trim((string) get_option('calliope_sp_pinterest_app_secret', ''));
        $raw_token = get_option('calliope_sp_pinterest_token', '');
        $raw_refresh_token = get_option('calliope_sp_pinterest_refresh_token', '');
        $token = ($raw_token === '' || $raw_token === null) ? '' : $this->normalize_opaque_token($raw_token);
        if (is_wp_error($token)) {
            $this->sync_legacy_status('token_invalid');
            return $token;
        }
        $refresh_token = ($raw_refresh_token === '' || $raw_refresh_token === null) ? '' : $this->normalize_opaque_token($raw_refresh_token, 'pinterest_refresh_token_invalid');
        if (is_wp_error($refresh_token)) {
            $this->sync_legacy_status('refresh_required');
            return $refresh_token;
        }
        $token_environment = $this->get_token_environment();
        $board_id = $this->get_board_id_for_environment($environment);
        $requested_auth_method = $this->normalize_auth_method($auth_method);
        if ($environment === '' || $app_id === '' || $app_secret === '' || $token === '') {
            $this->sync_legacy_status($app_id !== '' && $app_secret !== '' ? 'credentials_saved' : 'configuration_error');
            return $this->safe_error('pinterest_configuration', __('Complete the Pinterest application, environment, and token configuration.', 'calliope-social-publisher'), 'configuration', 0, false, 'local');
        }
        if ($requested_auth_method === 'oauth2' && !$this->has_required_scopes($granted_scopes === null ? get_option('calliope_sp_pinterest_granted_scopes', []) : $granted_scopes)) {
            $this->sync_legacy_status('permission_missing');
            return $this->safe_error('pinterest_scope_missing', __('Pinterest needs to be reconnected to grant the complete publishing permissions.', 'calliope-social-publisher'), 'configuration', 0, false, 'permission');
        }
        if ($token_environment === '' || !hash_equals($environment, $token_environment)) {
            $this->sync_legacy_status('environment_mismatch');
            return $this->safe_error('pinterest_environment_mismatch', __('Pinterest was connected for a different environment. Reconnect Pinterest.', 'calliope-social-publisher'), 'configuration', 0, false, 'environment');
        }
        $access_expires_at = $this->normalize_timestamp_option(get_option('calliope_sp_pinterest_access_token_expires_at', 0));
        $refresh_expires_at = $this->normalize_timestamp_option(get_option('calliope_sp_pinterest_refresh_token_expires_at', 0));

        $application_fingerprint = $this->application_fingerprint($environment, $app_id, $app_secret);
        $credentials_fingerprint = $this->credentials_fingerprint($environment, $app_id, $app_secret, $token);
        $target_fingerprint = $board_id !== '' ? $this->target_fingerprint($environment, $board_id) : '';
        $auth_evidence_marker = $this->get_validation_marker_for_credentials($application_fingerprint, $credentials_fingerprint, $environment);
        $previous_marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint);
        $auth_method = $this->get_effective_auth_method($previous_marker, $auth_evidence_marker);

        if ($access_expires_at > 0 && $access_expires_at <= time()) {
            $this->sync_legacy_status('token_expired');
            return $this->safe_error('pinterest_token_expired', __('The Pinterest access token has expired.', 'calliope-social-publisher'), 'configuration', 0, false, 'auth');
        }
        if ($auth_method === 'oauth2' && ($access_expires_at <= 0 || $refresh_token === '' || $refresh_expires_at <= 0)) {
            $this->sync_legacy_status('refresh_required');
            return $this->safe_error('pinterest_refresh_required', __('Pinterest must be verified again before publishing.', 'calliope-social-publisher'), 'configuration', 0, false, 'local');
        }

        $scope_source = $granted_scopes;
        if ($auth_method === 'oauth2' && ($requested_auth_method !== 'oauth2' || $scope_source === null)) {
            $scope_source = get_option('calliope_sp_pinterest_granted_scopes', []);
        }
        if ($auth_method !== 'oauth2') {
            $scope_source = [];
        }
        $scope_states = $this->get_scope_states($auth_method, $scope_source);
        if ($auth_method === 'oauth2' && in_array('denied', $scope_states, true)) {
            $error = $this->safe_error('pinterest_scope_missing', __('The Pinterest OAuth token does not include all required publishing permissions.', 'calliope-social-publisher'), 'permission', 0, false, 'permission');
            $this->save_invalid_marker('permission', $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, [
                'auth_method' => $auth_method, 'scope_states' => $scope_states,
                'access_token_expires_at' => $access_expires_at, 'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            $this->sync_legacy_status('permission_missing');
            return $error;
        }

        $account_response = $this->request('GET', '/user_account', $token, null, 'identity');
        if (is_wp_error($account_response)) {
            $this->apply_remote_failure_to_validation($account_response, $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, $previous_marker, [
                'auth_method' => $auth_method, 'scope_states' => $scope_states,
                'access_token_expires_at' => $access_expires_at, 'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            return $account_response;
        }
        $identity = $this->validate_user_account_response($account_response);
        if (is_wp_error($identity)) {
            $this->apply_remote_failure_to_validation($identity, $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, $previous_marker, [
                'auth_method' => $auth_method, 'scope_states' => $scope_states,
                'access_token_expires_at' => $access_expires_at, 'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            return $identity;
        }
        $identity_fingerprint = $this->identity_fingerprint($environment, $identity);
        if (is_array($previous_marker) && $previous_marker['identity_fingerprint'] !== '' && !hash_equals($previous_marker['identity_fingerprint'], $identity_fingerprint)) {
            $error = $this->safe_error('pinterest_identity_changed', __('Pinterest returned a different account identity.', 'calliope-social-publisher'), 'identity', 200, false, 'identity');
            $this->save_invalid_marker('identity', $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, [
                'identity_fingerprint' => $previous_marker['identity_fingerprint'], 'auth_method' => $auth_method,
                'scope_states' => $scope_states, 'access_token_expires_at' => $access_expires_at,
                'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            $this->sync_legacy_status('identity_missing');
            return $error;
        }

        if ($board_id === '') {
            $this->save_validation_marker([
                'health' => 'healthy', 'failure_kind' => '', 'validated_at' => time(), 'environment' => $environment, 'scope_fingerprint' => $this->get_scope_fingerprint(),
                'application_fingerprint' => $application_fingerprint, 'credentials_fingerprint' => $credentials_fingerprint,
                'identity_fingerprint' => $identity_fingerprint, 'target_fingerprint' => '',
                'target_identity_fingerprint' => '', 'resolved_board_id' => '', 'auth_method' => $auth_method,
                'user_account_scope_state' => 'granted', 'boards_scope_state' => $scope_states['boards'],
                'pins_scope_state' => $scope_states['pins'], 'access_token_expires_at' => $access_expires_at,
                'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            $this->sync_legacy_status('target_missing');
            return $this->safe_error('pinterest_board_required', __('Select a Pinterest board before testing publishing readiness.', 'calliope-social-publisher'), 'target', 0, false, 'target');
        }

        $board_response = $this->request('GET', '/boards/' . rawurlencode($board_id), $token, null, 'target');
        if (is_wp_error($board_response)) {
            $this->apply_remote_failure_to_validation($board_response, $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, $previous_marker, [
                'identity_fingerprint' => $identity_fingerprint, 'auth_method' => $auth_method,
                'scope_states' => array_merge($scope_states, ['user_account' => 'granted']),
                'access_token_expires_at' => $access_expires_at, 'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            return $board_response;
        }
        $resolved_board_id = $this->validate_board_response($board_response, $board_id);
        if (is_wp_error($resolved_board_id)) {
            $this->apply_remote_failure_to_validation($resolved_board_id, $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, $previous_marker, [
                'identity_fingerprint' => $identity_fingerprint, 'auth_method' => $auth_method,
                'scope_states' => array_merge($scope_states, ['user_account' => 'granted']),
                'access_token_expires_at' => $access_expires_at, 'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            return $resolved_board_id;
        }

        $scope_states['user_account'] = 'granted';
        $scope_states['boards'] = 'granted';
        if ($auth_method === 'oauth2' && $scope_states['pins'] === 'denied') {
            $error = $this->safe_error('pinterest_scope_missing', __('The Pinterest OAuth token does not include pins:write.', 'calliope-social-publisher'), 'permission', 0, false, 'permission');
            $this->save_invalid_marker('permission', $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, [
                'identity_fingerprint' => $identity_fingerprint,
                'target_identity_fingerprint' => $this->target_identity_fingerprint($environment, $resolved_board_id),
                'resolved_board_id' => $resolved_board_id, 'auth_method' => $auth_method, 'scope_states' => $scope_states,
                'access_token_expires_at' => $access_expires_at, 'refresh_token_expires_at' => $refresh_expires_at,
            ]);
            $this->sync_legacy_status('permission_missing');
            return $error;
        }

        $health = $auth_method === 'oauth2' && $scope_states['pins'] === 'granted' ? 'healthy' : 'degraded';
        $this->clear_publish_access_denial();
        $this->save_validation_marker([
            'health' => $health, 'failure_kind' => '', 'validated_at' => time(), 'environment' => $environment, 'scope_fingerprint' => $this->get_scope_fingerprint(),
            'application_fingerprint' => $application_fingerprint, 'credentials_fingerprint' => $credentials_fingerprint,
            'identity_fingerprint' => $identity_fingerprint, 'target_fingerprint' => $target_fingerprint,
            'target_identity_fingerprint' => $this->target_identity_fingerprint($environment, $resolved_board_id),
            'resolved_board_id' => $resolved_board_id, 'auth_method' => $auth_method,
            'user_account_scope_state' => $scope_states['user_account'], 'boards_scope_state' => $scope_states['boards'],
            'pins_scope_state' => $scope_states['pins'], 'access_token_expires_at' => $access_expires_at,
            'refresh_token_expires_at' => $refresh_expires_at,
        ]);
        $this->sync_legacy_status('ready_to_publish');
        return true;
    }

    /**
     * List boards explicitly for the administrator. Never selects a board or creates readiness.
     *
     * @param string $token Optional token override.
     * @return array|WP_Error
     */
    public function list_boards($token = '') {
        $token_source = $token !== '' ? $token : get_option('calliope_sp_pinterest_token', '');
        if ($token_source === '' || $token_source === null) {
            return $this->safe_error('pinterest_missing_token', __('Enter a Pinterest access token first.', 'calliope-social-publisher'), 'configuration', 0, false, 'local');
        }
        $token = $this->normalize_opaque_token($token_source);
        if (is_wp_error($token)) return $token;

        $boards = [];
        $bookmark = '';
        for ($page = 0; $page < self::MAX_LIST_PAGES && count($boards) < self::MAX_LIST_ITEMS; $page++) {
            $endpoint = '/boards?page_size=50';
            if ($bookmark !== '') $endpoint .= '&bookmark=' . rawurlencode($bookmark);
            $response = $this->request('GET', $endpoint, $token, null, 'boards');
            if (is_wp_error($response)) return $response;
            $json = $response['json'];
            if (!is_array($json) || !isset($json['items']) || !is_array($json['items'])) {
                return $this->safe_error('pinterest_boards_malformed', __('Pinterest returned an incomplete boards response.', 'calliope-social-publisher'), 'boards', $response['http_code'], true, 'temporary');
            }
            foreach ($json['items'] as $item) {
                if (!is_array($item)) continue;
                $id = $this->normalize_board_id($item['id'] ?? '');
                if ($id === '') continue;
                $name = isset($item['name']) && is_scalar($item['name']) ? $this->safe_text((string) $item['name'], 120) : '';
                $boards[$id] = ['id' => $id, 'name' => $name];
                if (count($boards) >= self::MAX_LIST_ITEMS) break;
            }
            $bookmark = isset($json['bookmark']) && is_scalar($json['bookmark']) ? trim((string) $json['bookmark']) : '';
            if ($bookmark === '') break;
        }
        return array_values($boards);
    }

    /** @return array|WP_Error */
    public function detect_board_id() {
        return $this->list_boards();
    }

    /** @return true|WP_Error */
    public function publish($data) {
        $lifecycle = $this->maybe_refresh_access_token();
        if (is_wp_error($lifecycle)) return $lifecycle;
        $state = $this->get_connection_state();
        if (is_wp_error($state) || !($state instanceof Calliope_SP_Connection_State) || !$state->is_ready()) {
            $status = !is_wp_error($state) && $state instanceof Calliope_SP_Connection_State ? $state->get_status() : '';
            $code = $status === 'publish_access_denied' ? 'pinterest_publish_access_denied'
                : ($status === 'environment_mismatch' ? 'pinterest_environment_mismatch'
                : ($status === 'permission_missing' ? 'pinterest_scope_missing' : 'pinterest_not_ready'));
            return new WP_Error($code, !is_wp_error($state) && $state instanceof Calliope_SP_Connection_State
                ? $state->get('message', __('Verify Pinterest and select a board before publishing.', 'calliope-social-publisher'))
                : __('Verify Pinterest and select a board before publishing.', 'calliope-social-publisher'), [
                'phase' => 'precheck', 'retryable' => false,
                'failure_kind' => $status === 'publish_access_denied' ? 'access_tier' : ($status === 'permission_missing' ? 'permission' : 'local'),
            ]);
        }

        $environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        $token_environment = $this->get_token_environment();
        if ($environment === '' || $token_environment === '' || !hash_equals($environment, $token_environment)) {
            return $this->safe_error('pinterest_environment_mismatch', __('Pinterest was connected for a different environment. Reconnect Pinterest.', 'calliope-social-publisher'), 'precheck', 0, false, 'environment');
        }
        $app_id = trim((string) get_option('calliope_sp_pinterest_app_id', ''));
        $app_secret = trim((string) get_option('calliope_sp_pinterest_app_secret', ''));
        $token = $this->normalize_opaque_token(get_option('calliope_sp_pinterest_token', ''));
        $board_id = $this->get_board_id_for_environment($environment);
        if (is_wp_error($token)) return $token;
        $application_fingerprint = $this->application_fingerprint($environment, $app_id, $app_secret);
        $credentials_fingerprint = $this->credentials_fingerprint($environment, $app_id, $app_secret, $token);
        $target_fingerprint = $this->target_fingerprint($environment, $board_id);
        $marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint);
        if (!is_array($marker) || $marker['resolved_board_id'] === '') {
            return new WP_Error('pinterest_board_required', __('Select and verify a Pinterest board before publishing.', 'calliope-social-publisher'), [
                'phase' => 'precheck', 'retryable' => false, 'failure_kind' => 'target',
            ]);
        }

        $media_mode = sanitize_key((string) ($data['media_mode'] ?? 'none'));
        if ($media_mode === 'multiple_images') {
            return $this->publish_limited_images((array) $data);
        }

        $title = $this->safe_text((string) ($data['title'] ?? ''), 100);
        $description = $this->safe_text(trim((string) ($data['content'] ?? '') . "\n\n" . (string) ($data['hashtags'] ?? '')), 500);
        $alt = !empty($data['alt_text']) ? $this->safe_text((string) $data['alt_text'], 500) : $this->safe_text($title, 500);
        $link = !empty($data['link']) ? esc_url_raw((string) $data['link']) : '';
        $image_url = '';
        if (!empty($data['image_url'])) $image_url = esc_url_raw((string) $data['image_url']);
        elseif (!empty($data['image_b64'])) {
            $image_result = $this->image_url_from_b64((string) $data['image_b64']);
            if (is_wp_error($image_result)) return $image_result;
            $image_url = $image_result;
        }
        if (!$this->is_safe_public_image_url($image_url)) {
            return new WP_Error('pinterest_media_url_invalid', __('Pinterest requires a valid public HTTPS image.', 'calliope-social-publisher'), [
                'phase' => 'image', 'retryable' => false, 'failure_kind' => 'local',
            ]);
        }

        $payload = [
            'title' => $title, 'description' => $description, 'board_id' => $marker['resolved_board_id'],
            'alt_text' => $alt, 'media_source' => ['source_type' => 'image_url', 'url' => $image_url],
        ];
        if ($link !== '') $payload['link'] = $link;
        $link_omitted_after_block = false;

        $response = $this->request('POST', '/pins', $token, $payload, 'publish');
        $first_auth_http = is_wp_error($response) ? $this->get_error_http_code($response) : 0;
        if (is_wp_error($response) && $first_auth_http === 401 && $this->normalize_auth_method(get_option('calliope_sp_pinterest_auth_method', '')) === 'oauth2') {
            $original_denial = $response;
            $refresh = $this->maybe_refresh_access_token(0, true);
            if (!is_wp_error($refresh)) {
                $token = $this->normalize_opaque_token(get_option('calliope_sp_pinterest_token', ''));
                if (!is_wp_error($token)) {
                    $credentials_fingerprint = $this->credentials_fingerprint($environment, $app_id, $app_secret, $token);
                    $marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint);
                    $response = $this->request('POST', '/pins', $token, $payload, 'publish');
                }
            } else {
                $refresh_data = $refresh->get_error_data();
                $refresh_data = is_array($refresh_data) ? $refresh_data : [];
                $tokens_were_cleared = trim((string) get_option('calliope_sp_pinterest_token', '')) === '';
                $response = (($refresh_data['failure_kind'] ?? '') === 'auth' && $tokens_were_cleared) ? $refresh : $original_denial;
            }
        }

        if (is_wp_error($response) && $link !== '' && $response->get_error_code() === 'pinterest_link_blocked') {
            $payload_without_link = $payload;
            unset($payload_without_link['link']);
            $response = $this->request('POST', '/pins', $token, $payload_without_link, 'publish_without_link');
            if (!is_wp_error($response)) {
                $link_omitted_after_block = true;
            }
        }

        if (is_wp_error($response)) {
            $http_code = $this->get_error_http_code($response);
            if (in_array($http_code, [401, 403], true)) {
                return $this->classify_final_publish_denial($response, $token, $environment, $board_id, $marker);
            }
            if ($response->get_error_code() === 'pinterest_refresh_failed') {
                $data_error = $response->get_error_data();
                $data_error = is_array($data_error) ? $data_error : [];
                if (($data_error['failure_kind'] ?? '') === 'auth') {
                    return $this->safe_error('pinterest_auth_invalid', __('Pinterest authorization is invalid or expired.', 'calliope-social-publisher'), 'publish', 401, false, 'auth');
                }
            }
            $this->apply_operation_failure($response, $marker);
            return $response;
        }

        $json = $response['json'];
        $pin_id = is_array($json) && isset($json['id']) && is_scalar($json['id']) ? trim((string) $json['id']) : '';
        if ($pin_id === '' || !preg_match('/^\d{1,40}$/D', $pin_id)) {
            $error = $this->safe_error('pinterest_publish_http', __('Pinterest returned an incomplete Pin response.', 'calliope-social-publisher'), 'publish', $response['http_code'], false, 'temporary');
            $this->apply_operation_failure($error, $marker);
            return $error;
        }
        if (isset($json['board_id'])) {
            $response_board = $this->normalize_board_id($json['board_id']);
            if ($response_board === '' || !hash_equals($marker['resolved_board_id'], $response_board)) {
                $error = $this->safe_error('pinterest_board_not_found', __('Pinterest returned a Pin for a different board.', 'calliope-social-publisher'), 'publish', $response['http_code'], false, 'target');
                $this->apply_operation_failure($error, $marker);
                return $error;
            }
        }
        $marker['health'] = 'healthy'; $marker['failure_kind'] = ''; $marker['validated_at'] = time();
        $marker['pins_scope_state'] = 'granted'; $marker['scope_fingerprint'] = $this->get_scope_fingerprint();
        $this->clear_publish_access_denial();
        $this->save_validation_marker($marker);
        $this->sync_legacy_status('ready_to_publish');
        if ($link_omitted_after_block) {
            return [
                'ok' => true,
                'warning' => true,
                'warning_code' => 'pinterest_link_omitted',
                'link_omitted' => true,
                'post_id' => sanitize_text_field($pin_id),
                'message' => __('Pinterest published the Pin without the destination link because Pinterest blocked that URL as possible spam.', 'calliope-social-publisher'),
            ];
        }
        return [
            'ok' => true,
            'post_id' => sanitize_text_field($pin_id),
            'media_type' => 'image',
        ];
    }

    /** @return array<string,mixed>|WP_Error */
    private function publish_limited_images(array $data) {
        $assets = $this->limited_image_assets_from_payload($data);
        if (is_wp_error($assets)) { return $assets; }

        $requested = isset($data['media']) && is_array($data['media']) ? count($data['media']) : count($assets);
        $published = 0;
        $link_omitted = false;
        $omit_link_for_remaining = false;
        $last_error = null;

        foreach ($assets as $asset) {
            $single = $data;
            $single['media_mode'] = 'single_image';
            $single['media'] = [];
            $single['image_b64'] = '';
            $single['image_url'] = (string) $asset['url'];
            $single['alt_text'] = (string) $asset['alt'];
            if ($omit_link_for_remaining) {
                $single['link'] = '';
            }

            $result = $this->publish($single);
            if (is_wp_error($result)) {
                $last_error = $result;
                break;
            }

            $published++;
            if (is_array($result) && (!empty($result['link_omitted']) || (string) ($result['warning_code'] ?? '') === 'pinterest_link_omitted')) {
                $link_omitted = true;
                $omit_link_for_remaining = true;
            }
        }

        $this->cleanup_limited_image_assets($assets);

        if (is_wp_error($last_error)) {
            if ($published === 0) { return $last_error; }
            $original_data = $last_error->get_error_data();
            $original_data = is_array($original_data) ? $original_data : [];
            return new WP_Error(
                'pinterest_pins_partially_published',
                sprintf(
                    /* translators: 1: published Pins, 2: attempted Pins. */
                    __('Pinterest created %1$d of %2$d allowed Pins before a later Pin failed. Review the board before retrying.', 'calliope-social-publisher'),
                    $published,
                    count($assets)
                ),
                array_merge($original_data, [
                    'phase' => 'publish',
                    'retryable' => false,
                    'failure_kind' => (string) ($original_data['failure_kind'] ?? 'temporary'),
                    'published_count' => $published,
                    'attempted_count' => count($assets),
                    'requested_count' => $requested,
                    'original_error_code' => $last_error->get_error_code(),
                    'original_error_message' => $last_error->get_error_message(),
                ])
            );
        }

        $omitted = max(0, $requested - $published);
        $response = [
            'ok' => true,
            'media_type' => 'multi_pin',
            'attachments' => $published,
            'posts' => $published,
            'requested_attachments' => $requested,
            'omitted_attachments' => $omitted,
        ];

        if ($omitted > 0) {
            $response['warning'] = true;
            $response['warning_code'] = 'pinterest_images_limited';
            $response['ui_status'] = 'warning';
            $response['short_message'] = sprintf(
                /* translators: 1: published Pins, 2: requested images. */
                __('published %1$d of %2$d selected images', 'calliope-social-publisher'),
                $published,
                $requested
            );
            $response['user_message'] = sprintf(
                /* translators: 1: published Pins, 2: requested images, 3: omitted images. */
                __('Pinterest published %1$d separate Pins from the first %1$d of %2$d selected images. The remaining %3$d images were omitted and no additional Pin was attempted.', 'calliope-social-publisher'),
                $published,
                $requested,
                $omitted
            );
        } else {
            $response['user_message'] = sprintf(
                /* translators: %d: published Pins. */
                _n('Pinterest published %d Pin.', 'Pinterest published %d Pins.', $published, 'calliope-social-publisher'),
                $published
            );
        }

        if ($link_omitted) {
            if ($omitted === 0) {
                $response['warning'] = true;
                $response['warning_code'] = 'pinterest_link_omitted';
                $response['ui_status'] = 'warning';
                $response['link_omitted'] = true;
                $response['short_message'] = __('published without destination link', 'calliope-social-publisher');
                $response['user_message'] = __('Pinterest published the Pins without the destination link because Pinterest blocked that URL as possible spam.', 'calliope-social-publisher');
            } else {
                $response['user_message'] .= ' ' . __('Pinterest also omitted the destination link after Pinterest blocked that URL.', 'calliope-social-publisher');
            }
        }

        return $response;
    }

    /** @return array<int,array{url:string,alt:string,temporary_path:string}>|WP_Error */
    private function limited_image_assets_from_payload(array $data) {
        if (!class_exists('Calliope_SPP_Media_Set') || !class_exists('Calliope_SPP_Media_Resolver')) {
            return new WP_Error('pinterest_multi_image_requires_pro', __('Pinterest multiple-image publishing requires Social Publisher Pro media support.', 'calliope-social-publisher'), ['phase' => 'image', 'retryable' => false, 'failure_kind' => '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 new WP_Error('pinterest_multi_image_invalid', __('Pinterest multiple-image publishing requires 2 to 10 images.', 'calliope-social-publisher'), ['phase' => 'image', 'retryable' => false, 'failure_kind' => 'local']);
        }

        // Pinterest organic Pins are single-image objects. For a multi-image selection,
        // intentionally create at most two separate Pins and never attempt a third.
        $normalized = array_slice($normalized, 0, 2);
        $resolved = Calliope_SPP_Media_Resolver::resolve($normalized, [
            'allowed_video_mimes' => [],
            'log_context' => ['network' => 'pinterest', 'mode' => 'multiple_images'],
        ]);
        if (is_wp_error($resolved)) {
            return new WP_Error('pinterest_multi_image_media_error', sprintf(
                /* translators: %s: media preparation error. */
                __('Pinterest could not prepare the selected images. %s', 'calliope-social-publisher'),
                $resolved->get_error_message()
            ), ['phase' => 'image', 'retryable' => false, 'failure_kind' => 'local']);
        }
        $assets = [];
        foreach ($resolved as $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')) {
                    $this->cleanup_limited_image_assets($assets);
                    return new WP_Error('pinterest_multi_image_read_failed', __('Pinterest could not prepare a public URL for one of the selected images.', 'calliope-social-publisher'), ['phase' => 'image', 'retryable' => false, 'failure_kind' => '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' => 'pinterest', 'filename_prefix' => 'calliope-social-publisher-pinterest', 'network' => 'pinterest', 'max_attempts' => 6,
                ]);
                if (is_wp_error($created)) {
                    $this->cleanup_limited_image_assets($assets);
                    return $created;
                }
                $url = esc_url_raw((string) ($created['url'] ?? ''));
                $temporary_path = (string) ($created['local_path'] ?? '');
            }
            $assets[] = [
                'url' => esc_url_raw((string) $url),
                'alt' => $this->safe_text((string) ($file['description'] ?? ($data['alt_text'] ?? '')), 500),
                'temporary_path' => $temporary_path,
            ];
        }
        return $assets;
    }

    /** @param array<int,array<string,mixed>> $assets */
    private function cleanup_limited_image_assets(array $assets) {
        foreach ($assets as $asset) {
            $path = (string) ($asset['temporary_path'] ?? '');
            if ($path !== '' && is_file($path)) { wp_delete_file($path); }
        }
    }

    /** @return void */
    public function invalidate_validation() {
        delete_option(self::VALIDATION_OPTION);
    }

    /** @return array|WP_Error */
    private function request($method, $endpoint, $token, $body = null, $phase = 'request') {
        $method = strtoupper((string) $method);
        $endpoint = (string) $endpoint;
        $token = $this->normalize_opaque_token($token);
        if (is_wp_error($token)) return $token;
        if (!in_array($method, ['GET', 'POST'], true) || !$this->is_allowlisted_endpoint($method, $endpoint)) {
            return $this->safe_error('pinterest_endpoint_rejected', __('Pinterest request was blocked because the endpoint is not allowed.', 'calliope-social-publisher'), $phase, 0, false, 'local');
        }
        $url = rtrim($this->api_base, '/') . '/' . ltrim($endpoint, '/');
        $args = [
            'method' => $method,
            'headers' => ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json', 'Content-Type' => 'application/json'],
            'timeout' => 30, 'redirection' => 0, 'reject_unsafe_urls' => true, 'sslverify' => true,
        ];
        if ($body !== null) $args['body'] = wp_json_encode($body);
        $response = wp_safe_remote_request($url, $args);
        if (is_wp_error($response)) {
            return $this->safe_error('pinterest_publish_http', __('Pinterest could not complete the request right now. Please try again.', 'calliope-social-publisher'), $phase, 0, true, 'temporary');
        }
        $http_code = (int) wp_remote_retrieve_response_code($response);
        $raw = (string) wp_remote_retrieve_body($response);
        $json = json_decode($raw, true);
        if ($http_code < 200 || $http_code >= 300) {
            return $this->build_http_error($phase, $http_code, is_array($json) ? $json : []);
        }
        if (!is_array($json)) {
            return $this->safe_error('pinterest_publish_http', __('Pinterest returned an unreadable response.', 'calliope-social-publisher'), $phase, $http_code, true, 'temporary');
        }
        return ['http_code' => $http_code, 'json' => $json];
    }

    /** @return bool */
    private function is_allowlisted_endpoint($method, $endpoint) {
        $parts = wp_parse_url($endpoint);
        if ($parts === false || isset($parts['scheme']) || isset($parts['host']) || isset($parts['fragment'])) return false;
        $path = (string) ($parts['path'] ?? '');
        $query = (string) ($parts['query'] ?? '');
        if ($method === 'GET' && $path === '/user_account' && $query === '') return true;
        if ($method === 'GET' && preg_match('#^/boards/\d{1,' . self::MAX_BOARD_ID_LENGTH . '}$#D', $path) && $query === '') return true;
        if ($method === 'GET' && $path === '/boards') {
            parse_str($query, $params);
            foreach (array_keys($params) as $key) if (!in_array($key, ['page_size', 'bookmark'], true)) return false;
            return isset($params['page_size']) && (string) $params['page_size'] === '50';
        }
        return $method === 'POST' && $path === '/pins' && $query === '';
    }

    /** @return string|WP_Error */
    private function validate_user_account_response($response) {
        if (!is_array($response) || !is_array($response['json'] ?? null)) {
            return $this->safe_error('pinterest_identity_malformed', __('Pinterest returned an incomplete account response.', 'calliope-social-publisher'), 'identity', (int) ($response['http_code'] ?? 0), true, 'temporary');
        }
        $username = $response['json']['username'] ?? '';
        if (!is_scalar($username)) {
            return $this->safe_error('pinterest_identity_malformed', __('Pinterest returned an incomplete account response.', 'calliope-social-publisher'), 'identity', (int) ($response['http_code'] ?? 0), true, 'temporary');
        }
        $username = strtolower(trim((string) $username));
        if ($username === '' || strlen($username) > 255 || preg_match('/[\x00-\x1F\x7F]/', $username)) {
            return $this->safe_error('pinterest_identity_malformed', __('Pinterest returned an incomplete account response.', 'calliope-social-publisher'), 'identity', (int) ($response['http_code'] ?? 0), true, 'temporary');
        }
        return $username;
    }

    /** @return string|WP_Error */
    private function validate_board_response($response, $expected_board_id) {
        if (!is_array($response) || !is_array($response['json'] ?? null)) {
            return $this->safe_error('pinterest_board_malformed', __('Pinterest returned an incomplete board response.', 'calliope-social-publisher'), 'target', (int) ($response['http_code'] ?? 0), true, 'temporary');
        }
        $resolved = $this->normalize_board_id($response['json']['id'] ?? '');
        if ($resolved === '') {
            return $this->safe_error('pinterest_board_malformed', __('Pinterest returned an incomplete board response.', 'calliope-social-publisher'), 'target', (int) ($response['http_code'] ?? 0), true, 'temporary');
        }
        if (!hash_equals((string) $expected_board_id, $resolved)) {
            return $this->safe_error('pinterest_board_mismatch', __('Pinterest returned a different board.', 'calliope-social-publisher'), 'target', (int) ($response['http_code'] ?? 0), false, 'target');
        }
        return $resolved;
    }

    /** @return WP_Error */
    private function build_http_error($phase, $http_code, $json) {
        $safe_message = strtolower($this->extract_api_message($json));
        $provider_code = $this->normalize_provider_code($json['code'] ?? '');
        $failure_kind = 'temporary';
        $retryable = in_array($http_code, [408, 425, 429], true) || $http_code >= 500;
        if ($http_code === 401 || $provider_code === '2' || strpos($safe_message, 'invalid access token') !== false || strpos($safe_message, 'authentication failed') !== false || strpos($safe_message, 'token revoked') !== false) {
            $failure_kind = 'auth'; $retryable = false;
        } elseif ($http_code === 403 && (strpos($safe_message, 'scope') !== false || strpos($safe_message, 'permission') !== false || strpos($safe_message, 'not authorized') !== false || strpos($safe_message, 'insufficient') !== false)) {
            $failure_kind = 'permission'; $retryable = false;
        } elseif ($phase === 'target' && $http_code === 404 && (strpos($safe_message, 'board') !== false || strpos($safe_message, 'not found') !== false)) {
            $failure_kind = 'target'; $retryable = false;
        } elseif ($phase === 'publish' && $http_code === 404 && strpos($safe_message, 'board') !== false) {
            $failure_kind = 'target'; $retryable = false;
        } elseif (in_array($phase, ['publish', 'publish_without_link'], true) && $http_code === 429 && (
            $provider_code === '9' ||
            strpos($safe_message, 'blocked this link') !== false ||
            strpos($safe_message, 'link because it may lead to spam') !== false ||
            strpos($safe_message, 'link may lead to spam') !== false
        )) {
            return $this->safe_error('pinterest_link_blocked', __('Pinterest rejected the destination link due to anti-spam protection.', 'calliope-social-publisher'), $phase, $http_code, false, 'local', $provider_code);
        } elseif (in_array($phase, ['publish', 'publish_without_link'], true) && in_array($http_code, [400, 422], true)) {
            return $this->safe_error('pinterest_editorial', __('Pinterest rejected the Pin content. Review the image, link, title, and description.', 'calliope-social-publisher'), $phase, $http_code, false, 'local', $provider_code);
        }
        $messages = [
            'auth' => __('Pinterest rejected the authorization for this request.', 'calliope-social-publisher'),
            'permission' => __('Pinterest denied the required permission for this operation.', 'calliope-social-publisher'),
            'target' => __('The selected Pinterest board is unavailable.', 'calliope-social-publisher'),
            'temporary' => __('Pinterest could not complete the request right now. Please try again.', 'calliope-social-publisher'),
        ];
        return $this->safe_error('pinterest_' . sanitize_key($phase) . '_http', $messages[$failure_kind], $phase, $http_code, $retryable, $failure_kind, $provider_code);
    }

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

    /** @return string */
    private function extract_api_message($json) {
        if (!is_array($json)) return '';
        foreach (['message', 'error_description', 'error'] as $key) {
            if (isset($json[$key]) && is_scalar($json[$key])) return $this->safe_text((string) $json[$key], 180);
        }
        return '';
    }

    /** @return string */
    private function normalize_provider_code($value) {
        if (!is_scalar($value)) return '';
        $value = trim((string) $value);
        if ($value === '' || strlen($value) > 40 || preg_match('/[^A-Za-z0-9_.:-]/', $value)) return '';
        return $value;
    }

    /** @return int */
    private function get_error_http_code($error) {
        if (!is_wp_error($error)) return 0;
        $data = $error->get_error_data();
        return is_array($data) ? max(0, (int) ($data['http_code'] ?? 0)) : 0;
    }

    /** @return WP_Error */
    private function classify_final_publish_denial($publish_error, $token, $environment, $board_id, $marker) {
        $error_data = is_wp_error($publish_error) ? $publish_error->get_error_data() : [];
        $error_data = is_array($error_data) ? $error_data : [];
        $http_code = max(0, (int) ($error_data['http_code'] ?? 0));
        $provider_code = $this->normalize_provider_code($error_data['provider_code'] ?? '');

        $account_response = $this->request('GET', '/user_account', $token, null, 'identity');
        if (is_wp_error($account_response)) {
            if ($this->get_error_http_code($account_response) === 401 || (($account_response->get_error_data()['failure_kind'] ?? '') === 'auth')) {
                $this->clear_rejected_oauth_tokens();
                return $this->safe_error('pinterest_auth_invalid', __('Pinterest authorization is invalid or expired.', 'calliope-social-publisher'), 'publish', 401, false, 'auth', $provider_code);
            }
            return $this->safe_error('pinterest_publish_access_denied', __('Pinterest could not verify publishing access after the request was denied. Review the connection before trying again.', 'calliope-social-publisher'), 'publish', $http_code, false, 'permission', $provider_code);
        }
        $identity = $this->validate_user_account_response($account_response);
        if (is_wp_error($identity)) {
            return $this->safe_error('pinterest_publish_access_denied', __('Pinterest could not verify publishing access after the request was denied. Review the connection before trying again.', 'calliope-social-publisher'), 'publish', $http_code, false, 'permission', $provider_code);
        }
        $board_response = $this->request('GET', '/boards/' . rawurlencode($board_id), $token, null, 'target');
        if (is_wp_error($board_response)) {
            if ($this->get_error_http_code($board_response) === 401) {
                $this->clear_rejected_oauth_tokens();
                return $this->safe_error('pinterest_auth_invalid', __('Pinterest authorization is invalid or expired.', 'calliope-social-publisher'), 'publish', 401, false, 'auth', $provider_code);
            }
            return $this->safe_error('pinterest_publish_access_denied', __('Pinterest accepted the account connection but the selected board could not be verified for publishing.', 'calliope-social-publisher'), 'publish', $http_code, false, 'permission', $provider_code);
        }
        $resolved = $this->validate_board_response($board_response, $board_id);
        if (is_wp_error($resolved)) return $resolved;

        $original_failure_kind = sanitize_key((string) ($error_data['failure_kind'] ?? ''));
        $failure_kind = $original_failure_kind === 'permission' ? 'permission' : ($environment === 'production' ? 'access_tier' : 'permission');
        if (is_array($marker)) {
            $this->save_publish_access_denial($marker, $http_code, $failure_kind, $provider_code);
        }
        $this->sync_legacy_status('publish_access_denied');
        $message = $failure_kind === 'permission'
            ? __('Pinterest accepted the account connection but the authorization does not permit this Pin publication. Reconnect and verify the complete scopes.', 'calliope-social-publisher')
            : __('Pinterest accepted the account connection but denied Pin publishing in Production. Verify that the app has Production publishing access or use Sandbox for Trial testing.', 'calliope-social-publisher');
        return $this->safe_error('pinterest_publish_access_denied', $message, 'publish', $http_code, false, $failure_kind, $provider_code);
    }

    /** @return void */
    private function save_publish_access_denial(array $marker, $http_code, $failure_kind, $provider_code = '') {
        $data = [
            'schema' => 'pinterest-publish-denial-v1', 'recorded_at' => time(),
            'environment' => (string) ($marker['environment'] ?? ''),
            'application_fingerprint' => (string) ($marker['application_fingerprint'] ?? ''),
            'credentials_fingerprint' => (string) ($marker['credentials_fingerprint'] ?? ''),
            'target_fingerprint' => (string) ($marker['target_fingerprint'] ?? ''),
            'http_code' => max(0, (int) $http_code),
            'failure_kind' => in_array($failure_kind, ['permission', 'access_tier'], true) ? $failure_kind : 'permission',
            'provider_code' => $this->normalize_provider_code($provider_code),
        ];
        if (get_option(self::PUBLISH_DENIAL_OPTION, null) === null) add_option(self::PUBLISH_DENIAL_OPTION, $data, '', 'no');
        else update_option(self::PUBLISH_DENIAL_OPTION, $data, false);
    }

    /** @return array|null */
    private function get_matching_publish_denial($application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint) {
        $data = get_option(self::PUBLISH_DENIAL_OPTION, null);
        if (!is_array($data) || ($data['schema'] ?? '') !== 'pinterest-publish-denial-v1') return null;
        if (!in_array((string) ($data['failure_kind'] ?? ''), ['permission', 'access_tier'], true)) return null;
        foreach (['application_fingerprint', 'credentials_fingerprint', 'target_fingerprint'] as $field) {
            if (!isset($data[$field]) || !is_string($data[$field])) return null;
        }
        if (!hash_equals((string) $data['application_fingerprint'], (string) $application_fingerprint)
            || !hash_equals((string) $data['credentials_fingerprint'], (string) $credentials_fingerprint)
            || !hash_equals((string) ($data['environment'] ?? ''), (string) $environment)
            || !hash_equals((string) $data['target_fingerprint'], (string) $target_fingerprint)) return null;
        return [
            'recorded_at' => max(0, (int) ($data['recorded_at'] ?? 0)),
            'failure_kind' => (string) $data['failure_kind'],
            'http_code' => max(0, (int) ($data['http_code'] ?? 0)),
            'provider_code' => $this->normalize_provider_code($data['provider_code'] ?? ''),
        ];
    }

    /** @return void */
    public function clear_publish_access_denial() {
        delete_option(self::PUBLISH_DENIAL_OPTION);
    }

    /** @return void */
    private function mark_permission_missing() {
        delete_option(self::VALIDATION_OPTION);
        delete_option(self::PUBLISH_DENIAL_OPTION);
        $this->sync_legacy_status('permission_missing');
    }

    private function apply_remote_failure_to_validation($error, $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, $previous_marker, array $context = []) {
        if (!is_wp_error($error)) return;
        $data = $error->get_error_data(); $data = is_array($data) ? $data : [];
        $failure_kind = (string) ($data['failure_kind'] ?? 'temporary');
        if (!empty($data['retryable']) || $failure_kind === 'temporary') {
            if (is_array($previous_marker) && $previous_marker['identity_fingerprint'] !== '' && $previous_marker['validated_at'] > 0 && in_array($previous_marker['health'], ['healthy', 'degraded'], true)) {
                $previous_marker['health'] = 'degraded'; $previous_marker['failure_kind'] = '';
                $this->save_validation_marker($previous_marker); $this->sync_legacy_status('ready_to_publish'); return;
            }
            $states = $context['scope_states'] ?? ['user_account' => 'unknown', 'boards' => 'unknown', 'pins' => 'unknown'];
            $this->save_validation_marker([
                'health' => 'degraded', 'failure_kind' => '', 'validated_at' => 0, 'environment' => $environment,
                'scope_fingerprint' => $this->get_scope_fingerprint(), 'application_fingerprint' => $application_fingerprint, 'credentials_fingerprint' => $credentials_fingerprint,
                'identity_fingerprint' => '', 'target_fingerprint' => $target_fingerprint,
                'target_identity_fingerprint' => '', 'resolved_board_id' => '',
                'auth_method' => (string) ($context['auth_method'] ?? 'manual'),
                'user_account_scope_state' => (string) ($states['user_account'] ?? 'unknown'),
                'boards_scope_state' => (string) ($states['boards'] ?? 'unknown'),
                'pins_scope_state' => (string) ($states['pins'] ?? 'unknown'),
                'access_token_expires_at' => (int) ($context['access_token_expires_at'] ?? 0),
                'refresh_token_expires_at' => (int) ($context['refresh_token_expires_at'] ?? 0),
            ]);
            $this->sync_legacy_status('connected_unverified'); return;
        }
        if (in_array($failure_kind, ['auth', 'application', 'environment', 'identity', 'target', 'permission', 'access_tier'], true)) {
            $base = is_array($previous_marker) ? array_merge($previous_marker, $context) : $context;
            $this->save_invalid_marker($failure_kind, $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, $base);
            $map = ['auth' => 'token_invalid', 'application' => 'configuration_error', 'environment' => 'environment_mismatch', 'identity' => 'identity_missing', 'target' => 'target_missing', 'permission' => 'permission_missing', 'access_tier' => 'publish_access_denied'];
            $this->sync_legacy_status($map[$failure_kind]);
        }
    }

    /** @return void */
    private function apply_operation_failure($error, array $marker) {
        if (!is_wp_error($error)) return;
        $data = $error->get_error_data(); $data = is_array($data) ? $data : [];
        $failure_kind = (string) ($data['failure_kind'] ?? '');
        if (!empty($data['retryable']) || $failure_kind === 'temporary') {
            $marker['health'] = 'degraded'; $marker['failure_kind'] = ''; $this->save_validation_marker($marker); return;
        }
        if ($failure_kind === 'local') return;
        if ($failure_kind === 'permission') $marker['pins_scope_state'] = 'denied';
        if (in_array($failure_kind, ['auth', 'application', 'environment', 'identity', 'target', 'permission', 'access_tier'], true)) {
            $this->save_invalid_marker($failure_kind, $marker['application_fingerprint'], $marker['credentials_fingerprint'], $marker['environment'], $marker['target_fingerprint'], $marker);
            $map = ['auth' => 'token_invalid', 'application' => 'configuration_error', 'environment' => 'environment_mismatch', 'identity' => 'identity_missing', 'target' => 'target_missing', 'permission' => 'permission_missing', 'access_tier' => 'publish_access_denied'];
            $this->sync_legacy_status($map[$failure_kind]);
        }
    }

    /** @return bool */
    private function is_http_401($error) {
        if (!is_wp_error($error)) return false;
        $data = $error->get_error_data();
        return is_array($data) && (int) ($data['http_code'] ?? 0) === 401;
    }

    /** @return void */
    public function clear_rejected_oauth_tokens() {
        $environment = $this->get_token_environment();
        foreach ([
            'calliope_sp_pinterest_token', 'calliope_sp_pinterest_refresh_token',
            'calliope_sp_pinterest_access_token_expires_at', 'calliope_sp_pinterest_refresh_token_expires_at',
            'calliope_sp_pinterest_granted_scopes', 'calliope_sp_pinterest_auth_method',
            self::TOKEN_ENVIRONMENT_OPTION, self::VALIDATION_OPTION, self::PUBLISH_DENIAL_OPTION,
            'calliope_sp_pinterest_boards_last',
        ] as $option_name) delete_option($option_name);
        if ($environment !== '') {
            $cache_name = $this->get_boards_option_name($environment);
            if ($cache_name !== '') delete_option($cache_name);
            $board = $this->get_board_id_for_environment($environment);
            if ($board !== '') update_option('calliope_sp_pinterest_board_id', $board, false);
        }
        $this->sync_legacy_status('credentials_saved');
    }

    /** @return string */
    private function capture_lifecycle_snapshot() {
        $names = [
            'calliope_sp_pinterest_env', 'calliope_sp_pinterest_board_id',
            'calliope_sp_pinterest_board_id_production', 'calliope_sp_pinterest_board_id_sandbox',
            'calliope_sp_pinterest_token', 'calliope_sp_pinterest_refresh_token',
            'calliope_sp_pinterest_access_token_expires_at', 'calliope_sp_pinterest_refresh_token_expires_at',
            'calliope_sp_pinterest_granted_scopes', 'calliope_sp_pinterest_auth_method',
            'calliope_sp_pinterest_boards_last', 'calliope_sp_pinterest_boards_production',
            'calliope_sp_pinterest_boards_sandbox', self::TOKEN_ENVIRONMENT_OPTION,
            self::VALIDATION_OPTION, self::PUBLISH_DENIAL_OPTION, 'calliope_sp_pinterest_connection_status',
        ];
        $snapshot = [];
        foreach ($names as $name) $snapshot[$name] = get_option($name, null);
        return $snapshot;
    }
    private function restore_lifecycle_snapshot(array $snapshot) {
        foreach($snapshot as $name=>$value){ if($value===null) delete_option($name); else update_option($name,$value,false); }
    }
    private function mark_refresh_degraded($marker) {
        if(!is_array($marker)) return; $marker['health']='degraded'; $marker['failure_kind']=''; $this->save_validation_marker($marker);
    }
    private function mark_refresh_invalid($marker,$failure_kind='auth') {
        if(!is_array($marker)) return; $marker['health']='invalid'; $marker['failure_kind']=in_array($failure_kind,['auth','application','environment','identity','target','permission'],true)?$failure_kind:'auth'; $this->save_validation_marker($marker); $this->sync_legacy_status('token_invalid');
    }

    private function application_fingerprint($environment, $app_id, $app_secret) {
        $redirect_uri = function_exists('admin_url') ? (string) admin_url('admin-post.php') : '';
        return $this->hmac_fingerprint([
            'schema' => self::VALIDATION_SCHEMA, 'environment' => $environment,
            'app_id' => $app_id, 'app_secret' => $app_secret, 'redirect_uri' => $redirect_uri,
        ]);
    }
    /** @return string */
    private function credentials_fingerprint($environment, $app_id, $app_secret, $token) {
        return $this->hmac_fingerprint(['schema' => self::VALIDATION_SCHEMA, 'environment' => $environment, 'app_id' => $app_id, 'app_secret' => $app_secret, 'access_token' => $token]);
    }
    /** @return string */
    private function identity_fingerprint($environment, $identity) {
        return $this->hmac_fingerprint(['schema' => self::VALIDATION_SCHEMA, 'environment' => $environment, 'identity' => $identity]);
    }
    /** @return string */
    private function target_fingerprint($environment, $board_id) {
        return $this->hmac_fingerprint(['schema' => self::VALIDATION_SCHEMA, 'environment' => $environment, 'board_id' => $board_id]);
    }
    /** @return string */
    private function target_identity_fingerprint($environment, $board_id) {
        return $this->hmac_fingerprint(['schema' => self::VALIDATION_SCHEMA, 'environment' => $environment, 'resolved_board_id' => $board_id]);
    }
    /** @return string */
    private function hmac_fingerprint(array $value) {
        return hash_hmac('sha256', wp_json_encode($value, JSON_UNESCAPED_SLASHES), wp_salt('auth'));
    }

    /**
     * Return the validation marker only when it matches the current application,
     * environment, and access token. This evidence can identify the authentication
     * method, but it must never be used as evidence for the current board target.
     *
     * @return array|null
     */
    public function get_auth_evidence_marker() {
        $environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
        $app_id = trim((string) get_option('calliope_sp_pinterest_app_id', ''));
        $app_secret = trim((string) get_option('calliope_sp_pinterest_app_secret', ''));
        $token = trim((string) get_option('calliope_sp_pinterest_token', ''));
        if ($environment === '' || $app_id === '' || $app_secret === '' || $token === '') return null;

        return $this->get_validation_marker_for_credentials(
            $this->application_fingerprint($environment, $app_id, $app_secret),
            $this->credentials_fingerprint($environment, $app_id, $app_secret, $token),
            $environment
        );
    }

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

    /** @return array|null */
    private function get_validation_marker_for_credentials($application_fingerprint, $credentials_fingerprint, $environment) {
        $marker = $this->normalize_validation_marker(get_option(self::VALIDATION_OPTION, null));
        if (!is_array($marker)) return null;
        if (!hash_equals($marker['application_fingerprint'], $application_fingerprint)
            || !hash_equals($marker['credentials_fingerprint'], $credentials_fingerprint)
            || !hash_equals($marker['environment'], $environment)) 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_kind = (string) ($marker['failure_kind'] ?? '');
        $environment = $this->normalize_environment($marker['environment'] ?? '');
        $scope_fingerprint = (string) ($marker['scope_fingerprint'] ?? '');
        if (!in_array($health, ['healthy', 'degraded', 'invalid'], true)
            || !in_array($failure_kind, ['', 'auth', 'application', 'environment', 'identity', 'target', 'permission', 'access_tier'], true)
            || $environment === '' || !preg_match('/^[a-f0-9]{64}$/D', $scope_fingerprint)
            || !hash_equals($this->get_scope_fingerprint(), $scope_fingerprint)) return null;
        foreach (['application_fingerprint', 'credentials_fingerprint'] as $field) {
            if (!isset($marker[$field]) || !is_string($marker[$field]) || !preg_match('/^[a-f0-9]{64}$/D', $marker[$field])) return null;
        }
        foreach (['identity_fingerprint', 'target_fingerprint', 'target_identity_fingerprint'] as $field) {
            $value = (string) ($marker[$field] ?? '');
            if ($value !== '' && !preg_match('/^[a-f0-9]{64}$/D', $value)) return null;
            $marker[$field] = $value;
        }
        $resolved = $this->normalize_board_id($marker['resolved_board_id'] ?? '');
        if ((string) ($marker['resolved_board_id'] ?? '') !== '' && $resolved === '') return null;
        $auth_method = $this->normalize_auth_method($marker['auth_method'] ?? '');
        if ($auth_method === '') return null;
        foreach (['user_account_scope_state', 'boards_scope_state', 'pins_scope_state'] as $field) {
            $value = (string) ($marker[$field] ?? 'unknown');
            if (!in_array($value, ['granted', 'unknown', 'denied'], true)) return null;
            $marker[$field] = $value;
        }
        return [
            'schema' => self::VALIDATION_SCHEMA, 'health' => $health, 'failure_kind' => $failure_kind,
            'validated_at' => max(0, (int) ($marker['validated_at'] ?? 0)), 'environment' => $environment,
            'scope_fingerprint' => $scope_fingerprint,
            '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_board_id' => $resolved, 'auth_method' => $auth_method,
            'user_account_scope_state' => (string) $marker['user_account_scope_state'],
            'boards_scope_state' => (string) $marker['boards_scope_state'],
            'pins_scope_state' => (string) $marker['pins_scope_state'],
            'access_token_expires_at' => $this->normalize_timestamp_option($marker['access_token_expires_at'] ?? 0),
            'refresh_token_expires_at' => $this->normalize_timestamp_option($marker['refresh_token_expires_at'] ?? 0),
        ];
    }

    /** @return void */
    private function save_validation_marker(array $marker) {
        $safe = $this->normalize_validation_marker(array_merge(['schema' => self::VALIDATION_SCHEMA], $marker));
        if (!is_array($safe)) return;
        if (get_option(self::VALIDATION_OPTION, null) === null) add_option(self::VALIDATION_OPTION, $safe, '', 'no');
        else update_option(self::VALIDATION_OPTION, $safe, false);
    }

    /** @return void */
    private function save_invalid_marker($failure_kind, $application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint, array $context = []) {
        $states = $context['scope_states'] ?? [
            'user_account' => $context['user_account_scope_state'] ?? 'unknown',
            'boards' => $context['boards_scope_state'] ?? 'unknown',
            'pins' => $context['pins_scope_state'] ?? 'unknown',
        ];
        $this->save_validation_marker([
            'health' => 'invalid', 'failure_kind' => $failure_kind, 'validated_at' => max(0, (int) ($context['validated_at'] ?? time())),
            'environment' => $environment, 'scope_fingerprint' => $this->get_scope_fingerprint(),
            '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_board_id' => (string) ($context['resolved_board_id'] ?? ''),
            'auth_method' => (string) ($context['auth_method'] ?? 'manual'),
            'user_account_scope_state' => (string) ($states['user_account'] ?? 'unknown'),
            'boards_scope_state' => (string) ($states['boards'] ?? 'unknown'),
            'pins_scope_state' => (string) ($states['pins'] ?? 'unknown'),
            'access_token_expires_at' => (int) ($context['access_token_expires_at'] ?? 0),
            'refresh_token_expires_at' => (int) ($context['refresh_token_expires_at'] ?? 0),
        ]);
    }

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

    /**
     * Normalize a persisted or submitted authentication method without guessing.
     *
     * @return string oauth2, manual, or an empty string for unknown/corrupt values.
     */
    public function normalize_auth_method($value) {
        if (!is_scalar($value)) return '';
        $value = strtolower(trim((string) $value));
        return in_array($value, ['oauth2', 'manual'], true) ? $value : '';
    }

    /**
     * Resolve the effective authentication method before lifecycle checks.
     * Target evidence and credential-bound authentication evidence are evaluated
     * independently so changing a board cannot downgrade an OAuth session.
     * This method performs no HTTP requests, writes, logging, or external calls.
     *
     * @param array|null $target_marker Optional marker matching the current target.
     * @param array|null $auth_evidence_marker Optional marker matching current credentials.
     * @return string oauth2 or manual.
     */
    public function get_effective_auth_method($target_marker = null, $auth_evidence_marker = null) {
        if ($target_marker !== null) {
            $target_marker = $this->normalize_validation_marker($target_marker);
        } else {
            $environment = $this->normalize_environment(get_option('calliope_sp_pinterest_env', 'production'));
            $app_id = trim((string) get_option('calliope_sp_pinterest_app_id', ''));
            $app_secret = trim((string) get_option('calliope_sp_pinterest_app_secret', ''));
            $token = trim((string) get_option('calliope_sp_pinterest_token', ''));
            $board_id = $this->normalize_board_id(get_option('calliope_sp_pinterest_board_id', ''));
            if ($environment !== '' && $app_id !== '' && $app_secret !== '' && $token !== '') {
                $application_fingerprint = $this->application_fingerprint($environment, $app_id, $app_secret);
                $credentials_fingerprint = $this->credentials_fingerprint($environment, $app_id, $app_secret, $token);
                $target_fingerprint = $board_id !== '' ? $this->target_fingerprint($environment, $board_id) : '';
                $target_marker = $this->get_matching_validation_marker($application_fingerprint, $credentials_fingerprint, $environment, $target_fingerprint);
            }
        }

        if ($auth_evidence_marker !== null) {
            $auth_evidence_marker = $this->normalize_validation_marker($auth_evidence_marker);
        } else {
            $auth_evidence_marker = $this->get_auth_evidence_marker();
        }

        $option_method = $this->normalize_auth_method(get_option('calliope_sp_pinterest_auth_method', ''));
        if (is_array($target_marker) && ($target_marker['auth_method'] ?? '') === 'oauth2') return 'oauth2';
        if (is_array($auth_evidence_marker) && ($auth_evidence_marker['auth_method'] ?? '') === 'oauth2') return 'oauth2';
        if ($option_method === 'oauth2') return 'oauth2';
        if ($this->has_oauth_metadata()) return 'oauth2';
        if (is_array($target_marker) && ($target_marker['auth_method'] ?? '') === 'manual') return 'manual';
        if (is_array($auth_evidence_marker) && ($auth_evidence_marker['auth_method'] ?? '') === 'manual') return 'manual';
        if ($option_method === 'manual') return 'manual';
        return 'manual';
    }

    /** @return bool */
    private function has_oauth_metadata() {
        $refresh_token = get_option('calliope_sp_pinterest_refresh_token', null);
        if (is_scalar($refresh_token) && trim((string) $refresh_token) !== '') return true;
        if ($this->normalize_timestamp_option(get_option('calliope_sp_pinterest_access_token_expires_at', 0)) > 0) return true;
        if ($this->normalize_timestamp_option(get_option('calliope_sp_pinterest_refresh_token_expires_at', 0)) > 0) return true;

        $scopes = get_option('calliope_sp_pinterest_granted_scopes', null);
        if (is_array($scopes) && $scopes !== []) return true;
        if (is_scalar($scopes) && trim((string) $scopes) !== '') return true;
        return false;
    }

    /** @return int */
    private function normalize_timestamp_option($value) {
        if (is_int($value)) return max(0, $value);
        if (is_string($value) && preg_match('/^\d{1,12}$/D', $value)) return max(0, (int) $value);
        return 0;
    }

    /** @return string */
    private function safe_text($value, $limit) {
        $value = wp_strip_all_tags((string) $value, true);
        $value = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $value);
        $value = preg_replace('/\s+/u', ' ', (string) $value);
        $value = trim((string) $value);
        return function_exists('mb_substr') ? mb_substr($value, 0, $limit, 'UTF-8') : substr($value, 0, $limit);
    }

    /** @return string|WP_Error */
    private function image_url_from_b64($b64) {
        $asset = class_exists('Calliope_SP_Media_Helper')
            ? Calliope_SP_Media_Helper::create_public_image_from_b64($b64, [
                'context' => 'pinterest', 'filename_prefix' => 'calliope-social-publisher-pinterest',
                'network' => 'pinterest', 'max_attempts' => 6,
                'url_strategy' => 'static_upload_with_proxy_fallback',
                'required_public_mime' => 'image/jpeg',
                'allow_partial_get' => true,
                'trust_local_on_public_validation_failure' => true,
            ])
            : new WP_Error('upload_failed', __('Pinterest could not prepare a public image URL.', 'calliope-social-publisher'));
        if (is_wp_error($asset)) {
            return $this->safe_error('pinterest_media_url_invalid', __('Pinterest could not prepare a safe public image URL.', 'calliope-social-publisher'), 'image', 0, false, 'local');
        }
        return esc_url_raw((string) ($asset['url'] ?? ''));
    }

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

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