<?php

namespace App\Services\Notification;

use App\Jobs\ProcessBroadcast;
use App\Jobs\ProcessNotificationDelivery;
use App\Jobs\ProcessSystemNotificationCampaign;
use App\Models\Customer;
use App\Models\CustomerContact;
use App\Models\NotificationBroadcast;
use App\Models\NotificationBroadcastRecipient;
use App\Models\NotificationChannel;
use App\Models\NotificationDelivery;
use App\Models\NotificationEvent;
use App\Models\NotificationMessage;
use App\Models\NotificationPreference;
use App\Models\NotificationTemplate;
use App\Models\SystemNotificationCampaign;
use App\Models\TopologyLink;
use App\Models\User;
use App\Models\UserNotification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class NotificationService
{
    public function __construct(protected NotificationManager $manager) {}

    public function channels(string $channel, User $actor, ?int $tenantId)
    {
        return NotificationChannel::with('tenant:id,tenant_code,tenant_name')
            ->where('channel', $channel)
            ->when(! $actor->isMasterAdmin(), fn ($q) => $q->where('tenant_id', $tenantId))
            ->when($actor->isMasterAdmin() && $tenantId, fn ($q) => $q->where(
                fn ($scope) => $scope->where('tenant_id', $tenantId)->orWhereNull('tenant_id')
            ))
            ->orderByDesc('is_default')->orderBy('name')->get()
            ->each(fn ($item) => $item->setAttribute('credentials_configured', ! empty($item->getRawOriginal('credentials'))));
    }

    public function systemCampaigns(array $filters, User $actor, ?int $tenantId)
    {
        return SystemNotificationCampaign::with(['tenant:id,tenant_code,tenant_name', 'creator:id,name'])
            ->when(! $actor->isMasterAdmin(), fn ($q) => $q->where('tenant_id', $tenantId))
            ->when($actor->isMasterAdmin() && ! empty($filters['tenant_id']), fn ($q) => $q->where('tenant_id', $filters['tenant_id']))
            ->when(! empty($filters['status']), fn ($q) => $q->where('status', $filters['status']))
            ->latest('id')->paginate($filters['per_page'] ?? 10);
    }

    public function createSystemCampaign(array $data, User $actor, ?int $tenantId): SystemNotificationCampaign
    {
        $effectiveTenantId = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? null) : $tenantId;
        if (! $actor->isMasterAdmin() && ! $effectiveTenantId) {
            throw ValidationException::withMessages(['tenant_id' => 'Tenant aktif wajib tersedia.']);
        }
        if (! $actor->isMasterAdmin() && ($data['target_scope'] ?? null) === 'all_users') {
            throw ValidationException::withMessages(['target_scope' => 'Hanya Master Admin yang dapat mengirim ke semua user.']);
        }
        if (($data['target_scope'] ?? null) === 'selected_users' && empty($data['user_ids'])) {
            throw ValidationException::withMessages(['user_ids' => 'Pilih minimal satu user penerima.']);
        }
        if ($effectiveTenantId && ! empty($data['user_ids'])) {
            $validCount = User::whereIn('id', $data['user_ids'])
                ->whereHas('userTenants', fn ($q) => $q->where('tenant_id', $effectiveTenantId))->count();
            if ($validCount !== count(array_unique($data['user_ids']))) {
                throw ValidationException::withMessages(['user_ids' => 'Terdapat user di luar tenant aktif.']);
            }
        }

        $campaign = SystemNotificationCampaign::create([
            'uuid' => Str::uuid(),
            'tenant_id' => $effectiveTenantId,
            'created_by' => $actor->id,
            'title' => $data['title'],
            'body' => $data['body'],
            'action_url' => $data['action_url'] ?? null,
            'icon' => $data['icon'] ?? 'cil-bell',
            'audience' => [
                'target_scope' => $data['target_scope'],
                'statuses' => $data['statuses'] ?? [],
                'access_levels' => $data['access_levels'] ?? [],
                'user_ids' => $data['user_ids'] ?? [],
            ],
        ]);
        ProcessSystemNotificationCampaign::dispatch($campaign->id)->onQueue('system-notifications');

        return $campaign;
    }

    public function systemAudienceOptions(User $actor, ?int $tenantId, ?int $requestedTenantId, ?string $search)
    {
        $effectiveTenantId = $actor->isMasterAdmin() ? $requestedTenantId : $tenantId;

        return User::query()
            ->when($effectiveTenantId, fn ($q) => $q->whereHas('userTenants', fn ($x) => $x->where('tenant_id', $effectiveTenantId)))
            ->when($search, fn ($q, $term) => $q->where(fn ($x) => $x
                ->where('name', 'ILIKE', "%{$term}%")
                ->orWhere('username', 'ILIKE', "%{$term}%")
                ->orWhere('email', 'ILIKE', "%{$term}%")))
            ->with(['userTenants' => fn ($q) => $q->when($effectiveTenantId, fn ($x) => $x->where('tenant_id', $effectiveTenantId))])
            ->orderBy('name')->limit(50)->get(['id', 'name', 'username', 'email', 'access_level', 'status'])
            ->map(fn (User $user) => [
                ...$user->only(['id', 'name', 'username', 'email', 'status']),
                'access_level' => $user->access_level->value,
                'tenant_role' => $user->userTenants->first()?->role,
            ]);
    }

    public function saveChannel(array $data, User $actor, ?int $tenantId, ?NotificationChannel $channel = null): NotificationChannel
    {
        $effectiveTenant = $actor->isMasterAdmin()
            ? (array_key_exists('tenant_id', $data) ? $data['tenant_id'] : ($channel ? $channel->tenant_id : $tenantId))
            : $tenantId;
        $isGlobalWhapi = $actor->isMasterAdmin()
            && ($data['channel'] ?? $channel?->channel) === 'whatsapp_unofficial'
            && ! $effectiveTenant;
        if (! $effectiveTenant && ! $isGlobalWhapi) {
            throw ValidationException::withMessages(['tenant_id' => 'Tenant wajib dipilih.']);
        }
        if ($channel && $channel->tenant_id !== null && (int) $channel->tenant_id !== (int) $effectiveTenant) {
            throw ValidationException::withMessages(['tenant_id' => 'Konfigurasi bukan milik tenant tersebut.']);
        }
        if ($channel && $channel->tenant_id === null && ! $isGlobalWhapi) {
            throw ValidationException::withMessages(['tenant_id' => 'Konfigurasi global hanya dapat digunakan untuk WHAPI Master Admin.']);
        }
        $credentials = array_filter($data['credentials'] ?? [], fn ($value) => $value !== '' && $value !== null);
        if ($channel && $credentials) {
            $credentials = [...($channel->credentials ?? []), ...$credentials];
        }
        if (($data['is_default'] ?? false) === true) {
            NotificationChannel::where('channel', $data['channel'])
                ->when($effectiveTenant, fn ($q) => $q->where('tenant_id', $effectiveTenant), fn ($q) => $q->whereNull('tenant_id'))
                ->when($channel, fn ($q) => $q->whereKeyNot($channel->id))
                ->update(['is_default' => false]);
        }
        if ($data['channel'] === 'whatsapp_unofficial') {
            $data['provider'] = 'whapi_id';
            $apiKey = $credentials['api_key'] ?? data_get($channel?->credentials, 'api_key');
            $baseUrl = data_get($data, 'settings.base_url', data_get($channel?->settings, 'base_url'));
            if (! $apiKey || ! $baseUrl) {
                throw ValidationException::withMessages([
                    'credentials' => 'API Key perangkat dan Base URL WHAPI wajib diisi.',
                ]);
            }
            if ($isGlobalWhapi && NotificationChannel::whereNull('tenant_id')
                ->where('channel', 'whatsapp_unofficial')
                ->when($channel, fn ($q) => $q->whereKeyNot($channel->id))
                ->exists()) {
                throw ValidationException::withMessages([
                    'channel' => 'Konfigurasi WHAPI global sudah tersedia. Silakan perbarui konfigurasi tersebut.',
                ]);
            }
        }
        $payload = [
            ...$data,
            'tenant_id' => $effectiveTenant,
            'provider' => $data['provider'] ?? $data['channel'],
            'settings' => $data['settings'] ?? [],
        ];
        if ($credentials) {
            $payload['credentials'] = $credentials;
        } else {
            unset($payload['credentials']);
        }
        $channel ? $channel->update($payload) : $channel = NotificationChannel::create($payload);

        return $channel->fresh('tenant:id,tenant_code,tenant_name')
            ->setAttribute('credentials_configured', ! empty($channel->getRawOriginal('credentials')));
    }

    public function deleteChannel(NotificationChannel $channel): void
    {
        if ($channel->templates()->exists() || $channel->deliveries()->exists()) {
            $channel->update(['enabled' => false, 'is_default' => false]);

            return;
        }
        $channel->delete();
    }

    public function templates(string $channel, User $actor, ?int $tenantId)
    {
        return NotificationTemplate::with(['event:id,code,name,category,variables', 'channelConfig:id,name'])
            ->where('channel', $channel)
            ->where(function ($q) use ($actor, $tenantId) {
                $q->whereNull('tenant_id')
                    ->when(! $actor->isMasterAdmin() || $tenantId, fn ($x) => $x->orWhere('tenant_id', $tenantId));
            })
            ->orderBy('notification_event_id')->orderByDesc('tenant_id')->orderByDesc('version')->get();
    }

    public function saveTemplate(array $data, User $actor, ?int $tenantId, ?NotificationTemplate $template = null): NotificationTemplate
    {
        $effectiveTenant = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? $template?->tenant_id ?? $tenantId) : $tenantId;
        if (! $effectiveTenant) {
            throw ValidationException::withMessages(['tenant_id' => 'Tenant wajib dipilih untuk membuat override template.']);
        }
        if ($template?->tenant_id === null) {
            $template = null;
        }
        $variables = $this->extractVariables(($data['subject'] ?? '').' '.($data['body'] ?? ''));
        $payload = [...$data, 'tenant_id' => $effectiveTenant, 'variables' => $variables];
        if (! $template) {
            $payload['version'] = ((int) NotificationTemplate::where('tenant_id', $effectiveTenant)
                ->where('notification_event_id', $data['notification_event_id'])
                ->where('channel', $data['channel'])->max('version')) + 1;
            $template = NotificationTemplate::create($payload);
        } else {
            $template->update($payload);
        }

        return $template->fresh(['event:id,code,name,category,variables', 'channelConfig:id,name']);
    }

    public function preferences(User $actor, ?int $tenantId): array
    {
        $effectiveTenant = $actor->isMasterAdmin() ? $tenantId : $tenantId;

        return [
            'events' => NotificationEvent::where('enabled', true)->orderBy('category')->orderBy('name')->get(),
            'preferences' => $effectiveTenant
                ? NotificationPreference::with(['event:id,code,name', 'channelConfig:id,name'])->where('tenant_id', $effectiveTenant)->get()
                : [],
        ];
    }

    public function savePreferences(array $rows, User $actor, ?int $tenantId): void
    {
        DB::transaction(function () use ($rows, $actor, $tenantId) {
            foreach ($rows as $row) {
                $effectiveTenant = $actor->isMasterAdmin() ? ($row['tenant_id'] ?? $tenantId) : $tenantId;
                if (! $effectiveTenant) {
                    throw ValidationException::withMessages(['tenant_id' => 'Tenant wajib dipilih.']);
                }
                if (! empty($row['notification_channel_id']) && ! NotificationChannel::whereKey($row['notification_channel_id'])
                    ->where('tenant_id', $effectiveTenant)->where('channel', $row['channel'])->exists()) {
                    throw ValidationException::withMessages(['notification_channel_id' => 'Konfigurasi channel bukan milik tenant atau channel tersebut.']);
                }
                NotificationPreference::updateOrCreate(
                    [
                        'tenant_id' => $effectiveTenant,
                        'notification_event_id' => $row['notification_event_id'],
                        'channel' => $row['channel'],
                    ],
                    [
                        'notification_channel_id' => $row['notification_channel_id'] ?? null,
                        'enabled' => $row['enabled'] ?? false,
                        'priority' => $row['priority'] ?? 1,
                        'send_delay_minutes' => $row['send_delay_minutes'] ?? 0,
                        'quiet_hours_start' => $row['quiet_hours_start'] ?? null,
                        'quiet_hours_end' => $row['quiet_hours_end'] ?? null,
                    ],
                );
            }
        });
    }

    public function deliveries(string $channel, array $filters, User $actor, ?int $tenantId)
    {
        return NotificationDelivery::with(['message:id,tenant_id,uuid,notification_event_id,recipient_type,recipient_id,created_at', 'message.event:id,code,name', 'channelConfig:id,name'])
            ->where('channel', $channel)
            ->whereHas('message', fn ($q) => $q
                ->when(! $actor->isMasterAdmin(), fn ($x) => $x->where('tenant_id', $tenantId))
                ->when($actor->isMasterAdmin() && $tenantId, fn ($x) => $x->where('tenant_id', $tenantId)))
            ->when(! empty($filters['status']), fn ($q) => $q->where('status', $filters['status']))
            ->when(! empty($filters['search']), fn ($q) => $q->where(fn ($x) => $x->where('destination', 'ILIKE', "%{$filters['search']}%")->orWhere('rendered_body', 'ILIKE', "%{$filters['search']}%")))
            ->orderByDesc('id')->paginate($filters['per_page'] ?? 10);
    }

    public function testChannel(NotificationChannel $channel): NotificationChannel
    {
        try {
            $credentials = $channel->credentials ?? [];
            $settings = $channel->settings ?? [];
            $message = match ($channel->channel) {
                'system' => 'Penyimpanan notifikasi aplikasi siap.',
                'telegram' => data_get(Http::timeout(15)->get('https://api.telegram.org/bot'.$credentials['bot_token'].'/getMe')->throw()->json(), 'result.username', 'Bot Telegram terhubung.'),
                'whatsapp_official' => data_get(Http::withToken($credentials['access_token'])->timeout(15)->get(rtrim($settings['base_url'] ?? 'https://graph.facebook.com/v22.0', '/').'/'.$credentials['phone_number_id'])->throw()->json(), 'display_phone_number', 'WhatsApp Official terhubung.'),
                'whatsapp_unofficial' => $channel->provider === 'whapi_id'
                    ? data_get(app(WhapiIdService::class)->state($channel), 'results.state', 'WHAPI terhubung.')
                    : data_get(Http::withHeaders(isset($credentials['api_key']) ? [($settings['api_key_header'] ?? 'X-API-Key') => $credentials['api_key']] : [])->timeout(15)->get($settings['health_url'])->throw()->json(), 'status', 'Gateway terhubung.'),
                'email' => ! empty($settings['host']) && ! empty($settings['from_address']) ? 'Konfigurasi SMTP lengkap.' : throw new \RuntimeException('Host dan alamat pengirim SMTP wajib diisi.'),
            };
            $channel->update(['last_health_status' => 'healthy', 'last_health_message' => (string) $message, 'last_health_check_at' => now()]);
        } catch (\Throwable $exception) {
            $channel->update(['last_health_status' => 'unhealthy', 'last_health_message' => Str::limit($exception->getMessage(), 1000), 'last_health_check_at' => now()]);
        }

        return $channel->fresh();
    }

    public function broadcasts(array $filters, User $actor, ?int $tenantId)
    {
        return NotificationBroadcast::with(['tenant:id,tenant_code,tenant_name', 'template:id,name', 'channelConfig:id,name'])
            ->when(! $actor->isMasterAdmin(), fn ($q) => $q->where('tenant_id', $tenantId))
            ->when($actor->isMasterAdmin() && $tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
            ->when(! empty($filters['status']), fn ($q) => $q->where('status', $filters['status']))
            ->when(! empty($filters['search']), fn ($q) => $q->where('name', 'ILIKE', "%{$filters['search']}%"))
            ->orderByDesc('id')->paginate($filters['per_page'] ?? 10);
    }

    public function previewBroadcast(array $data, User $actor, ?int $tenantId): array
    {
        $effectiveTenant = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? $tenantId) : $tenantId;
        if (! $effectiveTenant) {
            throw ValidationException::withMessages(['tenant_id' => 'Tenant wajib dipilih.']);
        }
        $query = $this->recipientQuery($effectiveTenant, $data['filters'] ?? []);
        $estimated = (clone $query)->count();
        $sample = $query->limit(10)->get(['id', 'customer_code', 'name', 'email', 'whatsapp_number']);

        return ['estimated_recipients' => $estimated, 'sample' => $sample];
    }

    public function createBroadcast(array $data, User $actor, ?int $tenantId): NotificationBroadcast
    {
        $effectiveTenant = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? $tenantId) : $tenantId;
        if (! $effectiveTenant) {
            throw ValidationException::withMessages(['tenant_id' => 'Tenant wajib dipilih.']);
        }
        $channelConfig = NotificationChannel::whereKey($data['notification_channel_id'] ?? null)
            ->where('tenant_id', $effectiveTenant)->where('channel', $data['channel'])->where('enabled', true)->first();
        if ($data['channel'] !== 'system' && ! $channelConfig) {
            throw ValidationException::withMessages(['notification_channel_id' => 'Konfigurasi channel aktif wajib dipilih.']);
        }
        $template = null;
        if (! empty($data['notification_template_id'])) {
            $template = NotificationTemplate::whereKey($data['notification_template_id'])
                ->where('channel', $data['channel'])
                ->where('enabled', true)
                ->where(fn ($q) => $q->whereNull('tenant_id')->orWhere('tenant_id', $effectiveTenant))
                ->first();
            if (! $template) {
                throw ValidationException::withMessages(['notification_template_id' => 'Template tidak tersedia untuk tenant dan channel ini.']);
            }
        }
        if ($data['channel'] === 'whatsapp_official'
            && (! $template || $template->approval_status !== 'approved' || ! $template->provider_template_name)) {
            throw ValidationException::withMessages(['notification_template_id' => 'WhatsApp Official wajib memakai template provider berstatus approved.']);
        }
        $estimated = $this->recipientQuery($effectiveTenant, $data['filters'] ?? [])->count();
        $filters = $data['filters'] ?? [];
        unset($data['filters']);
        $requiresApproval = $estimated >= 1000;
        $broadcast = NotificationBroadcast::create([
            ...$data,
            'uuid' => Str::uuid(),
            'tenant_id' => $effectiveTenant,
            'filter_snapshot' => $filters,
            'estimated_recipients' => $estimated,
            'status' => $requiresApproval
                ? 'pending_approval'
                : (! empty($data['scheduled_at']) && now()->lt($data['scheduled_at']) ? 'scheduled' : 'queued'),
            'created_by' => $actor->id,
        ]);
        if (! $requiresApproval) {
            $this->dispatchBroadcast($broadcast);
        }

        return $broadcast->load(['tenant:id,tenant_code,tenant_name', 'template:id,name', 'channelConfig:id,name']);
    }

    public function approveBroadcast(NotificationBroadcast $broadcast, User $actor): NotificationBroadcast
    {
        if ($broadcast->status !== 'pending_approval') {
            throw ValidationException::withMessages(['broadcast' => 'Siaran ini tidak sedang menunggu persetujuan.']);
        }
        $broadcast->update([
            'approved_by' => $actor->id,
            'status' => $broadcast->scheduled_at?->isFuture() ? 'scheduled' : 'queued',
        ]);
        $this->dispatchBroadcast($broadcast);

        return $broadcast->fresh(['tenant:id,tenant_code,tenant_name', 'template:id,name', 'channelConfig:id,name']);
    }

    public function processBroadcast(NotificationBroadcast $broadcast): void
    {
        if (in_array($broadcast->status, ['processing', 'completed', 'cancelled'], true)) {
            return;
        }
        $broadcast->update(['status' => 'processing']);
        $template = $broadcast->template;
        $channelConfig = $broadcast->channelConfig;
        $this->recipientQuery($broadcast->tenant_id, $broadcast->filter_snapshot)
            ->with('contacts')
            ->chunkById(200, function ($customers) use ($broadcast, $template, $channelConfig) {
                foreach ($customers as $customer) {
                    $destination = $this->manager->destination($customer, $broadcast->channel, true);
                    $recipient = NotificationBroadcastRecipient::firstOrCreate(
                        ['notification_broadcast_id' => $broadcast->id, 'customer_id' => $customer->id],
                        ['destination' => $destination, 'status' => $destination ? 'pending' : 'skipped', 'reason' => $destination ? null : 'Kontak broadcast tidak tersedia.'],
                    );
                    if (! $destination || $recipient->notification_message_id) {
                        continue;
                    }
                    $payload = ['customer' => $customer->toArray(), 'tenant' => $broadcast->tenant->toArray()];
                    $body = $this->manager->render($template?->body ?? $broadcast->body, $payload);
                    $subject = $this->manager->render($template?->subject ?? $broadcast->subject, $payload);
                    $message = NotificationMessage::create([
                        'uuid' => Str::uuid(), 'tenant_id' => $broadcast->tenant_id,
                        'notification_template_id' => $template?->id, 'notification_broadcast_id' => $broadcast->id,
                        'recipient_type' => $customer->getMorphClass(), 'recipient_id' => $customer->id,
                        'subject' => $subject, 'payload' => $payload, 'status' => 'queued', 'scheduled_at' => now(),
                        'context' => ['provider_template_name' => $template?->provider_template_name, 'language' => $template?->language],
                    ]);
                    $delivery = NotificationDelivery::create([
                        'uuid' => Str::uuid(), 'notification_message_id' => $message->id,
                        'notification_channel_id' => $channelConfig?->id, 'channel' => $broadcast->channel,
                        'destination' => $destination, 'rendered_subject' => $subject, 'rendered_body' => $body,
                        'status' => 'queued', 'queued_at' => now(),
                        'provider_response' => ['request_context' => [
                            'provider_template_name' => $template?->provider_template_name,
                            'language' => $template?->language,
                            'template_parameters' => collect($template?->variables ?? [])->map(fn ($key) => data_get($payload, $key))->values()->all(),
                        ]],
                    ]);
                    $recipient->update(['notification_message_id' => $message->id, 'status' => 'queued']);
                    $queue = str_starts_with($broadcast->channel, 'whatsapp') ? 'whatsapp' : $broadcast->channel;
                    ProcessNotificationDelivery::dispatch($delivery->id)->onQueue('notifications-'.$queue);
                }
            });
        $pending = $broadcast->recipients()->whereIn('status', ['pending', 'queued'])->count();
        $broadcast->update([
            'status' => $pending === 0 ? 'completed' : 'processing',
            'processed_recipients' => $broadcast->recipients()->count(),
            'success_count' => $broadcast->recipients()->where('status', 'sent')->count(),
            'failed_count' => $broadcast->recipients()->whereIn('status', ['failed', 'skipped'])->count(),
        ]);
    }

    public function userNotifications(User $user, array $filters)
    {
        return UserNotification::where('user_id', $user->id)
            ->when(($filters['unread'] ?? false), fn ($q) => $q->whereNull('read_at'))
            ->orderByDesc('id')->paginate($filters['per_page'] ?? 20);
    }

    public function saveContact(Customer $customer, array $data, ?CustomerContact $contact = null): CustomerContact
    {
        if ($contact && (int) $contact->customer_id !== (int) $customer->id) {
            throw ValidationException::withMessages(['contact' => 'Kontak bukan milik customer tersebut.']);
        }
        if (($data['is_primary'] ?? false) === true) {
            $customer->contacts()->where('type', $data['type'])->when($contact, fn ($q) => $q->whereKeyNot($contact->id))
                ->update(['is_primary' => false]);
        }
        $data = [...$data, 'tenant_id' => $customer->tenant_id];
        if (($data['is_verified'] ?? false) && ! $contact?->verified_at) {
            $data['verified_at'] = now();
        }
        $contact ? $contact->update($data) : $contact = $customer->contacts()->create($data);

        return $contact->fresh();
    }

    private function recipientQuery(int $tenantId, array $filters): Builder
    {
        $query = Customer::query()->where('tenant_id', $tenantId)->whereNull('deleted_at');
        $query->when(! empty($filters['customer_status']), fn ($q) => $q->whereIn('status', (array) $filters['customer_status']))
            ->when(! empty($filters['source']), fn ($q) => $q->where('source', $filters['source']))
            ->when(! empty($filters['package_profile_id']), fn ($q) => $q->where('package_profile_id', $filters['package_profile_id']))
            ->when(! empty($filters['billing_profile_id']), fn ($q) => $q->where('billing_profile_id', $filters['billing_profile_id']))
            ->when(! empty($filters['provinsi_id']), fn ($q) => $q->where('provinsi_id', $filters['provinsi_id']))
            ->when(! empty($filters['kabupaten_id']), fn ($q) => $q->where('kabupaten_id', $filters['kabupaten_id']))
            ->when(! empty($filters['kecamatan_id']), fn ($q) => $q->where('kecamatan_id', $filters['kecamatan_id']))
            ->when(! empty($filters['desa_id']), fn ($q) => $q->where('desa_id', $filters['desa_id']));
        if (! empty($filters['topology_node_id'])) {
            $nodeIds = $this->downstreamNodeIds((int) $filters['topology_node_id'], $tenantId);
            $query->whereHas('activeTopologyConnection', fn ($q) => $q->whereIn('odp_node_id', $nodeIds));
        }

        return $query;
    }

    private function downstreamNodeIds(int $rootId, int $tenantId): array
    {
        $ids = [$rootId];
        $frontier = [$rootId];
        while ($frontier && count($ids) < 10000) {
            $frontier = TopologyLink::where('tenant_id', $tenantId)->whereIn('source_node_id', $frontier)
                ->pluck('target_node_id')->diff($ids)->values()->all();
            $ids = [...$ids, ...$frontier];
        }

        return array_values(array_unique($ids));
    }

    private function extractVariables(string $value): array
    {
        preg_match_all('/{{\s*([a-zA-Z0-9_.]+)\s*}}/', $value, $matches);

        return array_values(array_unique($matches[1]));
    }

    private function dispatchBroadcast(NotificationBroadcast $broadcast): void
    {
        ProcessBroadcast::dispatch($broadcast->id)
            ->delay($broadcast->scheduled_at)
            ->onQueue('notifications-broadcast')
            ->afterCommit();
    }
}
