<?php

namespace App\Services\Domain;

use App\Models\ClientDomain;
use App\Models\Setting;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
 * PRC-322 / A-7 — IDN (Internationalized Domain Name) backend reject.
 *
 * Rejects non-ASCII (e.g. `müller.com`) AND Punycode (`xn--mller-kva.com`)
 * input when `allow_idn_domains=false`. Centralised so every basket /
 * checkout / domain-search entry point applies the same rule.
 *
 * Callers:
 *   - DomainController::search + check
 *   - BasketController::addItem (domain_name field)
 *   - CheckoutController::process (basket items, defense-in-depth)
 *
 * The frontend (window.domainSettings.allowIdn) hides the IDN input
 * client-side; this guard is the authoritative server-side gate.
 */
final class DomainPolicyGuard
{
    /**
     * Throws 422 when IDN is disabled and the domain is non-ASCII
     * or carries a Punycode `xn--` label. No-op when IDN is enabled.
     */
    public static function assertIdnAllowed(?string $domain): void
    {
        if ($domain === null || $domain === '') {
            return;
        }
        if ((bool) Setting::getValue('allow_idn_domains', false)) {
            return;
        }
        if (self::looksIdn($domain)) {
            throw new HttpException(422, __('store.domain_idn_not_allowed'));
        }
    }

    /**
     * PRC-323 / B-6 — Master gate for domain renewal flow.
     *
     * Authoritative read of `enable_renewal_orders` (canonical default `true`).
     * Use in client-area renewal pages + lifecycle invoice generation +
     * any future renewal entry point. Single source of truth so the
     * `Setting::getValue('enable_renewal_orders', ?)` default never drifts.
     */
    public static function renewalEnabled(): bool
    {
        return (bool) Setting::getValue('enable_renewal_orders', true);
    }

    /**
     * Throws 403 when renewal is disabled. Used on manual-renew page +
     * AJAX renewal-options endpoint to keep messaging consistent.
     */
    public static function assertRenewalEnabled(): void
    {
        if (! self::renewalEnabled()) {
            throw new HttpException(403, __('store.domain_renewal_disabled'));
        }
    }

    /**
     * PRC-643 — `block_existing_domains` tek karar noktası.
     *
     * Ayar açıkken sistemde aktif kayıtlı (client_domains) bir domain için
     * verilen kararı döner. Owner-agnostic VE action-agnostic: domain zaten
     * sistemdeyse register / transfer / use (own-domain) ayrımı yapılmadan
     * engellenir. (Soft-delete edilmiş — iptal sipariş — kayıtlar Eloquent
     * global scope ile zaten hariç.)
     *
     * Çağıranlar (her biri kendi response şeklini formatlar):
     *   - DomainController::check (structured JSON: already_in_system)
     *   - BasketController::addItem (domain-only + product+domain path)
     */
    public static function isExistingDomainBlocked(?string $domain): bool
    {
        if ($domain === null || $domain === '') {
            return false;
        }
        if (! (bool) Setting::getValue('block_existing_domains', false)) {
            return false;
        }

        return ClientDomain::where('domain', $domain)
            ->whereNotIn('status', ['cancelled', 'terminated', 'expired'])
            ->exists();
    }

    /**
     * PRC-645 — bir domain için kalan azami yenileme yılı.
     * En büyük tam yıl P: expire_date + P yıl <= now + tld.max_years (yoksa 10).
     * Takvim-yılı tabanlı (Carbon addYears, leap-safe). 0 = uzatılamaz.
     */
    public static function maxRenewalYears(\App\Models\ClientDomain $domain): int
    {
        $maxYears = ($domain->tld?->max_years) ?: 10;
        if (! $domain->expire_date) {
            return 0;
        }

        // En büyük tam yıl P: expire_date + P yıl <= now + max_years (takvim-doğru, leap-safe).
        $limit = now()->addYears($maxYears);
        $cap = 0;
        for ($p = 1; $p <= $maxYears; $p++) {
            if ($domain->expire_date->copy()->addYears($p)->lessThanOrEqualTo($limit)) {
                $cap = $p;
            } else {
                break;
            }
        }

        return $cap;
    }

    /**
     * True if the input is non-ASCII or contains a Punycode `xn--` label.
     * Lower-cased + label-by-label scan so `Foo.XN--mller-kva.COM` matches.
     */
    public static function looksIdn(string $domain): bool
    {
        // Non-ASCII codepoint anywhere
        if (preg_match('/[^\x00-\x7F]/u', $domain) === 1) {
            return true;
        }
        // Punycode `xn--` label prefix (case-insensitive, label-anchored)
        foreach (explode('.', strtolower($domain)) as $label) {
            if (str_starts_with($label, 'xn--')) {
                return true;
            }
        }
        return false;
    }
}
