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

class Calliope_SP_Instagram {

    private $api_base = 'https://graph.instagram.com/v24.0';

    /**
     * Meta fetches media from the public URL immediately after we create the file.
     * Some hosts/CDNs need a brief moment before the new upload is reachable externally.
     *
     * @param string $url
     * @param int    $max_attempts
     * @return true|WP_Error
     */
    private function wait_for_public_image_url($url, $max_attempts = 6) {
        $url = esc_url_raw((string) $url);
        if ($url === '') {
            return new WP_Error('upload_failed', 'Missing public image URL.');
        }

        $request_args = [
            'timeout'     => 20,
            'redirection' => 3,
            'user-agent'  => 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)',
        ];

        for ($attempt = 1; $attempt <= (int) $max_attempts; $attempt++) {
            $head = wp_remote_head($url, $request_args);
            if (!is_wp_error($head)) {
                $code = (int) wp_remote_retrieve_response_code($head);
                $type = strtolower((string) wp_remote_retrieve_header($head, 'content-type'));
                $len  = (string) wp_remote_retrieve_header($head, 'content-length');

                if ($code === 200 && strpos($type, 'image/') === 0 && ($len === '' || (int) $len > 0)) {
                    return true;
                }
            }

            $get = wp_remote_get($url, array_merge($request_args, [
                'headers' => [
                    'Range' => 'bytes=0-1023',
                ],
            ]));

            if (!is_wp_error($get)) {
                $code = (int) wp_remote_retrieve_response_code($get);
                $type = strtolower((string) wp_remote_retrieve_header($get, 'content-type'));
                $body = (string) wp_remote_retrieve_body($get);

                if (($code === 200 || $code === 206) && strpos($type, 'image/') === 0 && $body !== '') {
                    return true;
                }
            }

            sleep(min(4, $attempt));
        }

        if (class_exists('Calliope_SP_Logger')) {
            Calliope_SP_Logger::log('instagram', 'error', 'Public image URL was not externally reachable in time: ' . $url);
        }

        return new WP_Error(
            'upload_not_public',
            'The generated image is not publicly reachable yet. Please try publishing again in a few seconds.'
        );
    }

    /**
     * Convert a base64 image into an Instagram-feed-safe public image asset.
     *
     * Instagram feed images must stay within a 4:5 to 1.91:1 aspect ratio range.
     * If the selected image is outside that range, create a centered cropped copy
     * only for Instagram instead of failing the whole publish attempt.
     *
     * @param string $b64
     * @param bool   $force_square Use a safest 1:1 crop for retry fallback.
     * @return array<string,mixed>|WP_Error
     */
    private function image_asset_from_b64($b64, $force_square = false) {
        $asset = class_exists('Calliope_SP_Media_Helper')
            ? Calliope_SP_Media_Helper::create_public_image_from_b64($b64, Calliope_SP_Media_Helper::get_meta_static_image_args('instagram'))
            : new WP_Error('instagram_invalid_media_url', 'Instagram could not prepare a valid public image URL.');

        if (is_wp_error($asset)) {
            return new WP_Error(
                'instagram_invalid_media_url',
                'Instagram could not use this image because the public media URL is not accessible or does not return a valid image. Please try another image or use Original format.',
                [
                    'stage'                  => 'prepare_public_media_url',
                    'original_error_code'    => $asset->get_error_code(),
                    'original_error_message' => $asset->get_error_message(),
                    'original_error_data'    => $asset->get_error_data(),
                ]
            );
        }

        $width = (int) ($asset['width'] ?? 0);
        $height = (int) ($asset['height'] ?? 0);
        if ($width <= 0 || $height <= 0) {
            return $asset;
        }

        $ratio = $width / $height;
        $target_ratio = 0.0;

        if ($force_square) {
            $target_ratio = 1.0;
        } elseif ($ratio < 0.8) {
            // Too tall for Instagram feed. Crop to 4:5.
            $target_ratio = 0.8;
        } elseif ($ratio > 1.91) {
            // Too wide for Instagram feed. Crop to 1.91:1.
            $target_ratio = 1.91;
        }

        if ($target_ratio <= 0) {
            return $asset;
        }

        $cropped = $this->crop_asset_for_instagram_feed($asset, $target_ratio, $force_square);
        if (is_wp_error($cropped)) {
            if (class_exists('Calliope_SP_Logger')) {
                Calliope_SP_Logger::log('instagram', 'error', wp_json_encode([
                    'message'      => 'Could not auto-crop image for Instagram feed.',
                    'width'        => $width,
                    'height'       => $height,
                    'ratio'        => round($ratio, 4),
                    'target_ratio' => $target_ratio,
                    'force_square' => (bool) $force_square,
                    'error'        => $cropped->get_error_message(),
                ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
            }

            return $cropped;
        }

        return $cropped;
    }

    /**
     * Create a centered cropped JPEG copy that matches Instagram feed ratio limits.
     *
     * @param array<string,mixed> $asset
     * @param float               $target_ratio
     * @param bool                $force_square
     * @return array<string,mixed>|WP_Error
     */
    private function crop_asset_for_instagram_feed(array $asset, $target_ratio, $force_square = false) {
        if (!function_exists('wp_get_image_editor')) {
            return new WP_Error('instagram_image_crop_failed', 'WordPress image editing is not available on this site.');
        }

        $path = (string) ($asset['local_path'] ?? $asset['path'] ?? '');
        $width = (int) ($asset['width'] ?? 0);
        $height = (int) ($asset['height'] ?? 0);
        $target_ratio = (float) $target_ratio;

        if ($path === '' || !file_exists($path) || $width <= 0 || $height <= 0 || $target_ratio <= 0) {
            return new WP_Error('instagram_image_crop_failed', 'Instagram could not prepare a compatible cropped image.');
        }

        $current_ratio = $width / $height;
        if ($current_ratio > $target_ratio) {
            $src_w = max(1, (int) floor($height * $target_ratio));
            $src_h = $height;
            $src_x = max(0, (int) floor(($width - $src_w) / 2));
            $src_y = 0;
        } else {
            $src_w = $width;
            $src_h = max(1, (int) floor($width / $target_ratio));
            $src_x = 0;
            $src_y = max(0, (int) floor(($height - $src_h) / 2));
        }

        $editor = wp_get_image_editor($path);
        if (is_wp_error($editor)) {
            return $editor;
        }

        if (method_exists($editor, 'set_quality')) {
            $editor->set_quality(90);
        }

        $cropped = $editor->crop($src_x, $src_y, $src_w, $src_h, $src_w, $src_h, false);
        if (is_wp_error($cropped)) {
            return $cropped;
        }

        $suffix = $force_square ? '-ig-feed-square' : '-ig-feed-safe';
        $target_path = preg_replace('/\.[^.]+$/', '', $path) . $suffix . '.jpg';
        $saved = $editor->save($target_path, 'image/jpeg');
        if (is_wp_error($saved) || empty($saved['path'])) {
            return is_wp_error($saved)
                ? $saved
                : new WP_Error('instagram_image_crop_failed', 'The cropped Instagram image could not be saved.');
        }

        $saved_path = (string) $saved['path'];
        $meta = class_exists('Calliope_SP_Media_Helper')
            ? Calliope_SP_Media_Helper::inspect_image_file($saved_path)
            : new WP_Error('instagram_image_crop_failed', 'Instagram could not inspect the cropped image.');

        if (is_wp_error($meta)) {
            if (class_exists('Calliope_SP_Media_Helper')) {
                Calliope_SP_Media_Helper::delete_file($saved_path);
            }
            return $meta;
        }

        if (!class_exists('Calliope_SP_Media_Helper')) {
            return new WP_Error('invalid_public_media_url', 'The cropped Instagram image URL could not be validated.');
        }

        $static_url = Calliope_SP_Media_Helper::build_public_url_from_upload_path($saved_path);
        if (is_wp_error($static_url)) {
            return $static_url;
        }
        $static_url = (string) $static_url;

        $passthrough_url = '';
        $local_diagnostics = Calliope_SP_Media_Helper::get_local_file_diagnostics($saved_path, $meta);

        // Do not block cropped Instagram publishing on a server self-fetch check.
        // Some shared hosts/CDNs fail loopback TLS requests even when the public
        // image URL is reachable by Meta. The local JPEG is already validated here.
        $url = $static_url;
        $resolved_strategy = 'meta_static_safe';
        $public_meta = [
            'http_head_status' => 0,
            'http_content_type' => (string) ($meta['mime'] ?? 'image/jpeg'),
            'http_get_status' => 0,
            'http_content_length' => (string) ($local_diagnostics['filesize'] ?? ''),
            'http_accept_ranges' => '',
            'http_cache_control' => '',
            'http_x_content_type_options' => '',
            'http_server' => '',
            'http_via' => '',
            'http_cf_cache_status' => '',
            'http_x_litespeed_cache' => '',
            'http_x_hcdn_cache_status' => '',
            'http_x_hcdn_request_id' => '',
            'body_starts_with_jpeg' => !empty($local_diagnostics['starts_with_jpeg_magic']),
            'body_detected_mime' => (string) ($meta['mime'] ?? 'image/jpeg'),
            'body_magic' => (string) ($local_diagnostics['first_12_bytes_hex'] ?? ''),
            'body_matches_expected' => true,
            'body_size_matches_expected' => true,
            'body_magic_matches_expected' => true,
            'http_content_length_matches_expected' => true,
        ];

        if (class_exists('Calliope_SP_Logger')) {
            Calliope_SP_Logger::log('instagram', 'warning', wp_json_encode([
                'message' => 'Cropped Instagram image self-check skipped; local JPEG is valid and the direct public URL will be sent to Meta.',
                'public_url' => $url,
                'url_strategy' => $resolved_strategy,
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
        }

        $result = array_merge($meta, Calliope_SP_Media_Helper::get_local_file_diagnostics($saved_path, $meta), [
            'url'               => esc_url_raw($url),
            'passthrough_url'   => esc_url_raw($passthrough_url),
            'meta_static_url'   => esc_url_raw($static_url),
            'static_url'        => esc_url_raw($static_url),
            'url_strategy'      => $resolved_strategy,
            'local_path'        => $saved_path,
            'http_head_status'  => (int) ($public_meta['http_head_status'] ?? 0),
            'http_content_type' => (string) ($public_meta['http_content_type'] ?? ''),
            'http_get_status'   => (int) ($public_meta['http_get_status'] ?? 0),
            'http_content_length' => (string) ($public_meta['http_content_length'] ?? ''),
            'http_accept_ranges' => (string) ($public_meta['http_accept_ranges'] ?? ''),
            'http_cache_control' => (string) ($public_meta['http_cache_control'] ?? ''),
            'http_x_content_type_options' => (string) ($public_meta['http_x_content_type_options'] ?? ''),
            'http_server' => (string) ($public_meta['http_server'] ?? ''),
            'http_via' => (string) ($public_meta['http_via'] ?? ''),
            'http_cf_cache_status' => (string) ($public_meta['http_cf_cache_status'] ?? ''),
            'http_x_litespeed_cache' => (string) ($public_meta['http_x_litespeed_cache'] ?? ''),
            'http_x_hcdn_cache_status' => (string) ($public_meta['http_x_hcdn_cache_status'] ?? ''),
            'http_x_hcdn_request_id' => (string) ($public_meta['http_x_hcdn_request_id'] ?? ''),
            'body_starts_with_jpeg' => (bool) ($public_meta['body_starts_with_jpeg'] ?? false),
            'body_detected_mime' => (string) ($public_meta['body_detected_mime'] ?? ''),
            'body_magic' => (string) ($public_meta['body_magic'] ?? ''),
            'body_matches_local' => (bool) ($public_meta['body_matches_expected'] ?? false),
            'body_size_matches_local' => (bool) ($public_meta['body_size_matches_expected'] ?? false),
            'body_magic_matches_local' => (bool) ($public_meta['body_magic_matches_expected'] ?? false),
            'http_content_length_matches_local' => (bool) ($public_meta['http_content_length_matches_expected'] ?? false),
        ]);

        if (class_exists('Calliope_SP_Logger')) {
            Calliope_SP_Logger::log('instagram', 'info', wp_json_encode([
                'message'         => 'Image was auto-cropped for Instagram feed compatibility.',
                'original_width'  => $width,
                'original_height' => $height,
                'original_ratio'  => round($current_ratio, 4),
                'target_ratio'    => $target_ratio,
                'new_width'       => (int) ($result['width'] ?? 0),
                'new_height'      => (int) ($result['height'] ?? 0),
                'force_square'    => (bool) $force_square,
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
        }

        return $result;
    }

    /**
     * @param string $message Graph error message.
     * @return bool
     */
    private function is_aspect_ratio_not_supported_error($message) {
        $message = strtolower((string) $message);
        return strpos($message, 'aspect ratio is not supported') !== false
            || strpos($message, 'unsupported aspect ratio') !== false
            || strpos($message, 'invalid aspect ratio') !== false;
    }

    private function request($method, $endpoint, $token, $body = null) {
        $url = rtrim($this->api_base, '/') . '/' . ltrim($endpoint, '/');

        $args = [
            'method'  => strtoupper($method),
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
            ],
            'timeout' => 60,
        ];

        if ($body !== null) {
            if (strtoupper($method) === 'GET') {
                if (is_array($body) && !empty($body)) {
                    $url = add_query_arg($body, $url);
                }
            } elseif (strtoupper($method) === 'POST') {
                // Meta Graph media endpoints expect standard form parameters.
                $args['body'] = is_array($body) ? $body : (string) $body;
            } else {
                $args['headers']['Content-Type'] = 'application/json';
                $args['body'] = is_array($body) ? wp_json_encode($body) : (string) $body;
            }
        }

        $res = wp_remote_request($url, $args);

        if (is_wp_error($res)) {
            if (class_exists('Calliope_SP_Logger')) {
                Calliope_SP_Logger::log('instagram', 'error', 'HTTP WP_Error: ' . $res->get_error_message());
            }
            return $res;
        }

        $code = (int) wp_remote_retrieve_response_code($res);
        $raw  = (string) wp_remote_retrieve_body($res);

        $json = null;
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) $json = $decoded;

        if (class_exists('Calliope_SP_Logger')) {
            $lvl = ($code >= 200 && $code < 300) ? 'info' : 'error';
            $msg = 'HTTP ' . $code . ' | ' . mb_substr($raw, 0, 1200);
            Calliope_SP_Logger::log('instagram', $lvl, $msg);
        }

        return [
            'code'      => $code,
            'body_raw'  => $raw,
            'body_json' => $json,
        ];
    }

    /**
     * Build structured Graph/API diagnostics that are preserved in publisher logs.
     *
     * @param array<string,mixed> $resp
     * @param array<string,mixed> $image_asset
     * @param array<string,mixed> $extra
     * @return array<string,mixed>
     */
    private function build_graph_error_data(array $resp, array $image_asset = [], array $extra = []) {
        $json = isset($resp['body_json']) && is_array($resp['body_json']) ? $resp['body_json'] : [];
        $err = isset($json['error']) && is_array($json['error']) ? $json['error'] : [];

        return array_merge([
            'http_code'           => (int) ($resp['code'] ?? 0),
            'graph_error_message' => (string) ($err['message'] ?? ''),
            'graph_error_type'    => (string) ($err['type'] ?? ''),
            'graph_error_code'    => (int) ($err['code'] ?? 0),
            'graph_error_subcode' => (int) ($err['error_subcode'] ?? 0),
            'fbtrace_id'          => (string) ($err['fbtrace_id'] ?? ''),
            'raw_response'        => mb_substr((string) ($resp['body_raw'] ?? ''), 0, 1600),
            'image_url'           => (string) ($image_asset['url'] ?? ''),
            'passthrough_url'     => (string) ($image_asset['passthrough_url'] ?? ''),
            'meta_static_url'     => (string) ($image_asset['meta_static_url'] ?? ''),
            'static_url'          => (string) ($image_asset['static_url'] ?? ''),
            'url_strategy'        => (string) ($image_asset['url_strategy'] ?? ''),
            'mime'                => (string) ($image_asset['mime'] ?? ''),
            'width'               => (int) ($image_asset['width'] ?? 0),
            'height'              => (int) ($image_asset['height'] ?? 0),
            'filesize'            => (int) ($image_asset['filesize'] ?? 0),
            'local_path'          => (string) ($image_asset['local_path'] ?? $image_asset['path'] ?? ''),
            'http_head_status'    => (int) ($image_asset['http_head_status'] ?? 0),
            'http_get_status'     => (int) ($image_asset['http_get_status'] ?? 0),
            'http_content_type'   => (string) ($image_asset['http_content_type'] ?? ''),
            'http_content_length' => (string) ($image_asset['http_content_length'] ?? ''),
            'http_accept_ranges'  => (string) ($image_asset['http_accept_ranges'] ?? ''),
            'http_cache_control'  => (string) ($image_asset['http_cache_control'] ?? ''),
            'http_x_content_type_options' => (string) ($image_asset['http_x_content_type_options'] ?? ''),
            'http_server'         => (string) ($image_asset['http_server'] ?? ''),
            'http_via'            => (string) ($image_asset['http_via'] ?? ''),
            'http_cf_cache_status'=> (string) ($image_asset['http_cf_cache_status'] ?? ''),
            'http_x_litespeed_cache' => (string) ($image_asset['http_x_litespeed_cache'] ?? ''),
            'http_x_hcdn_cache_status' => (string) ($image_asset['http_x_hcdn_cache_status'] ?? ''),
            'http_x_hcdn_request_id' => (string) ($image_asset['http_x_hcdn_request_id'] ?? ''),
            'body_starts_with_jpeg' => (bool) ($image_asset['body_starts_with_jpeg'] ?? false),
            'body_detected_mime'  => (string) ($image_asset['body_detected_mime'] ?? ''),
            'body_magic'          => (string) ($image_asset['body_magic'] ?? ''),
            'body_matches_local'  => (bool) ($image_asset['body_matches_local'] ?? false),
            'body_size_matches_local' => (bool) ($image_asset['body_size_matches_local'] ?? false),
            'body_magic_matches_local' => (bool) ($image_asset['body_magic_matches_local'] ?? false),
            'http_content_length_matches_local' => (bool) ($image_asset['http_content_length_matches_local'] ?? false),
            'first_12_bytes_hex'  => (string) ($image_asset['first_12_bytes_hex'] ?? ''),
            'sha1'                => (string) ($image_asset['sha1'] ?? ''),
            'media_url_error_summary' => ((int) ($err['error_subcode'] ?? 0) === 2207052 || stripos((string) ($err['message'] ?? ''), 'Only photo or video can be accepted as media type') !== false)
                ? 'Meta could not fetch or accept the public media URL.'
                : '',
            'http_diagnostic_summary' => sprintf(
                'strategy=%s head=%d get=%d type=%s length=%s jpeg_magic=%s body_match=%s',
                (string) ($image_asset['url_strategy'] ?? ''),
                (int) ($image_asset['http_head_status'] ?? 0),
                (int) ($image_asset['http_get_status'] ?? 0),
                (string) ($image_asset['http_content_type'] ?? ''),
                (string) ($image_asset['http_content_length'] ?? ''),
                !empty($image_asset['body_starts_with_jpeg']) ? 'yes' : 'no',
                !empty($image_asset['body_matches_local']) ? 'yes' : 'no'
            ),
        ], $extra);
    }

    /**
     * Wait until an Instagram media container is ready (FINISHED) before publishing.
     *
     * Meta can return "Media ID is not available" if we publish too quickly.
     *
     * @param string $creation_id
     * @param string $token
     * @param int $max_attempts
     * @return true|WP_Error
     */
    private function wait_for_container_ready($creation_id, $token, $max_attempts = 10) {
        $creation_id = (string) $creation_id;
        if (!$creation_id) return new WP_Error('instagram', 'Missing creation_id.');

        // Give Instagram a moment to start processing.
        sleep(2);

        for ($i = 1; $i <= (int)$max_attempts; $i++) {
            $endpoint = '/' . rawurlencode($creation_id) . '?fields=status_code';
            $r = $this->request('GET', $endpoint, $token, null);

            if (is_wp_error($r)) return $r;
            if ($r['code'] < 200 || $r['code'] >= 300) {
                $msg = $r['body_json']['error']['message'] ?? $r['body_raw'] ?? 'Unknown error';
                return new WP_Error('instagram', 'Could not check media container status (' . $r['code'] . '): ' . $msg);
            }

            $status = strtoupper((string)($r['body_json']['status_code'] ?? ''));
            if ($status === 'FINISHED') return true;
            if ($status === 'ERROR' || $status === 'FAILED') {
                return new WP_Error('instagram', 'Instagram reported container status: ' . ($status ?: 'ERROR'));
            }

            // Still processing.
            // Backoff a bit: 2,2,3,3,4,...
            $sleep = 2 + (int)floor(($i - 1) / 2);
            sleep(min(6, $sleep));
        }

        return new WP_Error('instagram', 'The media is still processing. Please try again in a few seconds.');
    }

    /**
     * Publish an image to Instagram feed (image only).
     *
     * Requires options:
     * - calliope_sp_ig_access_token
     * - calliope_sp_ig_user_id (Instagram Business Account ID)
     *
     * @param array $data
     * @return true|WP_Error
     */
    /**
     * Renew a still-valid long-lived token for the Instagram Login product.
     * This deliberately uses graph.instagram.com and never mixes Facebook Login,
     * Basic Display, or Page-token endpoints into this connection.
     *
     * @return true|WP_Error
     */
    public function maybe_refresh_access_token($threshold_seconds = null) {
        $threshold_seconds = $threshold_seconds === null && class_exists('Calliope_SP_Master')
            ? Calliope_SP_Master::get_lifecycle_refresh_threshold_seconds()
            : max(0,(int)$threshold_seconds);
        $token=$this->meta_option_string('calliope_sp_ig_access_token');
        $expires_at=(int)get_option('calliope_sp_ig_expires_at',0);
        $now=time();
        if($token==='') return new WP_Error('instagram_refresh_missing_token',__('Instagram access token is missing.','calliope-social-publisher'),['phase'=>'lifecycle','retryable'=>false,'failure_kind'=>'auth']);
        if($expires_at<=$now) return new WP_Error('instagram_token_expired',__('The Instagram long-lived token expired. Reconnect the account.','calliope-social-publisher'),['phase'=>'lifecycle','retryable'=>false,'failure_kind'=>'auth']);
        if($expires_at>($now+$threshold_seconds)) return true;

        $lock=class_exists('Calliope_SP_Master')?Calliope_SP_Master::acquire_lifecycle_refresh_lock('instagram'):true;
        if(is_wp_error($lock)) return (int)get_option('calliope_sp_ig_expires_at',0)>(time()+$threshold_seconds)?true:$lock;
        try {
            $token=$this->meta_option_string('calliope_sp_ig_access_token');
            $expires_at=(int)get_option('calliope_sp_ig_expires_at',0); $now=time();
            if($expires_at>($now+$threshold_seconds)) return true;
            if($expires_at<=$now||$token==='') return new WP_Error('instagram_token_expired',__('The Instagram long-lived token expired. Reconnect the account.','calliope-social-publisher'),['phase'=>'lifecycle','retryable'=>false,'failure_kind'=>'auth']);

            $url=add_query_arg(['grant_type'=>'ig_refresh_token','access_token'=>$token],'https://graph.instagram.com/refresh_access_token');
            $response=wp_remote_get($url,['timeout'=>30,'redirection'=>0,'reject_unsafe_urls'=>true,'sslverify'=>true,'headers'=>['Accept'=>'application/json']]);
            if(is_wp_error($response)){ $this->mark_lifecycle_degraded(); return true; }
            $http=(int)wp_remote_retrieve_response_code($response); $json=json_decode((string)wp_remote_retrieve_body($response),true);
            if($http===429||$http>=500){ $this->mark_lifecycle_degraded(); return true; }
            if($http<200||$http>=300||!is_array($json)){ $this->mark_lifecycle_invalid('auth'); return new WP_Error('instagram_refresh_rejected',__('Instagram rejected the token renewal. Reconnect the account.','calliope-social-publisher'),['phase'=>'refresh','http_code'=>$http,'retryable'=>false,'failure_kind'=>'auth']); }
            $new_token=isset($json['access_token'])&&is_scalar($json['access_token'])?trim((string)$json['access_token']):'';
            $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;
            if($new_token===''||strlen($new_token)>4096||$token_type!=='bearer'||$expires_in<=0) return new WP_Error('instagram_refresh_invalid_response',__('Instagram returned an incomplete token renewal response.','calliope-social-publisher'),['phase'=>'refresh','http_code'=>$http,'retryable'=>true,'failure_kind'=>'temporary']);
            $new_expires_at=$now+$expires_in;
            $resp=$this->request('GET','/me',$new_token,['fields'=>'id,username,account_type']);
            if(is_wp_error($resp)) return $resp;
            if((int)$resp['code']<200||(int)$resp['code']>=300||!is_array($resp['body_json'])) return new WP_Error('instagram_refresh_identity',__('Instagram could not verify the renewed token.','calliope-social-publisher'),['phase'=>'identity','retryable'=>false,'failure_kind'=>'identity']);
            $id=isset($resp['body_json']['id'])&&is_scalar($resp['body_json']['id'])?trim((string)$resp['body_json']['id']):'';
            $account_type=isset($resp['body_json']['account_type'])&&is_scalar($resp['body_json']['account_type'])?strtoupper(trim((string)$resp['body_json']['account_type'])):'';
            $target=$this->meta_option_string('calliope_sp_ig_user_id');
            if($id===''||$target===''||!hash_equals($target,$id)||!in_array($account_type,['BUSINESS','CREATOR','MEDIA_CREATOR'],true)) return new WP_Error('instagram_refresh_identity',__('Instagram returned a different or unsupported professional account during renewal.','calliope-social-publisher'),['phase'=>'identity','retryable'=>false,'failure_kind'=>'identity']);

            $snapshot=['token'=>get_option('calliope_sp_ig_access_token',null),'expires'=>get_option('calliope_sp_ig_expires_at',null),'marker'=>get_option('calliope_sp_instagram_validation',null),'status'=>get_option('calliope_sp_ig_connection_status',null)];
            update_option('calliope_sp_ig_access_token',$new_token,false); update_option('calliope_sp_ig_expires_at',$new_expires_at,false);
            if(!$this->meta_save_validation_marker($id,$target,'granted',$account_type)) { $this->restore_lifecycle_snapshot($snapshot); return new WP_Error('instagram_refresh_commit',__('Instagram token renewal could not be committed safely.','calliope-social-publisher'),['phase'=>'commit','retryable'=>true,'failure_kind'=>'temporary']); }
            update_option('calliope_sp_ig_connection_status','ready_to_publish',false);
            $state=$this->get_connection_state();
            if(is_wp_error($state)||!$state->is_ready()||!hash_equals($target,$this->get_resolved_target_id())) { $this->restore_lifecycle_snapshot($snapshot); return new WP_Error('instagram_refresh_verify',__('Instagram token renewal could not be verified after saving.','calliope-social-publisher'),['phase'=>'commit','retryable'=>true,'failure_kind'=>'temporary']); }
            return true;
        } finally { if(class_exists('Calliope_SP_Master')&&is_string($lock)) Calliope_SP_Master::release_lifecycle_refresh_lock('instagram',$lock); }
    }

    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->is_ready()) return new WP_Error('instagram_not_ready','Instagram is not ready to publish. Reconnect and verify the professional account.');
        $token=$this->meta_option_string('calliope_sp_ig_access_token');
        $ig_user_id=$this->get_resolved_target_id();
        if($token===''||$ig_user_id==='') return new WP_Error('instagram_not_ready','Instagram validation is incomplete.');
        $image_b64 = $data['image_b64'] ?? '';
        if(empty($image_b64)) return new WP_Error('instagram_image_required','Instagram requires an image for this publication.');
        $image_asset = $this->image_asset_from_b64($image_b64, false);
        if (is_wp_error($image_asset)) return $image_asset;
        $image_url = esc_url_raw((string) ($image_asset['url'] ?? ''));
        if ($image_url === '') {
            return new WP_Error('instagram_invalid_media_url', 'Instagram could not prepare a valid public image URL.');
        }

        $caption_parts = [];
        $text = trim((string) ($data['content'] ?? ''));
        if ($text) $caption_parts[] = $text;

        $link = trim((string) ($data['link'] ?? ''));
        if ($link) $caption_parts[] = $link;

        $hashtags = trim((string) ($data['hashtags'] ?? ''));
        if ($hashtags) $caption_parts[] = $hashtags;

        $caption = trim(implode("\n\n", $caption_parts));

        $container_payload = [
            'image_url' => $image_url,
            'caption'   => $caption,
        ];

        if (class_exists('Calliope_SP_Logger')) {
            Calliope_SP_Logger::log('instagram', 'info', wp_json_encode([
                'message'      => 'Sending image URL to Instagram media container.',
                'image_url'    => $image_url,
                'url_strategy' => (string) ($image_asset['url_strategy'] ?? ''),
                'local_path'   => (string) ($image_asset['local_path'] ?? ''),
                'mime'         => (string) ($image_asset['mime'] ?? ''),
                'width'        => (int) ($image_asset['width'] ?? 0),
                'height'       => (int) ($image_asset['height'] ?? 0),
                'filesize'     => (int) ($image_asset['filesize'] ?? 0),
                'first_12_bytes_hex' => (string) ($image_asset['first_12_bytes_hex'] ?? ''),
                'sha1'         => (string) ($image_asset['sha1'] ?? ''),
                'http_head_status' => (int) ($image_asset['http_head_status'] ?? 0),
                'http_get_status' => (int) ($image_asset['http_get_status'] ?? 0),
                'http_content_type' => (string) ($image_asset['http_content_type'] ?? ''),
                'http_content_length' => (string) ($image_asset['http_content_length'] ?? ''),
                'http_accept_ranges' => (string) ($image_asset['http_accept_ranges'] ?? ''),
                'http_cache_control' => (string) ($image_asset['http_cache_control'] ?? ''),
                'http_x_content_type_options' => (string) ($image_asset['http_x_content_type_options'] ?? ''),
                'http_server' => (string) ($image_asset['http_server'] ?? ''),
                'http_via' => (string) ($image_asset['http_via'] ?? ''),
                'http_cf_cache_status' => (string) ($image_asset['http_cf_cache_status'] ?? ''),
                'http_x_litespeed_cache' => (string) ($image_asset['http_x_litespeed_cache'] ?? ''),
                'http_x_hcdn_cache_status' => (string) ($image_asset['http_x_hcdn_cache_status'] ?? ''),
                'http_x_hcdn_request_id' => (string) ($image_asset['http_x_hcdn_request_id'] ?? ''),
                'body_starts_with_jpeg' => (bool) ($image_asset['body_starts_with_jpeg'] ?? false),
                'body_matches_local' => (bool) ($image_asset['body_matches_local'] ?? false),
                'body_size_matches_local' => (bool) ($image_asset['body_size_matches_local'] ?? false),
                'body_magic_matches_local' => (bool) ($image_asset['body_magic_matches_local'] ?? false),
                'http_content_length_matches_local' => (bool) ($image_asset['http_content_length_matches_local'] ?? false),
                'meta_request_body' => $container_payload,
            ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
        }

        // 1) Create media container
        $resp = $this->request('POST', '/' . rawurlencode($ig_user_id) . '/media', $token, $container_payload);

        if (is_wp_error($resp)) return $resp;
        if ($resp['code'] < 200 || $resp['code'] >= 300) {
            $msg = $resp['body_json']['error']['message'] ?? $resp['body_raw'] ?? 'Unknown error';
            $err_sub = (int) ($resp['body_json']['error']['error_subcode'] ?? 0);
            $initial_error_data = $this->build_graph_error_data($resp, $image_asset, [
                'stage' => 'create_media_container',
            ]);

            if (class_exists('Calliope_SP_Logger')) {
                Calliope_SP_Logger::log('instagram', 'error', wp_json_encode(array_merge([
                    'message' => 'Meta media URL diagnostics: Instagram create container failed with structured details.',
                ], $initial_error_data), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
            }

            if ($this->is_aspect_ratio_not_supported_error($msg)) {
                $retry_asset = $this->image_asset_from_b64($image_b64, true);
                if (!is_wp_error($retry_asset) && !empty($retry_asset['url'])) {
                    if (class_exists('Calliope_SP_Logger')) {
                        Calliope_SP_Logger::log('instagram', 'warning', wp_json_encode([
                            'message' => 'Instagram rejected the original aspect ratio. Retrying with square cropped image.',
                            'graph_error_message' => (string) $msg,
                        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
                    }

                    $image_url = esc_url_raw((string) $retry_asset['url']);
                    $image_asset = $retry_asset;
                    $container_payload = [
                        'image_url' => $image_url,
                        'caption'   => $caption,
                    ];

                    if (class_exists('Calliope_SP_Logger')) {
                        Calliope_SP_Logger::log('instagram', 'info', wp_json_encode([
                            'message' => 'Instagram square retry media container payload (access token excluded).',
                            'meta_request_body' => $container_payload,
                            'url_strategy' => (string) ($image_asset['url_strategy'] ?? ''),
                            'http_diagnostic_summary' => sprintf(
                                'strategy=%s head=%d get=%d type=%s length=%s jpeg_magic=%s body_match=%s',
                                (string) ($image_asset['url_strategy'] ?? ''),
                                (int) ($image_asset['http_head_status'] ?? 0),
                                (int) ($image_asset['http_get_status'] ?? 0),
                                (string) ($image_asset['http_content_type'] ?? ''),
                                (string) ($image_asset['http_content_length'] ?? ''),
                                !empty($image_asset['body_starts_with_jpeg']) ? 'yes' : 'no',
                                !empty($image_asset['body_matches_local']) ? 'yes' : 'no'
                            ),
                        ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
                    }

                    $resp = $this->request('POST', '/' . rawurlencode($ig_user_id) . '/media', $token, $container_payload);

                    if (is_wp_error($resp)) return $resp;
                    if ($resp['code'] >= 200 && $resp['code'] < 300) {
                        // Retry worked, continue with the new container.
                    } else {
                        $msg = $resp['body_json']['error']['message'] ?? $resp['body_raw'] ?? 'Unknown error';
                        $err_sub = (int) ($resp['body_json']['error']['error_subcode'] ?? 0);
                        $initial_error_data = $this->build_graph_error_data($resp, $retry_asset, [
                            'stage' => 'create_media_container_square_retry',
                        ]);
                    }
                }
            }

            if ($resp['code'] < 200 || $resp['code'] >= 300) {
                if (
                    $err_sub === 2207052 ||
                    stripos((string) $msg, 'Only photo or video can be accepted as media type') !== false
                ) {
                    return new WP_Error(
                        'instagram_invalid_media_url',
                        'Instagram could not use this image because the public media URL is not accessible or does not return a valid image. Please try another image or use Original format.',
                        $initial_error_data
                    );
                }

                if ($this->is_aspect_ratio_not_supported_error($msg)) {
                    return new WP_Error(
                        'instagram_unsupported_aspect_ratio',
                        'Instagram rejected the image aspect ratio even after the plugin prepared a compatible crop. Please try a square or 4:5 image.',
                        $initial_error_data
                    );
                }

                return new WP_Error('instagram', 'Could not create media container (' . $resp['code'] . '): ' . $msg, $initial_error_data);
            }
        }

        $creation_id = $resp['body_json']['id'] ?? '';
        if (!$creation_id) {
            return new WP_Error('instagram', 'Missing creation_id from Instagram response.', $this->build_graph_error_data($resp, $image_asset, ['stage' => 'missing_creation_id']));
        }

        // 1.5) Wait until container is ready
        $ready = $this->wait_for_container_ready($creation_id, $token, 10);
        if (is_wp_error($ready)) return $ready;

        // 2) Publish container (with a small retry for "not ready" responses)
        $last_err = '';
        for ($try = 1; $try <= 5; $try++) {
            $resp2 = $this->request('POST', '/' . rawurlencode($ig_user_id) . '/media_publish', $token, [
                'creation_id' => $creation_id,
            ]);

            if (is_wp_error($resp2)) return $resp2;
            if ($resp2['code'] >= 200 && $resp2['code'] < 300) {
                $published_id = isset($resp2['body_json']['id']) && is_scalar($resp2['body_json']['id'])
                    ? trim((string) $resp2['body_json']['id'])
                    : '';

                if (
                    $published_id === '' ||
                    !preg_match('/^[A-Za-z0-9_.:-]{1,255}$/D', $published_id) ||
                    hash_equals((string) $creation_id, $published_id)
                ) {
                    return new WP_Error(
                        'instagram_publish_response_invalid',
                        'Instagram returned a successful HTTP status without a valid Media ID.',
                        [
                            'stage' => 'media_publish',
                            'http_code' => (int) $resp2['code'],
                            'retryable' => false,
                        ]
                    );
                }

                return [
                    'ok' => true,
                    'post_id' => sanitize_text_field($published_id),
                    'media_type' => 'image',
                ];
            }

            $err_code = (int)($resp2['body_json']['error']['code'] ?? 0);
            $err_sub  = (int)($resp2['body_json']['error']['error_subcode'] ?? 0);
            $last_err = $resp2['body_json']['error']['message'] ?? $resp2['body_raw'] ?? 'Unknown error';

            // 9007 / 2207027 => media not ready for publishing yet.
            if ($resp2['code'] === 400 && $err_code === 9007 && $err_sub === 2207027) {
                $ready2 = $this->wait_for_container_ready($creation_id, $token, 6);
                if (is_wp_error($ready2)) return $ready2;
                continue;
            }

            return new WP_Error('instagram', 'Could not publish media (' . $resp2['code'] . '): ' . $last_err, $this->build_graph_error_data($resp2, $image_asset, ['stage' => 'media_publish']));
        }

        return new WP_Error('instagram', 'Could not publish media: ' . ($last_err ?: 'Unknown error'));
    }

    /**
     * Test connection by fetching pages and attempting to find an Instagram Business account.
     *
     * @return true|WP_Error
     */
    public function test_connection() {
        $lifecycle=$this->maybe_refresh_access_token();
        if(is_wp_error($lifecycle) && $lifecycle->get_error_code()!=='instagram_refresh_missing_token') return $lifecycle;
        $token = $this->meta_option_string('calliope_sp_ig_access_token');
        if ($token === '') return new WP_Error('instagram_config', 'Missing Instagram access token.');
        $resp = $this->request('GET', '/me', $token, ['fields'=>'id,username,account_type']);
        if (is_wp_error($resp)) return $resp;
        if ((int)$resp['code'] < 200 || (int)$resp['code'] >= 300 || !is_array($resp['body_json'])) return new WP_Error('instagram_token_invalid', 'Instagram could not validate the professional account.');
        $json=$resp['body_json']; $id=isset($json['id'])&&is_scalar($json['id'])?trim((string)$json['id']):'';
        if ($id==='') return new WP_Error('instagram_identity', 'Instagram identity could not be validated.');
        $account_type=isset($json['account_type'])&&is_scalar($json['account_type'])?strtoupper(trim((string)$json['account_type'])):'';
        if ($account_type==='' || !in_array($account_type,['BUSINESS','CREATOR','MEDIA_CREATOR'],true)) {
            delete_option('calliope_sp_instagram_validation');
            return new WP_Error('instagram_account_type', 'Instagram publishing requires a verified Business or Creator professional account.');
        }
        $saved=$this->meta_option_string('calliope_sp_ig_user_id');
        if ($saved!=='' && !hash_equals($saved,$id)) {
            delete_option('calliope_sp_instagram_validation');
            return new WP_Error('instagram_identity', 'Instagram returned a different professional account.');
        }
        update_option('calliope_sp_ig_user_id',$id,false);
        if (!$this->meta_save_validation_marker($id,$id,'granted',$account_type)) return new WP_Error('instagram_marker','Instagram validation could not be saved.');
        return true;
    }

    private function restore_lifecycle_snapshot(array $snapshot){$map=['token'=>'calliope_sp_ig_access_token','expires'=>'calliope_sp_ig_expires_at','marker'=>'calliope_sp_instagram_validation','status'=>'calliope_sp_ig_connection_status'];foreach($map as $key=>$option){if(!array_key_exists($key,$snapshot)||$snapshot[$key]===null)delete_option($option);else update_option($option,$snapshot[$key],false);}}
    private function mark_lifecycle_degraded(){$m=$this->meta_validation_marker();if(!is_array($m))return;$m['health']='degraded';$m['failure_kind']='';update_option('calliope_sp_instagram_validation',$m,false);}
    private function mark_lifecycle_invalid($failure){$m=$this->meta_validation_marker();if(!is_array($m))return;$m['health']='invalid';$m['failure_kind']=in_array($failure,['auth','permission','identity','target'],true)?$failure:'auth';update_option('calliope_sp_instagram_validation',$m,false);}

    private function meta_option_string($name) {
        $value = get_option($name, '');
        return is_string($value) ? trim($value) : '';
    }
    private function meta_fingerprint($parts) { return hash_hmac('sha256', implode("\n", array_map('strval', $parts)), wp_salt('auth')); }
    private function meta_validation_marker() {
        $marker=get_option('calliope_sp_instagram_validation',null);
        if(!is_array($marker)||($marker['schema']??'')!=='instagram-validation-v1') return null;
        foreach(['application_fingerprint','credentials_fingerprint','identity_fingerprint','target_fingerprint','target_identity_fingerprint'] as $k) if(!isset($marker[$k])||!is_string($marker[$k])||!preg_match('/^[a-f0-9]{64}$/D',$marker[$k])) return null;
        $account_type=isset($marker['professional_account_type'])&&is_scalar($marker['professional_account_type'])?strtoupper(trim((string)$marker['professional_account_type'])):'';
        if(!in_array($account_type,['BUSINESS','CREATOR','MEDIA_CREATOR'],true)) return null;
        $app=$this->meta_fingerprint([$this->meta_option_string('calliope_sp_ig_app_id'),$this->meta_option_string('calliope_sp_ig_app_secret')]);
        $cred=$this->meta_fingerprint([$app,$this->meta_option_string('calliope_sp_ig_access_token')]); $target=$this->meta_option_string('calliope_sp_ig_user_id'); $tf=$this->meta_fingerprint([$cred,$target]);
        if(!hash_equals($app,$marker['application_fingerprint'])||!hash_equals($cred,$marker['credentials_fingerprint'])||!hash_equals($tf,$marker['target_fingerprint'])) return null;
        if(($marker['resolved_target_id']??'')!==$target||(int)($marker['validated_at']??0)<=0) return null;
        return $marker;
    }
    private function meta_save_validation_marker($identity,$target_id,$permission='granted',$account_type='') {
        $account_type=is_scalar($account_type)?strtoupper(trim((string)$account_type)):'';
        if(!is_string($identity)||!is_string($target_id)||$identity===''||$target_id===''||$permission!=='granted'||!in_array($account_type,['BUSINESS','CREATOR','MEDIA_CREATOR'],true)) return false;
        $app=$this->meta_fingerprint([$this->meta_option_string('calliope_sp_ig_app_id'),$this->meta_option_string('calliope_sp_ig_app_secret')]); $cred=$this->meta_fingerprint([$app,$this->meta_option_string('calliope_sp_ig_access_token')]); $tf=$this->meta_fingerprint([$cred,$target_id]);
        $marker=['schema'=>'instagram-validation-v1','health'=>'healthy','failure_kind'=>'','validated_at'=>time(),'application_fingerprint'=>$app,'credentials_fingerprint'=>$cred,'identity_fingerprint'=>$this->meta_fingerprint(['identity',$identity]),'target_fingerprint'=>$tf,'target_identity_fingerprint'=>$this->meta_fingerprint(['target',$identity,$target_id]),'resolved_target_id'=>$target_id,'permission_state'=>'granted','professional_account_type'=>$account_type,'auth_method'=>'oauth2','access_token_expires_at'=>(int)get_option('calliope_sp_ig_expires_at',0)];
        update_option('calliope_sp_instagram_validation',$marker,false); $saved=get_option('calliope_sp_instagram_validation',null); return is_array($saved)&&$saved==$marker;
    }
    public function get_connection_state() {
        $app_id=$this->meta_option_string('calliope_sp_ig_app_id'); $app_secret=$this->meta_option_string('calliope_sp_ig_app_secret'); $token=$this->meta_option_string('calliope_sp_ig_access_token'); $target=$this->meta_option_string('calliope_sp_ig_user_id'); $expires=(int)get_option('calliope_sp_ig_expires_at',0);
        $configured=$app_id!==''&&$app_secret!==''; $partial=(($app_id!=='') xor ($app_secret!=='')); $status='not_configured';$health='unknown';$e='E0';$identity=false;$permission=false;$ready=false;$message=__('Connect instagram to publish.','calliope-social-publisher');$action=__('Connect instagram','calliope-social-publisher');
        if($partial||($token!==''&&!$configured)){$status='configuration_error';$health='invalid';$e='E1';$message=__('The instagram application configuration is incomplete.','calliope-social-publisher');}
        elseif($configured&&$token===''){$status='credentials_saved';$e='E1';$message=__('Application credentials are saved. Connect instagram.','calliope-social-publisher');}
        elseif($configured&&$token!==''){$e='E2';if($expires>0&&$expires<=time()){$status='token_expired';$health='invalid';$message=__('The instagram access token expired. Reconnect the account.','calliope-social-publisher');}elseif($target===''){$status='connected_unverified';$message=__('Verification is required before publishing.','calliope-social-publisher');$action=__('Save and test','calliope-social-publisher');}else{$m=$this->meta_validation_marker();if($m){$f=$m['failure_kind']??'';if($f==='auth'){$status='token_invalid';$health='invalid';}elseif($f==='permission'){$status='permission_missing';$health='invalid';}elseif($f==='target'){$status='target_missing';$health='invalid';}else{$status='ready_to_publish';$health=(($m['health']??'')==='degraded'?'degraded':'healthy');$e='E5';$identity=true;$permission=true;$ready=true;$message=__('Ready to publish.','calliope-social-publisher');$action='';}}else{$status='connected_unverified';$message=__('Verification is required before publishing.','calliope-social-publisher');$action=__('Save and test','calliope-social-publisher');}}}
        $m=$this->meta_validation_marker(); return Calliope_SP_Connection_State::create(['network'=>'instagram','status'=>$status,'health'=>$health,'is_configured'=>$configured,'is_authenticated'=>$token!=='','is_identity_known'=>$identity,'has_target'=>$target!=='','has_permission'=>$permission,'is_ready'=>$ready,'needs_attention'=>!$ready||$health==='degraded','evidence_level'=>$e,'message'=>$message,'action'=>$action,'expires_at'=>$expires,'validated_at'=>$m?(int)$m['validated_at']:0,'connection_method'=>'oauth2'],'E5');
    }
    public function get_resolved_target_id(){$m=$this->meta_validation_marker();return $m?(string)$m['resolved_target_id']:'';}

    /**
     * Internal publishing context for compatible extensions.
     * The returned token must never be rendered, logged, filtered publicly, or persisted elsewhere.
     *
     * @return array{ready:bool,target_id:string,access_token:string,api_base:string}|WP_Error
     */
    public function get_extension_publish_context() {
        $lifecycle = $this->maybe_refresh_access_token();
        if (is_wp_error($lifecycle)) {
            return new WP_Error('instagram_not_ready', __('Instagram is not ready to publish.', 'calliope-social-publisher'));
        }
        $state = $this->get_connection_state();
        if (is_wp_error($state) || !$state->is_ready()) {
            return new WP_Error('instagram_not_ready', __('Instagram is not ready to publish.', 'calliope-social-publisher'));
        }
        $target_id = $this->get_resolved_target_id();
        $token = $this->meta_option_string('calliope_sp_ig_access_token');
        if ($target_id === '' || $token === '') {
            return new WP_Error('instagram_not_ready', __('Instagram validation is incomplete.', 'calliope-social-publisher'));
        }
        return [
            'ready'        => true,
            'target_id'    => $target_id,
            'access_token' => $token,
            'api_base'     => rtrim($this->api_base, '/'),
        ];
    }
}
