<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use App\Models\CampaignModel;

/**
 * TrackingService
 *
 * Every method here is keyed off a recipient's unique
 * `unsubscribe_token` (despite the name, it's really just "this
 * recipient's tracking token" — reused for open/click tracking too, so
 * one opaque token per send is enough instead of minting three). A
 * campaign's rollup stats (`campaigns.opens_count` etc.) are refreshed
 * via `CampaignModel::refreshStats()` after every event so the
 * Dashboard and Campaign detail page are never stale by more than one
 * request.
 */
final class TrackingService
{
    public function __construct(private readonly CampaignModel $campaigns = new CampaignModel())
    {
    }

    /** 1x1 tracking pixel hit — marks the first open only (repeat opens don't change opened_at). */
    public function recordOpen(string $token): void
    {
        $recipient = $this->findByToken($token);
        if ($recipient === null) {
            return;
        }

        Database::connection()->prepare(
            "UPDATE campaign_recipients SET status = IF(status IN ('sent','delivered'), 'opened', status), opened_at = COALESCE(opened_at, NOW()) WHERE id = :id"
        )->execute(['id' => $recipient['id']]);

        $this->campaigns->refreshStats((int) $recipient['campaign_id']);
    }

    /** Click-through link hit — always logs the click as the freshest tracked event, even if already opened. */
    public function recordClick(string $token): void
    {
        $recipient = $this->findByToken($token);
        if ($recipient === null) {
            return;
        }

        Database::connection()->prepare(
            "UPDATE campaign_recipients SET status = 'clicked', clicked_at = COALESCE(clicked_at, NOW()), opened_at = COALESCE(opened_at, NOW()) WHERE id = :id"
        )->execute(['id' => $recipient['id']]);

        $this->campaigns->refreshStats((int) $recipient['campaign_id']);
    }

    /** @return array{success:bool, email:?string} */
    public function recordUnsubscribe(string $token, ?string $reason = null): array
    {
        $recipient = $this->findByToken($token);
        if ($recipient === null) {
            return ['success' => false, 'email' => null];
        }

        $pdo = Database::connection();
        $pdo->prepare("UPDATE campaign_recipients SET status = 'unsubscribed' WHERE id = :id")->execute(['id' => $recipient['id']]);

        $pdo->prepare(
            'INSERT IGNORE INTO email_unsubscribes (email, reason, unsubscribed_at) VALUES (:email, :reason, NOW())'
        )->execute(['email' => $recipient['email_address'], 'reason' => $reason]);

        $this->campaigns->refreshStats((int) $recipient['campaign_id']);
        ActivityLogger::logAction("Unsubscribed: {$recipient['email_address']}", 'system', 'campaign', (int) $recipient['campaign_id']);

        return ['success' => true, 'email' => $recipient['email_address']];
    }

    /** Manual/admin bounce marking — a real ESP webhook (SES/Mailgun/SendGrid) would call this same method once configured. */
    public function recordBounce(int $campaignRecipientId, string $reason): void
    {
        $pdo = Database::connection();
        $pdo->prepare("UPDATE campaign_recipients SET status = 'bounced', bounced_at = NOW() WHERE id = :id")
            ->execute(['id' => $campaignRecipientId]);

        $row = $this->findRecipient($campaignRecipientId);
        if ($row !== null) {
            $this->campaigns->refreshStats((int) $row['campaign_id']);
            ActivityLogger::logAction("Email bounced: {$row['email_address']} ({$reason})", 'system', 'campaign', (int) $row['campaign_id']);
        }
    }

    /** Manual/admin reply marking — a real inbound-parsing webhook would call this once IMAP/ESP inbound parsing is wired up. */
    public function recordReply(int $campaignRecipientId): void
    {
        $pdo = Database::connection();
        $pdo->prepare('UPDATE campaign_recipients SET replied_at = NOW() WHERE id = :id')->execute(['id' => $campaignRecipientId]);

        $row = $this->findRecipient($campaignRecipientId);
        if ($row !== null) {
            $this->campaigns->refreshStats((int) $row['campaign_id']);
        }
    }

    /** @return array{delivery_rate:float, open_rate:float, click_rate:float, bounce_rate:float, unsubscribe_rate:float} */
    public function ratesFor(int $campaignId): array
    {
        $stmt = Database::connection()->prepare(
            "SELECT COUNT(*) AS total,
                    SUM(sent_at IS NOT NULL) AS delivered,
                    SUM(opened_at IS NOT NULL) AS opened,
                    SUM(clicked_at IS NOT NULL) AS clicked,
                    SUM(status = 'bounced') AS bounced,
                    SUM(status = 'unsubscribed') AS unsubscribed
             FROM campaign_recipients WHERE campaign_id = :id"
        );
        $stmt->execute(['id' => $campaignId]);
        $r = $stmt->fetch();

        $total = max(1, (int) $r['total']);

        return [
            'delivery_rate' => round(((int) $r['delivered'] / $total) * 100, 1),
            'open_rate' => round(((int) $r['opened'] / $total) * 100, 1),
            'click_rate' => round(((int) $r['clicked'] / $total) * 100, 1),
            'bounce_rate' => round(((int) $r['bounced'] / $total) * 100, 1),
            'unsubscribe_rate' => round(((int) $r['unsubscribed'] / $total) * 100, 1),
        ];
    }

    private function findByToken(string $token): ?array
    {
        $stmt = Database::connection()->prepare('SELECT * FROM campaign_recipients WHERE unsubscribe_token = :token LIMIT 1');
        $stmt->execute(['token' => $token]);
        $row = $stmt->fetch();

        return $row === false ? null : $row;
    }

    private function findRecipient(int $id): ?array
    {
        $stmt = Database::connection()->prepare('SELECT * FROM campaign_recipients WHERE id = :id LIMIT 1');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch();

        return $row === false ? null : $row;
    }
}
