<?php

namespace App\Livewire;

use App\Actions\ReapplyInstanceAddress;
use App\Livewire\Concerns\ResolvesCustomer;
use App\Models\Instance;
use App\Services\Billing\CustomDomainAccess;
use App\Services\Domains\DomainVerifier;
use App\Support\ProvisioningSettings;
use Illuminate\Support\Str;
use Livewire\Attributes\Validate;
use Livewire\Component;

/**
 * The customer's own domain, and the proof that it is theirs.
 *
 * Two things happen here and they are deliberately separate: pointing the
 * domain at us (an A record, which we cannot check for them because it may be
 * behind a CDN) and proving they own it (a TXT record, which we can). Only the
 * second one gates anything.
 *
 * A domain can be changed at any time. It was suggested that it should be
 * fixed once set — it should not: adding or moving a domain is a proxy entry, a
 * certificate and one line in trusted_domains, and a customer who wants to move
 * their address after three months is a customer who is still here.
 */
class CustomDomain extends Component
{
    use ResolvesCustomer;

    #[Validate('nullable|string|max:253')]
    public string $domain = '';

    public bool $justChecked = false;

    public function mount(): void
    {
        // Here rather than as route middleware: a Livewire component answers
        // POSTs to /livewire/update on its own, so a guard that only sits on
        // the page route leaves every action on this one reachable to a plan
        // that may not have a domain at all. CustomDomainAccess is the single
        // place the rule lives — plan feature or booked module, never both
        // asked separately.
        abort_unless(app(CustomDomainAccess::class)->allowsCustomer($this->customer()), 403);

        $this->domain = (string) ($this->instance()?->custom_domain ?? '');
    }

    private function instance(): ?Instance
    {
        // The shared resolver, not `auth()->user()->customer`: accounts created
        // before the explicit link exists are matched by address, and this page
        // must not be the one place that forgets that.
        return $this->customer()?->instances()
            ->whereIn('status', ['active', 'provisioning', 'cancellation_scheduled'])
            ->latest('id')
            ->first();
    }

    /**
     * Save the domain and mint a fresh token.
     *
     * A NEW token every time the domain changes. Reusing the old one would let
     * a customer who once verified example.com claim any other domain later
     * without touching its DNS — the record for the first domain would still be
     * sitting there, and the value is all that is compared.
     */
    public function save(DomainVerifier $verifier): void
    {
        $this->validate();

        $instance = $this->instance();

        if ($instance === null) {
            return;
        }

        $domain = Str::lower(trim($this->domain));
        $domain = preg_replace('#^https?://#', '', $domain);
        $domain = rtrim((string) $domain, '/.');

        // A hostname, not a URL and not a path. Loose on purpose — the DNS
        // lookup is the real test, and a pattern strict enough to be a
        // specification would reject somebody's perfectly valid domain.
        if ($domain !== '' && ! preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/', $domain)) {
            $this->addError('domain', __('domain.invalid'));

            return;
        }

        // Whether anything is being SERVED under the old domain right now. A
        // change here can only ever take a verified domain away — the new one
        // starts unproven — so this is the one question that decides whether
        // the proxy and Nextcloud have to be told.
        $wasVerified = $instance->domainIsVerified();

        // Clearing the field takes the domain away entirely, including its
        // verification — the instance falls back to its platform address.
        if ($domain === '') {
            $instance->forceFill([
                'custom_domain' => null, 'domain_token' => null,
                'domain_verified_at' => null, 'domain_cert_ok' => false,
                'domain_checked_at' => null,
                'domain_error' => null, 'domain_failures' => 0,
            ])->save();

            $this->stopOrStartServing($instance, $wasVerified);

            $this->domain = '';
            $this->dispatch('notify', message: __('domain.removed'));

            return;
        }

        if ($domain !== $instance->custom_domain) {
            $instance->forceFill([
                'custom_domain' => $domain,
                'domain_token' => $verifier->newToken(),
                'domain_verified_at' => null,
                'domain_cert_ok' => false,
                'domain_checked_at' => null,
                'domain_error' => null,
                'domain_failures' => 0,
            ])->save();

            $this->stopOrStartServing($instance, $wasVerified);
        }

        $this->domain = $domain;
        $this->dispatch('notify', message: __('domain.saved'));
    }

    /**
     * Look now, rather than waiting for tonight.
     *
     * The nightly run is what keeps a domain honest; this button is only so
     * that somebody who has just created the record does not have to wait until
     * the morning to find out whether they typed it correctly.
     */
    public function checkNow(DomainVerifier $verifier): void
    {
        $instance = $this->instance();

        if ($instance === null || blank($instance->custom_domain)) {
            return;
        }

        $wasVerified = $instance->domainIsVerified();
        $wasServed = $instance->domainIsServed();

        $present = $verifier->proofPresent($instance);

        $instance->forceFill([
            'domain_checked_at' => now(),
            'domain_verified_at' => $present ? ($instance->domain_verified_at ?? now()) : null,
            'domain_cert_ok' => $present ? $instance->domain_cert_ok : false,
            'domain_error' => $present ? null : 'missing_txt',
            'domain_failures' => $present ? 0 : $instance->domain_failures + 1,
        ])->save();

        // This button flips verification both ways, so it is a place the
        // address changes and has to be re-applied like any other. It also
        // re-applies a domain that is proven but still answering nothing —
        // which is what somebody is doing here when they add the A record
        // after the TXT one and press it again, and it is the only way they
        // have of asking us to look now rather than at the next change.
        if ($present !== $wasVerified || ($present && ! $wasServed)) {
            app(ReapplyInstanceAddress::class)($instance);
        }

        $this->justChecked = true;
        $this->dispatch('notify', message: __($present ? 'domain.verified' : 'domain.not_found'));
    }

    /**
     * Tell the proxy and Nextcloud that the address has moved.
     *
     * Only when something WAS being served: an unproven domain never reached a
     * router or a trusted_domains entry, so replacing one with another changes
     * nothing that has to be un-done, and starting a run for it would be remote
     * work on a live machine for no reason at all.
     */
    private function stopOrStartServing(Instance $instance, bool $wasVerified): void
    {
        if ($wasVerified) {
            app(ReapplyInstanceAddress::class)($instance);
        }
    }

    public function render()
    {
        $instance = $this->instance();
        $verifier = app(DomainVerifier::class);

        return view('livewire.custom-domain', [
            'instance' => $instance,
            'platformAddress' => $instance?->subdomain
                ? $instance->subdomain.'.'.ProvisioningSettings::dnsZone()
                : null,
            'recordName' => $instance && filled($instance->custom_domain)
                ? $verifier->recordName($instance->custom_domain)
                : null,
            'recordValue' => $instance && filled($instance->domain_token)
                ? $verifier->recordValue($instance)
                : null,
        ]);
    }
}
