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

/**
 * Builds a safe, provider-aware preview of the publication before it is queued.
 *
 * The preview intentionally mirrors the text assembly rules already used by the
 * bundled Free providers. It never calls a social API and never stores preview
 * data; it only returns a representation of the current Publisher form.
 */
class Calliope_SP_Platform_Preview {
    /** @return array<string,string> */
    public static function network_labels(): array {
        $labels = [
            'bluesky'   => __('Bluesky', 'calliope-social-publisher'),
            'mastodon'  => __('Mastodon', 'calliope-social-publisher'),
            'tumblr'    => __('Tumblr', 'calliope-social-publisher'),
            'pinterest' => __('Pinterest', 'calliope-social-publisher'),
            'threads'   => __('Threads', 'calliope-social-publisher'),
            'instagram' => __('Instagram', 'calliope-social-publisher'),
            'linkedin'  => __('LinkedIn', 'calliope-social-publisher'),
            'blogger'   => __('Blogger', 'calliope-social-publisher'),
            'telegram'  => __('Telegram', 'calliope-social-publisher'),
            'facebook'  => __('Facebook Page', 'calliope-social-publisher'),
            // Discord itself is supplied by Pro, but keeping its preview contract
            // known to Free makes the AJAX allow-list stable. Free never renders a
            // Discord destination on its own, so this does not expose a Pro feature.
            'discord'   => __('Discord', 'calliope-social-publisher'),
        ];

        /**
         * Allow compatible add-ons to expose additional destinations in Platform Preview
         * without making Free depend on Pro-only network classes.
         *
         * @param array<string,string> $labels
         */
        $filtered = apply_filters('calliope_sp_platform_preview_network_labels', $labels);
        return is_array($filtered) ? $filtered : $labels;
    }

    /** @return string[] */
    public static function allowed_networks(): array {
        return array_keys(self::network_labels());
    }

    /**
     * @param array<string,mixed> $payload
     * @param string[] $networks
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    public static function build(array $payload, array $networks, array $context = []): array {
        $image_present = !empty($context['image_present']);
        $media_type = sanitize_key((string) ($context['media_type'] ?? ($image_present ? 'image' : 'none')));
        $invalid_link = !empty($context['invalid_link']);
        if (!in_array($media_type, ['none', 'image', 'carousel', 'video'], true)) {
            $media_type = $image_present ? 'image' : 'none';
        }

        $items = [];
        $summary = ['ready' => 0, 'warning' => 0, 'invalid' => 0];
        foreach ($networks as $network) {
            $network = self::normalize_network((string) $network);
            if (!in_array($network, self::allowed_networks(), true)) {
                continue;
            }

            $network_payload = $payload;
            $network_context = [
                'post_id' => absint($context['post_id'] ?? 0),
                'user_id' => absint($context['user_id'] ?? get_current_user_id()),
                'platform_preview' => 1,
            ];
            if (class_exists('Calliope_SP_Tracking')) {
                $network_payload = Calliope_SP_Tracking::apply_to_payload($network_payload, $network, $network_context);
            }
            if (class_exists('Calliope_SP_Short_Link_Service')) {
                $network_payload = Calliope_SP_Short_Link_Service::apply_to_payload($network_payload, $network, $network_context);
            }
            if (class_exists('Calliope_SP_Social_Templates')) {
                $network_payload = Calliope_SP_Social_Templates::apply_to_payload($network_payload, $network, $network_context);
            }

            $item = self::build_network_preview($network_payload, $network, $media_type, $image_present, $invalid_link);
            /**
             * Let compatible add-ons replace/extend a network preview after Free has
             * applied tracking, short links and Social Templates.
             *
             * @param array<string,mixed> $item
             * @param array<string,mixed> $network_payload
             * @param string              $network
             * @param array<string,mixed> $preview_context
             */
            $filtered_item = apply_filters(
                'calliope_sp_platform_preview_item',
                $item,
                $network_payload,
                $network,
                [
                    'media_type' => $media_type,
                    'image_present' => $image_present,
                    'invalid_link' => $invalid_link,
                    'post_id' => absint($context['post_id'] ?? 0),
                    'user_id' => absint($context['user_id'] ?? get_current_user_id()),
                ]
            );
            if (is_array($filtered_item)) {
                $item = $filtered_item;
            }
            $items[$network] = $item;
            $state = sanitize_key((string) ($item['status'] ?? 'ready'));
            if (!isset($summary[$state])) {
                $state = 'ready';
            }
            $summary[$state]++;
        }

        return [
            'items' => $items,
            'summary' => $summary,
            'templates_enabled' => class_exists('Calliope_SP_Social_Templates') && Calliope_SP_Social_Templates::is_enabled(),
        ];
    }

    /** @return array<string,mixed> */
    private static function build_network_preview(array $payload, string $network, string $media_type, bool $image_present, bool $invalid_link): array {
        $label = self::network_labels()[$network] ?? ucfirst($network);
        $raw_title = trim((string) ($payload['title'] ?? ''));
        $raw_content = trim((string) ($payload['content'] ?? ''));
        $raw_link = trim((string) ($payload['link'] ?? ''));
        $raw_hashtags = trim((string) ($payload['hashtags'] ?? ''));
        $template_applied = !empty($payload['social_template_applied']);
        $template_text = trim((string) ($payload['social_template_text'] ?? ''));

        $formatted_hashtags = class_exists('Calliope_SP_Publish_Helper')
            ? Calliope_SP_Publish_Helper::format_hashtags($raw_hashtags, $network)
            : trim($raw_hashtags);

        $title = '';
        $text = '';
        $limit = 0;
        $was_trimmed = false;
        $provider_link = $raw_link;

        switch ($network) {
            case 'bluesky':
                $limit = 290;
                [$text, $was_trimmed] = self::format_clean_tags($raw_title, $raw_content, $formatted_hashtags, $raw_link, $limit, false);
                break;

            case 'mastodon':
                $limit = 490;
                [$text, $was_trimmed] = self::format_clean_tags($raw_title, $raw_content, $formatted_hashtags, $raw_link, $limit, true);
                break;

            case 'threads':
                $limit = 480;
                [$text, $was_trimmed] = self::format_threads($raw_title, $raw_content, $formatted_hashtags, $raw_link, $limit);
                break;

            case 'linkedin':
                $limit = 2900;
                $linkedin_text = trim(implode("\n\n", array_values(array_filter([$raw_title, $raw_content, $formatted_hashtags], static fn($v) => trim((string) $v) !== ''))));
                [$text, $was_trimmed] = self::truncate($linkedin_text, $limit);
                if ($raw_link !== '' && !self::text_contains_url($text, $raw_link)) {
                    $text = $text !== '' ? $text . "\n\n" . $raw_link : $raw_link;
                }
                break;

            case 'instagram':
                $text = trim(implode("\n\n", array_values(array_filter([$raw_content, $raw_link, $formatted_hashtags], static fn($v) => trim((string) $v) !== ''))));
                break;

            case 'facebook':
                $text = $raw_content;
                if ($raw_link !== '' && !self::text_contains_url($text, $raw_link)) {
                    $text = $text !== '' ? $text . "\n\n" . $raw_link : $raw_link;
                }
                if ($formatted_hashtags !== '') {
                    $text = $text !== '' ? $text . "\n\n" . $formatted_hashtags : $formatted_hashtags;
                }
                break;

            case 'telegram':
                $limit = $image_present || $media_type === 'video' || $media_type === 'carousel' ? 1000 : 3900;
                [$text, $was_trimmed] = self::format_telegram($raw_title, $raw_content, $formatted_hashtags, $raw_link, $limit);
                break;

            case 'pinterest':
                $title = self::truncate_plain($raw_title, 100);
                $limit = 500;
                [$text, $was_trimmed] = self::truncate(trim($raw_content . ($formatted_hashtags !== '' ? "\n\n" . $formatted_hashtags : '')), $limit);
                break;

            case 'blogger':
                $title = $raw_title;
                $text = $raw_content;
                if ($raw_link !== '' && !self::text_contains_url($text, $raw_link)) {
                    $text = $text !== '' ? $text . "\n\n" . $raw_link : $raw_link;
                }
                if ($formatted_hashtags !== '') {
                    $text = $text !== '' ? $text . "\n\n" . $formatted_hashtags : $formatted_hashtags;
                }
                break;

            case 'tumblr':
                $title = $raw_title;
                $text = $raw_content;
                if ($raw_link !== '') {
                    $text = $text !== '' ? $text . "\n\n" . $raw_link : $raw_link;
                }
                // Tumblr stores hashtags as post tags instead of visible caption text.
                break;

            case 'discord':
                // Pro replaces this item with its exact Discord payload preview via
                // calliope_sp_platform_preview_item. This provider-free fallback is
                // intentional so an AJAX request can never silently drop Discord if
                // add-on hook timing changes.
                $limit = 1900;
                $discord_text = trim(implode("\n\n", array_values(array_filter(
                    [$raw_title, $raw_content],
                    static fn($v) => trim((string) $v) !== ''
                ))));
                if ($raw_link !== '' && !self::text_contains_url($discord_text, $raw_link)) {
                    $discord_text = $discord_text !== '' ? $discord_text . "\n\n" . $raw_link : $raw_link;
                }
                [$text, $was_trimmed] = self::truncate($discord_text, $limit);
                // Discord has no discoverable hashtag field; hashtags are plain text
                // and the Pro payload intentionally omits the dedicated hashtag input.
                break;
        }

        // When a Social Template was applied, most providers receive the rendered
        // result as their final caption/body. Preserve provider-specific title/link
        // fields only where the adapter intentionally keeps them separate.
        if ($template_applied && $template_text !== '') {
            if ($network === 'pinterest') {
                $text = self::truncate_plain($template_text, 500);
                $was_trimmed = self::length($template_text) > 500;
                $title = self::truncate_plain((string) ($payload['title'] ?? ''), 100);
            } elseif ($network === 'blogger') {
                $text = $template_text;
                $title = trim((string) ($payload['title'] ?? ''));
            } elseif ($network === 'tumblr') {
                $text = $template_text;
                $title = '';
            } else {
                $text = $template_text;
                if ($limit > 0 && self::length($text) > $limit) {
                    [$text, $was_trimmed] = self::truncate($text, $limit);
                }
                $title = '';
            }
            $provider_link = trim((string) ($payload['link'] ?? ''));
        }

        $effective_link = $provider_link;
        if ($effective_link === '') {
            $effective_link = self::first_url($text);
        }
        $hashtags = self::hashtags_from_preview($text, $formatted_hashtags, $network, $template_applied);
        $tracked_link = trim((string) ($payload['utm_tracking_post_link'] ?? ''));
        $short_link = trim((string) ($payload['short_link_post_link'] ?? ''));
        $short_link_applied = !empty($payload['short_link_applied']) && $short_link !== '';
        $tracking_applied = !empty($payload['utm_tracking_applied']) && $tracked_link !== '' && (
            $short_link_applied ||
            $effective_link === $tracked_link ||
            strpos($text, $tracked_link) !== false ||
            trim((string) ($payload['link'] ?? '')) === $tracked_link
        );

        $messages = [];
        $status = 'ready';
        if ($invalid_link) {
            $status = 'invalid';
            $messages[] = __('The link is not a valid URL.', 'calliope-social-publisher');
        }
        if ($network === 'instagram' && !$image_present && $media_type !== 'carousel' && $media_type !== 'video') {
            $status = 'invalid';
            $messages[] = __('Instagram requires an image for this publication.', 'calliope-social-publisher');
        }
        if ($network === 'pinterest' && !$image_present && $media_type !== 'carousel') {
            $status = 'invalid';
            $messages[] = __('Pinterest requires an image for this publication.', 'calliope-social-publisher');
        }

        $has_publishable_content = trim($title . $text . $effective_link . $hashtags) !== '' || $image_present || in_array($media_type, ['carousel', 'video'], true);
        if (!$has_publishable_content) {
            $status = 'invalid';
            $messages[] = __('Add text, a link, or media before publishing.', 'calliope-social-publisher');
        }

        if ($network === 'blogger' && trim($title) === '') {
            if ($status !== 'invalid') $status = 'warning';
            $messages[] = __('Blogger works best with a post title.', 'calliope-social-publisher');
        }
        if ($network === 'pinterest' && trim($title) === '') {
            if ($status !== 'invalid') $status = 'warning';
            $messages[] = __('Pinterest works best with a Pin title.', 'calliope-social-publisher');
        }
        if ($was_trimmed) {
            if ($status !== 'invalid') $status = 'warning';
            $messages[] = sprintf(
                /* translators: %d: character limit. */
                __('This network will shorten the text to %d characters.', 'calliope-social-publisher'),
                $limit
            );
        }

        if (empty($messages)) {
            $messages[] = __('Ready to publish.', 'calliope-social-publisher');
        }

        return [
            'network' => $network,
            'label' => $label,
            'status' => $status,
            'status_label' => self::status_label($status),
            'messages' => $messages,
            'title' => $title,
            'text' => $text,
            'link' => $effective_link,
            'hashtags' => $hashtags,
            'media_type' => $media_type,
            'character_count' => self::length($text),
            'character_limit' => $limit,
            'template_applied' => $template_applied,
            'tracking_applied' => $tracking_applied,
            'short_link_applied' => $short_link_applied,
        ];
    }

    /** @return array{0:string,1:bool} */
    private static function format_clean_tags(string $title, string $content, string $hashtags, string $link, int $limit, bool $double_link_separator): array {
        $title = trim($title);
        $content = trim($content);
        $link = trim($link);
        $tags = preg_split('/\s+/u', trim($hashtags));
        $tags = is_array($tags) ? array_values(array_filter(array_map('trim', $tags))) : [];
        $original_content = $content;
        $original_tags = $tags;
        $hashtags_line = implode(' ', $tags);

        $fixed = array_values(array_filter([$title, $hashtags_line, $link], static fn($v) => $v !== ''));
        $reserved = 0;
        foreach ($fixed as $block) $reserved += self::length($block);
        if (!empty($fixed)) $reserved += count($fixed) + ($double_link_separator && $link !== '' ? 1 : 0);
        $content_limit = max(0, $limit - $reserved);
        if ($content !== '' && self::length($content) > $content_limit) {
            $content = $content_limit > 3 ? self::substr($content, 0, $content_limit - 3) . '...' : '';
        }

        $build = static function (string $candidate_tags) use ($title, $content, $link, $double_link_separator): string {
            $blocks = array_values(array_filter([$title, $content, $candidate_tags], static fn($v) => $v !== ''));
            $text = implode("\n", $blocks);
            if ($link !== '') {
                $sep = $double_link_separator ? "\n\n" : "\n";
                $text = $text !== '' ? $text . $sep . $link : $link;
            }
            return $text;
        };

        $text = $build($hashtags_line);
        if (self::length($text) > $limit && $hashtags_line !== '') {
            $kept = [];
            foreach ($tags as $tag) {
                $candidate = implode(' ', array_merge($kept, [$tag]));
                if (self::length($build($candidate)) <= $limit) $kept[] = $tag; else break;
            }
            $tags = $kept;
            $hashtags_line = implode(' ', $tags);
            $text = $build($hashtags_line);
        }
        $changed = $content !== $original_content || $tags !== $original_tags;
        if (self::length($text) > $limit) { [$text] = self::truncate($text, $limit); $changed = true; }
        return [$text, $changed];
    }

    /** @return array{0:string,1:bool} */
    private static function format_threads(string $title, string $content, string $hashtags, string $link, int $limit): array {
        $title = trim($title); $content = trim($content); $link = trim($link);
        $link_to_append = ($link !== '' && !self::text_contains_url($title . "\n" . $content, $link)) ? $link : '';
        $tags = preg_split('/\s+/u', trim($hashtags));
        $tags = is_array($tags) ? array_values(array_filter(array_map('trim', $tags))) : [];
        $original_content = $content; $original_tags = $tags;
        while (!empty($tags)) {
            $hashtags_line = implode(' ', $tags);
            $fixed = array_values(array_filter([$title, $hashtags_line, $link_to_append], static fn($v) => $v !== ''));
            $fixed_length = 0; foreach ($fixed as $block) $fixed_length += self::length($block);
            $separator_length = $content !== '' ? 2 * count($fixed) : 2 * max(0, count($fixed) - 1);
            if (($fixed_length + $separator_length) <= $limit) break;
            array_pop($tags);
        }
        $hashtags_line = implode(' ', $tags);
        $fixed = array_values(array_filter([$title, $hashtags_line, $link_to_append], static fn($v) => $v !== ''));
        $fixed_length = 0; foreach ($fixed as $block) $fixed_length += self::length($block);
        $separator_length = $content !== '' ? 2 * count($fixed) : 2 * max(0, count($fixed) - 1);
        $content_limit = max(0, $limit - $fixed_length - $separator_length);
        if ($content !== '' && self::length($content) > $content_limit) {
            if ($content_limit > 3) $content = rtrim(self::substr($content, 0, $content_limit - 3)) . '...';
            elseif ($content_limit > 0) $content = self::substr($content, 0, $content_limit);
            else $content = '';
        }
        $blocks = array_values(array_filter([$title, $content, $hashtags_line, $link_to_append], static fn($v) => $v !== ''));
        $text = implode("\n\n", $blocks);
        $changed = $content !== $original_content || $tags !== $original_tags;
        if (self::length($text) > $limit) { $text = self::substr($text, 0, $limit); $changed = true; }
        return [$text, $changed];
    }

    /** @return array{0:string,1:bool} */
    private static function format_telegram(string $title, string $content, string $hashtags, string $link, int $limit): array {
        $title = trim($title); $content = trim($content); $hashtags = trim($hashtags); $link = trim($link);
        $original_content = $content;
        $link_to_append = ($link !== '' && !self::text_contains_url($title . "\n" . $content, $link)) ? $link : '';
        $build = static function (string $body, string $final_link) use ($title, $hashtags, $limit): string {
            $fixed = array_values(array_filter([$title, $hashtags, $final_link], static fn($v) => $v !== ''));
            $fixed_length = 0; foreach ($fixed as $block) $fixed_length += self::length($block);
            $separator_length = $body !== '' ? 2 * count($fixed) : 2 * max(0, count($fixed) - 1);
            $body_limit = max(0, $limit - $fixed_length - $separator_length);
            if ($body !== '' && self::length($body) > $body_limit) {
                if ($body_limit > 1) $body = rtrim(self::substr($body, 0, $body_limit - 1)) . '…';
                elseif ($body_limit === 1) $body = '…';
                else $body = '';
            }
            return trim(implode("\n\n", array_values(array_filter([$title, $body, $hashtags, $final_link], static fn($v) => $v !== ''))));
        };
        $text = $build($content, $link_to_append);
        if ($link !== '' && !self::text_contains_url($text, $link)) $text = $build($original_content, $link);
        $full = trim(implode("\n\n", array_values(array_filter([$title, $original_content, $hashtags, $link_to_append], static fn($v) => $v !== ''))));
        $changed = $text !== $full;
        if (self::length($text) > $limit) { [$text] = self::truncate($text, $limit); $changed = true; }
        return [$text, $changed];
    }

    /** @return array{0:string,1:bool} */
    private static function truncate(string $text, int $limit): array {
        $text = trim($text);
        if ($limit <= 0 || self::length($text) <= $limit) {
            return [$text, false];
        }
        if ($limit <= 1) {
            return [self::substr($text, 0, $limit), true];
        }
        return [rtrim(self::substr($text, 0, max(1, $limit - 1))) . '…', true];
    }

    private static function truncate_plain(string $text, int $limit): string {
        return self::truncate($text, $limit)[0];
    }

    private static function hashtags_from_preview(string $text, string $formatted, string $network, bool $template_applied): string {
        if ($network === 'tumblr' && !$template_applied) {
            return $formatted;
        }
        preg_match_all('/(?<![\p{L}\p{N}_])#[\p{L}\p{N}_\-]+/u', $text, $matches);
        $tags = isset($matches[0]) && is_array($matches[0]) ? array_values(array_unique($matches[0])) : [];
        if (!empty($tags)) {
            return implode(' ', $tags);
        }
        return (!$template_applied && $formatted !== '' && strpos($text, $formatted) !== false) ? $formatted : '';
    }

    private static function first_url(string $text): string {
        if (preg_match('#https?://[^\s<>"\']+#iu', $text, $match)) {
            return esc_url_raw(rtrim((string) $match[0], '.,;:!?)]}'));
        }
        return '';
    }

    private static function text_contains_url(string $text, string $url): bool {
        $text = trim($text);
        $url = trim($url);
        if ($text === '' || $url === '') return false;
        $needle = preg_replace('#^https?://#iu', '', $url);
        $needle = preg_replace('#^www\.#iu', '', (string) $needle);
        $haystack = preg_replace('#https?://#iu', '', $text);
        $haystack = preg_replace('#\bwww\.#iu', '', (string) $haystack);
        return stripos((string) $haystack, rtrim((string) $needle, '/')) !== false;
    }

    private static function status_label(string $status): string {
        switch ($status) {
            case 'invalid': return __('Invalid', 'calliope-social-publisher');
            case 'warning': return __('Warning', 'calliope-social-publisher');
            default: return __('Ready', 'calliope-social-publisher');
        }
    }

    private static function normalize_network(string $network): string {
        $network = sanitize_key($network);
        if ($network === 'bsky') return 'bluesky';
        if ($network === 'remote_wordpress') return 'wordpress_remote';
        return $network;
    }

    private static function length(string $text): int {
        if (function_exists('mb_strlen')) return mb_strlen($text, 'UTF-8');
        if ($text === '') return 0;
        return preg_match_all('/./us', $text, $matches) ? count($matches[0]) : strlen($text);
    }

    private static function substr(string $text, int $start, int $length): string {
        if (function_exists('mb_substr')) return mb_substr($text, $start, $length, 'UTF-8');
        $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
        if (!is_array($chars)) return substr($text, $start, $length);
        return implode('', array_slice($chars, $start, $length));
    }
}
