<?php
declare(strict_types=1);

class TrackedLinkProcessor
{
    private PDO $db;
    private string $baseUrl;
    private PDOStatement $linkTrackingInsertStmt;
    private PDOStatement $linkActionInsertStmt;
    private PDOStatement $tagLookupStmt;
    private array $tagCache = [];

    public function __construct(PDO $db, string $baseUrl)
    {
        $this->db = $db;
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->linkTrackingInsertStmt = $db->prepare('INSERT INTO email_link_tracking (token, original_url, queue_id) VALUES (:token, :url, :queue_id)');
        $this->linkActionInsertStmt = $db->prepare('INSERT INTO email_link_actions (email_link_tracking_id, action_type, tag_id, only_once, created_at) VALUES (:link_tracking_id, :action_type, :tag_id, :only_once, NOW())');
        $this->tagLookupStmt = $db->prepare('SELECT id FROM tags WHERE name = :name LIMIT 1');
    }

    public function processHtml(string $html, int $queueId): string
    {
        if ($html === '' || stripos($html, '<a') === false) {
            return $html;
        }

        $previousUseInternalErrors = libxml_use_internal_errors(true);
        $dom = new DOMDocument('1.0', 'UTF-8');
        $wrapperId = 'mr-link-root-' . bin2hex(random_bytes(4));
        $loaded = $dom->loadHTML(
            '<?xml encoding="utf-8" ?><div id="' . $wrapperId . '">' . $html . '</div>',
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
        );
        libxml_clear_errors();
        libxml_use_internal_errors($previousUseInternalErrors);

        if (!$loaded) {
            ErrorLogger::logSqlError('tracked_link_processor: HTML parse failed | context: ' . json_encode(['queue_id' => $queueId], JSON_UNESCAPED_UNICODE));
            return $html;
        }

        $anchors = iterator_to_array($dom->getElementsByTagName('a'));
        foreach ($anchors as $anchor) {
            $originalUrl = trim(html_entity_decode($anchor->getAttribute('href'), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
            if ($this->shouldSkipTracking($originalUrl)) {
                $this->removeActionAttributes($anchor);
                continue;
            }

            $token = bin2hex(random_bytes(16));
            $this->linkTrackingInsertStmt->execute([
                ':token' => $token,
                ':url' => $originalUrl,
                ':queue_id' => $queueId,
            ]);
            $trackingId = (int) $this->db->lastInsertId();
            ErrorLogger::logSqlSuccess('tracked_link_processor: insert tracking link | SQL: ' . $this->linkTrackingInsertStmt->queryString . ' | params: ' . json_encode([$token, $originalUrl, $queueId], JSON_UNESCAPED_UNICODE));
            $this->registerOptionalAction($anchor, $trackingId);

            $anchor->setAttribute('href', $this->baseUrl . '/click.php?token=' . $token);
            $this->removeActionAttributes($anchor);
        }

        $root = $dom->getElementById($wrapperId);
        if (!$root instanceof DOMElement) {
            return $html;
        }

        return $this->innerHtml($root, $dom);
    }

    private function registerOptionalAction(DOMElement $anchor, int $trackingId): void
    {
        $actionType = strtolower(trim($anchor->getAttribute('data-mr-action')));
        $tagReference = trim($anchor->getAttribute('data-mr-tag'));
        if ($trackingId <= 0 || $actionType === '' || $tagReference === '') {
            return;
        }

        $mappedAction = match ($actionType) {
            'assign-tag', 'set-tag', 'tag-set', 'assign_tag' => 'assign_tag',
            'remove-tag', 'unset-tag', 'tag-remove', 'remove_tag' => 'remove_tag',
            default => '',
        };

        if ($mappedAction === '') {
            ErrorLogger::logSqlError('tracked_link_processor: unsupported link action | context: ' . json_encode(['action_type' => $actionType], JSON_UNESCAPED_UNICODE));
            return;
        }

        $tagId = $this->resolveTagId($tagReference);
        if ($tagId <= 0) {
            ErrorLogger::logSqlError('tracked_link_processor: tag not found for link action | context: ' . json_encode(['tag_reference' => $tagReference], JSON_UNESCAPED_UNICODE));
            return;
        }

        $onlyOnce = $this->normalizeOnlyOnceFlag($anchor->getAttribute('data-mr-only-once'));
        $this->linkActionInsertStmt->execute([
            ':link_tracking_id' => $trackingId,
            ':action_type' => $mappedAction,
            ':tag_id' => $tagId,
            ':only_once' => $onlyOnce ? 1 : 0,
        ]);
        ErrorLogger::logSqlSuccess('tracked_link_processor: insert link action | SQL: ' . $this->linkActionInsertStmt->queryString . ' | params: ' . json_encode([$trackingId, $mappedAction, $tagId, $onlyOnce ? 1 : 0], JSON_UNESCAPED_UNICODE));
    }

    private function resolveTagId(string $tagReference): int
    {
        if ($tagReference === '') {
            return 0;
        }

        $cacheKey = mb_strtolower($tagReference);
        if (isset($this->tagCache[$cacheKey])) {
            return $this->tagCache[$cacheKey];
        }

        if (ctype_digit($tagReference)) {
            $tagId = (int) $tagReference;
            $this->tagCache[$cacheKey] = $tagId;
            return $tagId;
        }

        $this->tagLookupStmt->execute([':name' => $tagReference]);
        ErrorLogger::logSqlSuccess('tracked_link_processor: resolve tag id | SQL: ' . $this->tagLookupStmt->queryString . ' | params: ' . json_encode([$tagReference], JSON_UNESCAPED_UNICODE));
        $tagId = (int) ($this->tagLookupStmt->fetchColumn() ?: 0);
        $this->tagCache[$cacheKey] = $tagId;

        return $tagId;
    }

    private function normalizeOnlyOnceFlag(string $raw): bool
    {
        $raw = strtolower(trim($raw));
        if ($raw === '') {
            return true;
        }

        return !in_array($raw, ['0', 'false', 'no', 'off'], true);
    }

    private function shouldSkipTracking(string $url): bool
    {
        if ($url === '') {
            return true;
        }

        if (
            str_contains($url, '[') ||
            str_contains($url, 'unsubscribe.php') ||
            str_contains($url, 'confirm.php') ||
            str_contains($url, 'view.php') ||
            str_contains($url, 'versandpause.php') ||
            str_contains($url, 'field:')
        ) {
            return true;
        }

        $lowerUrl = strtolower($url);
        return str_starts_with($lowerUrl, 'mailto:')
            || str_starts_with($lowerUrl, 'tel:')
            || str_starts_with($lowerUrl, 'javascript:')
            || str_starts_with($lowerUrl, '#');
    }

    private function removeActionAttributes(DOMElement $anchor): void
    {
        foreach (['data-mr-action', 'data-mr-tag', 'data-mr-only-once'] as $attribute) {
            if ($anchor->hasAttribute($attribute)) {
                $anchor->removeAttribute($attribute);
            }
        }
    }

    private function innerHtml(DOMElement $element, DOMDocument $dom): string
    {
        $html = '';
        foreach ($element->childNodes as $child) {
            $html .= $dom->saveHTML($child);
        }

        return $html;
    }
}
