<?php

namespace GriffinForms\Includes\Pipelines;

use GriffinForms\Config;
use GriffinForms\Includes\Mailer\MailHandler;
use GriffinForms\Includes\Pipelines\Accounts\JobProcessor as AccountsJobProcessor;
use GriffinForms\Log;
use GriffinForms\Includes\Pipelines\SubmissionContextBuilder;
use GriffinForms\Includes\Security\Capabilities;
use GriffinForms\Settings;

class JobWorker
{
    private const DEVICE_OPTION_PREFIX = 'api_device_registry_user_';
    private const FANOUT_BACKOFF_SECONDS = [60, 180, 600, 1800, 3600];

    /** @var JobRepository */
    protected $repository;

    /** @var \GriffinForms\Log */
    protected $log;

    /** @var string|null */
    protected $mail_log_category;

    /** @var \GriffinForms\Settings */
    protected $settings;
    protected $accounts_job_processor;

    public function __construct()
    {
        $this->repository = JobRepository::getInstance();
        $this->log = Log::getInstance();
        $this->settings = Settings::getInstance();
        $this->accounts_job_processor = new AccountsJobProcessor();
    }

    public function process(int $limit = 5): void
    {
        $jobs = $this->repository->fetchPending($limit);
        foreach ($jobs as $job) {
            $job_id = (int) ($job['id'] ?? 0);
            if ($job_id <= 0) {
                continue;
            }

            if (!$this->repository->markProcessing($job_id)) {
                continue;
            }

            try {
                $this->processJob($job);
                $this->repository->markCompleted($job_id);
            } catch (\Throwable $e) {
                $failure = $this->resolveFailurePolicy($job);
                $this->repository->markFailed($job_id, $e->getMessage(), $failure['retry'], $failure['delay']);
                $this->logJobFailure($job, $e);
            }
        }
    }

    private function processJob(array $job): void
    {
        $payload = json_decode($job['payload'] ?? '[]', true);
        if (!is_array($payload)) {
            $payload = [];
        }

        switch ($job['job_type']) {
            case 'mail_admin_alert':
                $this->handleAdminAlert($job, $payload);
                break;
            case 'mail_autoresponder':
                $this->handleAutoresponder($job, $payload);
                break;
            case 'mailchimp_sync_subscriber':
                $this->handleMailchimpSync($job);
                break;
            case 'webhook_submission_created':
                $this->handleSubmissionCreatedWebhook($job, $payload);
                break;
            case 'wordpress_account_process':
                $this->accounts_job_processor->process($job, $payload);
                break;
            case 'google_sheets_sync':
                $this->handleGoogleSheetsSync($job);
                break;
            case 'klaviyo_sync_profile':
                $this->handleKlaviyoSync($job);
                break;
            case 'notification_dispatch_email':
                (new \GriffinForms\Includes\Pipelines\Workers\NotificationDispatchWorker())->process($job, $payload);
                break;
            default:
                throw new \RuntimeException(sprintf('Unknown job type %s', $job['job_type'] ?? 'unknown'));
        }
    }

    /**
     * Deliver signed outbound webhook for submission.created event.
     */
    private function handleSubmissionCreatedWebhook(array $job, array $payload): void
    {
        $submission_id = absint($job['submission_id'] ?? 0);
        $form_id = absint($job['form_id'] ?? 0);
        $target_url = esc_url_raw((string) ($payload['target_url'] ?? ''));
        $secret = (string) ($payload['secret'] ?? '');
        $timeout = (int) ($payload['timeout'] ?? 10);
        $event = is_array($payload['event'] ?? null) ? $payload['event'] : [];
        $targets = is_array($payload['targets'] ?? null) ? $payload['targets'] : [];
        $event_id = sanitize_text_field((string) ($event['event_id'] ?? ''));
        $event_type = sanitize_text_field((string) ($event['event_type'] ?? 'submission.created'));
        $request_id = $this->buildJobRequestId($job, $event_id);

        if ($target_url === '' || !wp_http_validate_url($target_url) || $secret === '' || empty($event)) {
            Capabilities::logAudit('webhook_submission_created_delivery_failed', 'failed', [
                'target_type' => 'submission',
                'target_id' => $submission_id,
                'meta' => [
                    'request_id' => $request_id,
                    'channel' => 'api_webhook',
                    'request_method' => 'JOB',
                    'request_uri' => 'job://webhook_submission_created',
                    'job_type' => sanitize_text_field((string) ($job['job_type'] ?? '')),
                    'form_id' => $form_id,
                    'event_id' => $event_id,
                    'event_type' => $event_type,
                    'target_url' => $target_url,
                    'reason' => 'invalid_payload',
                ],
            ]);
            throw new \RuntimeException('Webhook job payload invalid (url/secret/event missing).');
        }

        $timestamp = isset($event['timestamp']) ? (int) $event['timestamp'] : time();

        // Backward-compatible legacy path (single event webhook, no per-device targets).
        if (empty($targets)) {
            $result = $this->deliverWebhookPayload(
                $target_url,
                $secret,
                $timeout,
                $timestamp,
                $event,
                [],
                $event_id,
                $event_type
            );
            if (!$result['ok']) {
                if (($result['classification'] ?? '') === 'permanent_failure') {
                    $this->log->add(
                        'warning',
                        'submission',
                        $submission_id,
                        0,
                        sprintf('Webhook permanent failure (%s): %s', $event_id, $result['exception_message']),
                        'API Webhook'
                    );
                    return;
                }
                throw new \RuntimeException($result['exception_message']);
            }

            Capabilities::logAudit('webhook_submission_created_delivered', 'success', [
                'target_type' => 'submission',
                'target_id' => $submission_id,
                'meta' => [
                    'request_id' => $request_id,
                    'channel' => 'api_webhook',
                    'request_method' => 'JOB',
                    'request_uri' => 'job://webhook_submission_created',
                    'job_type' => sanitize_text_field((string) ($job['job_type'] ?? '')),
                    'form_id' => $form_id,
                    'event_id' => $event_id,
                    'event_type' => $event_type,
                    'target_url' => $target_url,
                    'status_code' => (int) ($result['status_code'] ?? 0),
                ],
            ]);

            $this->log->add(
                'info',
                'submission',
                $submission_id,
                0,
                sprintf('Webhook delivered (%s) to %s', $event_id, $target_url),
                'API Webhook'
            );
            return;
        }

        $summary = [
            'target_total' => count($targets),
            'delivered' => 0,
            'failed_transient' => 0,
            'failed_permanent' => 0,
            'skipped' => 0,
        ];

        foreach ($targets as $target) {
            $normalized_target = $this->normalizeTarget($target);
            $user_id = (int) ($normalized_target['user_id'] ?? 0);
            $device_id = (string) ($normalized_target['device_id'] ?? '');
            $platform = (string) ($normalized_target['platform'] ?? 'unknown');
            $push_token = (string) ($normalized_target['push_token'] ?? '');

            if ($user_id <= 0 || $device_id === '') {
                $summary['skipped']++;
                continue;
            }

            $registry_record = $this->getDeviceRecord($user_id, $device_id);
            $token_state_before = (string) ($registry_record['token_state'] ?? 'unknown');
            if (empty($registry_record) || $token_state_before !== 'active' || !empty($registry_record['deleted_at'])) {
                $summary['skipped']++;
                $this->auditNotificationTarget(
                    'notification_target_skipped',
                    'skipped',
                    $submission_id,
                    $form_id,
                    $event_id,
                    $request_id,
                    $user_id,
                    $device_id,
                    $platform,
                    [
                        'result_class' => 'inactive_skip',
                        'attempt' => ((int) ($job['attempts'] ?? 0)) + 1,
                        'token_state_before' => $token_state_before,
                        'token_state_after' => $token_state_before,
                    ]
                );
                continue;
            }

            $push_token_current = sanitize_text_field((string) ($registry_record['push_token'] ?? ''));
            if ($push_token_current === '') {
                $summary['skipped']++;
                $this->auditNotificationTarget(
                    'notification_target_skipped',
                    'skipped',
                    $submission_id,
                    $form_id,
                    $event_id,
                    $request_id,
                    $user_id,
                    $device_id,
                    $platform,
                    [
                        'result_class' => 'missing_token_skip',
                        'attempt' => ((int) ($job['attempts'] ?? 0)) + 1,
                        'token_state_before' => $token_state_before,
                        'token_state_after' => $token_state_before,
                    ]
                );
                continue;
            }

            $delivery_payload = [
                'event' => $event,
                'target' => [
                    'user_id' => $user_id,
                    'device_id' => $device_id,
                    'platform' => $platform,
                    'push_token' => $push_token_current,
                    'labels' => is_array($normalized_target['labels']) ? $normalized_target['labels'] : [],
                    'last_seen' => (string) ($normalized_target['last_seen'] ?? ''),
                ],
            ];

            $result = $this->deliverWebhookPayload(
                $target_url,
                $secret,
                $timeout,
                $timestamp,
                $delivery_payload,
                [
                    'X-GriffinForms-Target-User' => (string) $user_id,
                    'X-GriffinForms-Target-Device' => $device_id,
                    'X-GriffinForms-Target-Platform' => $platform,
                ],
                $event_id,
                $event_type
            );

            $classification = (string) ($result['classification'] ?? 'transient_failure');
            if ($classification === 'success') {
                $summary['delivered']++;
                $this->mutateDeviceRecord($user_id, $device_id, static function (array $record): array {
                    $now = gmdate('Y-m-d H:i:s');
                    $record['token_state'] = 'active';
                    $record['last_seen'] = gmdate('c');
                    $record['updated_at'] = $now;
                    $record['notify_fail_transient_count'] = 0;
                    return $record;
                });

                $this->auditNotificationTarget(
                    'notification_target_delivered',
                    'success',
                    $submission_id,
                    $form_id,
                    $event_id,
                    $request_id,
                    $user_id,
                    $device_id,
                    $platform,
                    [
                        'result_class' => 'success',
                        'attempt' => ((int) ($job['attempts'] ?? 0)) + 1,
                        'status_code' => (int) ($result['status_code'] ?? 0),
                        'token_state_before' => $token_state_before,
                        'token_state_after' => 'active',
                    ]
                );
                continue;
            }

            if ($classification === 'permanent_failure') {
                $summary['failed_permanent']++;
                $this->mutateDeviceRecord($user_id, $device_id, static function (array $record): array {
                    $now = gmdate('Y-m-d H:i:s');
                    $record['token_state'] = 'invalid';
                    $record['deleted_at'] = $now;
                    $record['updated_at'] = $now;
                    $record['notify_fail_transient_count'] = 0;
                    return $record;
                });

                $this->auditNotificationTarget(
                    'notification_target_failed_permanent',
                    'failed',
                    $submission_id,
                    $form_id,
                    $event_id,
                    $request_id,
                    $user_id,
                    $device_id,
                    $platform,
                    [
                        'result_class' => 'permanent_failure',
                        'attempt' => ((int) ($job['attempts'] ?? 0)) + 1,
                        'status_code' => (int) ($result['status_code'] ?? 0),
                        'error_code' => (string) ($result['error_code'] ?? ''),
                        'error_message' => (string) ($result['error_message'] ?? ''),
                        'token_state_before' => $token_state_before,
                        'token_state_after' => 'invalid',
                    ]
                );
                continue;
            }

            $summary['failed_transient']++;
            $this->mutateDeviceRecord($user_id, $device_id, static function (array $record): array {
                $record['notify_fail_transient_count'] = ((int) ($record['notify_fail_transient_count'] ?? 0)) + 1;
                $record['updated_at'] = gmdate('Y-m-d H:i:s');
                return $record;
            });

            $this->auditNotificationTarget(
                'notification_target_failed_transient',
                'failed',
                $submission_id,
                $form_id,
                $event_id,
                $request_id,
                $user_id,
                $device_id,
                $platform,
                [
                    'result_class' => 'transient_failure',
                    'attempt' => ((int) ($job['attempts'] ?? 0)) + 1,
                    'status_code' => (int) ($result['status_code'] ?? 0),
                    'error_code' => (string) ($result['error_code'] ?? ''),
                    'error_message' => (string) ($result['error_message'] ?? ''),
                    'token_state_before' => $token_state_before,
                    'token_state_after' => $token_state_before,
                ]
            );
        }

        Capabilities::logAudit('notification_fanout_attempt_summary', 'success', [
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'meta' => [
                'request_id' => $request_id,
                'channel' => 'api_webhook',
                'request_method' => 'JOB',
                'request_uri' => 'job://webhook_submission_created',
                'job_type' => 'webhook_submission_created',
                'event_id' => $event_id,
                'form_id' => $form_id,
                'attempt' => ((int) ($job['attempts'] ?? 0)) + 1,
                'target_total' => $summary['target_total'],
                'delivered' => $summary['delivered'],
                'failed_transient' => $summary['failed_transient'],
                'failed_permanent' => $summary['failed_permanent'],
                'skipped' => $summary['skipped'],
            ],
        ]);

        $this->log->add(
            $summary['failed_transient'] > 0 ? 'warning' : 'info',
            'submission',
            $submission_id,
            0,
            sprintf(
                'Notification fan-out (%s) targets=%d delivered=%d transient=%d permanent=%d skipped=%d',
                $event_id,
                $summary['target_total'],
                $summary['delivered'],
                $summary['failed_transient'],
                $summary['failed_permanent'],
                $summary['skipped']
            ),
            'API Webhook'
        );

        if ($summary['failed_transient'] > 0) {
            throw new \RuntimeException(sprintf('Fan-out transient failures remain for event %s', $event_id));
        }
    }

    private function handleGoogleSheetsSync(array $job): void
    {
        $submission_id = (int) ($job['submission_id'] ?? 0);
        $form_id       = (int) ($job['form_id'] ?? 0);

        if ($submission_id <= 0 || $form_id <= 0) {
            return;
        }

        $integration = new \GriffinFormsPro\Includes\Integrations\GoogleSheets\Main();
        $effective   = $integration->getEffectiveFormSettings($form_id);
        $result      = (new \GriffinFormsPro\Includes\Integrations\GoogleSheets\WriteEngine())->write($submission_id, $form_id, $effective);
        $error_store = new \GriffinFormsPro\Includes\Integrations\GoogleSheets\ErrorStateStore();

        if ($result->isOk()) {
            $this->log->add('info', 'submission', $submission_id, 0,
                sprintf('Google Sheets: row appended for submission %d on form %d.', $submission_id, $form_id),
                \GriffinFormsPro\Includes\Integrations\GoogleSheets\Contract::LOG_CATEGORY
            );
            $error_store->clearError($form_id);
            if (class_exists(\GriffinFormsPro\Includes\Integrations\GoogleSheets\SyncStateStore::class)) {
                (new \GriffinFormsPro\Includes\Integrations\GoogleSheets\SyncStateStore())
                    ->saveState($submission_id, ['status' => 'synced']);
            }
        } else {
            $truncated_message = mb_substr($result->error_message, 0, 300);
            $this->log->add('error', 'submission', $submission_id, 0,
                sprintf('Google Sheets write failed for submission %d on form %d. Code: %s. %s',
                    $submission_id, $form_id, $result->error_code, $truncated_message),
                \GriffinFormsPro\Includes\Integrations\GoogleSheets\Contract::LOG_CATEGORY
            );
            $error_store->recordError($form_id, $result->error_code, $truncated_message);
            if (class_exists(\GriffinFormsPro\Includes\Integrations\GoogleSheets\SyncStateStore::class)) {
                (new \GriffinFormsPro\Includes\Integrations\GoogleSheets\SyncStateStore())
                    ->saveState($submission_id, ['status' => 'failed', 'error_code' => $result->error_code]);
            }

            $attempts = (int) ($job['attempts'] ?? 0) + 1;
            if ($result->error_code !== \GriffinFormsPro\Includes\Integrations\GoogleSheets\Contract::ERROR_AUTH_FAILED && $attempts < 5) {
                throw new \RuntimeException($result->error_message);
            }
        }
    }

    private function handleAdminAlert(array $job, array $payload): void
    {
        $recipient = sanitize_email($payload['recipient'] ?? '');
        $message_id = isset($payload['message_id']) ? (int) $payload['message_id'] : 0;
        $form_id = isset($job['form_id']) ? (int) $job['form_id'] : 0;
        $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0;
        if (empty($recipient)) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                0,
                'Admin alert email ignored: invalid recipient address.',
                $this->getMailLogCategory()
            );
            return;
        }

        $context = SubmissionContextBuilder::getInstance()->build((int) $job['submission_id']);
        $form = $this->loadForm((int) ($job['form_id'] ?? 0));
        $context['form_object'] = $form;
        if ($submission_id > 0 && (empty($context) || empty($context['submission']))) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                0,
                'Merge context missing for admin alert; tokens may not resolve.',
                $this->getMailLogCategory()
            );
        }

        $template_data = [
            'form'       => $form,
            'submission' => $context['submission'] ?? null,
        ];

        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
        $subject = '';
        $content = '';
        $default_subject = '';
        $default_content = '';

        if ($message_id > 0) {
            $message = $this->getMessageById($message_id);
            if ($message) {
                $subject = $merge_engine->render($message['name'] ?? '', $context);
                $content = $merge_engine->render($message['content'] ?? '', $context);
                if ($submission_id > 0 && trim($content) === '') {
                    $this->log->add(
                        'warning',
                        'submission',
                        $submission_id,
                        0,
                        sprintf('Admin alert message %d has empty content; using default template.', $message_id),
                        $this->getMailLogCategory()
                    );
                }
            } else {
                if ($submission_id > 0) {
                    $this->log->add(
                        'warning',
                        'submission',
                        $submission_id,
                        0,
                        sprintf('Admin alert message %d no longer exists; using default template.', $message_id),
                        $this->getMailLogCategory()
                    );
                }
            }
        }

        if ($subject === '' || trim($content) === '') {
            $template = new \GriffinForms\Includes\MailTemplates\AdminSubmissionAlert($template_data);
            $default_subject = $merge_engine->render($template->getSubject(), $context);
            $default_content = $merge_engine->render($template->getContent(), $context);
        }

        if ($subject === '') {
            $subject = $default_subject;
        }

        if (trim($content) === '') {
            $content = $default_content;
        }

        $mailer = $this->createMailer((int) ($job['submission_id'] ?? 0));
        $mailer->setEmailSubject($subject);
        $mailer->setEmailContent($content);
        $mailer->setEmailRecipient($recipient);
        $mailer->sendMail();
    }

    private function handleAutoresponder(array $job, array $payload): void
    {
        $recipient = sanitize_email($payload['recipient'] ?? '');
        $message_id = isset($payload['message_id']) ? (int) $payload['message_id'] : 0;
        $form_id = isset($job['form_id']) ? (int) $job['form_id'] : 0;
        $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0;
        if (empty($recipient) || $message_id <= 0) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                0,
                'Autoresponder email ignored: invalid recipient or missing message mapping.',
                $this->getMailLogCategory()
            );
            return;
        }

        $message = $this->getMessageById($message_id);
        if (!$message) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                0,
                sprintf('Autoresponder message %d no longer exists; email not sent.', $message_id),
                $this->getMailLogCategory()
            );
            return;
        }

        $context = SubmissionContextBuilder::getInstance()->build((int) $job['submission_id']);
        $form = $this->loadForm((int) ($job['form_id'] ?? 0));
        $context['form_object'] = $form;
        if ($submission_id > 0 && (empty($context) || empty($context['submission']))) {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                0,
                'Merge context missing for autoresponder; tokens may not resolve.',
                $this->getMailLogCategory()
            );
        }
        $merge_engine = new \GriffinForms\Includes\MergeTokens\Engine();
        $subject = $merge_engine->render($message['name'] ?? '', $context);
        $content = $merge_engine->render($message['content'] ?? '', $context);
        if (trim($content) === '') {
            $this->log->add(
                'warning',
                'submission',
                $submission_id,
                0,
                sprintf('Autoresponder message %d has empty content; email not sent.', $message_id),
                $this->getMailLogCategory()
            );
            return;
        }

        $mailer = $this->createMailer((int) ($job['submission_id'] ?? 0));
        $mailer->setEmailSubject($subject);
        $mailer->setEmailContent($content);
        $mailer->setEmailRecipient($recipient);
        $mailer->sendMail();
    }

    private function handleMailchimpSync(array $job): void
    {
        $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0;
        $form_id = isset($job['form_id']) ? (int) $job['form_id'] : 0;
        if ($submission_id <= 0 || $form_id <= 0) {
            return;
        }

        $context = SubmissionContextBuilder::getInstance()->build($submission_id);
        if (empty($context['submission'])) {
            throw new \RuntimeException(sprintf('Mailchimp sync context could not be rebuilt for submission %d.', $submission_id));
        }

        $service = new \GriffinForms\Includes\Integrations\Mailchimp\SyncService();
        $service->syncSubmission($job, $context);
    }

    private function handleKlaviyoSync(array $job): void
    {
        $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0;
        $form_id       = isset($job['form_id'])       ? (int) $job['form_id']       : 0;

        if ($submission_id <= 0 || $form_id <= 0) {
            return;
        }

        if (!class_exists('\\GriffinFormsPro\\Includes\\Integrations\\Klaviyo\\SyncService')) {
            throw new \RuntimeException(
                sprintf('Klaviyo SyncService not available for submission %d — is Pro active?', $submission_id)
            );
        }

        $context = SubmissionContextBuilder::getInstance()->build($submission_id);
        if (empty($context['submission'])) {
            throw new \RuntimeException(
                sprintf('Klaviyo sync context could not be rebuilt for submission %d.', $submission_id)
            );
        }

        $service = new \GriffinFormsPro\Includes\Integrations\Klaviyo\SyncService();
        $service->syncSubmission($job, $context);
    }

    private function createMailer(int $submission_id): MailHandler
    {
        $mailer = new MailHandler();
        $mailer->setLogContext('submission', $submission_id);
        $mailer->setSubmissionId($submission_id);
        return $mailer;
    }

    private function loadForm(int $form_id)
    {
        if ($form_id <= 0 || !class_exists('\GriffinForms\Items\Form')) {
            return null;
        }

        $form = new \GriffinForms\Items\Form();
        $loaded = $form->loadItem($form_id);
        return $loaded ? $form : null;
    }

    private function getMessageById(int $message_id): ?array
    {
        global $wpdb;
        $table = Config::getInstance()->getTable('message');
        if (!$table) {
            return null;
        }

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $message_id),
            ARRAY_A
        );

        return $row ?: null;
    }

    private function logJobFailure(array $job, \Throwable $error): void
    {
        $submission_id = isset($job['submission_id']) ? (int) $job['submission_id'] : 0;
        $job_type = isset($job['job_type']) ? (string) $job['job_type'] : 'unknown';
        $integration = isset($job['integration_id']) ? (string) $job['integration_id'] : '';
        $job_id = isset($job['id']) ? (int) $job['id'] : 0;
        $user_id = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;

        $message = sprintf(
            'Job %d (%s%s) failed: %s',
            $job_id,
            $job_type,
            $integration !== '' ? ' / ' . $integration : '',
            $error->getMessage()
        );

        $this->log->add(
            'error',
            'submission',
            $submission_id,
            $user_id,
            $message,
            'Integration (pipeline)'
        );
    }

    private function resolveFailurePolicy(array $job): array
    {
        $job_type = (string) ($job['job_type'] ?? '');
        if ($job_type === 'wordpress_account_process') {
            return ['retry' => false, 'delay' => 0];
        }

        if ($job_type === 'notification_dispatch_email') {
            // Terminal failure when channel is not registered (integration inactive).
            // Retry up to 3 attempts for transient failures.
            if (str_starts_with($job['last_error'] ?? '', '__NO_RETRY__')) {
                return ['retry' => false, 'delay' => 0];
            }
            $attempts = (int) ($job['attempts'] ?? 0) + 1;
            return $attempts >= 3 ? ['retry' => false, 'delay' => 0] : ['retry' => true, 'delay' => 60];
        }

        if ($job_type === 'google_sheets_sync') {
            $attempts = (int) ($job['attempts'] ?? 0) + 1;
            if ($attempts >= 5) {
                return ['retry' => false, 'delay' => 0];
            }
            return ['retry' => true, 'delay' => 60];
        }

        if ($job_type === 'klaviyo_sync_profile') {
            $attempts = (int) ($job['attempts'] ?? 0) + 1;
            if ($attempts >= 5) {
                return ['retry' => false, 'delay' => 0];
            }
            return ['retry' => true, 'delay' => 60];
        }

        if ($job_type !== 'webhook_submission_created') {
            return ['retry' => true, 'delay' => 60];
        }

        $payload = json_decode((string) ($job['payload'] ?? '[]'), true);
        if (!is_array($payload)) {
            return ['retry' => true, 'delay' => 60];
        }

        $targets = is_array($payload['targets'] ?? null) ? $payload['targets'] : [];
        if (empty($targets)) {
            $attempts     = (int) ($job['attempts'] ?? 0);
            $max_attempts = 5;
            if ($attempts + 1 >= $max_attempts) {
                return ['retry' => false, 'delay' => 0];
            }
            return ['retry' => true, 'delay' => 60];
        }

        $attempts = (int) ($job['attempts'] ?? 0);
        $max_attempts = (int) ($payload['attempt']['max_fanout_attempts'] ?? 5);
        $max_attempts = max(1, $max_attempts);
        $next_attempt = $attempts + 1;
        if ($next_attempt >= $max_attempts) {
            return ['retry' => false, 'delay' => 0];
        }

        $backoff = self::FANOUT_BACKOFF_SECONDS;
        $index = max(0, min($attempts, count($backoff) - 1));
        return ['retry' => true, 'delay' => (int) $backoff[$index]];
    }

    private function normalizeTarget(array $target): array
    {
        $labels = [];
        $raw_labels = $target['labels'] ?? [];
        if (is_array($raw_labels)) {
            foreach ($raw_labels as $label) {
                $clean = sanitize_text_field((string) $label);
                if ($clean !== '') {
                    $labels[] = $clean;
                }
            }
            $labels = array_values(array_unique($labels));
        }

        return [
            'user_id' => absint($target['user_id'] ?? 0),
            'device_id' => sanitize_text_field((string) ($target['device_id'] ?? '')),
            'platform' => sanitize_key((string) ($target['platform'] ?? 'unknown')),
            'push_token' => sanitize_text_field((string) ($target['push_token'] ?? '')),
            'labels' => $labels,
            'last_seen' => sanitize_text_field((string) ($target['last_seen'] ?? '')),
        ];
    }

    private function deliverWebhookPayload(
        string $target_url,
        string $secret,
        int $timeout,
        int $timestamp,
        array $payload,
        array $extra_headers,
        string $event_id,
        string $event_type
    ): array {
        $body = wp_json_encode($payload);
        if (!is_string($body) || $body === '') {
            return [
                'ok' => false,
                'classification' => 'permanent_failure',
                'status_code' => 0,
                'error_code' => 'json_encode_failed',
                'error_message' => 'Unable to encode webhook payload.',
                'exception_message' => 'Webhook event payload encoding failed.',
            ];
        }

        $signed_payload = $timestamp . '.' . $body;
        $signature = hash_hmac('sha256', $signed_payload, $secret);
        $headers = [
            'Content-Type' => 'application/json',
            'X-GriffinForms-Event-Id' => $event_id,
            'X-GriffinForms-Event' => $event_type,
            'X-GriffinForms-Timestamp' => (string) $timestamp,
            'X-GriffinForms-Signature' => 't=' . $timestamp . ',v1=' . $signature,
        ];
        foreach ($extra_headers as $k => $v) {
            $headers[sanitize_text_field((string) $k)] = sanitize_text_field((string) $v);
        }

        $response = wp_safe_remote_post($target_url, [
            'timeout' => max(3, min(30, $timeout)),
            'redirection' => 2,
            'blocking' => true,
            'headers' => $headers,
            'body' => $body,
        ]);

        if (is_wp_error($response)) {
            return [
                'ok' => false,
                'classification' => 'transient_failure',
                'status_code' => 0,
                'error_code' => 'transport_error',
                'error_message' => sanitize_text_field($response->get_error_message()),
                'exception_message' => 'Webhook delivery failed: ' . sanitize_text_field($response->get_error_message()),
            ];
        }

        $status_code = (int) wp_remote_retrieve_response_code($response);
        $response_body = (string) wp_remote_retrieve_body($response);
        $response_excerpt = mb_substr(sanitize_text_field($response_body), 0, 180);
        if ($status_code >= 200 && $status_code < 300) {
            return [
                'ok' => true,
                'classification' => 'success',
                'status_code' => $status_code,
                'error_code' => '',
                'error_message' => '',
                'exception_message' => '',
            ];
        }

        if ($status_code === 429 || ($status_code >= 500 && $status_code <= 599)) {
            return [
                'ok' => false,
                'classification' => 'transient_failure',
                'status_code' => $status_code,
                'error_code' => 'non_2xx',
                'error_message' => $response_excerpt,
                'exception_message' => sprintf('Webhook returned HTTP %d: %s', $status_code, $response_excerpt),
            ];
        }

        $body_lc = strtolower($response_body);
        $permanent = ($status_code === 400 || $status_code === 404 || $status_code === 410 || $status_code === 422);
        if (!$permanent && (str_contains($body_lc, 'invalid token') || str_contains($body_lc, 'unregistered') || str_contains($body_lc, 'not registered'))) {
            $permanent = true;
        }

        return [
            'ok' => false,
            'classification' => $permanent ? 'permanent_failure' : 'transient_failure',
            'status_code' => $status_code,
            'error_code' => 'non_2xx',
            'error_message' => $response_excerpt,
            'exception_message' => sprintf('Webhook returned HTTP %d: %s', $status_code, $response_excerpt),
        ];
    }

    private function getDeviceRecord(int $user_id, string $device_id): array
    {
        $registry = $this->getDeviceRegistryForUser($user_id);
        foreach ($registry as $record) {
            if (!is_array($record)) {
                continue;
            }
            if ((string) ($record['device_id'] ?? '') === $device_id) {
                return $record;
            }
        }
        return [];
    }

    private function mutateDeviceRecord(int $user_id, string $device_id, callable $mutator): void
    {
        if ($user_id <= 0 || $device_id === '') {
            return;
        }

        $registry = $this->getDeviceRegistryForUser($user_id);
        if (empty($registry)) {
            return;
        }

        $updated = false;
        foreach ($registry as $index => $record) {
            if (!is_array($record)) {
                continue;
            }
            if ((string) ($record['device_id'] ?? '') !== $device_id) {
                continue;
            }
            $next = $mutator($record);
            if (!is_array($next)) {
                break;
            }
            $registry[$index] = $next;
            $updated = true;
            break;
        }

        if (!$updated) {
            return;
        }

        $option_name = self::DEVICE_OPTION_PREFIX . $user_id;
        $this->settings->updateOption($option_name, array_values($registry), 'capabilitymatrix');
    }

    private function getDeviceRegistryForUser(int $user_id): array
    {
        if ($user_id <= 0) {
            return [];
        }

        $option_name = self::DEVICE_OPTION_PREFIX . $user_id;
        $registry = $this->settings->getOption($option_name, []);
        return is_array($registry) ? $registry : [];
    }

    private function auditNotificationTarget(
        string $action,
        string $outcome,
        int $submission_id,
        int $form_id,
        string $event_id,
        string $request_id,
        int $user_id,
        string $device_id,
        string $platform,
        array $meta
    ): void {
        $base = [
            'request_id' => $request_id,
            'channel' => 'api_webhook',
            'request_method' => 'JOB',
            'request_uri' => 'job://webhook_submission_created',
            'job_type' => 'webhook_submission_created',
            'event_id' => $event_id,
            'form_id' => $form_id,
            'user_id' => $user_id,
            'device_id' => $device_id,
            'platform' => $platform,
        ];
        Capabilities::logAudit($action, $outcome, [
            'target_type' => 'submission',
            'target_id' => $submission_id,
            'meta' => array_merge($base, $meta),
        ]);
    }

    private function buildJobRequestId(array $job, string $event_id): string
    {
        $job_id = (int) ($job['id'] ?? 0);
        $attempt = ((int) ($job['attempts'] ?? 0)) + 1;
        if ($job_id > 0) {
            return sprintf('job-%d-a%d', $job_id, $attempt);
        }

        if ($event_id !== '') {
            return sprintf('job-%s-a%d', substr(hash('sha256', $event_id), 0, 8), $attempt);
        }

        return sprintf('job-%s', substr(hash('sha256', (string) microtime(true) . '|' . wp_rand()), 0, 12));
    }

    private function getMailLogCategory(): string
    {
        if (!empty($this->mail_log_category)) {
            return $this->mail_log_category;
        }

        $registry = \GriffinForms\Includes\Integrations\Categories\Mailer::getInstance();
        $provider = $registry ? $registry->getActiveProviderId() : 'server_mail';
        if (empty($provider)) {
            $provider = 'server_mail';
        }

        $this->mail_log_category = sprintf('Mail Integration (%s)', $provider);
        return $this->mail_log_category;
    }
}
