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

class Calliope_SP_Mastodon {

    private function normalize_instance($instance) {
        $instance = trim((string) $instance);

        if ($instance === '') {
            return '';
        }

        if (preg_match('/@[^@\s]+@([^@\s\/]+)/', $instance, $m)) {
            $instance = $m[1];
        }

        if (preg_match('#^https?://#i', $instance)) {
            $host = wp_parse_url($instance, PHP_URL_HOST);
            if (!empty($host)) {
                $instance = (string) $host;
            }
        }

        $instance = preg_replace('#^https?://#i', '', $instance);
        $instance = preg_replace('#^www\.#i', '', $instance);
        $instance = trim($instance);
        $instance = preg_replace('#/.*$#', '', $instance);
        $instance = rtrim($instance, "/ \t\n\r\0\x0B");

        return sanitize_text_field($instance);
    }

    public function test_connection() {
        $instance = $this->normalize_instance(get_option('calliope_sp_mstdn_instance'));
        $token = get_option('calliope_sp_mstdn_token');

        if (!$instance || !$token) {
            return new WP_Error('cfg', __('Missing Mastodon instance or token.', 'calliope-social-publisher'));
        }

        $res = wp_remote_get("https://{$instance}/api/v1/accounts/verify_credentials", [
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Accept'        => 'application/json',
            ],
            'timeout' => 30,
        ]);

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

        $code = (int) wp_remote_retrieve_response_code($res);
        if ($code < 200 || $code >= 300) {
            $message = $this->extract_api_error(wp_remote_retrieve_body($res));
            if ($message === '') {
                $message = sprintf(
                    /* translators: %d: HTTP status code returned by the Mastodon API. */
                    __('Error (%d): could not validate the token.', 'calliope-social-publisher'),
                    $code
                );
            } else {
                $message = sprintf(
                    /* translators: 1: HTTP status code returned by the Mastodon API, 2: API error message. */
                    __('Error (%1$d): %2$s', 'calliope-social-publisher'),
                    $code,
                    $message
                );
            }
            return new WP_Error('mastodon', $message);
        }

        return true;
    }

    public function publish($data) {
        $instance = $this->normalize_instance(get_option('calliope_sp_mstdn_instance'));
        $token = get_option('calliope_sp_mstdn_token');

        if (!$instance || !$token) {
            return new WP_Error('cfg', __('Missing Mastodon configuration.', 'calliope-social-publisher'));
        }

        $text = $this->format_text_clean_tags($data, 490);

        $media_id = null;
        if (!empty($data['image_b64'])) {
            $media_id = $this->upload_media($instance, $token, $data['image_b64'], $data['alt_text'] ?? '');
            if (is_wp_error($media_id)) {
                return $media_id;
            }
        }

        $body = [
            'status'     => $text,
            'visibility' => 'public',
        ];

        if ($media_id) {
            $body['media_ids'] = [(string) $media_id];
        }

        $res = wp_remote_post("https://{$instance}/api/v1/statuses", [
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type'  => 'application/json',
                'Accept'        => 'application/json',
            ],
            'body'    => wp_json_encode($body),
            'timeout' => 45,
        ]);

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

        $code = (int) wp_remote_retrieve_response_code($res);
        if ($code < 200 || $code >= 300) {
            $message = $this->extract_api_error(wp_remote_retrieve_body($res));
            if ($message === '') {
                $message = sprintf(
                    /* translators: %d: HTTP status code returned by the Mastodon API. */
                    __('Error (%d): could not publish.', 'calliope-social-publisher'),
                    $code
                );
            } else {
                $message = sprintf(
                    /* translators: 1: HTTP status code returned by the Mastodon API, 2: API error message. */
                    __('Error (%1$d): %2$s', 'calliope-social-publisher'),
                    $code,
                    $message
                );
            }
            return new WP_Error('mastodon', $message);
        }

        return true;
    }

    private function upload_media($instance, $token, $b64, $alt) {
        $file = $this->decode_image_data($b64);
        if (is_wp_error($file)) {
            return $file;
        }

        $last_error = null;
        foreach (['/api/v2/media', '/api/v1/media'] as $endpoint) {
            $result = $this->request_media_upload($instance, $token, $endpoint, $file['binary'], $file['filename'], $file['mime'], $alt);
            if (is_wp_error($result)) {
                if ($result->get_error_code() === 'mastodon_media_retry') {
                    $last_error = $result;
                    continue;
                }
                return $result;
            }

            return $result;
        }

        return $last_error instanceof WP_Error
            ? $last_error
            : new WP_Error('mastodon', __('Could not upload the image.', 'calliope-social-publisher'));
    }

    private function decode_image_data($b64) {
        $mime_type = 'image/jpeg';
        $filename = 'upload.jpg';

        if (preg_match('#^data:([^;]+);base64,#i', $b64, $matches)) {
            $detected_mime = strtolower((string) $matches[1]);
            $allowed_mimes = [
                'image/jpeg' => 'jpg',
                'image/png'  => 'png',
                'image/webp' => 'webp',
                'image/gif'  => 'gif',
            ];

            if (isset($allowed_mimes[$detected_mime])) {
                $mime_type = $detected_mime;
                $filename = 'upload.' . $allowed_mimes[$detected_mime];
            }
        }

        if (strpos($b64, 'base64,') !== false) {
            $parts = explode('base64,', $b64, 2);
            $b64 = $parts[1];
        }

        $bin = base64_decode($b64, true);
        if ($bin === false || $bin === '') {
            return new WP_Error('img', __('Could not decode the image.', 'calliope-social-publisher'));
        }

        return [
            'binary'   => $bin,
            'mime'     => $mime_type,
            'filename' => $filename,
        ];
    }

    private function request_media_upload($instance, $token, $endpoint, $binary, $filename, $mime_type, $alt) {
        $boundary = '----CalliopeSPBoundary' . wp_generate_password(24, false, false);
        $eol = "\r\n";
        $body = '';

        $body .= '--' . $boundary . $eol;
        $body .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol;
        $body .= 'Content-Type: ' . $mime_type . $eol;
        $body .= 'Content-Transfer-Encoding: binary' . $eol . $eol;
        $body .= $binary . $eol;

        if (!empty($alt)) {
            $body .= '--' . $boundary . $eol;
            $body .= 'Content-Disposition: form-data; name="description"' . $eol . $eol;
            $body .= trim((string) $alt) . $eol;
        }

        $body .= '--' . $boundary . '--' . $eol;

        $res = wp_remote_post("https://{$instance}{$endpoint}", [
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Accept'        => 'application/json',
                'Content-Type'  => 'multipart/form-data; boundary=' . $boundary,
            ],
            'body'        => $body,
            'timeout'     => 60,
            'data_format' => 'body',
        ]);

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

        $http_code = (int) wp_remote_retrieve_response_code($res);
        $raw = (string) wp_remote_retrieve_body($res);
        $json = json_decode($raw, true);

        if (($http_code === 404 || $http_code === 405) && $endpoint === '/api/v2/media') {
            return new WP_Error('mastodon_media_retry', 'fallback-to-v1');
        }

        if ($http_code < 200 || $http_code >= 300) {
            $message = $this->extract_api_error($raw, $json);
            if ($message === '') {
                $message = sprintf(
                    /* translators: %d: HTTP status code returned by the Mastodon API. */
                    __('Error (%d): could not upload the image.', 'calliope-social-publisher'),
                    $http_code
                );
            } else {
                $message = sprintf(
                    /* translators: 1: HTTP status code returned by the Mastodon API, 2: API error message. */
                    __('Error (%1$d): %2$s', 'calliope-social-publisher'),
                    $http_code,
                    $message
                );
            }
            return new WP_Error('mastodon', $message);
        }

        if (!is_array($json) || empty($json['id'])) {
            return new WP_Error('mastodon', __('No media ID was returned from Mastodon.', 'calliope-social-publisher'));
        }

        if ($endpoint === '/api/v2/media' && $http_code === 202) {
            $this->wait_for_media_ready($instance, $token, (string) $json['id']);
        }

        return (string) $json['id'];
    }

    private function wait_for_media_ready($instance, $token, $media_id) {
        $attempts = 6;

        for ($i = 0; $i < $attempts; $i++) {
            usleep(600000);

            $res = wp_remote_get("https://{$instance}/api/v1/media/{$media_id}", [
                'headers' => [
                    'Authorization' => 'Bearer ' . $token,
                    'Accept'        => 'application/json',
                ],
                'timeout' => 20,
            ]);

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

            $http_code = (int) wp_remote_retrieve_response_code($res);
            if ($http_code < 200 || $http_code >= 300) {
                return;
            }

            $json = json_decode((string) wp_remote_retrieve_body($res), true);
            if (!is_array($json)) {
                return;
            }

            if (!empty($json['url']) || !empty($json['preview_url'])) {
                return;
            }
        }
    }

    private function extract_api_error($raw, $json = null) {
        if (!is_array($json)) {
            $json = json_decode((string) $raw, true);
        }

        if (is_array($json)) {
            foreach (['error_description', 'error', 'message'] as $key) {
                if (!empty($json[$key]) && is_string($json[$key])) {
                    return trim($json[$key]);
                }
            }

            if (!empty($json['errors']) && is_array($json['errors'])) {
                $first = reset($json['errors']);
                if (is_string($first) && $first !== '') {
                    return trim($first);
                }
            }
        }

        $raw = trim(wp_strip_all_tags((string) $raw));
        return $raw !== '' ? $raw : '';
    }

    /**
     * Format title, content, hashtags, and the link while respecting the limit.
     */
    private function format_text_clean_tags($data, $limit) {
        $title = trim((string) ($data['title'] ?? ''));
        $content = trim((string) ($data['content'] ?? ''));
        $link = trim((string) ($data['link'] ?? ''));
        $tags = !empty($data['hashtags']) ? preg_split('/\s+/', trim((string) $data['hashtags'])) : [];

        $tags_clean = [];
        foreach ($tags as $tag) {
            $tag = trim((string) $tag);
            if ($tag === '') {
                continue;
            }
            $tags_clean[] = $tag;
        }
        $hashtags_line = implode(' ', $tags_clean);

        $fixed_blocks = [];
        if ($title !== '') {
            $fixed_blocks[] = $title;
        }
        if ($hashtags_line !== '') {
            $fixed_blocks[] = $hashtags_line;
        }
        if ($link !== '') {
            $fixed_blocks[] = $link;
        }

        $reserved_length = 0;
        if (!empty($fixed_blocks)) {
            $reserved_length = array_sum(array_map('mb_strlen', $fixed_blocks));
            $reserved_length += count($fixed_blocks) + ($link !== '' ? 1 : 0);
        }

        $content_limit = max(0, $limit - $reserved_length);
        if ($content !== '' && mb_strlen($content) > $content_limit) {
            $content = $content_limit > 3 ? mb_substr($content, 0, $content_limit - 3) . '...' : '';
        }

        $blocks = [];
        foreach ([$title, $content, $hashtags_line] as $block) {
            if ($block !== '') {
                $blocks[] = $block;
            }
        }

        $text = implode("\n", $blocks);
        if ($link !== '') {
            $text = $text !== '' ? $text . "\n\n" . $link : $link;
        }

        if (mb_strlen($text) <= $limit) {
            return $text;
        }

        if ($hashtags_line !== '') {
            $tags_final = [];
            foreach ($tags_clean as $tag) {
                $candidate = implode(' ', array_merge($tags_final, [$tag]));
                $candidate_blocks = [];
                foreach ([$title, $content, $candidate] as $block) {
                    if ($block !== '') {
                        $candidate_blocks[] = $block;
                    }
                }
                $candidate_text = implode("\n", $candidate_blocks);
                if ($link !== '') {
                    $candidate_text = $candidate_text !== '' ? $candidate_text . "\n\n" . $link : $link;
                }
                if (mb_strlen($candidate_text) <= $limit) {
                    $tags_final[] = $tag;
                } else {
                    break;
                }
            }
            $hashtags_line = implode(' ', $tags_final);
        }

        $blocks = [];
        foreach ([$title, $content, $hashtags_line] as $block) {
            if ($block !== '') {
                $blocks[] = $block;
            }
        }

        $text = implode("\n", $blocks);
        if ($link !== '') {
            $text = $text !== '' ? $text . "\n\n" . $link : $link;
        }

        return $text;
    }
}
