<?php
/**
 * open.php – E-Mail-Öffnungs-Tracking
 * ----------------------------------------------------------------------
 * Ziel:
 *  • 1x1 Pixel IMMER ausliefern
 *  • Öffnungen nur dann speichern und als Engagement werten,
 *    wenn der Request nicht verdächtig ist
 *
 * Verhalten analog zu click.php:
 *  • suspicious_score >= 5 => suspected
 *  • suspected Opens:
 *      - werden geloggt
 *      - setzen KEIN last_opened_at
 *      - erzeugen KEINEN Datensatz in email_opens
 *      - entfernen KEIN "inaktiv"-Tag
 *      - liefern NUR den Pixel aus
 *
 * Sicherheit / MR9-Style:
 *  • try/catch Fehlerbehandlung
 *  • SQL-Logging über ErrorLogger::logSqlSuccess / logSqlError
 *  • ausführlich kommentiert
 */

require_once __DIR__ . '/includes/init.php';
require_once __DIR__ . '/includes/core/ErrorLogger.php';

/* =========================================================================
 * KONFIGURATION
 * ========================================================================= */
const SUSPICIOUS_MIN_SCORE = 5;

/**
 * -------------------------------------------------------------------------
 * 1x1 Tracking-Pixel ausgeben und Skript beenden
 * -------------------------------------------------------------------------
 * Der Pixel wird immer ausgeliefert – auch bei Fehlern, Preload-Zugriffen
 * oder ungültigen Request-Daten.
 */
function outputTrackingPixel(): void
{
    header('Content-Type: image/gif');
    header('Cache-Control: no-cache, no-store, must-revalidate');
    header('Pragma: no-cache');
    header('Expires: 0');

    echo base64_decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==');
    exit;
}

/**
 * -------------------------------------------------------------------------
 * Entfernt den Tag "inaktiv" bei einem Empfänger (fail-safe)
 * -------------------------------------------------------------------------
 * Diese Funktion ist bewusst robust gebaut:
 *  • ungültige Parameter -> kein Fehler, sondern Rückgabe 0
 *  • SQL-Fehler -> werden geloggt, Tracking läuft trotzdem weiter
 */
function removeTagByNameForRecipient(PDO $db, int $recipientId, string $tagName = 'inaktiv'): int
{
    if ($recipientId <= 0 || trim($tagName) === '') {
        return 0;
    }

    try {
        // Passende Tag-ID laden
        $stmt = $db->prepare("SELECT id FROM tags WHERE name = ? LIMIT 1");
        $stmt->execute([$tagName]);
        ErrorLogger::logSqlSuccess('open: load tag id | SQL: ' . $stmt->queryString . ' | params: ' . json_encode([$tagName], JSON_UNESCAPED_UNICODE));

        $tagId = (int)($stmt->fetchColumn() ?: 0);
        if ($tagId <= 0) {
            return 0;
        }

        // Tag-Verknüpfung beim Empfänger löschen
        $stmt = $db->prepare("DELETE FROM recipient_tags WHERE recipient_id = ? AND tag_id = ?");
        $stmt->execute([$recipientId, $tagId]);
        ErrorLogger::logSqlSuccess('open: remove tag from recipient | SQL: ' . $stmt->queryString . ' | params: ' . json_encode([$recipientId, $tagId], JSON_UNESCAPED_UNICODE));

        return (int)$stmt->rowCount();

    } catch (Throwable $e) {
        ErrorLogger::logSqlError('open: remove tag failed | error: ' . $e->getMessage());
        return 0;
    }
}

/**
 * -------------------------------------------------------------------------
 * Prüft, ob die aktuelle IP als verdächtig eingestuft ist
 * -------------------------------------------------------------------------
 * Logik identisch zu click.php:
 *  • suspicious_ips enthält die IP
 *  • aktuelles Zeitfenster ist aktiv
 *  • suspicion_score >= SUSPICIOUS_MIN_SCORE
 *
 * Fail-safe:
 *  • Bei Fehlern wird false zurückgegeben
 */
function detectSuspiciousOpen(PDO $db, string $ip): bool
{
    if (trim($ip) === '' || $ip === 'unknown') {
        return false;
    }

    try {
        $stmt = $db->prepare(
            "SELECT 1
               FROM suspicious_ips
              WHERE ip_address = (CONVERT(? USING utf8mb4) COLLATE utf8mb4_unicode_ci)
                AND NOW() BETWEEN window_start AND window_end
                AND suspicion_score >= ?
              LIMIT 1"
        );

        $stmt->execute([$ip, SUSPICIOUS_MIN_SCORE]);
        ErrorLogger::logSqlSuccess('open: suspicious ip check | SQL: ' . $stmt->queryString . ' | params: ' . json_encode([$ip, SUSPICIOUS_MIN_SCORE], JSON_UNESCAPED_UNICODE));

        return (bool)$stmt->fetchColumn();

    } catch (Throwable $e) {
        ErrorLogger::logSqlError('open: suspicious check failed (fail-safe) | error: ' . $e->getMessage());
        return false;
    }
}

try {
    /* ---------------------------------------------------------------------
     * 0) Datenbankverbindung holen und Transaktion starten
     * ------------------------------------------------------------------- */
    $db = Database::getInstance();
    $db->beginTransaction();

    /* ---------------------------------------------------------------------
     * 1) Optionaler Testmodus
     * ------------------------------------------------------------------- */
    $token = $_GET['token'] ?? null;
    if ($token === 'test') {
        if ($db->inTransaction()) {
            $db->commit();
        }

        echo '<h1>Danke für ihren Test</h1>';
        exit;
    }

    /* ---------------------------------------------------------------------
     * 2) Queue-ID aus dem Request lesen
     * -------------------------------------------------------------------
     * open.php arbeitet mit ?id= aus send_queue.
     * Ohne gültige ID wird nur der Pixel geliefert.
     */
    $id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
    if ($id <= 0) {
        if ($db->inTransaction()) {
            $db->commit();
        }
        outputTrackingPixel();
    }

    /* ---------------------------------------------------------------------
     * 3) Request-Metadaten erfassen
     * ------------------------------------------------------------------- */
    $ip        = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
    $device    = 'unknown';

    if (preg_match('/mobile|android|touch|webos|hpwos/i', $userAgent)) {
        $device = 'mobile';
    } elseif (preg_match('/windows|macintosh|linux/i', $userAgent)) {
        $device = 'desktop';
    }

    /* ---------------------------------------------------------------------
     * 4) Preload / Proxy-Zugriffe erkennen
     * -------------------------------------------------------------------
     * Diese Zugriffe sollen keine echte Öffnung erzeugen.
     */
    $isPreload = false;

    // Gmail Image Proxy
    if (stripos($userAgent, 'GoogleImageProxy') !== false || stripos($userAgent, 'Google-Image-Proxy') !== false) {
        $isPreload = true;
    }

    // Apple Mail Privacy Protection
    if (stripos($userAgent, 'MailPrivacyProtection') !== false || stripos($userAgent, 'AppleMail') !== false) {
        $isPreload = true;
    }

    /* ---------------------------------------------------------------------
     * 5) Preload erkannt -> keine Speicherung, nur Pixel
     * ------------------------------------------------------------------- */
    if ($isPreload) {
        ErrorLogger::logSqlSuccess(
            'open: preload/proxy request ignored | context: ' . json_encode(
                ['queue_id' => $id, 'ip' => $ip, 'ua' => $userAgent],
                JSON_UNESCAPED_UNICODE
            )
        );

        if ($db->inTransaction()) {
            $db->commit();
        }

        outputTrackingPixel();
    }

    /* ---------------------------------------------------------------------
     * 6) Queue-Eintrag laden, um recipient_id zu ermitteln
     * ------------------------------------------------------------------- */
    $stmt = $db->prepare(
        "SELECT id, recipient_id
           FROM send_queue
          WHERE id = ?"
    );
    $stmt->execute([$id]);
    ErrorLogger::logSqlSuccess('open: load queue entry | SQL: ' . $stmt->queryString . ' | params: ' . json_encode([$id], JSON_UNESCAPED_UNICODE));

    $entry = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$entry) {
        if ($db->inTransaction()) {
            $db->commit();
        }
        outputTrackingPixel();
    }

    $recipientId = (int)$entry['recipient_id'];

    /* ---------------------------------------------------------------------
     * 7) Suspicious-IP prüfen
     * -------------------------------------------------------------------
     * Genau wie in click.php:
     *  • suspected Requests dürfen keine Engagement-Wirkung haben
     *  • und in dieser Version auch KEIN email_opens-Insert auslösen,
     *    damit kein indirektes Update im Empfängerdatensatz entsteht
     */
    $isSuspected = detectSuspiciousOpen($db, $ip);

    /* ---------------------------------------------------------------------
     * 8) Suspected Open sofort beenden
     * -------------------------------------------------------------------
     * WICHTIG:
     *  Hier liegt der entscheidende Fix.
     *  Sobald die IP als verdächtig erkannt wird, wird nur noch geloggt
     *  und der Pixel ausgeliefert.
     *
     *  Dadurch passiert:
     *   • kein Insert in email_opens
     *   • kein Update von recipients.last_opened_at
     *   • kein Entfernen des Tags "inaktiv"
     */
    if ($isSuspected) {
        ErrorLogger::logSqlSuccess(
            'open: suspected open ignored for engagement | context: ' . json_encode(
                ['queue_id' => $id, 'recipient_id' => $recipientId, 'ip' => $ip],
                JSON_UNESCAPED_UNICODE
            )
        );

        if ($db->inTransaction()) {
            $db->commit();
        }

        outputTrackingPixel();
    }

    /* ---------------------------------------------------------------------
     * 9) Prüfen, ob bereits ein Open für diese Queue existiert
     * -------------------------------------------------------------------
     * Analog zu click.php:
     *  nur ein Open-Datensatz pro queue_id
     */
    $stmt = $db->prepare(
        "SELECT 1
           FROM email_opens
          WHERE queue_id = ?
          LIMIT 1"
    );
    $stmt->execute([$id]);
    ErrorLogger::logSqlSuccess('open: check existing open | SQL: ' . $stmt->queryString . ' | params: ' . json_encode([$id], JSON_UNESCAPED_UNICODE));

    $openExists = (bool)$stmt->fetchColumn();

    /* ---------------------------------------------------------------------
     * 10) Nur wenn noch kein Open vorhanden ist, Datensatz anlegen
     * ------------------------------------------------------------------- */
    if (!$openExists) {
        $stmt = $db->prepare(
            "INSERT INTO email_opens
                (queue_id, opened_at, ip_address, user_agent, device_type)
             VALUES (?, NOW(), ?, ?, ?)"
        );
        $stmt->execute([$id, $ip, $userAgent, $device]);
        ErrorLogger::logSqlSuccess('open: insert email open | SQL: ' . $stmt->queryString . ' | params: ' . json_encode([$id, $ip, $userAgent, $device], JSON_UNESCAPED_UNICODE));

        /* -----------------------------------------------------------------
         * 11) Nur beim ersten echten Open Engagement aktualisieren
         * -----------------------------------------------------------------
         * Da suspected oben bereits komplett beendet wurde, sind wir hier
         * nur noch bei echten, zulässigen Öffnungen.
         */
        $stmt = $db->prepare(
            "UPDATE recipients
                SET last_opened_at = NOW()
              WHERE id = ?"
        );
        $stmt->execute([$recipientId]);
        ErrorLogger::logSqlSuccess('open: update recipient last_opened_at | SQL: ' . $stmt->queryString . ' | params: ' . json_encode([$recipientId], JSON_UNESCAPED_UNICODE));

        // "inaktiv"-Tag entfernen, da eine echte Öffnung vorliegt
        removeTagByNameForRecipient($db, $recipientId, 'inaktiv');

    } else {
        /* -----------------------------------------------------------------
         * 12) Bereits vorhandenes Open -> keine doppelte Wirkung
         * ----------------------------------------------------------------- */
        ErrorLogger::logSqlSuccess(
            'open: open already exists, no duplicate insert/update | context: ' . json_encode(
                ['queue_id' => $id, 'recipient_id' => $recipientId, 'ip' => $ip],
                JSON_UNESCAPED_UNICODE
            )
        );
    }

    /* ---------------------------------------------------------------------
     * 13) Alles erfolgreich abschließen
     * ------------------------------------------------------------------- */
    $db->commit();

} catch (Throwable $e) {
    /* ---------------------------------------------------------------------
     * 14) Fehlerbehandlung mit Rollback
     * ------------------------------------------------------------------- */
    if (isset($db) && $db instanceof PDO && $db->inTransaction()) {
        $db->rollBack();
    }

    ErrorLogger::logSqlError('open: workflow failed | error: ' . $e->getMessage());
}

/* -------------------------------------------------------------------------
 * 15) Pixel immer ausliefern
 * ----------------------------------------------------------------------- */
outputTrackingPixel();