<?php

declare( strict_types=1 );

namespace Brightfern\Validation;

use DOMDocument;
use Brightfern\Support\Http\HttpClient;

/**
 * Peppol SMP recipient lookup — the "is the buyer actually reachable on the
 * network?" check. Without it you transmit blind and find out days later via a
 * negative Message Level Response.
 *
 * How Peppol addressing works (implemented below):
 *  1. A participant is identified as "iso6523-actorid-upis::{scheme}:{value}",
 *     e.g. "iso6523-actorid-upis::0208:0123456789".
 *  2. The participant's SMP is discovered through the SML (DNS): the hostname is
 *       B-{md5hex(lowercased participant id)}.iso6523-actorid-upis.{SML zone}
 *     where the SML zone is edelivery.tech.ec.europa.eu (production) or
 *     acc.edelivery.tech.ec.europa.eu (test network).
 *  3. GET http://{host}/{url-encoded participant id} returns the participant's
 *     ServiceGroup (the document types they can receive) if registered, or 404
 *     if not.
 *
 * We treat transport/DNS errors as "unknown" (null) rather than "unreachable",
 * so a flaky network never silently blocks a legitimate send.
 */
final class RecipientResolver {

	public const SML_PRODUCTION = 'edelivery.tech.ec.europa.eu';
	public const SML_TEST       = 'acc.edelivery.tech.ec.europa.eu';

	public function __construct(
		private HttpClient $http,
		private string $smlZone = self::SML_PRODUCTION,
	) {}

	public function resolve( string $scheme, string $value ): RecipientResult {
		$scheme = trim( $scheme );
		$value  = trim( $value );

		if ( '' === $scheme || '' === $value ) {
			return new RecipientResult( null, '', '', array(), 'Missing participant scheme or value.' );
		}

		$participantId = sprintf( 'iso6523-actorid-upis::%s:%s', $scheme, $value );
		$canonical     = strtolower( $participantId );
		$host          = sprintf( 'B-%s.iso6523-actorid-upis.%s', md5( $canonical ), $this->smlZone );
		$url           = sprintf( 'http://%s/%s', $host, rawurlencode( $canonical ) );

		$response = $this->http->get( $url );

		if ( $response->transportFailed() ) {
			// DNS or network failure → indeterminate, not "unreachable".
			return new RecipientResult( null, $participantId, $url, array(), $response->error ?: 'SMP lookup failed (network).' );
		}

		if ( 404 === $response->status ) {
			return new RecipientResult( false, $participantId, $url, array(), 'Recipient is not registered on the Peppol network.' );
		}

		if ( ! $response->ok() ) {
			return new RecipientResult( null, $participantId, $url, array(), 'SMP returned HTTP ' . $response->status . '.' );
		}

		$docs = $this->extractDocumentTypes( $response->body );

		return new RecipientResult( true, $participantId, $url, $docs );
	}

	/**
	 * Pull the supported document type identifiers from an SMP ServiceGroup so
	 * we can (later) confirm the recipient accepts Peppol BIS Billing 3.0
	 * specifically, not just "exists".
	 *
	 * @return string[]
	 */
	private function extractDocumentTypes( string $xml ): array {
		if ( '' === trim( $xml ) ) {
			return array();
		}

		$doc  = new DOMDocument();
		$prev = libxml_use_internal_errors( true );
		$ok   = $doc->loadXML( $xml );
		libxml_clear_errors();
		libxml_use_internal_errors( $prev );
		if ( ! $ok ) {
			return array();
		}

		$types = array();
		// SMP namespaces vary by version; match the local element name to be robust.
		foreach ( $doc->getElementsByTagName( '*' ) as $node ) {
			if ( 'DocumentIdentifier' === $node->localName ) {
				$value = trim( $node->textContent );
				if ( '' !== $value ) {
					$types[] = $value;
				}
			}
		}

		return array_values( array_unique( $types ) );
	}

	/** Does the resolved recipient support Peppol BIS Billing 3.0 invoices? */
	public static function supportsBisBilling( RecipientResult $result ): bool {
		foreach ( $result->supportedDocumentTypes as $type ) {
			if ( str_contains( $type, 'peppol.eu:2017:poacc:billing:3.0' ) || str_contains( $type, 'Invoice-2' ) ) {
				return true;
			}
		}
		return false;
	}
}
