<?php

namespace App\Livewire\Project\Service;

use App\Livewire\Concerns\InteractsWithCloudflareDomainConnect;
use App\Livewire\Project\Shared\ConfigurationChecker;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Support\DomainUrlParts;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Livewire\Component;

class Domains extends Component
{
    use AuthorizesRequests;
    use InteractsWithCloudflareDomainConnect;

    protected bool $notifyRedirectUpdate = true;

    public Service $service;

    /** @var array<int, array{id: int, name: string, image: ?string, required_port: ?int}> */
    public array $serviceApps = [];

    /**
     * Per service-application www/non-www redirect direction.
     *
     * @var array<int|string, string>
     */
    public array $serviceRedirects = [];

    /** Service application id when a pending domain conflict belongs to setServiceRedirect. */
    public ?int $pendingRedirectServiceApplicationId = null;

    /** @var array<int, array<string, mixed>> */
    public array $domainRows = [];

    public ?int $newServiceApplicationId = null;

    public string $newDomain = '';

    public ?int $editingIndex = null;

    public string $editingDomain = '';

    public ?int $editingServiceApplicationId = null;

    public bool $showEditDomainModal = false;

    public bool $forceSaveDomains = false;

    public bool $forceSaveDns = false;

    public bool $addDomainDnsFailed = false;

    public string $addDomainDnsMessage = '';

    public bool $forceSaveEditDns = false;

    public bool $editDomainDnsFailed = false;

    public string $editDomainDnsMessage = '';

    public ?int $forceAddSuggestedIndex = null;

    public array $domainConflicts = [];

    public bool $showDomainConflictModal = false;

    public bool $showPortWarningModal = false;

    public bool $forceRemovePort = false;

    public ?int $requiredPort = null;

    public bool $isCheckingDns = false;

    public bool $dnsValidationEnabled = true;

    public ?string $serverIp = null;

    public ?string $serverIpConfigured = null;

    /** Pending save payload after conflict/port confirmation */
    public ?string $pendingAction = null;

    protected $listeners = [
        'refresh' => 'refreshDomains',
        'refreshServices' => 'refreshDomains',
        'confirmDomainUsage',
    ];

    protected function rules(): array
    {
        return [
            'newDomain' => ValidationPatterns::applicationDomainRules(),
            'editingDomain' => ValidationPatterns::applicationDomainRules(),
            'newServiceApplicationId' => 'nullable|integer',
            'serviceRedirects' => 'array',
            'serviceRedirects.*' => 'string|in:both,www,non-www',
        ];
    }

    public function mount(): void
    {
        $this->authorize('view', $this->service);
        $this->loadDomainState();
    }

    public function refreshDomains(): void
    {
        $this->service->refresh();
        $this->service->load(['applications', 'server']);
        $this->loadDomainState();
    }

    public function toggleNoindexDomain(int $serviceApplicationId, string $domain, string|bool $indexing): void
    {
        $application = $this->service->applications()->findOrFail($serviceApplicationId);
        $this->authorize('update', $application);

        $noindex = $indexing === true || $indexing === 'noindex';
        $domains = $application->noindexDomains();
        $domains = $noindex ? $domains->push($domain) : $domains->reject(fn (string $item) => $item === $domain);

        $application->setNoindexDomains($domains);
        $application->save();
        $this->service->parse();
        $this->refreshDomains();
        $this->dispatch('configurationChanged')->to(ConfigurationChecker::class);
        $this->dispatch('success', 'Search engine indexing updated.');
    }

    public function loadDomainState(): void
    {
        $this->service->loadMissing(['applications', 'server']);

        $settings = instanceSettings();
        $this->dnsValidationEnabled = (bool) data_get($settings, 'is_dns_validation_enabled', true);

        $server = $this->service->server;
        if ($server) {
            if ($server->id === 0) {
                $configured = data_get($settings, 'public_ipv4')
                    ?: data_get($settings, 'public_ipv6')
                    ?: $server->ip;
            } else {
                $configured = $server->ip;
            }
            $resolved = resolveServerIpAddress(is_string($configured) ? $configured : null);
            $this->serverIpConfigured = $resolved['configured'];
            $this->serverIp = $resolved['ip'] ?? $resolved['configured'];
        } else {
            $this->serverIp = null;
            $this->serverIpConfigured = null;
        }

        $this->serviceApps = $this->service->applications
            ->sortBy(fn (ServiceApplication $app) => strtolower($app->human_name ?: $app->name))
            ->values()
            ->map(fn (ServiceApplication $app) => [
                'id' => $app->id,
                'name' => $app->human_name ?: $app->name,
                'image' => $app->image,
                'required_port' => $app->getRequiredPort(),
            ])
            ->all();

        $this->serviceRedirects = [];
        foreach ($this->service->applications as $app) {
            $this->serviceRedirects[$app->id] = $this->normalizeRedirect($app->redirect ?? null);
        }

        if ($this->newServiceApplicationId === null && count($this->serviceApps) > 0) {
            $this->newServiceApplicationId = $this->serviceApps[0]['id'];
        }

        // Do not auto-promote www/non-www pairs here: load/refresh also runs after
        // removeDomain, and re-adding counterparts would undo intentional deletes.
        // Pairs are still ensured on setServiceRedirect, addDomain, etc.

        $this->domainRows = $this->buildDomainRows();
    }

    protected function normalizeRedirect(?string $redirect): string
    {
        return in_array($redirect, ['www', 'non-www', 'both'], true) ? $redirect : 'both';
    }

    protected function serviceRedirectFor(?int $serviceApplicationId): string
    {
        if (! $serviceApplicationId) {
            return 'both';
        }

        return $this->normalizeRedirect($this->serviceRedirects[$serviceApplicationId] ?? null);
    }

    public function updatedServiceRedirects(string $redirect, int|string $serviceApplicationId): void
    {
        $this->updateServiceRedirect((int) $serviceApplicationId, $redirect);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    protected function buildDomainRows(): array
    {
        $rows = [];

        foreach ($this->service->applications->sortBy(fn (ServiceApplication $app) => strtolower($app->human_name ?: $app->name)) as $app) {
            $stored = is_array($app->domain_dns_statuses) ? $app->domain_dns_statuses : [];
            $configured = [];

            foreach ($this->splitDomains($app->fqdn) as $url) {
                $row = $this->domainRowFromStored($url, $app, $stored);
                $rows[] = $row;
                $configured[] = $row;
            }

        }

        return collect($rows)
            ->sortBy(fn (array $row): int => ($row['dns_status'] ?? null) === 'failed' ? 0 : 1)
            ->values()
            ->all();
    }

    /**
     * @param  array<string, mixed>  $stored
     * @return array<string, mixed>
     */
    protected function domainRowFromStored(string $url, ServiceApplication $app, array $stored): array
    {
        $entry = $stored[$url] ?? null;
        $displayName = $app->human_name ?: $app->name;

        if (is_array($entry) && filled(data_get($entry, 'status'))) {
            return [
                'service_application_id' => $app->id,
                'service_name' => $displayName,
                'service_image' => $app->image,
                'url' => $url,
                'dns_status' => (string) data_get($entry, 'status', 'pending'),
                'dns_message' => (string) data_get($entry, 'message', 'Not checked yet.'),
                'expected_ip' => data_get($entry, 'expected_ip') ?: $this->serverIp,
                'checked_at' => data_get($entry, 'checked_at'),
                'is_suggested' => false,
                'suggested_for' => null,
                'suggestion_label' => null,
                'needs_force_add' => false,
            ];
        }

        return [
            'service_application_id' => $app->id,
            'service_name' => $displayName,
            'service_image' => $app->image,
            'url' => $url,
            'dns_status' => 'pending',
            'dns_message' => 'Not checked yet.',
            'expected_ip' => $this->serverIp,
            'checked_at' => null,
            'is_suggested' => false,
            'suggested_for' => null,
            'suggestion_label' => null,
            'needs_force_add' => false,
        ];
    }

    /**
     * @param  array<int, array<string, mixed>>  $configuredRows
     * @param  array<string, mixed>  $stored
     * @return array<int, array<string, mixed>>
     */
    protected function buildSuggestedWwwRows(array $configuredRows, ServiceApplication $app, array $stored): array
    {
        $knownHosts = [];
        foreach ($configuredRows as $row) {
            $host = $this->domainHost((string) ($row['url'] ?? ''));
            if ($host !== null) {
                $knownHosts[strtolower($host)] = true;
            }
        }

        $suggested = [];
        foreach ($configuredRows as $row) {
            $url = (string) ($row['url'] ?? '');
            $counterpart = $this->wwwCounterpartUrl($url);
            if ($counterpart === null) {
                continue;
            }

            $counterpartHost = $this->domainHost($counterpart);
            if ($counterpartHost === null) {
                continue;
            }

            $hostKey = strtolower($counterpartHost);
            if (isset($knownHosts[$hostKey])) {
                continue;
            }
            $knownHosts[$hostKey] = true;

            $base = $this->domainRowFromStored($counterpart, $app, $stored);
            $isWww = str_starts_with($hostKey, 'www.');
            $meta = $this->suggestedDomainMeta($isWww, $this->serviceRedirectFor($app->id));

            $base['is_suggested'] = true;
            $base['suggested_for'] = $url;
            $base['suggestion_label'] = null;
            $base['suggestion_role'] = $meta['role'];
            $base['needs_force_add'] = false;
            $base['dns_message'] = $meta['pending_message'];

            $suggested[] = $base;
        }

        return $suggested;
    }

    /**
     * @return array<int, string>
     */
    protected function splitDomains(?string $value): array
    {
        if (blank($value)) {
            return [];
        }

        return collect(explode(',', $value))
            ->map(fn ($domain) => trim((string) $domain))
            ->filter()
            ->values()
            ->all();
    }

    public function dnsTargetLabel(): ?string
    {
        if (blank($this->serverIp)) {
            return $this->serverIpConfigured;
        }

        if (
            filled($this->serverIpConfigured)
            && $this->serverIpConfigured !== $this->serverIp
            && filter_var($this->serverIpConfigured, FILTER_VALIDATE_IP) === false
        ) {
            return "{$this->serverIp} ({$this->serverIpConfigured})";
        }

        return $this->serverIp;
    }

    protected function authorizeUpdateForDomainConnect(): void
    {
        $this->authorize('update', $this->service);
    }

    public function checkAllDns(): void
    {
        $this->authorize('update', $this->service);

        $this->isCheckingDns = true;

        try {
            $server = $this->service->server;
            $skipDns = ! $this->dnsValidationEnabled || ! $server;

            foreach ($this->domainRows as $index => $row) {
                if ($skipDns) {
                    $this->domainRows[$index]['dns_status'] = 'skipped';
                    $this->domainRows[$index]['dns_message'] = ! $this->dnsValidationEnabled
                        ? 'DNS validation is disabled in instance settings.'
                        : 'No server available for DNS validation.';
                    $this->domainRows[$index]['checked_at'] = now()->toIso8601String();

                    continue;
                }

                $this->applyDnsStatus($index, $row['url'], $server);
            }

            $this->persistAllDomainDnsStatuses();
        } finally {
            $this->isCheckingDns = false;
        }
    }

    public function checkDomainDns(int $index): void
    {
        $this->authorize('update', $this->service);

        if (! isset($this->domainRows[$index])) {
            return;
        }

        $server = $this->service->server;
        if (! $server || ! $this->dnsValidationEnabled) {
            $this->domainRows[$index]['dns_status'] = 'skipped';
            $this->domainRows[$index]['dns_message'] = 'DNS check skipped.';
            $this->domainRows[$index]['checked_at'] = now()->toIso8601String();
            $this->decorateSuggestedDomainAfterDnsCheck($index);
            $this->persistAllDomainDnsStatuses();

            return;
        }

        $this->applyDnsStatus($index, $this->domainRows[$index]['url'], $server);
        $this->persistAllDomainDnsStatuses();
    }

    protected function applyDnsStatus(int $index, string $url, Server $server): void
    {
        $target = $this->dnsTargetLabel();

        try {
            $isValid = validateDNSEntry($url, $server);
            if ($isValid) {
                $this->domainRows[$index]['dns_status'] = 'ok';
                $this->domainRows[$index]['dns_message'] = $target
                    ? "DNS points to {$target} (or Cloudflare)."
                    : 'DNS looks correct.';
            } else {
                $this->domainRows[$index]['dns_status'] = 'failed';
                $this->domainRows[$index]['dns_message'] = dnsMismatchGuidanceMessage($target, $this->serverIp);
            }
        } catch (\Throwable) {
            $this->domainRows[$index]['dns_status'] = 'failed';
            $this->domainRows[$index]['dns_message'] = 'Could not validate DNS for this domain.';
        }

        $this->domainRows[$index]['expected_ip'] = $this->serverIp;
        $this->domainRows[$index]['checked_at'] = now()->toIso8601String();
        $this->decorateSuggestedDomainAfterDnsCheck($index);
    }

    /**
     * Keep suggested-row copy short after a DNS check (no role badge).
     */
    protected function decorateSuggestedDomainAfterDnsCheck(int $index): void
    {
        if (! ($this->domainRows[$index]['is_suggested'] ?? false)) {
            return;
        }

        $isWww = str_starts_with(strtolower((string) $this->domainHost((string) $this->domainRows[$index]['url'])), 'www.');
        $appId = (int) ($this->domainRows[$index]['service_application_id'] ?? 0);
        $meta = $this->suggestedDomainMeta($isWww, $this->serviceRedirectFor($appId > 0 ? $appId : null));
        $this->domainRows[$index]['dns_message'] = $meta['pending_message'];
        $this->domainRows[$index]['suggestion_label'] = null;
        $this->domainRows[$index]['suggestion_role'] = $meta['role'];
    }

    protected function persistAllDomainDnsStatuses(): void
    {
        $byApp = [];

        foreach ($this->domainRows as $row) {
            $appId = (int) ($row['service_application_id'] ?? 0);
            $url = $row['url'] ?? null;
            if ($appId === 0 || ! is_string($url) || $url === '') {
                continue;
            }

            $status = $row['dns_status'] ?? 'pending';
            if ($status === 'pending') {
                $app = $this->service->applications->firstWhere('id', $appId);
                $existing = is_array($app?->domain_dns_statuses) ? ($app->domain_dns_statuses[$url] ?? null) : null;
                if (is_array($existing) && filled(data_get($existing, 'status'))) {
                    $byApp[$appId][$url] = $existing;
                }

                continue;
            }

            $byApp[$appId][$url] = [
                'status' => $status,
                'message' => (string) ($row['dns_message'] ?? ''),
                'expected_ip' => $row['expected_ip'] ?? $this->serverIp,
                'checked_at' => $row['checked_at'] ?? now()->toIso8601String(),
            ];
        }

        foreach ($this->service->applications as $app) {
            $statuses = $byApp[$app->id] ?? [];
            // Keep only URLs that still exist (configured or suggested currently shown)
            $currentUrls = collect($this->domainRows)
                ->where('service_application_id', $app->id)
                ->pluck('url')
                ->all();
            $statuses = array_intersect_key($statuses, array_flip($currentUrls));

            $app->domain_dns_statuses = $statuses === [] ? null : $statuses;
            $app->save();
        }

        $this->service->load('applications');
    }

    protected function pruneDomainDnsStatusesToCurrentDomains(): void
    {
        $this->refreshDomains();
        $this->persistAllDomainDnsStatuses();
    }

    public function updatedNewDomain(): void
    {
        $this->addDomainDnsFailed = false;
        $this->addDomainDnsMessage = '';
        $this->forceSaveDns = false;
    }

    public function updatedEditingDomain(): void
    {
        $this->editDomainDnsFailed = false;
        $this->editDomainDnsMessage = '';
        $this->forceSaveEditDns = false;
    }

    public function confirmAddDomainDespiteDns(): void
    {
        $this->forceSaveDns = true;
        $this->addDomain();
    }

    public function confirmUpdateDomainDespiteDns(): void
    {
        $this->forceSaveEditDns = true;
        $this->updateDomain();
    }

    public function confirmDomainUsage(): void
    {
        $this->forceSaveDomains = true;
        $this->showDomainConflictModal = false;

        if ($this->pendingAction === 'update') {
            $this->updateDomain();

            return;
        }

        if ($this->pendingAction === 'suggested') {
            $this->addSuggestedDomain((int) ($this->editingIndex ?? -1));

            return;
        }

        if ($this->pendingAction === 'redirect' && $this->pendingRedirectServiceApplicationId) {
            $this->setServiceRedirect((int) $this->pendingRedirectServiceApplicationId);

            return;
        }

        $this->addDomain();
    }

    /**
     * Labels/copy for a suggested www or non-www host based on Direction.
     *
     * @return array{label: string, role: string, pending_message: string, dns_suffix: string}
     */
    protected function suggestedDomainMeta(bool $suggestedIsWww, ?string $redirectOverride = null): array
    {
        $pendingMessage = 'Not configured yet.';
        $redirect = $this->normalizeRedirect($redirectOverride);

        return match ($redirect) {
            'www' => $suggestedIsWww
                ? [
                    'label' => 'Not added · canonical www',
                    'role' => 'canonical',
                    'pending_message' => $pendingMessage,
                    'dns_suffix' => '',
                ]
                : [
                    'label' => 'Not added · redirect source',
                    'role' => 'redirect_source',
                    'pending_message' => $pendingMessage,
                    'dns_suffix' => '',
                ],
            'non-www' => $suggestedIsWww
                ? [
                    'label' => 'Not added · redirect source',
                    'role' => 'redirect_source',
                    'pending_message' => $pendingMessage,
                    'dns_suffix' => '',
                ]
                : [
                    'label' => 'Not added · canonical non-www',
                    'role' => 'canonical',
                    'pending_message' => $pendingMessage,
                    'dns_suffix' => '',
                ],
            default => [
                'label' => $suggestedIsWww ? 'Not added · www' : 'Not added · non-www',
                'role' => 'pair',
                'pending_message' => $pendingMessage,
                'dns_suffix' => '',
            ],
        };
    }

    public function updateServiceRedirect(int $serviceApplicationId, string $redirect): void
    {
        $this->serviceRedirects[$serviceApplicationId] = $redirect;
        $this->setServiceRedirect($serviceApplicationId);
    }

    /**
     * @param  mixed  ...$modalArgs  Extra args from modal-confirmation (password, etc.)
     */
    public function setServiceRedirect(int $serviceApplicationId, mixed ...$modalArgs): void
    {
        try {
            $this->authorize('update', $this->service);

            $app = $this->findServiceApp($serviceApplicationId);
            if (! $app) {
                $this->dispatch('error', 'Service application not found.');

                return;
            }

            $this->validateOnly("serviceRedirects.{$serviceApplicationId}");
            $redirect = $this->normalizeRedirect($this->serviceRedirects[$serviceApplicationId] ?? null);
            $this->serviceRedirects[$serviceApplicationId] = $redirect;
            $this->pendingRedirectServiceApplicationId = $serviceApplicationId;

            $addedDomains = [];
            $saved = DB::transaction(function () use ($app, $redirect, &$addedDomains): bool {
                // Promote the optional www/non-www suggestion to a real domain for redirects.
                if (in_array($redirect, ['www', 'non-www'], true)) {
                    $domainsBeforePairing = collect($this->splitDomains($app->fqdn));
                    if (! $this->ensureWwwNonWwwPairsConfigured($app)) {
                        return false;
                    }
                    $app->refresh();
                    $addedDomains = collect($this->splitDomains($app->fqdn))->diff($domainsBeforePairing)->values()->all();
                }

                $domains = collect($this->splitDomains($app->fqdn));
                if (! $this->assertRedirectDomainsPresent($redirect, $domains)) {
                    return false;
                }

                $app->redirect = $redirect;
                $app->save();
                updateCompose($app);
                $this->service->parse();

                return true;
            });

            if (! $saved) {
                return;
            }

            $this->pendingAction = null;
            $this->pendingRedirectServiceApplicationId = null;
            $this->forceSaveDomains = false;
            $this->forceRemovePort = false;
            if ($this->notifyRedirectUpdate) {
                $this->dispatch('success', 'Redirect updated.');
            }
            $this->dispatch('configurationChanged');
            $this->pruneDomainDnsStatusesToCurrentDomains();
            $this->refreshDomains();
            $this->checkUrlsDns($addedDomains, $serviceApplicationId);
        } catch (\Throwable $e) {
            handleError($e, $this);
        }
    }

    /**
     * @param  Collection<int, string>  $domains
     */
    protected function assertRedirectDomainsPresent(string $redirect, Collection $domains): bool
    {
        if (! in_array($redirect, ['www', 'non-www'], true)) {
            return true;
        }

        $hasWww = $domains->filter(
            fn ($fqdn) => str_starts_with(strtolower((string) $this->domainHost((string) $fqdn)), 'www.')
        )->count();
        $hasNonWww = $domains->filter(
            function ($fqdn) {
                $host = strtolower((string) $this->domainHost((string) $fqdn));

                return $host !== '' && ! str_starts_with($host, 'www.');
            }
        )->count();

        $dnsHint = dnsMismatchGuidanceMessage(
            $this->dnsTargetLabel() ?? $this->serverIp,
            $this->serverIp,
        );

        // Redirects need both hosts: canonical target + source the proxy redirects from.
        if ($hasWww === 0 || $hasNonWww === 0) {
            $missing = $hasWww === 0 ? 'www' : 'non-www';
            $this->dispatch(
                'error',
                "Redirect requires both www and non-www domains, but the {$missing} host could not be added automatically (e.g. only IP/sslip hosts).<br><br>Please add the {$missing} domain manually ({$dnsHint})."
            );

            return false;
        }

        return true;
    }

    /**
     * When saved redirect is www/non-www, ensure missing counterparts exist as real domains.
     *
     * @return array<int, string> newly added domain URLs
     */
    protected function syncRedirectDomainPairs(?ServiceApplication $app = null): array
    {
        $user = auth()->user();
        if ($user === null || ! $user->can('update', $this->service)) {
            return [];
        }

        if ($app === null) {
            $added = [];
            foreach ($this->service->applications as $serviceApp) {
                $added = array_merge($added, $this->syncRedirectDomainPairs($serviceApp));
            }

            return array_values(array_unique($added));
        }

        $redirect = $this->normalizeRedirect($app->redirect ?? null);
        if (! in_array($redirect, ['www', 'non-www'], true)) {
            return [];
        }

        $before = collect($this->splitDomains($app->fqdn))->all();
        if (! $this->ensureWwwNonWwwPairsConfigured($app)) {
            return [];
        }

        $app->refresh();
        $after = collect($this->splitDomains($app->fqdn));

        return $after->reject(fn (string $url) => in_array($url, $before, true))->values()->all();
    }

    /**
     * Persist missing www/non-www counterparts as normal domains (not suggestions).
     *
     * @return bool false when save was blocked (e.g. domain conflict modal shown)
     */
    protected function ensureWwwNonWwwPairsConfigured(ServiceApplication $app): bool
    {
        $current = collect($this->splitDomains($app->fqdn));
        $knownHosts = [];

        foreach ($current as $url) {
            $host = $this->domainHost($url);
            if ($host !== null) {
                $knownHosts[strtolower($host)] = true;
            }
        }

        $toAdd = collect();
        foreach ($current as $url) {
            $counterpart = $this->wwwCounterpartUrl($url, forRedirectPairing: true);
            if ($counterpart === null) {
                continue;
            }

            $counterpartHost = $this->domainHost($counterpart);
            if ($counterpartHost === null) {
                continue;
            }

            $hostKey = strtolower($counterpartHost);
            if (isset($knownHosts[$hostKey])) {
                continue;
            }

            $knownHosts[$hostKey] = true;
            $toAdd->push($counterpart);
        }

        if ($toAdd->isEmpty()) {
            return true;
        }

        $merged = $current->merge($toAdd)->unique()->values();
        $this->pendingAction = 'redirect';
        $this->pendingRedirectServiceApplicationId = $app->id;

        // Skip DNS: pairing for redirects must still be configured even when DNS is not ready.
        if (! $this->saveDomainListForApp($app, $merged)) {
            return false;
        }

        $this->pendingAction = null;
        $this->pendingRedirectServiceApplicationId = null;

        return true;
    }

    public function confirmRemovePort(): void
    {
        $this->forceRemovePort = true;
        $this->showPortWarningModal = false;

        if ($this->pendingAction === 'update') {
            $this->updateDomain();

            return;
        }

        if ($this->pendingAction === 'suggested') {
            $this->addSuggestedDomain((int) ($this->editingIndex ?? -1));

            return;
        }

        $this->addDomain();
    }

    public function cancelRemovePort(): void
    {
        $this->showPortWarningModal = false;
        $this->forceSaveDomains = false;
        $this->forceRemovePort = false;
        $this->pendingAction = null;
    }

    public function addDomain(): void
    {
        try {
            $this->authorize('update', $this->service);
            $this->validateOnly('newDomain');

            $app = $this->findServiceApp($this->newServiceApplicationId);
            if (! $app) {
                $this->dispatch('error', 'Select a service application.');

                return;
            }

            $normalized = ValidationPatterns::normalizeApplicationDomains($this->newDomain);
            if (blank($normalized)) {
                $this->addError('newDomain', 'Please enter a valid domain URL.');

                return;
            }

            $newUrls = $this->splitDomains($normalized);
            $pairedUrls = in_array($this->normalizeRedirect($app->redirect), ['www', 'non-www'], true)
                ? collect($newUrls)
                    ->map(fn (string $url) => $this->wwwCounterpartUrl($url))
                    ->filter()
                    ->values()
                    ->all()
                : [];
            $current = collect($this->splitDomains($app->fqdn));
            foreach ($newUrls as $url) {
                if ($current->contains($url)) {
                    $this->addError('newDomain', "Domain {$url} is already configured for this service.");

                    return;
                }
            }

            if (! $this->forceSaveDns && $this->shouldValidateDns()) {
                $dnsFailure = $this->findDnsFailureMessage($newUrls);
                if ($dnsFailure !== null) {
                    $this->addDomainDnsFailed = true;
                    $this->addDomainDnsMessage = $dnsFailure;

                    return;
                }
            }

            $merged = $current->merge($newUrls)->merge($pairedUrls)->unique()->values();
            $this->pendingAction = 'add';

            if (! $this->saveDomainListForApp($app, $merged)) {
                return;
            }

            $this->newDomain = '';
            $this->addDomainDnsFailed = false;
            $this->addDomainDnsMessage = '';
            $this->forceSaveDns = false;
            $this->forceSaveDomains = false;
            $this->forceRemovePort = false;
            $this->pendingAction = null;
            $this->dispatch('close-modal');
            $this->dispatch('success', 'Domain added.');
            $this->refreshDomains();
            $this->checkUrlsDns(array_values(array_unique(array_merge($newUrls, $pairedUrls))), (int) $app->id);
        } catch (\Throwable $e) {
            handleError($e, $this);
        }
    }

    public function startEdit(int $index): void
    {
        if (! isset($this->domainRows[$index]) || ($this->domainRows[$index]['is_suggested'] ?? false)) {
            return;
        }

        $this->editingIndex = $index;
        $this->editingDomain = $this->domainRows[$index]['url'];
        $this->editingServiceApplicationId = (int) $this->domainRows[$index]['service_application_id'];
        $this->editDomainDnsFailed = false;
        $this->editDomainDnsMessage = '';
        $this->forceSaveEditDns = false;
        $this->resetErrorBag('editingDomain');
        $this->showEditDomainModal = true;
    }

    public function cancelEdit(): void
    {
        $this->showEditDomainModal = false;
        $this->editingIndex = null;
        $this->editingDomain = '';
        $this->editingServiceApplicationId = null;
        $this->editDomainDnsFailed = false;
        $this->editDomainDnsMessage = '';
        $this->forceSaveEditDns = false;
        $this->resetErrorBag('editingDomain');
    }

    public function updateDomain(): void
    {
        try {
            $this->authorize('update', $this->service);

            if ($this->editingIndex === null || ! isset($this->domainRows[$this->editingIndex])) {
                return;
            }

            $this->validateOnly('editingDomain');

            $app = $this->findServiceApp($this->editingServiceApplicationId);
            if (! $app) {
                return;
            }

            $normalized = ValidationPatterns::normalizeApplicationDomains($this->editingDomain);
            if (blank($normalized) || count($this->splitDomains($normalized)) !== 1) {
                $this->addError('editingDomain', 'Please enter a single valid domain URL.');

                return;
            }

            $newUrl = $this->splitDomains($normalized)[0];
            $oldUrl = $this->domainRows[$this->editingIndex]['url'];
            $current = collect($this->splitDomains($app->fqdn));
            $wasNoindexed = $app->isDomainNoindexed($oldUrl);

            if ($newUrl !== $oldUrl && $current->contains($newUrl)) {
                $this->addError('editingDomain', "Domain {$newUrl} is already configured for this service.");

                return;
            }

            if (! $this->forceSaveEditDns && $this->shouldValidateDns()) {
                $dnsFailure = $this->findDnsFailureMessage([$newUrl]);
                if ($dnsFailure !== null) {
                    $this->editDomainDnsFailed = true;
                    $this->editDomainDnsMessage = $dnsFailure;
                    $this->showEditDomainModal = true;

                    return;
                }
            }

            $updated = $current->map(fn (string $url) => $url === $oldUrl ? $newUrl : $url)->unique()->values();
            $this->pendingAction = 'update';

            if (! $this->saveDomainListForApp($app, $updated)) {
                return;
            }

            $noindexDomains = $app->noindexDomains()->reject(fn (string $domain) => $domain === $oldUrl);
            if ($wasNoindexed) {
                $noindexDomains->push($newUrl);
            }
            $app->setNoindexDomains($noindexDomains);
            $app->save();

            $this->cancelEdit();
            $this->dispatch('edit-domain-saved');
            $this->forceSaveDomains = false;
            $this->forceRemovePort = false;
            $this->pendingAction = null;
            $this->dispatch('success', 'Domain updated.');
            $this->refreshDomains();
            $this->checkUrlsDns([$newUrl], (int) $app->id);
        } catch (\Throwable $e) {
            handleError($e, $this);
        }
    }

    public function removeDomain(int $index): void
    {
        try {
            $this->authorize('update', $this->service);

            if (! isset($this->domainRows[$index]) || ($this->domainRows[$index]['is_suggested'] ?? false)) {
                return;
            }

            $app = $this->findServiceApp((int) $this->domainRows[$index]['service_application_id']);
            if (! $app) {
                return;
            }

            $url = $this->domainRows[$index]['url'];
            $updated = collect($this->splitDomains($app->fqdn))->reject(fn (string $item) => $item === $url)->values();

            $this->forceSaveDomains = true;
            $this->forceRemovePort = true;
            if (! $this->saveDomainListForApp($app, $updated, checkConflicts: false)) {
                return;
            }

            $this->forceSaveDomains = false;
            $this->forceRemovePort = false;
            $this->dispatch('success', 'Domain removed.');
            $this->pruneDomainDnsStatusesToCurrentDomains();
            $this->refreshDomains();
        } catch (\Throwable $e) {
            handleError($e, $this);
        }
    }

    public function addSuggestedDomain(int $index): void
    {
        try {
            $this->authorize('update', $this->service);

            if (! isset($this->domainRows[$index]) || ! ($this->domainRows[$index]['is_suggested'] ?? false)) {
                return;
            }

            $app = $this->findServiceApp((int) $this->domainRows[$index]['service_application_id']);
            if (! $app) {
                return;
            }

            $url = (string) $this->domainRows[$index]['url'];
            $normalized = ValidationPatterns::normalizeApplicationDomains($url);
            $newUrls = $this->splitDomains($normalized);
            $current = collect($this->splitDomains($app->fqdn));

            foreach ($newUrls as $newUrl) {
                if ($current->contains($newUrl)) {
                    $this->refreshDomains();

                    return;
                }
            }

            $force = $this->forceAddSuggestedIndex === $index
                || (bool) ($this->domainRows[$index]['needs_force_add'] ?? false);

            if (! $force && $this->shouldValidateDns()) {
                $dnsFailure = $this->findDnsFailureMessage($newUrls);
                if ($dnsFailure !== null) {
                    $this->domainRows[$index]['dns_status'] = 'failed';
                    $this->domainRows[$index]['dns_message'] = $dnsFailure;
                    $this->domainRows[$index]['checked_at'] = now()->toIso8601String();
                    $this->domainRows[$index]['needs_force_add'] = true;
                    $this->forceAddSuggestedIndex = $index;
                    $this->editingIndex = $index;
                    $this->persistAllDomainDnsStatuses();

                    return;
                }
            }

            $merged = $current->merge($newUrls)->unique()->values();
            $this->pendingAction = 'suggested';
            $this->editingIndex = $index;

            if (! $this->saveDomainListForApp($app, $merged)) {
                return;
            }

            $this->forceAddSuggestedIndex = null;
            $this->editingIndex = null;
            $this->pendingAction = null;
            $this->forceSaveDomains = false;
            $this->forceRemovePort = false;
            $this->dispatch('success', 'Domain added.');
            $this->refreshDomains();
            $this->checkUrlsDns($newUrls, (int) $app->id);
        } catch (\Throwable $e) {
            handleError($e, $this);
        }
    }

    public function generateDomain(): void
    {
        try {
            $this->authorize('update', $this->service);

            $app = $this->findServiceApp($this->newServiceApplicationId);
            $server = $this->service->server;
            if (! $app || ! $server) {
                $this->dispatch('error', 'Service application or server not found.');

                return;
            }

            $domain = generateUrl(server: $server, random: new_public_id());
            $requiredPort = $app->getRequiredPort();
            if ($requiredPort !== null) {
                $parts = DomainUrlParts::split($domain);
                if ($parts['port'] === '') {
                    $domain = DomainUrlParts::compose($parts['scheme'], $parts['host'], (string) $requiredPort, $parts['path']);
                }
            }

            $this->newDomain = $domain;
            $this->updatedNewDomain();
        } catch (\Throwable $e) {
            handleError($e, $this);
        }
    }

    /**
     * @param  Collection<int, string>  $domains
     */
    protected function saveDomainListForApp(
        ServiceApplication $app,
        Collection $domains,
        bool $checkConflicts = true,
    ): bool {
        $domainString = $domains->filter()->unique()->implode(',');
        $domainString = $domainString === '' ? null : ValidationPatterns::normalizeApplicationDomains($domainString);

        if ($domainString) {
            $errors = ValidationPatterns::validateApplicationDomains($domainString);
            if ($errors !== []) {
                $this->dispatch('error', $errors[0]);

                return false;
            }
        }

        $app->fqdn = $domainString;

        if ($checkConflicts && ! $this->forceSaveDomains) {
            $result = checkDomainUsage(resource: $app);
            if ($result['hasConflicts']) {
                $this->domainConflicts = $result['conflicts'];
                $this->showDomainConflictModal = true;
                $app->refresh();

                return false;
            }
        }

        if (! $this->forceRemovePort) {
            $requiredPort = $app->getRequiredPort();
            if ($requiredPort !== null && $domainString) {
                foreach ($this->splitDomains($domainString) as $fqdn) {
                    if (ServiceApplication::extractPortFromUrl($fqdn) === null) {
                        $this->requiredPort = $requiredPort;
                        $this->showPortWarningModal = true;
                        $app->refresh();

                        return false;
                    }
                }
            }
        }

        $warning = sslipDomainWarning($domainString ?? '');
        if ($warning) {
            $this->dispatch('warning', __('warning.sslipdomain'));
        }

        DB::transaction(function () use ($app): void {
            $app->save();
            updateCompose($app);
            $this->service->parse();
        });

        if (str($app->fqdn)->contains(',')) {
            $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
        }

        $this->dispatch('configurationChanged');

        return true;
    }

    /**
     * @param  array<int, string>  $urls
     */
    /**
     * Run a first-time DNS check for newly added/updated domain URLs and persist results.
     *
     * @param  array<int, string>  $urls
     */
    protected function checkUrlsDns(array $urls, ?int $serviceApplicationId = null): void
    {
        if ($urls === []) {
            return;
        }

        $urlSet = array_fill_keys($urls, true);
        $server = $this->service->server;
        $skipDns = ! $this->dnsValidationEnabled || ! $server;

        foreach ($this->domainRows as $index => $row) {
            $url = $row['url'] ?? null;
            if (! is_string($url) || ! isset($urlSet[$url])) {
                continue;
            }

            if ($serviceApplicationId !== null
                && (int) ($row['service_application_id'] ?? 0) !== $serviceApplicationId) {
                continue;
            }

            if ($skipDns) {
                $this->domainRows[$index]['dns_status'] = 'skipped';
                $this->domainRows[$index]['dns_message'] = ! $this->dnsValidationEnabled
                    ? 'DNS validation is disabled in instance settings.'
                    : 'No server available for DNS validation.';
                $this->domainRows[$index]['checked_at'] = now()->toIso8601String();
                $this->domainRows[$index]['expected_ip'] = $this->serverIp;

                continue;
            }

            $this->applyDnsStatus($index, $url, $server);
        }

        $this->persistAllDomainDnsStatuses();
    }

    protected function shouldValidateDns(): bool
    {
        return $this->dnsValidationEnabled && $this->service->server !== null;
    }

    /**
     * @param  array<int, string>  $urls
     */
    protected function findDnsFailureMessage(array $urls): ?string
    {
        $server = $this->service->server;
        if (! $server) {
            return null;
        }

        $target = $this->dnsTargetLabel() ?? $server->ip;

        foreach ($urls as $url) {
            try {
                if (! validateDNSEntry($url, $server)) {
                    return dnsMismatchGuidanceMessage($target, $this->serverIp);
                }
            } catch (\Throwable) {
                return 'Could not validate DNS for this domain.';
            }
        }

        return null;
    }

    protected function findServiceApp(?int $id): ?ServiceApplication
    {
        if (! $id) {
            return null;
        }

        return $this->service->applications->firstWhere('id', $id)
            ?? ServiceApplication::query()->where('service_id', $this->service->id)->find($id);
    }

    protected function domainHost(string $url): ?string
    {
        try {
            $host = parse_url($url, PHP_URL_HOST);

            return is_string($host) && $host !== '' ? $host : null;
        } catch (\Throwable) {
            return null;
        }
    }

    /**
     * @param  bool  $forRedirectPairing  When true, also pair sslip/nip hosts for www↔non-www redirects.
     */
    protected function wwwCounterpartUrl(string $url, bool $forRedirectPairing = false): ?string
    {
        $host = $this->domainHost($url);
        if ($host === null) {
            return null;
        }

        $lowerHost = strtolower($host);

        if (filter_var($host, FILTER_VALIDATE_IP) !== false || $lowerHost === 'localhost') {
            return null;
        }

        if (
            ! $forRedirectPairing
            && (str_contains($lowerHost, 'sslip.io') || str_contains($lowerHost, 'nip.io'))
        ) {
            return null;
        }

        $counterpartHost = str_starts_with($lowerHost, 'www.')
            ? substr($host, 4)
            : 'www.'.$host;

        if ($counterpartHost === '' || $counterpartHost === $host) {
            return null;
        }

        $parts = parse_url($url);
        if ($parts === false) {
            return null;
        }

        $scheme = $parts['scheme'] ?? 'https';
        $port = isset($parts['port']) ? ':'.$parts['port'] : '';
        $path = $parts['path'] ?? '';
        $query = isset($parts['query']) ? '?'.$parts['query'] : '';
        $fragment = isset($parts['fragment']) ? '#'.$parts['fragment'] : '';

        return "{$scheme}://{$counterpartHost}{$port}{$path}{$query}{$fragment}";
    }

    public function render()
    {
        return view('livewire.project.service.domains');
    }
}
