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

/**
 * Free global UTM tracking for publication links.
 *
 * The Free plugin exposes one global UTM rule set. Values are resolved per
 * destination immediately before Social Templates and provider formatting run,
 * so {post_link}, provider link fields, History snapshots and Preview all see
 * the same tracked URL. Pro can extend these settings with per-network rules.
 */
class Calliope_SP_Tracking {
    public const ENABLED_OPTION = 'calliope_sp_tracking_enabled';
    public const SETTINGS_OPTION = 'calliope_sp_tracking_settings';

    /** @return array<string,string> */
    public static function defaults(): array {
        return [
            'utm_source'   => '{network}',
            'utm_medium'   => 'social',
            'utm_campaign' => 'wordpress',
        ];
    }

    public static function is_enabled(): bool {
        return get_option(self::ENABLED_OPTION, '0') === '1';
    }

    /** @return array<string,string> */
    public static function get_settings(): array {
        $saved = get_option(self::SETTINGS_OPTION, null);
        if (!is_array($saved)) {
            return self::defaults();
        }
        return array_merge(self::defaults(), self::sanitize_settings($saved));
    }

    /** @param mixed $raw @return array<string,string> */
    public static function sanitize_settings($raw): array {
        $raw = is_array($raw) ? $raw : [];
        $clean = [];
        foreach (array_keys(self::defaults()) as $key) {
            $value = isset($raw[$key]) && is_scalar($raw[$key]) ? (string) $raw[$key] : '';
            $value = trim(sanitize_text_field($value));
            if (function_exists('mb_substr')) {
                $value = mb_substr($value, 0, 160, 'UTF-8');
            } else {
                $value = substr($value, 0, 160);
            }
            // Only the documented token is accepted. Unknown placeholders are
            // removed so malformed values never leak into public URLs.
            $value = preg_replace('/\{(?!network\})[a-z0-9_]+\}/iu', '', (string) $value);
            $clean[$key] = trim((string) $value);
        }
        return $clean;
    }

    /**
     * Add the configured UTM parameters to a URL for one destination.
     * Existing UTM parameters with the same names are replaced rather than
     * duplicated; unrelated query parameters and URL fragments are preserved.
     */
    public static function track_url(string $url, string $network, ?array $settings = null): string {
        $url = trim($url);
        if ($url === '' || !wp_http_validate_url($url)) {
            return $url;
        }

        $scheme = strtolower((string) wp_parse_url($url, PHP_URL_SCHEME));
        if (!in_array($scheme, ['http', 'https'], true)) {
            return $url;
        }

        $network = self::normalize_network($network);
        if ($network === '') {
            return $url;
        }

        $settings = is_array($settings) ? self::sanitize_settings($settings) : self::get_settings();
        $params = [];
        foreach (['utm_source', 'utm_medium', 'utm_campaign'] as $key) {
            $value = trim((string) ($settings[$key] ?? ''));
            if ($value === '') {
                continue;
            }
            $value = str_replace('{network}', $network, $value);
            // Never expose unresolved/mistyped template-looking values.
            $value = preg_replace('/\{[a-z0-9_]+\}/iu', '', (string) $value);
            $value = trim((string) $value);
            if ($value !== '') {
                $params[$key] = $value;
            }
        }

        if (empty($params)) {
            return $url;
        }

        return (string) add_query_arg($params, $url);
    }

    /**
     * Apply tracking to the current network payload. Idempotent so History
     * Retry republishes the exact URL that originally failed even if Tracking
     * settings change later.
     *
     * @param array<string,mixed> $payload
     * @param array<string,mixed> $history_context
     * @return array<string,mixed>
     */
    public static function apply_to_payload(array $payload, string $network, array $history_context = []): array {
        if (!empty($payload['utm_tracking_applied'])) {
            return $payload;
        }
        if (!self::is_enabled()) {
            return $payload;
        }

        $network = self::normalize_network($network);
        if ($network === '') {
            return $payload;
        }

        $original_link = trim((string) ($payload['link'] ?? ''));
        $source_link = $original_link;
        $post_id = absint($history_context['post_id'] ?? ($payload['post_id'] ?? 0));

        // If the Publisher link field is empty, only derive the WordPress
        // permalink when the active Social Template for this network actually
        // uses {post_link}. This prevents Tracking from inventing a provider
        // link that the user never intended to publish.
        if ($source_link === '' && $post_id > 0 && class_exists('Calliope_SP_Social_Templates') && Calliope_SP_Social_Templates::is_enabled()) {
            $template = Calliope_SP_Social_Templates::get_template($network);
            if ($template !== '' && strpos($template, '{post_link}') !== false) {
                $permalink = get_permalink($post_id);
                if (is_string($permalink) && $permalink !== '') {
                    $source_link = $permalink;
                }
            }
        }

        if ($source_link === '') {
            return $payload;
        }

        $settings = self::get_settings();
        $has_configured_value = false;
        foreach (['utm_source', 'utm_medium', 'utm_campaign'] as $key) {
            if (trim((string) ($settings[$key] ?? '')) !== '') {
                $has_configured_value = true;
                break;
            }
        }
        if (!$has_configured_value) {
            return $payload;
        }

        $tracked = self::track_url($source_link, $network, $settings);
        if ($tracked === '' || $tracked === $source_link && !wp_http_validate_url($source_link)) {
            return $payload;
        }

        // This marker is also consumed by Smart Tags so {post_link} uses the
        // same tracked URL even when the Publisher link field itself was empty.
        $payload['utm_tracking_applied'] = 1;
        $payload['utm_tracking_network'] = $network;
        $payload['utm_tracking_post_link'] = $tracked;

        // Preserve existing behavior: only replace the provider's structural
        // link when the Publisher payload already contained a link. A source
        // post permalink discovered solely for {post_link} remains template-only.
        if ($original_link !== '') {
            $payload['link'] = $tracked;
        }

        return $payload;
    }

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