<?php

declare(strict_types=1);

namespace MailerPress\Core;

\defined('ABSPATH') || exit;

final class HtmlParser
{
    private string $htmlContent = '';
    private array $variables = [];

    /**
     * Initialize parser with HTML and variables.
     */
    public function init(string $htmlContent, array $variables): static
    {
        $this->htmlContent = $htmlContent;
        $this->variables = $variables;

        // 🟢 Inject open-tracking pixel
        if (!empty($variables['TRACK_OPEN'])) {
            $trackingUrl = htmlspecialchars($variables['TRACK_OPEN'], ENT_QUOTES);

            $trackingTable = sprintf(
                '<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="1" height="1" style="display:none;">' .
                    '<tr><td><img src="%s" alt="" width="1" height="1" style="display:block;"/></td></tr>' .
                    '</table>',
                htmlspecialchars($trackingUrl, ENT_QUOTES)
            );

            libxml_use_internal_errors(true);
            $dom = new \DOMDocument();
            // Suppress deprecated warning for mb_convert_encoding with HTML-ENTITIES
            // This is still functional but deprecated in PHP 8.2+
            @$dom->loadHTML(mb_convert_encoding($this->htmlContent, 'HTML-ENTITIES', 'UTF-8'));
            $pattern = '#(<div[^>]*class=["\'][^"\']*footer-email[^"\']*["\'][^>]*>.*?</div>)#is';

            $this->htmlContent = preg_replace_callback($pattern, function ($matches) use ($trackingTable) {
                // Append the tracking table right after the footer-email div
                return $matches[1] . $trackingTable;
            }, $this->htmlContent);


            // If footer-email was not found, add tracking pixel before closing </body> tag
            if (strpos($this->htmlContent, $trackingTable) === false) {
                // Try to add before </body>
                if (preg_match('#</body>#i', $this->htmlContent)) {
                    $this->htmlContent = preg_replace('#(</body>)#i', $trackingTable . '$1', $this->htmlContent);
                } else {
                    // If no </body> tag, append at the end
                    $this->htmlContent .= $trackingTable;
                }
            }
        }

        // NOTE: Click tracking is now done AFTER merge tags are replaced in replaceVariables()
        // This ensures dynamic URLs (like {{first_product_review_link}}) are properly replaced before tracking

        return $this;
    }

    /**
     * Supprime les spans d'emoji et garde uniquement l'emoji.
     *
     * @param string $html Le contenu HTML
     * @return string Le contenu HTML avec les spans d'emoji supprimés
     */
    private function removeEmojiSpans(string $html): string
    {
        // Pattern pour détecter les spans avec data-emoji-id et garder uniquement le contenu
        $pattern = '#<span[^>]*data-emoji-id=["\'][^"\']*["\'][^>]*>(.*?)</span>#i';

        return preg_replace_callback($pattern, function ($matches) {
            // Retourner uniquement le contenu du span (l'emoji)
            return $matches[1] ?? '';
        }, $html);
    }

    /**
     * Remplace les variables dynamiques dans le contenu HTML.
     *
     * @return string le contenu HTML avec les variables remplacées par leurs valeurs
     */
    public function replaceVariables(): string
    {
        // 0️⃣ Supprimer les spans d'emoji et garder uniquement l'emoji
        $replacedContent = $this->removeEmojiSpans($this->htmlContent);

        // 1️⃣ Remove merge-tag spans but keep placeholder inside
        // Match spans with merge-tag-span class (even if there are other classes) or data-merge-tag-id attribute
        // This pattern matches spans that have either:
        // - class attribute containing "merge-tag-span" (with word boundaries)
        // - data-merge-tag-id attribute
        // Use a more flexible pattern that handles multiple attributes and classes
        $replacedContent = preg_replace_callback(
            '#<span[^>]*(?:class=["\'][^"\']*\bmerge-tag-span\b[^"\']*["\']|data-merge-tag-id=["\'][^"\']*["\'])[^>]*>(.*?)</span>#is',
            fn($m) => $m[1] ?? '',
            $replacedContent
        );

        // 2️⃣ Replace {{VAR}} and {{VAR default="value"}}
        // Use a more permissive pattern that matches merge tags even in attributes
        $pattern = '/{{\s*([a-zA-Z0-9_]+)(?:\s+default=["\']([^"\']*)["\'])?\s*}}/';
        $content = preg_replace_callback($pattern, function ($matches) {
            $key = $matches[1];
            $default = $matches[2] ?? '';
            $value = $this->variables[$key] ?? ($default ?: '');
            $value = (string) $value; // cast to string

            return $value === '' ? '§EMPTY_VAR§' : $value;
        }, $replacedContent);

        // 3️⃣ Replace %VAR%
        foreach ($this->variables as $key => $value) {
            $pattern = sprintf('/%%%s%%/', preg_quote($key, '/'));
            $content = preg_replace($pattern, (string) ($value === '' ? '§EMPTY_VAR§' : $value), $content);
        }

        // 4️⃣ Clean up empty markers
        $content = preg_replace('/(&nbsp;|\s)+§EMPTY_VAR§/', '§EMPTY_VAR§', $content);
        $content = str_replace('§EMPTY_VAR§', '', $content);
        $content = preg_replace('/\s{2,}/', ' ', $content);
        $content = preg_replace('/\s+([,.!?])/', '$1', $content);
        $content = preg_replace('/>\s+</', '><', $content);

        // 5️⃣ Add click tracking to links that had merge tags (now replaced)
        // This ensures links with dynamic URLs (like {{first_product_review_link}}) are tracked
        // Use isset() instead of !empty() to allow CONTACT_ID = 0 for non-subscribers
        if (isset($this->variables['CONTACT_ID']) && isset($this->variables['CAMPAIGN_ID'])) {
            $contactId = (int) $this->variables['CONTACT_ID'];
            $campaignId = (int) $this->variables['CAMPAIGN_ID'];
            // Only add tracking if campaign_id is valid (contact_id can be 0 for non-subscribers)
            if ($campaignId > 0) {
                $content = $this->appendClickTracking($content, $contactId, $campaignId);
            }
        }

        return trim($content);
    }

    /**
     * Append click tracking query params to all <a> links.
     */
    private function appendClickTracking(string $html, int $contactId, int $campaignId): string
    {
        libxml_use_internal_errors(true);

        $dom = new \DOMDocument();
        // Suppress deprecated warning for mb_convert_encoding with HTML-ENTITIES
        // This is still functional but deprecated in PHP 8.2+
        @$dom->loadHTML(mb_convert_encoding('<div id="mp-temp-wrapper">' . $html . '</div>', 'HTML-ENTITIES', 'UTF-8'));

        $xpath = new \DOMXPath($dom);
        $nodes = $xpath->query('//a[@href]');

        $site_host = parse_url(home_url(), PHP_URL_HOST);
        $trackedLinks = 0;
        $skippedLinks = 0;

        // Get optional jobId and stepId for automation emails
        $jobId = isset($this->variables['JOB_ID']) ? (int) $this->variables['JOB_ID'] : null;
        $stepId = isset($this->variables['STEP_ID']) ? (string) $this->variables['STEP_ID'] : null;

        foreach ($nodes as $link) {
            /** @var \DOMElement $link */
            $href = $link->getAttribute('href');

            // Skip links that contain merge tags (they will be replaced later in replaceVariables())
            // Merge tags are in format {{VAR}} or %VAR%
            if (preg_match('/{{[^}]+}}|%[^%]+%/', $href)) {
                continue;
            }

            // Skip links that already have mp_utm tracking
            if (strpos($href, 'mp_utm=') !== false) {
                continue;
            }

            // Handle both internal and external links for A/B testing tracking
            // For external links, we still want to track clicks
            $host = parse_url($href, PHP_URL_HOST);
            // Note: We track both internal and external links for A/B testing

            // Always include campaign + contact for click tracking
            // For automation emails, also include jobId and stepId
            $params = [
                'mp_utm'      => $this->generateContactTrackingToken($contactId, $campaignId, $href, $jobId, $stepId),
            ];

            // If WooCommerce product, add marker param for revenue tracking
            $post_id = $this->mailerpress_product_url_to_id($href);
            if ($post_id && get_post_type($post_id) === 'product') {
                $params['mp_track_product'] = 1;
                $params['mp_product_id'] = $post_id;
            }

            $separator = str_contains($href, '?') ? '&' : '?';
            $newHref = $href . $separator . http_build_query($params);

            $link->setAttribute('href', $newHref);
            $trackedLinks++;
        }

        // Extract inner HTML
        $wrapper = $dom->getElementById('mp-temp-wrapper');
        $newHtml = '';
        foreach ($wrapper->childNodes as $child) {
            $newHtml .= $dom->saveHTML($child);
        }

        return $newHtml;
    }

    private function mailerpress_product_url_to_id($url)
    {
        $parsed = wp_parse_url($url);
        if (empty($parsed['path'])) {
            return 0;
        }

        $path = trim($parsed['path'], '/');
        $segments = explode('/', $path);
        $slug = end($segments);

        if (!$slug) {
            return 0;
        }

        global $wpdb;
        $product_id = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT ID FROM {$wpdb->posts}
             WHERE post_name = %s
             AND post_type = 'product'
             AND post_status = 'publish'
             LIMIT 1",
                $slug
            )
        );

        return $product_id ?: 0;
    }


    /**
     * Generate a secure tracking token for email link clicks.
     * 
     * For automation emails: includes jobId and stepId to enable workflow re-evaluation
     * For newsletter emails: only includes contactId, campaignId, and url
     * 
     * @param int $contactId Contact ID
     * @param int $campaignId Campaign ID
     * @param string $url Original URL to track
     * @param int|null $jobId Optional job ID for automation emails
     * @param string|null $stepId Optional step ID for automation emails
     * @return string Base64url-encoded tracking token
     */
    private function generateContactTrackingToken(int $contactId, int $campaignId, string $url, ?int $jobId = null, ?string $stepId = null): string
    {
        $payloadData = [
            'cid' => $contactId,
            'cmp' => $campaignId,
            'url' => $url,
            'ts'  => time(),
        ];

        // Add jobId and stepId for automation emails (workflow)
        if ($jobId !== null && $jobId > 0) {
            $payloadData['job'] = $jobId;
        }
        if ($stepId !== null && $stepId !== '') {
            $payloadData['step'] = $stepId;
        }

        $payload = json_encode($payloadData);

        $secret = wp_salt('auth');
        $signature = hash_hmac('sha256', $payload, $secret);

        $encoded = base64_encode($payload . '::' . $signature);

        return rtrim(strtr($encoded, '+/', '-_'), '=');
    }

    /**
     * Generate a secure tracking token for email open tracking (TRACK_OPEN).
     * 
     * This token contains all necessary information to track email opens for both
     * automation emails (workflow) and newsletter emails (campaign).
     * 
     * For automation emails: includes contactId, campaignId, jobId, stepId
     * For newsletter emails: includes contactId, campaignId, batchId
     * 
     * @param int $contactId Contact ID
     * @param int $campaignId Campaign ID
     * @param int|null $batchId Optional batch ID for newsletter emails
     * @param int|null $jobId Optional job ID for automation emails
     * @param string|null $stepId Optional step ID for automation emails
     * @return string Base64url-encoded tracking token
     */
    public static function generateTrackOpenToken(int $contactId, int $campaignId, ?int $batchId = null, ?int $jobId = null, ?string $stepId = null): string
    {
        $payloadData = [
            'cid' => $contactId,
            'cmp' => $campaignId,
            'ts'  => time(),
        ];

        // Add batchId for newsletter emails
        if ($batchId !== null && $batchId > 0) {
            $payloadData['batch'] = $batchId;
        }

        // Add jobId and stepId for automation emails (workflow)
        if ($jobId !== null && $jobId > 0) {
            $payloadData['job'] = $jobId;
        }
        if ($stepId !== null && $stepId !== '') {
            $payloadData['step'] = $stepId;
        }

        $payload = json_encode($payloadData);

        $secret = wp_salt('auth');
        $signature = hash_hmac('sha256', $payload, $secret);

        $encoded = base64_encode($payload . '::' . $signature);

        return rtrim(strtr($encoded, '+/', '-_'), '=');
    }

    /**
     * Decode a tracking token and return the data.
     * 
     * @param string $token The base64url-encoded token
     * @return array|null Decoded data or null if invalid
     */
    public static function decodeTrackingToken(string $token): ?array
    {
        $remainder = strlen($token) % 4;
        if ($remainder) {
            $token .= str_repeat('=', 4 - $remainder);
        }

        $decoded = base64_decode(strtr($token, '-_', '+/'));
        if (!$decoded) {
            return null;
        }

        [$payloadJson, $signature] = explode('::', $decoded, 2) + [null, null];
        if (!$payloadJson || !$signature) {
            return null;
        }

        $expectedSignature = hash_hmac('sha256', $payloadJson, wp_salt('auth'));
        if (!hash_equals($expectedSignature, $signature)) {
            return null;
        }

        return json_decode($payloadJson, true);
    }
}
