HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/app/Http/Controllers/Customer/ListSubscriberController.php
<?php

namespace App\Http\Controllers\Customer;

use App\Http\Controllers\Controller;
use App\Models\CampaignLog;
use App\Models\CampaignRecipient;
use App\Models\CampaignTracking;
use App\Models\EmailList;
use App\Models\ListSubscriber;
use App\Models\SubscriberImport;
use App\Services\ListSubscriberService;
use App\Services\SubscriberImportProcessor;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;

class ListSubscriberController extends Controller
{
    public function __construct(
        protected ListSubscriberService $listSubscriberService
    ) {}

    /**
     * Display a listing of subscribers for a list.
     */
    public function index(Request $request, EmailList $list)
    {
        $filters = $request->only(['search', 'status', 'tag', 'segment_id']);
        $subscribers = $this->listSubscriberService->getPaginated($list, $filters);
        $availableTags = $this->availableTagsForList($list);

        $performanceMap = $this->subscriberPerformanceMap(
            (int) $list->customer_id,
            (int) $list->id,
            $subscribers->getCollection()->pluck('email')->all()
        );
        $this->appendPerformanceToSubscribers($subscribers, $performanceMap);

        $activeSegment = null;
        if (!empty($filters['segment_id'])) {
            $activeSegment = \App\Models\ListSegment::where('list_id', $list->id)
                ->where('id', (int) $filters['segment_id'])
                ->first();
        }

        $availableSegments = \App\Models\ListSegment::where('list_id', $list->id)->pluck('name', 'id')->all();

        return view('customer.lists.subscribers.index', compact('list', 'subscribers', 'filters', 'availableTags', 'activeSegment', 'availableSegments'));
    }

    private function availableTagsForList(EmailList $list): array
    {
        $tags = [];

        foreach ((is_array($list->tags ?? null) ? $list->tags : []) as $tag) {
            $value = trim((string) $tag);
            if ($value !== '') {
                $tags[] = $value;
            }
        }

        $subscriberTags = ListSubscriber::query()
            ->where('list_id', $list->id)
            ->pluck('tags');

        foreach ($subscriberTags as $tagSet) {
            foreach ((is_array($tagSet) ? $tagSet : []) as $tag) {
                $value = trim((string) $tag);
                if ($value !== '') {
                    $tags[] = $value;
                }
            }
        }

        return collect($tags)
            ->unique(fn ($tag) => mb_strtolower($tag))
            ->sortBy(fn ($tag) => mb_strtolower($tag))
            ->values()
            ->all();
    }

    private function subscriberPerformanceMap(int $customerId, int $listId, array $emails): array
    {
        $normalizedEmails = array_values(array_unique(array_filter(array_map(
            static fn ($email) => strtolower(trim((string) $email)),
            $emails
        ))));

        if ($normalizedEmails === []) {
            return [];
        }

        $rows = CampaignRecipient::query()
            ->whereHas('campaign', function ($query) use ($customerId, $listId) {
                $query->where('customer_id', $customerId)
                    ->where('list_id', $listId);
            })
            ->whereIn(DB::raw('LOWER(email)'), $normalizedEmails)
            ->selectRaw('LOWER(email) as email_key')
            ->selectRaw('COUNT(*) as total_campaigns')
            ->selectRaw('SUM(CASE WHEN sent_at IS NOT NULL OR status IN ("sent", "opened", "clicked", "bounced", "failed") THEN 1 ELSE 0 END) as sent_count')
            ->selectRaw('SUM(CASE WHEN opened_at IS NOT NULL OR status IN ("opened", "clicked") THEN 1 ELSE 0 END) as opened_count')
            ->selectRaw('SUM(CASE WHEN clicked_at IS NOT NULL OR status = "clicked" THEN 1 ELSE 0 END) as clicked_count')
            ->selectRaw('SUM(CASE WHEN bounced_at IS NOT NULL OR status = "bounced" THEN 1 ELSE 0 END) as bounced_count')
            ->selectRaw('SUM(CASE WHEN status = "failed" THEN 1 ELSE 0 END) as failed_count')
            ->groupBy('email_key')
            ->get();

        $map = [];
        foreach ($rows as $row) {
            $key = strtolower((string) ($row->email_key ?? ''));
            if ($key === '') {
                continue;
            }

            $sent = (int) ($row->sent_count ?? 0);
            $opened = (int) ($row->opened_count ?? 0);
            $clicked = (int) ($row->clicked_count ?? 0);

            $map[$key] = [
                'total_campaigns' => (int) ($row->total_campaigns ?? 0),
                'sent_count' => $sent,
                'opened_count' => $opened,
                'clicked_count' => $clicked,
                'bounced_count' => (int) ($row->bounced_count ?? 0),
                'failed_count' => (int) ($row->failed_count ?? 0),
                'open_rate' => $sent > 0 ? round(($opened / $sent) * 100, 2) : 0.0,
                'click_rate' => $sent > 0 ? round(($clicked / $sent) * 100, 2) : 0.0,
            ];
        }

        return $map;
    }

    private function appendPerformanceToSubscribers(LengthAwarePaginator $subscribers, array $performanceMap): void
    {
        $collection = $subscribers->getCollection();
        if (!$collection instanceof Collection) {
            return;
        }

        $collection->transform(function (ListSubscriber $subscriber) use ($performanceMap) {
            $key = strtolower(trim((string) $subscriber->email));
            $metrics = $performanceMap[$key] ?? [
                'total_campaigns' => 0,
                'sent_count' => 0,
                'opened_count' => 0,
                'clicked_count' => 0,
                'bounced_count' => 0,
                'failed_count' => 0,
                'open_rate' => 0.0,
                'click_rate' => 0.0,
            ];

            $subscriber->setAttribute('total_campaigns', $metrics['total_campaigns']);
            $subscriber->setAttribute('sent_count', $metrics['sent_count']);
            $subscriber->setAttribute('opened_count', $metrics['opened_count']);
            $subscriber->setAttribute('clicked_count', $metrics['clicked_count']);
            $subscriber->setAttribute('bounced_count', $metrics['bounced_count']);
            $subscriber->setAttribute('failed_count', $metrics['failed_count']);
            $subscriber->setAttribute('open_rate', $metrics['open_rate']);
            $subscriber->setAttribute('click_rate', $metrics['click_rate']);

            return $subscriber;
        });
    }

    public function export(Request $request, EmailList $list)
    {
        $customer = auth('customer')->user();
        if (!$customer || !$this->workspaceResourceMatches($list, $customer)) {
            abort(404);
        }

        $filters = $request->only(['search', 'status', 'segment_id']);

        $fileName = 'subscribers_' . (string) $list->id . '_' . now()->format('Ymd_His') . '.csv';

        return response()->streamDownload(function () use ($list, $filters) {
            $out = fopen('php://output', 'w');
            if ($out === false) {
                return;
            }

            fputcsv($out, [
                'email',
                'first_name',
                'last_name',
                'status',
                'source',
                'subscribed_at',
                'confirmed_at',
                'unsubscribed_at',
                'tags',
                'custom_fields',
                'notes',
            ]);

            $this->listSubscriberService
                ->query($list, $filters)
                ->orderBy('id')
                ->chunk(1000, function ($chunk) use ($out) {
                    foreach ($chunk as $subscriber) {
                        fputcsv($out, [
                            (string) ($subscriber->email ?? ''),
                            (string) ($subscriber->first_name ?? ''),
                            (string) ($subscriber->last_name ?? ''),
                            (string) ($subscriber->status ?? ''),
                            (string) ($subscriber->source ?? ''),
                            optional($subscriber->subscribed_at)->toIso8601String(),
                            optional($subscriber->confirmed_at)->toIso8601String(),
                            optional($subscriber->unsubscribed_at)->toIso8601String(),
                            json_encode($subscriber->tags ?? [], JSON_UNESCAPED_UNICODE),
                            json_encode($subscriber->custom_fields ?? [], JSON_UNESCAPED_UNICODE),
                            (string) ($subscriber->notes ?? ''),
                        ]);
                    }
                });

            fclose($out);
        }, $fileName, [
            'Content-Type' => 'text/csv; charset=UTF-8',
        ]);
    }

    /**
     * Show the form for creating a new subscriber.
     */
    public function create(EmailList $list)
    {
        return view('customer.lists.subscribers.create', compact('list'));
    }

    /**
     * Store a newly created subscriber.
     */
    public function store(Request $request, EmailList $list)
    {
        $validated = $request->validate([
            'email' => ['required', 'email', 'max:255'],
            'first_name' => ['nullable', 'string', 'max:255'],
            'last_name' => ['nullable', 'string', 'max:255'],
            'source' => ['nullable', 'string', 'max:255'],
            'tags' => ['nullable'],
        ]);

        $validated['tags'] = $this->normalizeTags($request->input('tags'));

        $subscriber = $this->listSubscriberService->create($list, $validated);

        return redirect()
            ->route('customer.lists.subscribers.index', $list)
            ->with('success', 'Subscriber added successfully.');
    }

    /**
     * Display the specified subscriber.
     */
    public function show(EmailList $list, ListSubscriber $subscriber)
    {
        $customer = auth('customer')->user();
        if (!$customer || !$this->workspaceResourceMatches($list, $customer) || (int) $subscriber->list_id !== (int) $list->id) {
            abort(404);
        }

        $customerId = $customer->id;

        $email = strtolower((string) $subscriber->email);

        $campaignRecipientStats = CampaignRecipient::query()
            ->whereHas('campaign', function ($query) use ($customerId, $list) {
                $query->where('customer_id', (int) $customerId)
                    ->where('list_id', (int) $list->id);
            })
            ->whereRaw('LOWER(email) = ?', [$email])
            ->selectRaw('COUNT(*) as total_campaigns')
            ->selectRaw('SUM(CASE WHEN sent_at IS NOT NULL OR status IN ("sent", "opened", "clicked", "bounced", "failed") THEN 1 ELSE 0 END) as sent_count')
            ->selectRaw('SUM(CASE WHEN opened_at IS NOT NULL OR status IN ("opened", "clicked") THEN 1 ELSE 0 END) as opened_count')
            ->selectRaw('SUM(CASE WHEN clicked_at IS NOT NULL OR status = "clicked" THEN 1 ELSE 0 END) as clicked_count')
            ->selectRaw('SUM(CASE WHEN bounced_at IS NOT NULL OR status = "bounced" THEN 1 ELSE 0 END) as bounced_count')
            ->first();

        $sentCount = (int) ($campaignRecipientStats->sent_count ?? 0);
        $openedCount = (int) ($campaignRecipientStats->opened_count ?? 0);
        $clickedCount = (int) ($campaignRecipientStats->clicked_count ?? 0);

        $contactPerformance = [
            'total_campaigns' => (int) ($campaignRecipientStats->total_campaigns ?? 0),
            'sent_count' => $sentCount,
            'opened_count' => $openedCount,
            'clicked_count' => $clickedCount,
            'bounced_count' => (int) ($campaignRecipientStats->bounced_count ?? 0),
            'open_rate' => $sentCount > 0 ? round(($openedCount / $sentCount) * 100, 2) : 0.0,
            'click_rate' => $sentCount > 0 ? round(($clickedCount / $sentCount) * 100, 2) : 0.0,
        ];

        $trackingEvents = CampaignTracking::query()
            ->where(function ($query) use ($subscriber, $email) {
                $query->where('subscriber_id', (int) $subscriber->id)
                    ->orWhereRaw('LOWER(email) = ?', [$email]);
            })
            ->whereHas('campaign', function ($query) use ($customerId, $list) {
                $query->where('customer_id', (int) $customerId)
                    ->where('list_id', (int) $list->id);
            })
            ->with(['campaign:id,name'])
            ->orderByDesc('event_at')
            ->limit(100)
            ->get()
            ->toBase()
            ->map(function (CampaignTracking $event) {
                return [
                    'source' => 'tracking',
                    'event' => (string) $event->event_type,
                    'campaign_name' => (string) ($event->campaign->name ?? 'Campaign #' . $event->campaign_id),
                    'occurred_at' => $event->event_at ?? $event->created_at,
                    'url' => $event->url,
                    'ip_address' => $event->ip_address,
                    'user_agent' => $event->user_agent,
                    'details' => $event->bounce_reason ?: $event->complaint_reason,
                ];
            });

        $campaignLogEvents = CampaignLog::query()
            ->whereHas('campaign', function ($query) use ($customerId, $list) {
                $query->where('customer_id', (int) $customerId)
                    ->where('list_id', (int) $list->id);
            })
            ->whereHas('recipient', function ($query) use ($email) {
                $query->whereRaw('LOWER(email) = ?', [$email]);
            })
            ->with(['campaign:id,name'])
            ->latest('created_at')
            ->limit(100)
            ->get()
            ->toBase()
            ->map(function (CampaignLog $event) {
                return [
                    'source' => 'campaign_log',
                    'event' => (string) $event->event,
                    'campaign_name' => (string) ($event->campaign->name ?? 'Campaign #' . $event->campaign_id),
                    'occurred_at' => $event->created_at,
                    'url' => $event->url,
                    'ip_address' => $event->ip_address,
                    'user_agent' => $event->user_agent,
                    'details' => $event->error_message,
                ];
            });

        $activityHistory = $trackingEvents
            ->merge($campaignLogEvents)
            ->sortByDesc(function (array $item) {
                return optional($item['occurred_at'])->getTimestamp() ?? 0;
            })
            ->take(150)
            ->values();

        return view('customer.lists.subscribers.show', compact('list', 'subscriber', 'contactPerformance', 'activityHistory'));
    }

    /**
     * Show the form for editing the specified subscriber.
     */
    public function edit(EmailList $list, ListSubscriber $subscriber)
    {
        return view('customer.lists.subscribers.edit', compact('list', 'subscriber'));
    }

    /**
     * Update the specified subscriber.
     */
    public function update(Request $request, EmailList $list, ListSubscriber $subscriber)
    {
        $validated = $request->validate([
            'email' => ['required', 'email', 'max:255'],
            'first_name' => ['nullable', 'string', 'max:255'],
            'last_name' => ['nullable', 'string', 'max:255'],
            'status' => ['nullable', 'in:confirmed,unconfirmed,unsubscribed,blacklisted,bounced'],
            'tags' => ['nullable'],
            'notes' => ['nullable', 'string'],
        ]);

        $validated['tags'] = $this->normalizeTags($request->input('tags'));

        $this->listSubscriberService->update($subscriber, $validated);

        return redirect()
            ->route('customer.lists.subscribers.show', [$list, $subscriber])
            ->with('success', 'Subscriber updated successfully.');
    }

    /**
     * Remove the specified subscriber.
     */
    public function destroy(EmailList $list, ListSubscriber $subscriber)
    {
        $this->listSubscriberService->delete($subscriber);

        return redirect()
            ->route('customer.lists.subscribers.index', $list)
            ->with('success', 'Subscriber deleted successfully.');
    }

    /**
     * Show CSV import form.
     */
    public function showImport(EmailList $list)
    {
        return view('customer.lists.subscribers.import', compact('list'));
    }

    /**
     * Handle CSV import.
     */
    public function import(Request $request, EmailList $list)
    {
        $validated = $request->validate([
            'csv_file' => ['required', 'file', 'mimes:csv,txt', 'max:51200'],
            'column_mapping' => ['required', 'array'],
            'column_mapping.email' => ['required', 'string'],
            'column_mapping.first_name' => ['nullable', 'string'],
            'column_mapping.last_name' => ['nullable', 'string'],
            'column_mapping.custom_fields' => ['nullable', 'array'],
            'column_mapping.custom_fields.*' => ['nullable', 'string'],
            'column_mapping.capture_unmapped' => ['nullable', 'boolean'],
            'column_mapping.add_list_custom_fields' => ['nullable', 'boolean'],
            'skip_duplicates' => ['nullable', 'boolean'],
            'update_existing' => ['nullable', 'boolean'],
        ]);

        $file = $request->file('csv_file');
        
        if (!$file || !$file->isValid()) {
            return redirect()
                ->route('customer.lists.subscribers.import', $list)
                ->with('error', 'Invalid file upload. Please try again.')
                ->withInput();
        }
        
        // Ensure imports directory exists using Storage facade
        try {
            if (!Storage::disk('local')->exists('imports')) {
                Storage::disk('local')->makeDirectory('imports');
            }
        } catch (\Exception $e) {
            Log::error("Failed to create imports directory: " . $e->getMessage());
            return redirect()
                ->route('customer.lists.subscribers.import', $list)
                ->with('error', 'Failed to create import directory. Please check storage permissions.')
                ->withInput();
        }
        
        // Store file and get full path
        $fileName = 'subscribers_' . time() . '_' . uniqid() . '.csv';
        
        try {
            // Use Storage facade for more reliable file handling
            $filePath = Storage::disk('local')->putFileAs('imports', $file, $fileName);
            
            if ($filePath === false) {
                Log::error("Storage::putFileAs() returned false for file: {$fileName}");
                return redirect()
                    ->route('customer.lists.subscribers.import', $list)
                    ->with('error', 'Failed to save uploaded file. Please try again.')
                    ->withInput();
            }
            
            $fullPath = Storage::disk('local')->path($filePath);

            // Verify file was saved
            if (!Storage::disk('local')->exists($filePath)) {
                Log::error("File not found after storage: {$filePath}");
                return redirect()
                    ->route('customer.lists.subscribers.import', $list)
                    ->with('error', 'File was not saved correctly. Please try again.')
                    ->withInput();
            }
            
            // Verify file is readable
            if (!is_readable($fullPath)) {
                Log::error("File is not readable: {$fullPath}");
                return redirect()
                    ->route('customer.lists.subscribers.import', $list)
                    ->with('error', 'File is not readable. Please check permissions.')
                    ->withInput();
            }
            
            Log::info("File successfully stored: {$fullPath}");
            
        } catch (\Exception $e) {
            Log::error("Exception during file storage: " . $e->getMessage(), [
                'file' => $fileName,
                'trace' => $e->getTraceAsString()
            ]);
            return redirect()
                ->route('customer.lists.subscribers.import', $list)
                ->with('error', 'Error saving file: ' . $e->getMessage())
                ->withInput();
        }

        $customerId = (int) ($list->customer_id ?? 0);

        $subscriberImport = SubscriberImport::create([
            'customer_id' => $customerId,
            'list_id' => (int) $list->id,
            'status' => 'queued',
            'source' => 'csv_import',
            'ip_address' => $request->ip(),
            'stored_path' => $filePath,
            'total_rows' => 0,
            'processed_count' => 0,
            'imported_count' => 0,
            'updated_count' => 0,
            'skipped_count' => 0,
            'error_count' => 0,
        ]);

        \App\Jobs\ImportSubscribersJob::dispatch(
            $list,
            $fullPath,
            $validated['column_mapping'],
            $validated['skip_duplicates'] ?? true,
            $validated['update_existing'] ?? false,
            'csv_import',
            $request->ip(),
            (int) $subscriberImport->id
        )->onQueue('imports');

        return redirect()
            ->route('customer.lists.subscribers.index', $list)
            ->with('success', 'Import started. You will be notified when it completes.');
    }

    public function importAjaxStart(Request $request, EmailList $list)
    {
        $customer = auth('customer')->user();
        if (!$customer || !$this->workspaceResourceMatches($list, $customer)) {
            abort(404);
        }

        $customerId = $customer->id;

        $validated = $request->validate([
            'csv_file' => ['required', 'file', 'mimes:csv,txt', 'max:51200'],
            'column_mapping' => ['required', 'array'],
            'column_mapping.email' => ['required', 'string'],
            'column_mapping.first_name' => ['nullable', 'string'],
            'column_mapping.last_name' => ['nullable', 'string'],
            'column_mapping.custom_fields' => ['nullable', 'array'],
            'column_mapping.custom_fields.*' => ['nullable', 'string'],
            'column_mapping.capture_unmapped' => ['nullable', 'boolean'],
            'column_mapping.add_list_custom_fields' => ['nullable', 'boolean'],
            'skip_duplicates' => ['nullable', 'boolean'],
            'update_existing' => ['nullable', 'boolean'],
        ]);

        $file = $request->file('csv_file');
        if (!$file || !$file->isValid()) {
            return response()->json([
                'success' => false,
                'message' => 'Invalid file upload.',
            ], 422);
        }

        try {
            if (!Storage::disk('local')->exists('imports')) {
                Storage::disk('local')->makeDirectory('imports');
            }
        } catch (\Throwable $e) {
            Log::error('Failed to create imports directory', ['error' => $e->getMessage()]);
            return response()->json([
                'success' => false,
                'message' => 'Failed to create import directory. Please check storage permissions.',
            ], 500);
        }

        $fileName = 'subscribers_' . time() . '_' . uniqid() . '.csv';

        try {
            $filePath = Storage::disk('local')->putFileAs('imports', $file, $fileName);
            if ($filePath === false) {
                return response()->json([
                    'success' => false,
                    'message' => 'Failed to save uploaded file. Please try again.',
                ], 500);
            }

            if (!Storage::disk('local')->exists($filePath)) {
                return response()->json([
                    'success' => false,
                    'message' => 'Upload completed but the file could not be found on the server. Please check storage permissions.',
                ], 500);
            }
        } catch (\Throwable $e) {
            Log::error('Exception during file storage', ['error' => $e->getMessage()]);
            return response()->json([
                'success' => false,
                'message' => 'Error saving file: ' . $e->getMessage(),
            ], 500);
        }

        $subscriberImport = SubscriberImport::create([
            'customer_id' => (int) $customerId,
            'list_id' => (int) $list->id,
            'status' => 'queued',
            'source' => 'csv_import',
            'ip_address' => $request->ip(),
            'stored_path' => $filePath,
            'column_mapping' => $validated['column_mapping'],
            'skip_duplicates' => (bool) ($validated['skip_duplicates'] ?? true),
            'update_existing' => (bool) ($validated['update_existing'] ?? false),
            'file_offset' => 0,
            'total_rows' => 0,
            'processed_count' => 0,
            'imported_count' => 0,
            'updated_count' => 0,
            'skipped_count' => 0,
            'error_count' => 0,
        ]);

        try {
            app(SubscriberImportProcessor::class)->processImportNow($subscriberImport, 400, 6);
        } catch (\Throwable $e) {
            // Processor will mark failed itself; just continue.
        }

        return response()->json([
            'success' => true,
            'import_id' => (int) $subscriberImport->id,
        ]);
    }

    public function importAjaxStep(Request $request, EmailList $list)
    {
        $customer = auth('customer')->user();
        if (!$customer || !$this->workspaceResourceMatches($list, $customer)) {
            abort(404);
        }

        $customerId = $customer->id;

        $validated = $request->validate([
            'import_id' => ['required', 'integer'],
        ]);

        $import = SubscriberImport::query()
            ->whereKey((int) $validated['import_id'])
            ->where('customer_id', (int) $customerId)
            ->where('list_id', (int) $list->id)
            ->firstOrFail();

        if (in_array((string) $import->status, ['completed', 'failed'], true)) {
            return $this->importStats($request, $list);
        }

        try {
            app(SubscriberImportProcessor::class)->processImportNow($import, 800, 6);
        } catch (\Throwable $e) {
            // Processor handles marking failed.
        }

        return $this->importStats($request, $list);
    }

    public function importStats(Request $request, EmailList $list)
    {
        $customer = auth('customer')->user();
        if (!$customer || !$this->workspaceResourceMatches($list, $customer)) {
            abort(404);
        }

        $customerId = $customer->id;

        $import = SubscriberImport::query()
            ->where('customer_id', (int) $customerId)
            ->where('list_id', (int) $list->id)
            ->latest()
            ->first();

        if (!$import) {
            return response()->json([
                'success' => true,
                'import' => null,
            ]);
        }

        $total = max(0, (int) $import->total_rows);
        $processed = max(0, (int) $import->processed_count);
        $percent = $total > 0 ? round(($processed / $total) * 100, 1) : 0;

        if ((string) $import->status === 'failed'
            && (string) ($import->failure_reason ?? '') === 'Import file not found.'
            && $total > 0
            && $processed >= $total
        ) {
            $import->update([
                'status' => 'completed',
                'failure_reason' => null,
            ]);

            $import = $import->fresh();
        }

        return response()->json([
            'success' => true,
            'import' => [
                'id' => (int) $import->id,
                'status' => (string) $import->status,
                'total_rows' => $total,
                'processed_count' => $processed,
                'imported_count' => (int) $import->imported_count,
                'updated_count' => (int) $import->updated_count,
                'skipped_count' => (int) $import->skipped_count,
                'error_count' => (int) $import->error_count,
                'percent' => $percent,
                'failure_reason' => $import->failure_reason,
                'started_at' => $import->started_at?->toIso8601String(),
                'finished_at' => $import->finished_at?->toIso8601String(),
            ],
        ]);
    }

    /**
     * Confirm a subscriber.
     */
    public function confirm(EmailList $list, ListSubscriber $subscriber)
    {
        $this->listSubscriberService->confirm($subscriber);

        return redirect()
            ->route('customer.lists.subscribers.show', [$list, $subscriber])
            ->with('success', 'Subscriber confirmed successfully.');
    }

    /**
     * Unsubscribe a subscriber.
     */
    public function unsubscribe(EmailList $list, ListSubscriber $subscriber)
    {
        $this->listSubscriberService->unsubscribe($subscriber);

        return redirect()
            ->route('customer.lists.subscribers.show', [$list, $subscriber])
            ->with('success', 'Subscriber unsubscribed successfully.');
    }

    /**
     * Resend confirmation email to subscriber.
     */
    public function resendConfirmation(EmailList $list, ListSubscriber $subscriber)
    {
        try {
            $this->listSubscriberService->resendConfirmationEmail($subscriber);

            return redirect()
                ->route('customer.lists.subscribers.show', [$list, $subscriber])
                ->with('success', 'Confirmation email sent successfully.');
        } catch (\Exception $e) {
            return redirect()
                ->route('customer.lists.subscribers.show', [$list, $subscriber])
                ->with('error', $e->getMessage());
        }
    }

    /**
     * Bulk delete subscribers.
     */
    public function bulkDelete(Request $request, EmailList $list)
    {
        $validated = $request->validate([
            'subscriber_ids' => ['nullable', 'string'],
            'all_matching' => ['nullable', 'boolean'],
            'search' => ['nullable', 'string'],
            'status' => ['nullable', 'string'],
            'segment_id' => ['nullable', 'integer'],
        ]);

        $allMatching = (bool) ($validated['all_matching'] ?? false);
        $filters = [
            'search' => $validated['search'] ?? null,
            'status' => $validated['status'] ?? null,
            'segment_id' => $validated['segment_id'] ?? null,
        ];

        $ids = array_filter(array_map('intval', explode(',', (string) ($validated['subscriber_ids'] ?? ''))));

        if (!$allMatching && empty($ids)) {
            return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
                ->with('error', 'No subscribers selected.');
        }

        $query = $this->listSubscriberService->query($list, $filters);
        if (!$allMatching) {
            $query->whereIn('id', $ids);
        }

        $count = $query->delete();

        return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
            ->with('success', "Successfully deleted {$count} subscriber(s).");
    }

    /**
     * Bulk confirm subscribers.
     */
    public function bulkConfirm(Request $request, EmailList $list)
    {
        $validated = $request->validate([
            'subscriber_ids' => ['nullable', 'string'],
            'all_matching' => ['nullable', 'boolean'],
            'search' => ['nullable', 'string'],
            'status' => ['nullable', 'string'],
            'segment_id' => ['nullable', 'integer'],
        ]);

        $allMatching = (bool) ($validated['all_matching'] ?? false);
        $filters = [
            'search' => $validated['search'] ?? null,
            'status' => $validated['status'] ?? null,
            'segment_id' => $validated['segment_id'] ?? null,
        ];

        $ids = array_filter(array_map('intval', explode(',', (string) ($validated['subscriber_ids'] ?? ''))));

        if (!$allMatching && empty($ids)) {
            return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
                ->with('error', 'No subscribers selected.');
        }

        $query = $this->listSubscriberService
            ->query($list, $filters)
            ->where('status', 'unconfirmed');

        if (!$allMatching) {
            $query->whereIn('id', $ids);
        }

        $count = $query->update([
            'status' => 'confirmed',
            'confirmed_at' => now(),
        ]);

        return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
            ->with('success', "Successfully confirmed {$count} subscriber(s).");
    }

    /**
     * Bulk unsubscribe subscribers.
     */
    public function bulkUnsubscribe(Request $request, EmailList $list)
    {
        $validated = $request->validate([
            'subscriber_ids' => ['nullable', 'string'],
            'all_matching' => ['nullable', 'boolean'],
            'search' => ['nullable', 'string'],
            'status' => ['nullable', 'string'],
            'segment_id' => ['nullable', 'integer'],
        ]);

        $allMatching = (bool) ($validated['all_matching'] ?? false);
        $filters = [
            'search' => $validated['search'] ?? null,
            'status' => $validated['status'] ?? null,
            'segment_id' => $validated['segment_id'] ?? null,
        ];

        $ids = array_filter(array_map('intval', explode(',', (string) ($validated['subscriber_ids'] ?? ''))));

        if (!$allMatching && empty($ids)) {
            return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
                ->with('error', 'No subscribers selected.');
        }

        $query = $this->listSubscriberService->query($list, $filters);
        if (!$allMatching) {
            $query->whereIn('id', $ids);
        }

        $subscribers = $query->get();

        $count = 0;
        foreach ($subscribers as $subscriber) {
            $this->listSubscriberService->unsubscribe($subscriber);
            $count++;
        }

        return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
            ->with('success', "Successfully unsubscribed {$count} subscriber(s).");
    }

    /**
     * Bulk resend confirmation emails.
     */
    public function bulkResend(Request $request, EmailList $list)
    {
        $validated = $request->validate([
            'subscriber_ids' => ['nullable', 'string'],
            'all_matching' => ['nullable', 'boolean'],
            'search' => ['nullable', 'string'],
            'status' => ['nullable', 'string'],
            'segment_id' => ['nullable', 'integer'],
        ]);

        $allMatching = (bool) ($validated['all_matching'] ?? false);
        $filters = [
            'search' => $validated['search'] ?? null,
            'status' => $validated['status'] ?? null,
            'segment_id' => $validated['segment_id'] ?? null,
        ];

        $ids = array_filter(array_map('intval', explode(',', (string) ($validated['subscriber_ids'] ?? ''))));

        if (!$allMatching && empty($ids)) {
            return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
                ->with('error', 'No subscribers selected.');
        }

        $query = $this->listSubscriberService
            ->query($list, $filters)
            ->where('status', 'unconfirmed');

        if (!$allMatching) {
            $query->whereIn('id', $ids);
        }

        $subscribers = $query->get();

        $count = 0;
        $errors = 0;

        foreach ($subscribers as $subscriber) {
            try {
                $this->listSubscriberService->resendConfirmationEmail($subscriber);
                $count++;
            } catch (\Exception $e) {
                Log::error("Failed to resend confirmation to subscriber {$subscriber->id}: " . $e->getMessage());
                $errors++;
            }
        }

        $message = "Successfully sent confirmation emails to {$count} subscriber(s).";
        if ($errors > 0) {
            $message .= " {$errors} email(s) failed to send.";
        }

        return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
            ->with($errors > 0 ? 'warning' : 'success', $message);
    }

    /**
     * Bulk add subscribers to a segment.
     */
    public function bulkAddToSegment(Request $request, EmailList $list)
    {
        $validated = $request->validate([
            'subscriber_ids' => ['nullable', 'string'],
            'all_matching' => ['nullable', 'boolean'],
            'search' => ['nullable', 'string'],
            'status' => ['nullable', 'string'],
            'tag' => ['nullable', 'string'],
            'segment_id' => ['nullable', 'integer'],
            'target_segment_id' => ['required', 'integer', 'exists:list_segments,id'],
        ]);

        $allMatching = (bool) ($validated['all_matching'] ?? false);
        $filters = [
            'search' => $validated['search'] ?? null,
            'status' => $validated['status'] ?? null,
            'tag' => $validated['tag'] ?? null,
            'segment_id' => $validated['segment_id'] ?? null,
        ];

        $ids = array_filter(array_map('intval', explode(',', (string) ($validated['subscriber_ids'] ?? ''))));

        if (!$allMatching && empty($ids)) {
            return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
                ->with('error', 'No subscribers selected.');
        }

        $targetSegment = \App\Models\ListSegment::where('list_id', $list->id)
            ->where('id', (int) $validated['target_segment_id'])
            ->first();

        if (!$targetSegment) {
            return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
                ->with('error', 'Segment not found.');
        }

        $query = $this->listSubscriberService->query($list, $filters);

        if (!$allMatching) {
            $query->whereIn('id', $ids);
        }

        $subscribers = $query->pluck('id')->all();

        if (empty($subscribers)) {
            return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
                ->with('error', 'No subscribers match the selection.');
        }

        $existing = \Illuminate\Support\Facades\DB::table('list_segment_subscriber')
            ->where('segment_id', $targetSegment->id)
            ->whereIn('subscriber_id', $subscribers)
            ->pluck('subscriber_id')
            ->all();

        $newIds = array_diff($subscribers, $existing);

        foreach ($newIds as $subscriberId) {
            \Illuminate\Support\Facades\DB::table('list_segment_subscriber')->insert([
                'segment_id' => $targetSegment->id,
                'subscriber_id' => $subscriberId,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }

        $targetSegment->subscribers_count = $targetSegment->getSubscribersCountAttribute(0);
        $targetSegment->saveQuietly();

        $count = count($newIds);
        $skipped = count($subscribers) - $count;

        $message = "Added {$count} subscriber(s) to segment '{$targetSegment->name}'.";
        if ($skipped > 0) {
            $message .= " {$skipped} were already in the segment.";
        }

        return redirect(url()->previous() ?: route('customer.lists.subscribers.index', $list))
            ->with('success', $message);
    }

    private function normalizeTags(mixed $tagsInput): array
    {
        $tags = [];

        if (is_string($tagsInput)) {
            $tags = preg_split('/[,\n]+/', $tagsInput) ?: [];
        } elseif (is_array($tagsInput)) {
            $tags = $tagsInput;
        }

        return array_values(array_unique(array_filter(array_map(
            static fn ($tag) => trim((string) $tag),
            $tags
        ), static fn (string $tag) => $tag !== '')));
    }
}