<?php

namespace App\Console\Commands;

use App\Actions\ReapplyInstanceAddress;
use App\Models\Instance;
use App\Services\Domains\DomainVerifier;
use Illuminate\Console\Command;

/**
 * Re-reads every custom domain's proof, nightly.
 *
 * Asked for in exactly these words: the token must be checked again and again,
 * so that nobody puts it in, passes the check, and takes it straight back out.
 * At night, because a domain that is withdrawn is a domain that stops being
 * served, and that is not a thing to do to somebody at eleven in the morning.
 *
 * Three consecutive misses before a verified domain is withdrawn. One failed
 * lookup is a nameserver having a bad minute; taking a working Nextcloud
 * offline over a hiccup would be far worse than the hole it closes.
 */
class VerifyCustomDomains extends Command
{
    protected $signature = 'clupilot:verify-domains {--instance= : one instance uuid, for checking on demand}';

    protected $description = 'Re-read the DNS proof for every custom domain and withdraw the ones that lost it';

    public function handle(DomainVerifier $verifier, ReapplyInstanceAddress $reapply): int
    {
        $query = Instance::query()->whereNotNull('custom_domain')->whereNotNull('domain_token');

        if ($uuid = $this->option('instance')) {
            $query->where('uuid', $uuid);
        }

        $checked = 0;
        $withdrawn = 0;

        foreach ($query->cursor() as $instance) {
            $checked++;
            $present = $verifier->proofPresent($instance);

            // What was true before this check, so the CHANGE can be acted on
            // rather than the state. Re-applying an address is remote work on a
            // live machine; doing it nightly for every domain that is simply
            // still fine would be a hundred pointless runs a night.
            $wasVerified = $instance->domainIsVerified();

            if ($present) {
                $instance->forceFill([
                    'domain_verified_at' => $instance->domain_verified_at ?? now(),
                    'domain_checked_at' => now(),
                    'domain_error' => null,
                    'domain_failures' => 0,
                ])->save();

                // A domain that has just become provable is a domain nothing is
                // serving yet: the router was written without it and Nextcloud
                // does not trust it. This is where it becomes an address.
                if (! $wasVerified) {
                    $reapply($instance);
                }

                continue;
            }

            $failures = $instance->domain_failures + 1;

            // Withdrawal clears verified_at, which is the single flag every
            // consumer reads. Nothing else is deleted: the domain and its token
            // stay on the row, so the customer sees what is wrong and can put
            // the record back rather than starting over.
            $withdraw = $instance->domain_verified_at !== null
                && $failures >= DomainVerifier::FAILURES_BEFORE_WITHDRAWAL;

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

            if ($withdraw) {
                $withdrawn++;

                // The other half of the flip, and the one that actually takes
                // the domain away: the router stops carrying it and Nextcloud
                // stops trusting it. Clearing verified_at alone only stops us
                // TELLING people about it.
                $reapply($instance);

                $this->warn("Withdrew {$instance->custom_domain}: proof missing on {$failures} consecutive checks.");
            }
        }

        $this->info("Checked {$checked} domain(s), withdrew {$withdrawn}.");

        return self::SUCCESS;
    }
}
