<?php

namespace App\Jobs;

use App\Models\Campaign;
use App\Models\CampaignEvent;
use App\Models\Contact;
use App\Models\SmtpConfig;
use App\Services\BusinessSettings;
use App\Services\EmailDeliverabilityChecker;
use App\Services\EmailRenderer;
use App\Services\SmtpConfigService;
use App\Services\AutomationEngine;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Mail\Message;

class ProcessAutomationNodeJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $timeout = 120;

    public function __construct(
        public readonly int $automationId,
        public readonly int $contactId,
        public readonly string $nodeId
    ) {}

    public function handle(EmailRenderer $renderer): void
    {
        $automation = Campaign::find($this->automationId);
        $contact = Contact::find($this->contactId);

        if (!$automation || !$contact || $automation->status !== 'active') {
            return;
        }

        $settings = $automation->settings;
        $nodes = $settings['nodes'] ?? [];
        $edges = $settings['edges'] ?? [];

        // Find current node
        $node = collect($nodes)->first(fn($n) => $n['id'] === $this->nodeId);
        if (!$node) {
            return;
        }

        $runOnUnsubscribed = $settings['run_on_unsubscribed'] ?? false;
        if ($contact->status === 'unsubscribed' && !$runOnUnsubscribed && $node['type'] !== 'trigger' && $node['type'] !== 'incoming_webhook') {
            return;
        }

        $nextBranch = null;

        // Record start / processing state for this step
        \App\Models\CampaignEvent::updateOrCreate(
            [
                'campaign_id' => $automation->id,
                'contact_id'  => $contact->id,
                'node_id'     => $node['id'],
            ],
            [
                'event_type'  => 'node_processing',
                'reason'      => 'Step is currently executing.',
            ]
        );

        try {
            switch ($node['type']) {
                case 'incoming_webhook':
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Triggered via incoming webhook.'
                        ]);
                    break;

                case 'email':
                case 'campaign_email':
                    $recipientEmail = $contact->email;
                    $sendToType = $node['config']['send_to_type'] ?? 'contact';
                    if ($sendToType === 'custom' && !empty($node['config']['custom_email_address'])) {
                        $recipientEmail = $node['config']['custom_email_address'];
                        $recipientEmail = str_replace(
                            ['{{contact.email}}', '{{contact.first_name}}', '{{contact.last_name}}', '{{contact.full_name}}'],
                            [$contact->email, $contact->first_name ?? '', $contact->last_name ?? '', trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? ''))],
                            $recipientEmail
                        );
                    }

                    if ($reason = EmailDeliverabilityChecker::reason($recipientEmail)) {
                        $automation->increment('emails_failed');
                        \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                            ->where('contact_id', $contact->id)
                            ->where('node_id', $node['id'])
                            ->update([
                                'event_type' => 'node_error',
                                'reason'     => 'Email deliverability check failed: ' . $reason
                            ]);
                        return;
                    }
                    $status = $this->executeEmailNode($automation, $contact, $node, $renderer);
                    if ($status === 'skipped_audience') {
                        \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                            ->where('contact_id', $contact->id)
                            ->where('node_id', $node['id'])
                            ->update([
                                'event_type' => 'node_success',
                                'reason'     => 'Skipped: Contact does not match campaign list/tag targets.'
                            ]);
                    } elseif ($status === 'skipped_deliverability') {
                        \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                            ->where('contact_id', $contact->id)
                            ->where('node_id', $node['id'])
                            ->update([
                                'event_type' => 'node_success',
                                'reason'     => 'Skipped: Deliverability check failed.'
                            ]);
                    } else {
                        \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                            ->where('contact_id', $contact->id)
                            ->where('node_id', $node['id'])
                            ->update([
                                'event_type' => 'node_success',
                                'reason'     => 'Email sent successfully.'
                            ]);
                    }
                    break;

                case 'wait':
                    $this->executeWaitNode($automation, $contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Delay registered.'
                        ]);
                    return; // Wait node handles its own downstream dispatch with delay

                case 'condition':
                    $nextBranch = $this->executeConditionNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Condition evaluated to ' . strtoupper($nextBranch)
                        ]);
                    break;

                case 'webhook':
                    $this->executeWebhookNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Webhook request sent.'
                        ]);
                    break;

                case 'ab_split':
                    $nextBranch = $this->executeAbSplitNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'A/B split evaluated to path: ' . strtoupper($nextBranch)
                        ]);
                    break;

                case 'update_field':
                    $this->executeUpdateFieldNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Contact property updated.'
                        ]);
                    break;

                case 'apply_tag':
                    $this->executeApplyTagNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Tag applied.'
                        ]);
                    break;

                case 'remove_tag':
                    $this->executeRemoveTagNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Tag removed.'
                        ]);
                    break;

                case 'apply_list':
                    $this->executeApplyListNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Added to list.'
                        ]);
                    break;

                case 'remove_list':
                    $this->executeRemoveListNode($contact, $node);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Removed from list.'
                        ]);
                    break;

                case 'end':
                    AutomationEngine::complete($automation, $contact);
                    \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                        ->where('contact_id', $contact->id)
                        ->where('node_id', $node['id'])
                        ->update([
                            'event_type' => 'node_success',
                            'reason'     => 'Funnel completed.'
                        ]);
                    return;
            }
        } catch (\Throwable $e) {
            \App\Models\CampaignEvent::where('campaign_id', $automation->id)
                ->where('contact_id', $contact->id)
                ->where('node_id', $node['id'])
                ->update([
                    'event_type' => 'node_error',
                    'reason'     => 'Error: ' . $e->getMessage()
                ]);
            throw $e;
        }

        $this->dispatchDownstream($automation, $contact, $node['id'], $edges, $nextBranch);
    }

    private function executeEmailNode(Campaign $automation, Contact $contact, array $node, EmailRenderer $renderer): string
    {
        $config = $node['config'] ?? [];

        $recipientEmail = $contact->email;
        $sendToType = $config['send_to_type'] ?? 'contact';
        if ($sendToType === 'custom' && !empty($config['custom_email_address'])) {
            $recipientEmail = $config['custom_email_address'];
            $recipientEmail = str_replace(
                ['{{contact.email}}', '{{contact.first_name}}', '{{contact.last_name}}', '{{contact.full_name}}'],
                [$contact->email, $contact->first_name ?? '', $contact->last_name ?? '', trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? ''))],
                $recipientEmail
            );
        }

        if ($reason = EmailDeliverabilityChecker::reason($recipientEmail)) {
            $automation->increment('emails_failed');
            return 'skipped_deliverability';
        }

        $subject = $config['subject'] ?? $automation->title;
        $bodyHtml = $config['email_body'] ?? '';
        $blocks = $config['blocks'] ?? [];

        // Dynamically load from imported campaign if referenced
        $importedCampaignId = $config['imported_campaign_id'] ?? null;
        if ($importedCampaignId) {
            $importedCampaign = \App\Models\Campaign::find($importedCampaignId);
            if ($importedCampaign) {
                // Check if contact matches list/tag criteria for this campaign
                if (!empty($importedCampaign->recipients_config)) {
                    $matchedIds = \App\Services\CampaignRecipientResolver::resolve($importedCampaign->recipients_config, $importedCampaign->user_id);
                    if (!in_array($contact->id, $matchedIds)) {
                        return 'skipped_audience';
                    }
                }

                if (!empty($importedCampaign->settings['blocks'])) {
                    $blocks = $importedCampaign->settings['blocks'];
                } elseif (!empty($importedCampaign->email_body)) {
                    $blocks = [
                        [
                            'type' => 'html',
                            'html' => $importedCampaign->email_body
                        ]
                    ];
                }

                if (empty($config['subject']) && !empty($importedCampaign->email_subject)) {
                    $subject = $importedCampaign->email_subject;
                }
            }
        }

        if (empty($blocks)) {
            $blocks = [
                [
                    'type' => 'html',
                    'html' => $bodyHtml ?: '<p>Hello!</p>'
                ]
            ];
        }

        $mergeData = [
            'contact.first_name' => $contact->first_name ?? '',
            'contact.last_name'  => $contact->last_name  ?? '',
            'contact.email'      => $contact->email       ?? '',
            'contact.full_name'  => trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')),
            'first_name'         => $contact->first_name ?? '',
            'last_name'          => $contact->last_name  ?? '',
            'email'              => $contact->email       ?? '',
            'full_name'          => trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')),
        ];

        $preHeader = $config['pre_header'] ?? '';
        $html = $renderer->render($blocks, $mergeData, $automation->id, $contact->id, $preHeader);

        foreach ($mergeData as $k => $v) {
            $subject = str_replace('{{' . $k . '}}', $v, $subject);
        }

        $smtp = null;
        $smtpId = $config['smtp_config_id'] ?? null;
        if ($smtpId) {
            $smtp = SmtpConfig::where('user_id', $automation->user_id)->where('id', $smtpId)->first();
        }
        if (!$smtp) {
            $smtp = SmtpConfig::where('user_id', $automation->user_id)->where('is_default', true)->first();
        }
        $fromEmail = $smtp?->from_email ?: null;
        $fromName  = $smtp?->from_name  ?: null;

        if (!empty($config['from_email'])) {
            $fromEmail = $config['from_email'];
        }
        if (!empty($config['from_name'])) {
            $fromName = $config['from_name'];
        }

        $emailSettings = BusinessSettings::emailSettings($automation->id);
        if (empty($fromEmail) && !empty($emailSettings['from_email'])) {
            $fromEmail = $emailSettings['from_email'];
        }
        if (empty($fromName) && !empty($emailSettings['from_name'])) {
            $fromName = $emailSettings['from_name'];
        }

        $replyEmail = $config['reply_to_email'] ?? ($emailSettings['reply_to_email'] ?: null);
        $replyName  = $emailSettings['reply_to_name'] ?: null;
        $returnPath = ($smtp?->return_path_match && $fromEmail) ? $fromEmail : null;

        if ($smtp) {
            $mailer = Mail::mailer(SmtpConfigService::configureRuntime($smtp));
        } else {
            $mailer = Mail::mailer();
        }

        $mailer->html($html, function (Message $msg) use ($automation, $recipientEmail, $subject, $fromEmail, $fromName, $replyEmail, $replyName, $returnPath) {
            $msg->to($recipientEmail)->subject($subject);

            if ($fromEmail) {
                $msg->from($fromEmail, $fromName);
            }
            if ($replyEmail) {
                $msg->replyTo($replyEmail, $replyName);
            }
            if ($returnPath) {
                $msg->returnPath($returnPath);
            }
        });

        $automation->increment('emails_sent');

        CampaignEvent::create([
            'campaign_id' => $automation->id,
            'contact_id'  => $contact->id,
            'event_type'  => 'sent',
        ]);

        $contact->logActivity('email_sent', "Automation email sent: \"{$subject}\"");
        return 'sent';
    }

    private function executeWaitNode(Campaign $automation, Contact $contact, array $node): void
    {
        $config = $node['config'] ?? [];
        $val = (int) ($config['value'] ?? 1);
        $unit = $config['unit'] ?? 'days';

        $seconds = match ($unit) {
            'minutes' => $val * 60,
            'hours'   => $val * 3600,
            'weeks'   => $val * 7 * 86400,
            default   => $val * 86400,
        };

        $edges = $automation->settings['edges'] ?? [];

        $nextEdges = collect($edges)->filter(fn($e) => $e['source'] === $node['id']);
        foreach ($nextEdges as $edge) {
            ProcessAutomationNodeJob::dispatch($automation->id, $contact->id, $edge['target'])
                ->delay(now()->addSeconds($seconds));
        }

        $contact->logActivity('automation_wait', "Wait step active: delay for {$val} {$unit}");
    }

    private function executeConditionNode(Contact $contact, array $node): string
    {
        $config = $node['config'] ?? [];
        $conditions = $config['conditions'] ?? [];

        if (empty($conditions)) {
            return 'yes';
        }

        $allMatch = true;

        foreach ($conditions as $cond) {
            if (!$this->evaluateSingleCondition($contact, $cond)) {
                $allMatch = false;
                break;
            }
        }

        $branch = $allMatch ? 'yes' : 'no';
        $contact->logActivity('automation_condition', "Condition evaluated to: " . strtoupper($branch));
        return $branch;
    }

    private function evaluateSingleCondition(Contact $contact, array $cond): bool
    {
        $field = $cond['field'] ?? '';
        $operator = $cond['operator'] ?? 'equals';
        $value = $cond['value'] ?? null;

        switch ($field) {
            case 'First Name':
                return $this->compareStrings($contact->first_name, $operator, $value);

            case 'Last Name':
                return $this->compareStrings($contact->last_name, $operator, $value);

            case 'Email Address':
                return $this->compareStrings($contact->email, $operator, $value);

            case 'Phone Number':
                return $this->compareStrings($contact->phone, $operator, $value);

            case 'City':
                return $this->compareStrings($contact->city, $operator, $value);

            case 'Country':
                return $this->compareStrings($contact->country, $operator, $value);

            case 'Signup Source':
            case 'Signup source':
                return $this->compareStrings($contact->source, $operator, $value);

            case 'Company':
            case 'Company ID':
                $actual = (int) $contact->company_id;
                $expected = (int) $value;
                return $this->compareNumeric($actual, $operator, $expected);

            case 'Contact Rating':
            case 'Lead Points':
            case 'Lead Score Points':
                $actual = (int) $contact->total_points;
                $expected = (int) $value;
                return $this->compareNumeric($actual, $operator, $expected);

            case 'Lifetime Value':
            case 'LTV':
            case 'Lifetime Value (LTV)':
                $actual = (float) $contact->life_time_value;
                $expected = (float) $value;
                return $this->compareNumeric($actual, $operator, $expected);

            case 'Contact Type':
                return $this->compareStrings($contact->contact_type, $operator, $value);

            case 'Subscription Status':
            case 'Email subscription status':
            case 'Email Subscription Status':
                return $this->compareStrings($contact->status, $operator, $value);

            case 'Birthday':
                return $this->compareDates($contact->date_of_birth, $operator, $value);

            case 'Date Added':
            case 'Email Date Added':
                return $this->compareDates($contact->created_at, $operator, $value);

            case 'Last Activity':
            case 'Last Purchase Date':
            case 'Last Activity (Purchase/Send)':
                // Check if value is a numeric relative offset (like "30" days ago)
                if (is_numeric($value)) {
                    $actual = $contact->last_activity;
                    if (!$actual) return false;
                    $days = Carbon::parse($actual)->diffInDays(now());
                    if ($operator === 'is before') {
                        return ($days > $value);
                    } else {
                        return ($days <= $value);
                    }
                }
                return $this->compareDates($contact->last_activity, $operator, $value);

            case 'List':
            case 'List Membership':
                $inList = $contact->lists()->where('lists.id', (int)$value)->exists();
                return ($operator === 'does not equal' || $operator === 'does_not_equal') ? !$inList : $inList;

            case 'Tag':
            case 'Tag Membership':
                $hasTag = $contact->tags()->where('tags.id', (int)$value)->exists();
                return ($operator === 'does not equal' || $operator === 'does_not_equal') ? !$hasTag : $hasTag;

            default:
                return false;
        }
    }

    private function compareStrings(?string $actual, string $operator, ?string $expected): bool
    {
        $actual = strtolower($actual ?? '');
        $expected = strtolower($expected ?? '');

        return match ($operator) {
            'equals' => ($actual === $expected),
            'does not equal', 'does_not_equal' => ($actual !== $expected),
            'contains' => str_contains($actual, $expected),
            'does not contain', 'does_not_contain' => !str_contains($actual, $expected),
            'starts with', 'starts_with' => str_starts_with($actual, $expected),
            'ends with', 'ends_with' => str_ends_with($actual, $expected),
            default => ($actual === $expected),
        };
    }

    private function compareNumeric($actual, string $operator, $expected): bool
    {
        return match ($operator) {
            'greater than', 'is greater than' => ($actual > $expected),
            'less than', 'is less than' => ($actual < $expected),
            'does not equal', 'does_not_equal' => ($actual != $expected),
            'equals', 'is' => ($actual == $expected),
            default => ($actual == $expected),
        };
    }

    private function compareDates(?string $actual, string $operator, ?string $expected): bool
    {
        if (empty($actual) || empty($expected)) {
            return false;
        }

        try {
            $actualDate = Carbon::parse($actual)->startOfDay();
            $expectedDate = Carbon::parse($expected)->startOfDay();

            return match ($operator) {
                'is before' => $actualDate->lessThan($expectedDate),
                'is after' => $actualDate->greaterThan($expectedDate),
                'is on', 'equals' => $actualDate->equalTo($expectedDate),
                'does not equal' => !$actualDate->equalTo($expectedDate),
                default => false,
            };
        } catch (\Exception $e) {
            return false;
        }
    }

    private function executeWebhookNode(Contact $contact, array $node): void
    {
        $config = $node['config'] ?? [];
        $url = $config['url'] ?? '';
        $method = strtoupper($config['method'] ?? 'POST');
        $payloadType = $config['payload_type'] ?? 'json';
        $payloadMode = $config['payload_mode'] ?? 'merged';

        if (empty($url)) {
            return;
        }

        $headers = [];
        if (!empty($config['headers'])) {
            foreach ($config['headers'] as $h) {
                if (!empty($h['key'])) {
                    $headers[$h['key']] = $h['value'] ?? '';
                }
            }
        }

        $customParams = [];
        if (!empty($config['params'])) {
            foreach ($config['params'] as $p) {
                if (!empty($p['key'])) {
                    $customParams[$p['key']] = $p['value'] ?? '';
                }
            }
        }

        $contactData = [
            'id' => $contact->id,
            'email' => $contact->email,
            'first_name' => $contact->first_name,
            'last_name' => $contact->last_name,
            'phone' => $contact->phone,
            'status' => $contact->status,
            'contact_type' => $contact->contact_type,
            'total_points' => $contact->total_points,
            'life_time_value' => $contact->life_time_value,
            'timezone' => $contact->timezone,
        ];

        $payload = ($payloadMode === 'custom') ? $customParams : array_merge($contactData, $customParams);

        try {
            $request = Http::withHeaders($headers)->timeout(10);

            if ($method === 'GET') {
                $response = $request->get($url, $payload);
            } elseif ($method === 'PUT') {
                $response = ($payloadType === 'form') ? $request->asForm()->put($url, $payload) : $request->put($url, $payload);
            } else {
                $response = ($payloadType === 'form') ? $request->asForm()->post($url, $payload) : $request->post($url, $payload);
            }

            if ($response->successful()) {
                $contact->logActivity('webhook_sent', "Webhook successfully delivered to: {$url}");
            } else {
                Log::warning("Automation Webhook failed: {$url} - Status: " . $response->status());
                $contact->logActivity('webhook_failed', "Webhook returned code {$response->status()} for: {$url}");
                throw new \Exception("Webhook returned status code: " . $response->status());
            }
        } catch (\Throwable $e) {
            Log::error("Automation Webhook exception: " . $e->getMessage());
            $contact->logActivity('webhook_failed', "Webhook connection error: " . substr($e->getMessage(), 0, 100));
            throw $e;
        }
    }



    private function executeAbSplitNode(Contact $contact, array $node): string
    {
        $config = $node['config'] ?? [];
        $pct = (int) ($config['split_percentage'] ?? 50);

        $hashVal = $contact->id % 100;
        $branch = ($hashVal < $pct) ? 'no' : 'yes';

        $contact->logActivity('ab_split', "A/B Test evaluated. Contact assigned to " . ($branch === 'no' ? 'Path A' : 'Path B'));
        return $branch;
    }

    private function executeUpdateFieldNode(Contact $contact, array $node): void
    {
        $config = $node['config'] ?? [];
        $fieldName = $config['field_name'] ?? '';
        $value = $config['value'] ?? '';

        $columnMap = [
            'Lead Status' => 'contact_type',
            'Lifecycle Stage' => 'contact_type',
            'Subscription Status' => 'status',
            'Lead Score / Points' => 'total_points',
            'Deal Value / LTV' => 'life_time_value',
            'contact_type' => 'contact_type',
            'status' => 'status',
            'total_points' => 'total_points',
            'life_time_value' => 'life_time_value'
        ];

        $column = $columnMap[$fieldName] ?? null;

        if ($column) {
            $oldVal = $contact->{$column};
            $contact->update([$column => $value]);
            $contact->logActivity('field_updated', "Updated contact field '{$column}' from '{$oldVal}' to '{$value}'");
        }
    }

    private function executeApplyTagNode(Contact $contact, array $node): void
    {
        $config = $node['config'] ?? [];
        $tagId = $config['tag_id'] ?? null;
        if ($tagId) {
            $contact->tags()->syncWithoutDetaching([$tagId]);
            $tagTitle = \App\Models\Tag::find($tagId)?->title ?? "#{$tagId}";
            $contact->logActivity('tag_added', "Tag attached in automation: {$tagTitle}");
        }
    }

    private function executeRemoveTagNode(Contact $contact, array $node): void
    {
        $config = $node['config'] ?? [];
        $tagId = $config['tag_id'] ?? null;
        if ($tagId) {
            $contact->tags()->detach($tagId);
            $tagTitle = \App\Models\Tag::find($tagId)?->title ?? "#{$tagId}";
            $contact->logActivity('tag_removed', "Tag detached in automation: {$tagTitle}");
        }
    }

    private function executeApplyListNode(Contact $contact, array $node): void
    {
        $config = $node['config'] ?? [];
        $listId = $config['list_id'] ?? null;
        if ($listId) {
            $contact->lists()->syncWithoutDetaching([$listId]);
            $listTitle = \App\Models\ContactList::find($listId)?->title ?? "#{$listId}";
            $contact->logActivity('list_added', "Contact subscribed to list via automation: {$listTitle}");
        }
    }

    private function executeRemoveListNode(Contact $contact, array $node): void
    {
        $config = $node['config'] ?? [];
        $listId = $config['list_id'] ?? null;
        if ($listId) {
            $contact->lists()->detach($listId);
            $listTitle = \App\Models\ContactList::find($listId)?->title ?? "#{$listId}";
            $contact->logActivity('list_removed', "Contact unsubscribed from list via automation: {$listTitle}");
        }
    }

    private function dispatchDownstream(Campaign $automation, Contact $contact, string $nodeId, array $edges, ?string $branch): void
    {
        $nextEdges = collect($edges)->filter(function($e) use ($nodeId, $branch, $automation) {
            if ($e['source'] !== $nodeId) {
                return false;
            }
            if ($branch !== null) {
                // If branch is explicitly defined on the edge, it must match
                if (isset($e['branch']) && $e['branch'] !== '') {
                    return $e['branch'] === $branch;
                }

                // Fallback for custom flow layout where edges might not have explicit branch key.
                // We determine branch by sourcePort or default to 'yes'.
                // If source node is a condition or ab_split:
                $nodes = $automation->settings['nodes'] ?? [];
                $sourceNode = collect($nodes)->first(fn($n) => $n['id'] === $nodeId);
                if ($sourceNode && in_array($sourceNode['type'] ?? '', ['condition', 'ab_split'])) {
                    $port = $e['sourcePort'] ?? 'bottom';
                    $edgeBranch = ($port === 'left') ? 'no' : 'yes';
                    return $edgeBranch === $branch;
                }

                return false;
            }
            return empty($e['branch']);
        });

        foreach ($nextEdges as $edge) {
            ProcessAutomationNodeJob::dispatch($automation->id, $contact->id, $edge['target']);
        }
    }
}
