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

class Calliope_SP_Facebook {

    private $api_base = 'https://graph.facebook.com/v20.0';

    /**
     * Convert base64 image into a temporary local image asset.
     * Facebook uploads this file directly via multipart/form-data so it does not depend
     * on Meta being able to fetch the wp-content/uploads URL.
     *
     * @param string $b64
     * @return array{url:string,static_url:string,path:string,mime:string}|WP_Error
     */
    private function image_url_from_b64($b64) {
        $asset = class_exists('Calliope_SP_Media_Helper')
            ? Calliope_SP_Media_Helper::create_public_image_from_b64($b64, [
                'context'         => 'facebook',
                'filename_prefix' => 'calliope-social-publisher-fb',
                'preferred_mime'  => 'image/jpeg',
                'network'         => 'facebook',
                'max_attempts'    => 8,
            ])
            : new WP_Error('img', 'Facebook could not prepare a public image URL.');

        if (is_wp_error($asset)) {
            return $asset;
        }

        return [
            'url'        => (string) $asset['url'],
            'static_url' => (string) ($asset['static_url'] ?? ''),
            'path'       => (string) $asset['local_path'],
            'mime'       => (string) ($asset['mime'] ?? 'image/jpeg'),
        ];
    }

    /**
     * Remove a temporary image created for Facebook publishing.
     *
     * @param string $path
     * @return void
     */
    private function delete_temp_image($path) {
        $path = (string) $path;
        if ($path !== '' && file_exists($path) && is_file($path)) {
            wp_delete_file($path);
        }
    }

    /**
     * Build a safe endpoint label for logs. Never include tokens or query params.
     *
     * @param string $path Graph API path.
     * @return string
     */
    private function safe_endpoint_label($path) {
        $path = '/' . ltrim((string) $path, '/');
        $path = preg_replace('/\?.*$/', '', $path);
        return (string) $path;
    }

    private function request($method, $path, $access_token, $params = []) {
        $url = rtrim($this->api_base, '/') . '/' . ltrim($path, '/');
        $endpoint = $this->safe_endpoint_label($path);

        $args = [
            'method'  => strtoupper($method),
            'timeout' => 60,
        ];

        // Graph likes urlencoded form posts.
        $params['access_token'] = $access_token;

        if ($args['method'] === 'GET') {
            $url = add_query_arg($params, $url);
        } else {
            $args['headers'] = ['Content-Type' => 'application/x-www-form-urlencoded'];
            $args['body'] = http_build_query($params, '', '&');
        }

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

        if (is_wp_error($res)) {
            $this->log_transport_failure($endpoint, $res);
            return $res;
        }

        $code = (int) wp_remote_retrieve_response_code($res);
        $raw  = (string) wp_remote_retrieve_body($res);
        $json = null;
        if ($raw) {
            $json = json_decode($raw, true);
        }

        return [
            'code'      => $code,
            'body_raw'  => $raw,
            'body_json' => $json,
            // Kept for backward compatibility inside this class, but never log it.
            'url'       => $url,
            'endpoint'  => $endpoint,
            'method'    => $args['method'],
        ];
    }

    /**
     * Send a Graph API multipart/form-data request with a local image file.
     *
     * This avoids the fragile "Facebook must fetch our public URL" path for Page
     * photos. It is especially useful on hosts/CDNs that block Meta's image crawler
     * from wp-content/uploads URLs.
     *
     * @param string              $path         Graph API path.
     * @param string              $access_token Page access token.
     * @param array<string,mixed> $params       Form fields, without access_token.
     * @param string              $file_path    Absolute image file path.
     * @param string              $mime         Image MIME type.
     * @return array<string,mixed>|WP_Error
     */
    private function request_multipart_file($path, $access_token, array $params, $file_path, $mime = 'image/jpeg') {
        $file_path = (string) $file_path;
        if ($file_path === '' || !file_exists($file_path) || !is_file($file_path) || !is_readable($file_path)) {
            return new WP_Error('facebook_media_error', 'Facebook could not read the prepared image file.');
        }

        $file_contents = file_get_contents($file_path);
        if ($file_contents === false || $file_contents === '') {
            return new WP_Error('facebook_media_error', 'Facebook could not read the prepared image file.');
        }

        $url = rtrim($this->api_base, '/') . '/' . ltrim($path, '/');
        $endpoint = $this->safe_endpoint_label($path);
        $boundary = 'calliope_sp_' . wp_generate_password(24, false, false);
        $eol = "\r\n";
        $body = '';
        $params['access_token'] = $access_token;

        foreach ($params as $key => $value) {
            if ($value === null) {
                continue;
            }
            $name = preg_replace('/[^A-Za-z0-9_\-]/', '', (string) $key);
            if ($name === '') {
                continue;
            }
            $body .= '--' . $boundary . $eol;
            $body .= 'Content-Disposition: form-data; name="' . $name . '"' . $eol . $eol;
            $body .= (string) $value . $eol;
        }

        $filename = sanitize_file_name(basename($file_path));
        $mime = strtolower((string) $mime);
        if (strpos($mime, 'image/') !== 0) {
            $mime = 'image/jpeg';
        }

        $body .= '--' . $boundary . $eol;
        $body .= 'Content-Disposition: form-data; name="source"; filename="' . $filename . '"' . $eol;
        $body .= 'Content-Type: ' . $mime . $eol . $eol;
        $body .= $file_contents . $eol;
        $body .= '--' . $boundary . '--' . $eol;

        $res = wp_remote_request($url, [
            'method'  => 'POST',
            'timeout' => 120,
            'headers' => [
                'Content-Type'   => 'multipart/form-data; boundary=' . $boundary,
                'Content-Length' => strlen($body),
            ],
            'body'    => $body,
        ]);

        if (is_wp_error($res)) {
            $this->log_transport_failure($endpoint, $res);
            return $res;
        }

        $code = (int) wp_remote_retrieve_response_code($res);
        $raw  = (string) wp_remote_retrieve_body($res);
        $json = null;
        if ($raw !== '') {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) {
                $json = $decoded;
            }
        }

        return [
            'code'      => $code,
            'body_raw'  => $raw,
            'body_json' => $json,
            'url'       => $url,
            'endpoint'  => $endpoint,
            'method'    => 'POST',
        ];
    }

    /**
     * Extract Graph error data in a consistent shape.
     *
     * @param array<string,mixed> $resp
     * @return array<string,mixed>
     */
    private function graph_error_details($resp) {
        $error = [];
        if (is_array($resp) && isset($resp['body_json']['error']) && is_array($resp['body_json']['error'])) {
            $error = $resp['body_json']['error'];
        }

        return [
            'endpoint'            => (string) ($resp['endpoint'] ?? ''),
            'http_code'           => (int) ($resp['code'] ?? 0),
            'graph_error_message' => (string) ($error['message'] ?? ($resp['body_raw'] ?? 'Unknown error')),
            'graph_error_type'    => (string) ($error['type'] ?? ''),
            'graph_error_code'    => isset($error['code']) ? (int) $error['code'] : 0,
            'graph_error_subcode' => isset($error['error_subcode']) ? (int) $error['error_subcode'] : (isset($error['subcode']) ? (int) $error['subcode'] : 0),
            'fbtrace_id'          => (string) ($error['fbtrace_id'] ?? ''),
        ];
    }

    /**
     * Detect true identity/security checkpoint messages. Do not infer this from code 368 alone.
     *
     * @param string $message
     * @return bool
     */
    private function has_identity_confirmation_signal($message) {
        $message = (string) $message;
        if ($message === '') {
            return false;
        }

        $signals = [
            'confirm your identity',
            'con' . 'firma tu iden' . 'tidad',
            'identity confirmation',
            'checkpoint',
            'security check',
            'page publishing authorization',
            'account verification',
            'verify your identity',
            'veri' . 'fica tu iden' . 'tidad',
            'veri' . 'ficacion de iden' . 'tidad',
            'facebook mobile app',
            'app mo' . 'vil de face' . 'book',
            'confirm your account or phone',
            'con' . 'firmar tu cue' . 'nta o tu cel' . 'ular',
            'con' . 'firma tu cue' . 'nta o tu cel' . 'ular',
        ];

        foreach ($signals as $signal) {
            if (stripos($message, $signal) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Detect permission errors related to Page publishing.
     *
     * @param array<string,mixed> $details
     * @return bool
     */
    private function is_missing_publish_permission($details) {
        $message = (string) ($details['graph_error_message'] ?? '');
        $code = (int) ($details['graph_error_code'] ?? 0);

        if (stripos($message, 'pages_manage_posts') !== false || stripos($message, 'manage_posts') !== false) {
            return true;
        }

        if (
            stripos($message, 'permission') !== false &&
            (
                stripos($message, 'publish') !== false ||
                stripos($message, 'manage') !== false ||
                stripos($message, 'page') !== false
            )
        ) {
            return true;
        }

        // Common Meta OAuth/permission failure codes. Keep these narrower than 368.
        return in_array($code, [10, 200, 2500], true);
    }

    /**
     * Detect image/media failures so they do not get mixed with identity or generic restrictions.
     *
     * @param array<string,mixed> $details
     * @return bool
     */
    private function is_media_error($details) {
        $message = (string) ($details['graph_error_message'] ?? '');
        $endpoint = (string) ($details['endpoint'] ?? '');

        if (stripos($endpoint, '/photos') === false) {
            return false;
        }

        $signals = [
            'image',
            'photo',
            'media',
            'picture',
            'url supplied is invalid',
            'could not fetch',
            'could not download',
            'invalid url',
            'unsupported get request',
        ];

        foreach ($signals as $signal) {
            if (stripos($message, $signal) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Classify Graph failures without treating every code 368 as identity confirmation.
     *
     * @param array<string,mixed> $resp
     * @return WP_Error
     */
    private function build_graph_wp_error($resp) {
        $details = $this->graph_error_details($resp);
        $message = (string) ($details['graph_error_message'] ?? 'Unknown error');
        $type = (string) ($details['graph_error_type'] ?? '');
        $code = (int) ($details['graph_error_code'] ?? 0);

        $classification = 'facebook_graph_error';
        $user_message = 'Facebook publish failed: ' . trim($message . ' ' . $type . ' ' . $code);

        if ($this->has_identity_confirmation_signal($message)) {
            $classification = 'facebook_identity_confirmation';
            $user_message = 'Facebook requires identity confirmation before publishing to this Page. Open the Facebook mobile app with the connected account and follow the instructions to confirm your account or phone. After completing it, disconnect and reconnect Facebook in the plugin, then try again.';
        } elseif ($this->is_missing_publish_permission($details)) {
            $classification = 'facebook_missing_publish_permission';
            $user_message = 'Facebook is connected, but the Page token does not include permission to publish posts. Please disconnect and reconnect Facebook, making sure the selected Page is allowed and pages_manage_posts is granted.';
        } elseif ($this->is_media_error($details)) {
            $classification = 'facebook_media_error';
            $user_message = 'Facebook rejected the image/media for this post. The Page is connected, but Meta could not process the image URL or media payload. Please try a different image or publish a text-only test.';
        } elseif ($code === 368) {
            $classification = 'facebook_action_restricted';
            $user_message = 'Facebook rejected this publish request due to a publishing restriction or temporary action block. The Page is connected, but Meta did not allow this specific post to be published. Please try a simpler text-only post, wait a while, or review the app/Page permissions.';
        }

        $details['classification'] = $classification;
        $this->log_graph_failure((string) ($details['endpoint'] ?? ''), $details);

        return new WP_Error($classification, $user_message, $details);
    }

    /**
     * Log transport failures without tokens.
     *
     * @param string   $endpoint
     * @param WP_Error $error
     * @return void
     */
    private function log_transport_failure($endpoint, $error) {
        if (!class_exists('Calliope_SP_Logger')) {
            return;
        }

        Calliope_SP_Logger::log('facebook', 'error', wp_json_encode([
            'endpoint'       => $endpoint,
            'transport_error'=> $error->get_error_message(),
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    /**
     * Log Graph failures without tokens.
     *
     * @param string              $endpoint
     * @param array<string,mixed> $details
     * @return void
     */
    private function log_graph_failure($endpoint, $details) {
        if (!class_exists('Calliope_SP_Logger')) {
            return;
        }

        Calliope_SP_Logger::log('facebook', 'error', wp_json_encode([
            'graph_endpoint'      => $endpoint !== '' ? $endpoint : (string) ($details['endpoint'] ?? ''),
            'http_code'           => (int) ($details['http_code'] ?? 0),
            'graph_error_message' => (string) ($details['graph_error_message'] ?? ''),
            'graph_error_type'    => (string) ($details['graph_error_type'] ?? ''),
            'graph_error_code'    => (int) ($details['graph_error_code'] ?? 0),
            'graph_error_subcode' => (int) ($details['graph_error_subcode'] ?? 0),
            'fbtrace_id'          => (string) ($details['fbtrace_id'] ?? ''),
            'classification'      => (string) ($details['classification'] ?? ''),
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    /**
     * Publish to a Facebook Page.
     *
     * - If image is provided: publish as photo (with caption)
     * - Else if link is provided: publish as feed post with link
     * - Else: publish as text post
     *
     * @param array $data
     * @return true|WP_Error
     */
    public function publish($data) {
        $page_id = (string) get_option('calliope_sp_fb_page_id');
        $page_token = (string) get_option('calliope_sp_fb_page_access_token');

        if (!$page_id || !$page_token) {
            return new WP_Error('facebook_missing_page_token', 'Facebook is connected, but no publishable Page token was found.');
        }

        $text = trim((string)($data['content'] ?? ''));
        $link = trim((string)($data['link'] ?? ''));
        $hashtags = trim((string)($data['hashtags'] ?? ''));
        $alt_text = trim((string)($data['alt_text'] ?? ''));
        $img_b64 = (string)($data['image_b64'] ?? '');

        $message_parts = [];
        if ($text) $message_parts[] = $text;
        if ($link) $message_parts[] = $link; // keep link also in message when posting photo
        if ($hashtags) $message_parts[] = $hashtags;
        $message = trim(implode("\n\n", $message_parts));

        // Image -> /photos
        if ($img_b64) {
            $image_asset = $this->image_url_from_b64($img_b64);
            if (is_wp_error($image_asset)) return $image_asset;

            $resp = $this->request_multipart_file(
                '/' . rawurlencode($page_id) . '/photos',
                $page_token,
                [
                    'caption'   => $message,
                    'published' => 'true',
                ],
                $image_asset['path'],
                $image_asset['mime'] ?? 'image/jpeg'
            );

            if (is_wp_error($resp)) {
                $this->delete_temp_image($image_asset['path']);
                return $resp;
            }

            if ($resp['code'] < 200 || $resp['code'] >= 300) {
                $this->delete_temp_image($image_asset['path']);
                return $this->build_graph_wp_error($resp);
            }

            $photo_id = $resp['body_json']['id'] ?? $resp['body_json']['post_id'] ?? null;

            // Optional alt text (best-effort)
            if ($alt_text && $photo_id) {
                $this->request('POST', '/' . rawurlencode($photo_id), $page_token, [
                    'alt_text_custom' => $alt_text,
                ]);
                // Ignore failures intentionally: publishing already succeeded.
            }

            $this->delete_temp_image($image_asset['path']);
            return true;
        }

        // Link or text -> /feed
        $params = [
            'message' => $message,
        ];
        if ($link) $params['link'] = $link;

        $resp = $this->request('POST', '/' . rawurlencode($page_id) . '/feed', $page_token, $params);

        if (is_wp_error($resp)) return $resp;

        if ($resp['code'] < 200 || $resp['code'] >= 300) {
            return $this->build_graph_wp_error($resp);
        }

        return true;
    }

    /**
     * Test connection: fetch page name.
     */
    public function test_connection() {
        $page_id = (string) get_option('calliope_sp_fb_page_id');
        $page_token = (string) get_option('calliope_sp_fb_page_access_token');

        if (!$page_id || !$page_token) {
            return new WP_Error('cfg', 'Facebook Page is not connected.');
        }

        $resp = $this->request('GET', '/' . rawurlencode($page_id), $page_token, [
            'fields' => 'id,name',
        ]);

        if (is_wp_error($resp)) return $resp;

        if ($resp['code'] < 200 || $resp['code'] >= 300) {
            return $this->build_graph_wp_error($resp);
        }

        return $resp['body_json'] ?? true;
    }

    /**
     * Diagnose the saved Facebook Page token and permissions.
     *
     * @return array<string,mixed>|WP_Error
     */
    public function diagnose_page_token() {
        $page_id = (string) get_option('calliope_sp_fb_page_id');
        $page_token = (string) get_option('calliope_sp_fb_page_access_token');

        if (!$page_id || !$page_token) {
            return new WP_Error('facebook_missing_page_token', 'Facebook is connected, but no publishable Page token was found.');
        }

        $page_resp = $this->request('GET', '/' . rawurlencode($page_id), $page_token, [
            'fields' => 'id,name',
        ]);

        if (is_wp_error($page_resp)) {
            return $page_resp;
        }

        if ($page_resp['code'] < 200 || $page_resp['code'] >= 300) {
            return $this->build_graph_wp_error($page_resp);
        }

        $page_data = is_array($page_resp['body_json'] ?? null) ? $page_resp['body_json'] : [];
        $diagnostic = [
            'ok'                         => true,
            'page_id'                    => (string) ($page_data['id'] ?? $page_id),
            'page_name'                  => (string) ($page_data['name'] ?? ''),
            'page_lookup_ok'             => true,
            'debug_token_checked'        => false,
            'debug_token_valid'          => null,
            'granted_scopes'             => [],
            'required_scopes'            => ['pages_manage_posts', 'pages_read_engagement', 'pages_show_list'],
            'missing_required_scopes'    => [],
            'permissions_status_message' => 'Page lookup works. Token scopes could not be checked because debug_token is unavailable or not configured.',
        ];

        $app_id = (string) get_option('calliope_sp_fb_app_id');
        $app_secret = (string) get_option('calliope_sp_fb_app_secret');

        if ($app_id !== '' && $app_secret !== '') {
            $debug_resp = $this->request('GET', '/debug_token', $app_id . '|' . $app_secret, [
                'input_token' => $page_token,
            ]);

            if (!is_wp_error($debug_resp) && $debug_resp['code'] >= 200 && $debug_resp['code'] < 300) {
                $debug_data = $debug_resp['body_json']['data'] ?? [];
                $scopes = [];

                if (!empty($debug_data['scopes']) && is_array($debug_data['scopes'])) {
                    $scopes = array_merge($scopes, array_map('strval', $debug_data['scopes']));
                }

                if (!empty($debug_data['granular_scopes']) && is_array($debug_data['granular_scopes'])) {
                    foreach ($debug_data['granular_scopes'] as $granular_scope) {
                        if (is_array($granular_scope) && !empty($granular_scope['scope'])) {
                            $scopes[] = (string) $granular_scope['scope'];
                        }
                    }
                }

                $scopes = array_values(array_unique(array_filter($scopes)));
                $missing = [];
                if (!empty($scopes)) {
                    $missing = array_values(array_diff($diagnostic['required_scopes'], $scopes));
                }

                $diagnostic['debug_token_checked'] = true;
                $diagnostic['debug_token_valid'] = !empty($debug_data['is_valid']);
                $diagnostic['granted_scopes'] = $scopes;
                $diagnostic['missing_required_scopes'] = $missing;

                if (empty($scopes)) {
                    $diagnostic['permissions_status_message'] = 'debug_token worked, but Meta did not return token scopes for this Page token. Permissions are unknown, not failed.';
                } else {
                    $diagnostic['permissions_status_message'] = empty($missing)
                        ? 'Page token includes the expected Page publishing permissions.'
                        : 'Page token is valid, but one or more expected Page permissions were not visible in debug_token.';
                }

                if (!empty($scopes) && in_array('pages_manage_posts', $missing, true)) {
                    $details = $diagnostic;
                    $details['classification'] = 'facebook_missing_publish_permission';
                    if (class_exists('Calliope_SP_Logger')) {
                        Calliope_SP_Logger::log('facebook', 'warning', wp_json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
                    }
                    return new WP_Error(
                        'facebook_missing_publish_permission',
                        'Facebook is connected, but the Page token does not include permission to publish posts. Please disconnect and reconnect Facebook, making sure the selected Page is allowed and pages_manage_posts is granted.',
                        $details
                    );
                }
            } elseif (!is_wp_error($debug_resp)) {
                $details = $this->graph_error_details($debug_resp);
                $diagnostic['debug_token_checked'] = false;
                $diagnostic['debug_token_error'] = [
                    'http_code'           => (int) ($details['http_code'] ?? 0),
                    'graph_error_message' => (string) ($details['graph_error_message'] ?? ''),
                    'graph_error_type'    => (string) ($details['graph_error_type'] ?? ''),
                    'graph_error_code'    => (int) ($details['graph_error_code'] ?? 0),
                    'graph_error_subcode' => (int) ($details['graph_error_subcode'] ?? 0),
                    'fbtrace_id'          => (string) ($details['fbtrace_id'] ?? ''),
                ];
            }
        }

        if (class_exists('Calliope_SP_Logger')) {
            Calliope_SP_Logger::log('facebook', 'info', wp_json_encode($diagnostic, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        }

        return $diagnostic;
    }

    /**
     * Run a real Facebook publish test against the connected Page.
     *
     * @param string $mode text|link|image
     * @return true|WP_Error
     */
    public function run_publish_test($mode = 'text') {
        $mode = sanitize_key((string) $mode);
        if (!in_array($mode, ['text', 'link', 'image'], true)) {
            $mode = 'text';
        }

        $timestamp = current_time('mysql');
        $payload = [
            'content'  => 'Calliope Social Publisher Facebook test post. You can delete this post. Time: ' . $timestamp,
            'link'     => '',
            'hashtags' => '',
            'alt_text' => 'Calliope Social Publisher test image',
        ];

        if ($mode === 'link') {
            $payload['link'] = home_url('/');
        }

        if ($mode === 'image') {
            // Small 1x1 PNG test image.
            $payload['image_b64'] = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
        }

        return $this->publish($payload);
    }
}
