<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\SupplierLead;
use App\Services\DaData\DaDataBudgetGuard;
use App\Services\DaData\DaDataException;
use App\Services\DaData\DaDataPhoneClient;
use App\Services\DaData\DaDataPhoneResponse;
use App\Services\Dto\RegionResolution;
use App\Support\DaDataRegionMap;
use Illuminate\Contracts\Cache\Repository as CacheRepository;

/**
 * Оркестратор резолва региона лида: DaData → Россвязь → tag-fallback (spec §3.3, §3.4).
 *
 * Каскад решений по qc:
 *   qc 0/3 + region не-ambiguous и маппится → source=dadata;
 *   qc 0/3 + region ambiguous/null/не-маппится → Россвязь (оператор от DaData сохраняем, §3.4.1);
 *   qc 1 / таймаут / 5xx / бюджет исчерпан → Россвязь;
 *   qc 2/7 → tag (Россвязь бессмысленна).
 * Если ничего не дало код → source=tag (или unknown при пустом теге).
 *
 * Кэш по sha256(phone) (без сырого номера в ключе/значении, §7.3). Persistent-idempotency
 * по supplier_leads.resolved_subject_code (защита от двойной оплаты DaData на retry, §3.11).
 * Feature-flag services.dadata.enabled=false → сразу tag (текущее поведение, §6.5).
 */
class LeadRegionResolver
{
    public function __construct(
        private readonly DaDataPhoneClient $dadataClient,
        private readonly DaDataBudgetGuard $budgetGuard,
        private readonly RossvyazPrefixLookup $rossvyazLookup,
        private readonly RegionTagResolver $tagResolver,
        private readonly CacheRepository $cache,
    ) {}

    public function resolve(SupplierLead $lead): RegionResolution
    {
        // Feature-flag: резолвер выключен → текущее tag-поведение.
        if (! (bool) config('services.dadata.enabled', false)) {
            return $this->tagFallback($lead, provider: null, qc: null, masked: null, start: null);
        }

        // Persistent-idempotency: уже резолвили на предыдущем try → без DaData.
        if ($lead->resolved_subject_code !== null || $lead->region_source !== null) {
            return RegionResolution::fromSupplierLead($lead);
        }

        $phone = (string) $lead->phone;
        if (! preg_match('/^7\d{10}$/', $phone)) {
            return $this->tagFallback($lead, provider: null, qc: null, masked: null, start: null);
        }

        $cacheKey = $this->cacheKey($phone);
        $cached = $this->cache->get($cacheKey);
        if ($cached instanceof RegionResolution) {
            return $cached->withCacheHit(true);
        }

        $resolution = $this->doResolve($lead, $phone);

        $ttlDays = max(1, (int) config('services.dadata.cache_ttl_days', 30));
        $this->cache->put($cacheKey, $resolution->forCache(), now()->addDays($ttlDays));

        return $resolution;
    }

    private function doResolve(SupplierLead $lead, string $phone): RegionResolution
    {
        $start = microtime(true);
        $provider = null;
        $qc = null;
        $masked = null;

        // 1. DaData (под дневным бюджетом).
        if ($this->budgetGuard->canSpend()) {
            try {
                $dadata = $this->dadataClient->cleanPhone($phone);
                $this->budgetGuard->recordSpend((int) config('services.dadata.call_cost_kopecks', 60));
                $qc = $dadata->qc;
                $provider = $dadata->provider;
                $masked = $this->maskResponse($dadata);

                if (in_array($dadata->qc, [0, 3], true)) {
                    $region = (string) ($dadata->region ?? '');
                    if ($region !== '' && ! DaDataRegionMap::isAmbiguous($region)) {
                        $code = DaDataRegionMap::toSubjectCode($region);
                        if ($code !== null) {
                            return RegionResolution::make(
                                $code, 'dadata',
                                operator: $provider, qc: $qc,
                                dadataMasked: $masked, durationMs: $this->ms($start),
                            );
                        }
                        // qc=0/3, но регион не маппится → страховка Россвязью.
                    }
                    // ambiguous / region=null / не-маппится → Россвязь (provider от DaData сохраняем).
                } elseif ($dadata->qc === 2 || $dadata->qc === 7) {
                    // Мусор / иностранец → Россвязь не поможет, сразу tag.
                    return $this->tagFallback($lead, $provider, $qc, $masked, $start);
                }
                // qc=1 → Россвязь.
            } catch (DaDataException) {
                // Сеть / таймаут / 5xx → деградируем на Россвязь, не падаем.
            }
        }

        // 2. Россвязь.
        $rossvyaz = $this->rossvyazLookup->find($phone);
        if ($rossvyaz !== null) {
            $code = $rossvyaz->subjectCode ?? DaDataRegionMap::toSubjectCode($rossvyaz->region);
            if ($code !== null) {
                return RegionResolution::make(
                    $code, 'rossvyaz',
                    operator: $provider ?? $rossvyaz->operator, // оператор от DaData приоритетнее (MNP)
                    qc: $qc, dadataMasked: $masked,
                    durationMs: $this->ms($start), rossvyazMatched: true,
                );
            }
        }

        // 3. Tag-fallback.
        return $this->tagFallback($lead, $provider, $qc, $masked, $start);
    }

    private function tagFallback(SupplierLead $lead, ?string $provider, ?int $qc, ?array $masked, ?float $start): RegionResolution
    {
        $tag = (string) (is_array($lead->raw_payload) ? ($lead->raw_payload['tag'] ?? '') : '');
        $tagCode = $this->tagResolver->resolve($tag);

        return RegionResolution::make(
            $tagCode,
            $tagCode !== null ? 'tag' : 'unknown',
            operator: $provider,
            qc: $qc,
            dadataMasked: $masked,
            durationMs: $start !== null ? $this->ms($start) : null,
        );
    }

    private function cacheKey(string $phone): string
    {
        return 'phone-region:'.hash('sha256', $phone);
    }

    private function ms(float $start): int
    {
        return (int) round((microtime(true) - $start) * 1000);
    }

    /**
     * @return array<string, mixed> сырой ответ DaData с маскированным телефоном (§7.1)
     */
    private function maskResponse(DaDataPhoneResponse $response): array
    {
        $raw = $response->raw;
        if (isset($raw['phone']) && is_string($raw['phone'])) {
            $raw['phone'] = $this->maskPhone($raw['phone']);
        }

        return $raw;
    }

    private function maskPhone(string $phone): string
    {
        $digits = preg_replace('/\D+/', '', $phone) ?? '';
        if (strlen($digits) < 8) {
            return '***';
        }

        return substr($digits, 0, 4).'***'.substr($digits, -4);
    }
}
