<?php
/**
 * Safe queue processing, compliance footer, and first-party tracking.
 *
 * @package apCampaign
 */

declare(strict_types=1);

if (!defined('ABSPATH')) {
    exit;
}

final class APCampaign_Delivery
{
    private const CRON_HOOK = 'apcampaign_process_send_job';

    public static function boot(): void
    {
        add_action(self::CRON_HOOK, [self::class, 'process_job']);
        add_action('template_redirect', [self::class, 'maybe_handle_public_event'], 0);
    }

    public static function schedule(int $job_id, int $timestamp): bool
    {
        if ($job_id < 1 || $timestamp <= time()) {
            return false;
        }
        return (bool) wp_schedule_single_event($timestamp, self::CRON_HOOK, [$job_id]);
    }

    /**
     * Process at most 100 recipients. A continuation is scheduled when needed.
     *
     * @return array<string,mixed>
     */
    public static function process_job(int $job_id): array
    {
        global $wpdb;
        $t = APCampaign_Schema::tables();
        $job = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$t['send_jobs']} WHERE id = %d", $job_id), ARRAY_A);
        if (!$job || !in_array((string) $job['status'], ['queued', 'scheduled', 'sending'], true)) {
            return ['success' => false, 'error' => 'Send job is not processable.'];
        }

        if (!self::compliance_ready()) {
            $wpdb->update($t['send_jobs'], [
                'status' => 'failed',
                'error_message' => 'A physical mailing address is required before live sending.',
                'updated_at' => current_time('mysql'),
            ], ['id' => $job_id]);
            return ['success' => false, 'error' => 'Physical mailing address is required.'];
        }

        $campaign = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$t['campaigns']} WHERE id = %d", (int) $job['campaign_id']), ARRAY_A);
        $snapshot = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$t['content_snapshots']} WHERE id = %d", (int) $job['snapshot_id']), ARRAY_A);
        if (!$campaign || !$snapshot) {
            return ['success' => false, 'error' => 'Campaign content is missing.'];
        }

        $now = current_time('mysql');
        $wpdb->update($t['send_jobs'], [
            'status' => 'sending',
            'started_at' => $job['started_at'] ?: $now,
            'updated_at' => $now,
        ], ['id' => $job_id]);

        $recipients = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM {$t['send_job_recipients']} WHERE send_job_id = %d AND status = 'queued' ORDER BY id ASC LIMIT 100",
            $job_id
        ), ARRAY_A) ?: [];
        $sent = 0;
        $failed = 0;
        $skipped = 0;

        foreach ($recipients as $recipient) {
            $email = sanitize_email((string) $recipient['email']);
            if ($email === '' || self::is_suppressed($email)) {
                $skipped++;
                $wpdb->update($t['send_job_recipients'], [
                    'status' => 'skipped',
                    'error_message' => 'Recipient is suppressed or invalid.',
                    'updated_at' => current_time('mysql'),
                ], ['id' => (int) $recipient['id']]);
                continue;
            }

            $personalized = self::personalize(
                (string) $snapshot['rendered_html'],
                (string) $campaign['subject_line'],
                $recipient
            );
            $html = self::decorate_html(
                APCampaign_Renderer::wrap_document($personalized['html'], $personalized['subject']),
                (string) $recipient['tracking_token']
            );
            $headers = [
                'Content-Type: text/html; charset=UTF-8',
                'From: ' . sanitize_text_field((string) $campaign['from_name']) . ' <' . sanitize_email((string) $campaign['from_email']) . '>',
            ];
            if (is_email((string) $campaign['reply_to_email'])) {
                $headers[] = 'Reply-To: ' . sanitize_email((string) $campaign['reply_to_email']);
            }

            $accepted = wp_mail($email, $personalized['subject'], $html, $headers);
            $wpdb->update($t['send_job_recipients'], [
                'status' => $accepted ? 'sent' : 'failed',
                'sent_at' => $accepted ? current_time('mysql') : null,
                'error_message' => $accepted ? '' : 'WordPress mail transport rejected the message.',
                'updated_at' => current_time('mysql'),
            ], ['id' => (int) $recipient['id']]);
            self::record_event($recipient, $accepted ? 'sent' : 'failed');
            $accepted ? $sent++ : $failed++;
        }

        $remaining = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$t['send_job_recipients']} WHERE send_job_id = %d AND status = 'queued'",
            $job_id
        ));
        $counts = $wpdb->get_row($wpdb->prepare(
            "SELECT
                SUM(status = 'sent') AS sent_count,
                SUM(status = 'failed') AS failed_count,
                SUM(status = 'skipped') AS skipped_count
             FROM {$t['send_job_recipients']} WHERE send_job_id = %d",
            $job_id
        ), ARRAY_A) ?: [];
        $complete = $remaining === 0;
        $wpdb->update($t['send_jobs'], [
            'status' => $complete ? 'completed' : 'sending',
            'sent_count' => (int) ($counts['sent_count'] ?? 0),
            'failed_count' => (int) ($counts['failed_count'] ?? 0),
            'skipped_count' => (int) ($counts['skipped_count'] ?? 0),
            'completed_at' => $complete ? current_time('mysql') : null,
            'updated_at' => current_time('mysql'),
        ], ['id' => $job_id]);

        if ($complete) {
            $wpdb->update($t['campaigns'], [
                'status' => 'sent',
                'sent_at' => current_time('mysql'),
                'updated_at' => current_time('mysql'),
            ], ['id' => (int) $job['campaign_id']]);
        } else {
            self::schedule($job_id, time() + 60);
        }

        return [
            'success' => true,
            'complete' => $complete,
            'sent_this_run' => $sent,
            'failed_this_run' => $failed,
            'skipped_this_run' => $skipped,
            'remaining' => $remaining,
        ];
    }

    public static function maybe_handle_public_event(): void
    {
        $event = sanitize_key((string) ($_GET['apcampaign_event'] ?? ''));
        $token = sanitize_text_field((string) ($_GET['token'] ?? ''));
        if ($event === '' || $token === '') {
            return;
        }

        global $wpdb;
        $t = APCampaign_Schema::tables();
        $recipient = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$t['send_job_recipients']} WHERE tracking_token = %s LIMIT 1",
            $token
        ), ARRAY_A);
        if (!$recipient) {
            status_header(404);
            exit;
        }

        if ($event === 'open') {
            self::record_event($recipient, 'open');
            nocache_headers();
            header('Content-Type: image/gif');
            echo base64_decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==');
            exit;
        }

        if ($event === 'unsubscribe') {
            APCampaign_Lifecycle::suppress_email((string) $recipient['email'], 'unsubscribe', 'apcampaign');
            $wpdb->update($t['send_job_recipients'], [
                'status' => 'unsubscribed',
                'updated_at' => current_time('mysql'),
            ], ['id' => (int) $recipient['id']]);
            self::record_event($recipient, 'unsubscribe');
            status_header(200);
            nocache_headers();
            echo '<!doctype html><meta charset="utf-8"><title>Unsubscribed</title><main style="max-width:640px;margin:10vh auto;font:18px/1.5 system-ui;padding:24px"><h1>You are unsubscribed</h1><p>This email address will not receive future campaigns from this site.</p></main>';
            exit;
        }

        if ($event === 'click') {
            $destination = esc_url_raw((string) ($_GET['url'] ?? ''));
            if (!self::destination_is_allowed($recipient, $destination)) {
                status_header(400);
                exit;
            }
            self::record_event($recipient, 'click', $destination);
            wp_redirect($destination, 302, 'apCampaign');
            exit;
        }
    }

    private static function decorate_html(string $html, string $token): string
    {
        $html = preg_replace_callback(
            '/href=(["\'])(https?:\/\/[^"\']+)\1/i',
            static function (array $matches) use ($token): string {
                $url = html_entity_decode((string) $matches[2], ENT_QUOTES | ENT_HTML5, 'UTF-8');
                return 'href=' . $matches[1] . esc_url(self::event_url('click', $token, $url)) . $matches[1];
            },
            $html
        ) ?: $html;

        $address = esc_html((string) get_option('apcampaign_physical_address', ''));
        $unsubscribe = esc_url(self::event_url('unsubscribe', $token));
        $open = esc_url(self::event_url('open', $token));
        $footer = '<div style="margin:32px auto 0;padding:18px;max-width:600px;text-align:center;font:12px/1.5 Arial,sans-serif;color:#64748b">'
            . $address . '<br><a href="' . $unsubscribe . '">Unsubscribe</a></div>'
            . '<img src="' . $open . '" width="1" height="1" alt="" style="display:block;border:0">';
        return str_ireplace('</body>', $footer . '</body>', $html);
    }

    /**
     * Replace customer-owned merge tags at delivery time, after the audience
     * snapshot has frozen each recipient's values.
     *
     * Supported tags include first_name, last_name, full_name, company, email,
     * and any scalar value stored in the recipient merge map.
     *
     * @param array<string,mixed> $recipient
     * @return array{subject:string,html:string}
     */
    private static function personalize(string $html, string $subject, array $recipient): array
    {
        $stored = json_decode((string) ($recipient['recipient_data_json'] ?? '{}'), true);
        $stored = is_array($stored) ? $stored : [];
        $merge = isset($stored['merge']) && is_array($stored['merge']) ? $stored['merge'] : [];
        $merge['email'] = sanitize_email((string) ($recipient['email'] ?? ''));
        $merge['first_name'] = (string) ($merge['first_name'] ?? $stored['first_name'] ?? '');
        $merge['last_name'] = (string) ($merge['last_name'] ?? $stored['last_name'] ?? '');
        $merge['company'] = (string) ($merge['company'] ?? $stored['company'] ?? '');
        $merge['full_name'] = trim($merge['first_name'] . ' ' . $merge['last_name']);

        $replace = [];
        foreach ($merge as $key => $value) {
            if (!is_scalar($value)) {
                continue;
            }
            $safe_key = sanitize_key((string) $key);
            if ($safe_key === '') {
                continue;
            }
            $replace['{{' . $safe_key . '}}'] = esc_html((string) $value);
            $replace['%%' . $safe_key . '%%'] = esc_html((string) $value);
        }

        return [
            'subject' => sanitize_text_field(html_entity_decode(strtr($subject, $replace), ENT_QUOTES | ENT_HTML5, 'UTF-8')),
            'html' => strtr($html, $replace),
        ];
    }

    private static function event_url(string $event, string $token, string $destination = ''): string
    {
        $args = ['apcampaign_event' => $event, 'token' => $token];
        if ($destination !== '') {
            $args['url'] = $destination;
        }
        return add_query_arg($args, home_url('/'));
    }

    private static function destination_is_allowed(array $recipient, string $destination): bool
    {
        if (!wp_http_validate_url($destination)) {
            return false;
        }
        global $wpdb;
        $t = APCampaign_Schema::tables();
        $html = (string) $wpdb->get_var($wpdb->prepare(
            "SELECT s.rendered_html FROM {$t['content_snapshots']} s
             INNER JOIN {$t['send_jobs']} j ON j.snapshot_id = s.id
             WHERE j.id = %d LIMIT 1",
            (int) $recipient['send_job_id']
        ));
        return strpos(html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $destination) !== false;
    }

    private static function is_suppressed(string $email): bool
    {
        global $wpdb;
        $t = APCampaign_Schema::tables();
        return (bool) $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$t['suppressions']} WHERE email_hash = %s AND status = 'active' LIMIT 1",
            hash('sha256', strtolower($email))
        ));
    }

    public static function compliance_ready(): bool
    {
        return trim((string) get_option('apcampaign_physical_address', '')) !== '';
    }

    private static function record_event(array $recipient, string $event, string $destination = ''): void
    {
        global $wpdb;
        $t = APCampaign_Schema::tables();
        $wpdb->insert($t['tracking_events'], [
            'campaign_id' => (int) $recipient['campaign_id'],
            'send_job_id' => (int) $recipient['send_job_id'],
            'recipient_id' => (int) $recipient['id'],
            'tracking_token' => (string) $recipient['tracking_token'],
            'event_type' => sanitize_key($event),
            'email' => sanitize_email((string) $recipient['email']),
            'destination_url' => $destination,
            'event_data_json' => '{}',
            'occurred_at' => current_time('mysql'),
            'created_at' => current_time('mysql'),
        ]);
    }
}
