<?php

namespace App\Services\Tax;

use App\Models\Customer;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * The Commission's VIES register, over its REST endpoint.
 *
 * REST rather than the SOAP service the documentation leads with: one request,
 * one JSON answer, and no generated client to keep in step. No credentials —
 * VIES is public.
 *
 * Why any of this exists: reverse charge is only lawful against a VAT number
 * that is actually registered in another member state, and until this class was
 * written nothing in the codebase ever checked one. `vat_id_verified_at` was
 * declared on the model, read by TaxTreatment, and set by no code at all — so
 * reverse charge could never trigger and every EU business customer was invoiced
 * Austrian VAT they cannot reclaim at home.
 */
final class ViesVatIdVerifier implements VatIdVerifier
{
    /**
     * The gateway's own errors, as opposed to a verdict on the number.
     *
     * VIES answers `valid: false` for both "this number is not registered" and
     * "I could not ask" — the difference is only in `userError`. Reading the
     * boolean alone would turn every outage into a customer losing their reverse
     * charge, which is exactly the mistake VatIdCheck exists to prevent.
     */
    private const GATEWAY_ERRORS = [
        'SERVICE_UNAVAILABLE', 'MS_UNAVAILABLE', 'MS_MAX_CONCURRENT_REQ',
        'TIMEOUT', 'SERVER_BUSY', 'GLOBAL_MAX_CONCURRENT_REQ',
    ];

    public function check(string $vatId): VatIdCheck
    {
        // Normalised here so no caller has to: the same number arrives as
        // "ATU12345678", "atu 123 456 78" and "ATU12345678 " from three
        // different forms, and the register accepts exactly one of those.
        $normalised = Customer::normaliseVatId($vatId);

        // Shared with the fake, so a test cannot prove something about a
        // simplification that the real register would never have been asked.
        $parts = VatIdShape::split($normalised);

        if ($parts === null) {
            // Not a verdict from the register, but there is nothing to ask about:
            // no member state could hold a number like this.
            return VatIdCheck::invalid();
        }

        try {
            $response = Http::acceptJson()
                // Short on purpose. This runs while somebody waits for a form to
                // save, and VIES answering slowly must cost them seconds, not the
                // request. A timeout is an outage, and an outage changes nothing.
                ->timeout(8)
                ->post($this->endpoint(), [
                    'countryCode' => $parts[1],
                    'vatNumber' => $parts[2],
                ]);
        } catch (Throwable $e) {
            return VatIdCheck::unavailable($e->getMessage());
        }

        if (! $response->successful()) {
            return VatIdCheck::unavailable('http_'.$response->status());
        }

        $error = (string) ($response->json('userError') ?? '');

        if (in_array($error, self::GATEWAY_ERRORS, true)) {
            Log::info('VIES could not answer for a VAT number', ['reason' => $error]);

            return VatIdCheck::unavailable($error);
        }

        if ($response->json('valid') !== true) {
            return VatIdCheck::invalid();
        }

        // The register returns "---" for a company that has asked not to have its
        // details published. That is a registered number with no name attached,
        // not an empty answer, so it is stored as nothing rather than as dashes.
        return VatIdCheck::valid(
            $this->orNull($response->json('name')),
            $this->orNull($response->json('address')),
        );
    }

    private function orNull(mixed $value): ?string
    {
        $value = trim((string) $value);

        return $value === '' || trim($value, '- ') === '' ? null : $value;
    }

    private function endpoint(): string
    {
        return rtrim((string) config(
            'services.vies.endpoint',
            'https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number',
        ), '/');
    }
}
