<?php

namespace App\Services\Dns;

use App\Support\ProvisioningSettings;
use Illuminate\Support\Str;
use RuntimeException;

/**
 * How an fqdn becomes an address in the Hetzner Cloud DNS API, in one place.
 *
 * There are no individual records any more — an RRSet is addressed by
 * `{name}/{type}`, and that pair IS its id (cloud.spec.json: "The ID is
 * composed of the name and type, joined with a slash"). Everything this app
 * stores in `hosts.dns_record_id`, `dns_records.record_id` and the
 * `dns_record_id` run resource is such a pair.
 *
 * Shared by HttpHetznerDnsClient and FakeHetznerDnsClient on purpose. A fake
 * that hands back `rec-1` where the real client hands back `berger/A` lets
 * every test that purges a host go green over a teardown that would throw in
 * production — the fake has to speak the same dialect or it proves nothing.
 */
final class RrsetId
{
    /** The configured zone, lower case and without a trailing dot. */
    public static function zone(): string
    {
        $zone = Str::lower(rtrim(trim((string) ProvisioningSettings::dnsZone()), '.'));

        return $zone !== '' ? $zone : throw new RuntimeException('No DNS zone configured');
    }

    /** `{name}/{TYPE}` for an fqdn inside the configured zone. */
    public static function for(string $fqdn, string $type): string
    {
        return self::relativeName($fqdn).'/'.Str::upper($type);
    }

    /**
     * The zone-relative name Hetzner wants: lower case, no trailing dot, and
     * NOT ending in the zone name.
     *
     * The last part is a trap with no floor under it. Measured against the live
     * account on 31 July 2026: a POST carrying `berger.clupilot.cloud` as the
     * name of a set in zone `clupilot.cloud` comes back 201 — the API does not
     * reject it, it simply creates `berger.clupilot.cloud.clupilot.cloud`.
     * Nothing downstream notices either; the run goes green and the customer's
     * cloud has an address that resolves nowhere.
     *
     * Same reason an fqdn from outside the zone is refused rather than sent: it
     * would be accepted just as quietly, under a name nobody looks for.
     */
    public static function relativeName(string $fqdn): string
    {
        $zone = self::zone();
        $name = rtrim(Str::lower(trim($fqdn)), '.');

        if ($name === $zone) {
            return '@';
        }

        if (! str_ends_with($name, '.'.$zone)) {
            throw new RuntimeException("{$fqdn} does not sit in DNS zone {$zone}");
        }

        return substr($name, 0, -strlen('.'.$zone));
    }

    /** Is this the cloud API's `{name}/{type}`, or an id from the old record API? */
    public static function isRrsetId(string $id): bool
    {
        $parts = explode('/', trim($id));

        return count($parts) === 2 && $parts[0] !== '' && $parts[1] !== '';
    }

    /**
     * `{name}/{type}` back into its two halves.
     *
     * An id from the old record API has no slash in it and cannot address
     * anything here: passing one in would build `/rrsets/rec-123/` with an
     * empty type — a call that goes somewhere else and can still come back
     * looking like a success.
     *
     * This used to claim no such id existed anywhere, on the strength of three
     * tables being empty on the DEVELOPMENT database. That proved nothing about
     * any other installation, and the live server is one (Codex review
     * 2026-07-31, P1). Callers therefore ask isRrsetId() first and treat a
     * legacy id as a leak to report, not as a reason to die — see
     * HttpHetznerDnsClient::deleteRecord() and LegacyRecordIds.
     *
     * @return array{0: string, 1: string}
     */
    public static function split(string $id): array
    {
        $parts = explode('/', trim($id));

        if (count($parts) !== 2 || $parts[0] === '' || $parts[1] === '') {
            throw new RuntimeException(
                "Not a Hetzner RRSet id: {$id} — expected {name}/{type}, e.g. berger/A"
            );
        }

        return [$parts[0], Str::upper($parts[1])];
    }
}
