<?php

declare(strict_types=1);

namespace ImovelZapAi\Publishing\Application\Services;

use DOMDocument;
use DOMXPath;
use ImovelZapAi\Publishing\Application\Data\XmlValidationResult;
use ImovelZapAi\Publishing\Domain\Feeds\VrSync\ListingDataFeed;
use ImovelZapAi\Publishing\Domain\Feeds\VrSync\ListingDataFeedListing;
use ImovelZapAi\Publishing\Domain\Feeds\VrSync\ListingDataFeedMediaItem;
use ImovelZapAi\Publishing\Domain\Feeds\VrSync\VrSyncEnumerations;

/**
 * Valida feeds VRSync antes da publicação — nunca persistir XML inválido.
 */
final class ListingDataFeedXmlValidator
{
    public function validateFeed(ListingDataFeed $feed): XmlValidationResult
    {
        $errors = [];

        if (trim($feed->header->provider) === '') {
            $errors[] = 'Header.Provider é obrigatório.';
        }

        if (trim($feed->header->email) === '' || ! filter_var($feed->header->email, FILTER_VALIDATE_EMAIL)) {
            $errors[] = 'Header.Email inválido ou ausente.';
        }

        if (trim($feed->header->publishDate) === '') {
            $errors[] = 'Header.PublishDate é obrigatório.';
        }

        if ($feed->header->logo !== null && $feed->header->logo !== '' && ! $this->isValidUrl($feed->header->logo)) {
            $errors[] = 'Header.Logo deve ser uma URL válida.';
        }

        if ($feed->listings === []) {
            $errors[] = 'Listings deve conter ao menos um imóvel.';
        }

        foreach ($feed->listings as $index => $listing) {
            $errors = array_merge($errors, $this->validateListing($listing, $index));
        }

        return new XmlValidationResult($errors);
    }

    public function validateXml(string $xml): XmlValidationResult
    {
        $errors = [];

        if (! str_contains($xml, 'encoding="UTF-8"')) {
            $errors[] = 'XML deve declarar encoding UTF-8.';
        }

        if (! mb_check_encoding($xml, 'UTF-8')) {
            $errors[] = 'Conteúdo XML não está em UTF-8 válido.';
        }

        $document = new DOMDocument();
        $previous = libxml_use_internal_errors(true);

        if (! $document->loadXML($xml, LIBXML_NONET)) {
            $errors[] = 'XML malformado: '.$this->firstLibXmlError();
            libxml_clear_errors();
            libxml_use_internal_errors($previous);

            return new XmlValidationResult($errors);
        }

        libxml_clear_errors();

        $errors = array_merge($errors, $this->validateDom($document));
        $errors = array_merge($errors, $this->validateXsd($document));

        libxml_use_internal_errors($previous);

        return new XmlValidationResult($errors);
    }

    /**
     * @return list<string>
     */
    private function validateListing(ListingDataFeedListing $listing, int $index): array
    {
        $errors = [];
        $prefix = 'Listing #'.($index + 1);

        if (trim($listing->listingId) === '') {
            $errors[] = "{$prefix}: ListingID é obrigatório.";
        }

        if (trim($listing->title) === '') {
            $errors[] = "{$prefix}: Title é obrigatório.";
        }

        if (! VrSyncEnumerations::isTransactionType($listing->transactionType)) {
            $errors[] = "{$prefix}: TransactionType inválido.";
        }

        if ($listing->details->listPrice === null || (float) $listing->details->listPrice <= 0) {
            $errors[] = "{$prefix}: ListPrice é obrigatório.";
        }

        if (! VrSyncEnumerations::isCurrency($listing->details->currency)) {
            $errors[] = "{$prefix}: Currency inválida.";
        }

        if (trim($listing->details->propertyType) === '') {
            $errors[] = "{$prefix}: PropertyType é obrigatório.";
        }

        if (trim($listing->details->description) === '') {
            $errors[] = "{$prefix}: Description é obrigatória.";
        }

        if ($listing->detailUrl !== null && $listing->detailUrl !== '' && ! $this->isValidUrl($listing->detailUrl)) {
            $errors[] = "{$prefix}: DetailUrl inválida.";
        }

        $errors = array_merge($errors, $this->validateCoordinates(
            $listing->location->latitude,
            $listing->location->longitude,
            $prefix,
        ));

        foreach ($listing->media as $mediaIndex => $media) {
            $errors = array_merge($errors, $this->validateMedia($media, $prefix, $mediaIndex));
        }

        return $errors;
    }

    /**
     * @return list<string>
     */
    private function validateMedia(ListingDataFeedMediaItem $media, string $prefix, int $mediaIndex): array
    {
        $label = "{$prefix} Media #".($mediaIndex + 1);

        if (! $this->isValidUrl($media->url)) {
            return ["{$label}: URL de imagem inválida."];
        }

        if (! str_starts_with(strtolower($media->url), 'http://') && ! str_starts_with(strtolower($media->url), 'https://')) {
            return ["{$label}: imagem deve usar HTTP ou HTTPS."];
        }

        return [];
    }

    /**
     * @return list<string>
     */
    private function validateCoordinates(?string $latitude, ?string $longitude, string $prefix): array
    {
        $latitude = $latitude === '' ? null : $latitude;
        $longitude = $longitude === '' ? null : $longitude;

        if ($latitude === null && $longitude === null) {
            return [];
        }

        if ($latitude === null || $longitude === null) {
            return ["{$prefix}: Latitude e Longitude devem ser informadas em par."];
        }

        if (! is_numeric($latitude) || ! is_numeric($longitude)) {
            return ["{$prefix}: coordenadas devem ser numéricas."];
        }

        $lat = (float) $latitude;
        $lng = (float) $longitude;

        if ($lat < -90 || $lat > 90) {
            return ["{$prefix}: Latitude fora do intervalo (-90 a 90)."];
        }

        if ($lng < -180 || $lng > 180) {
            return ["{$prefix}: Longitude fora do intervalo (-180 a 180)."];
        }

        return [];
    }

    /**
     * @return list<string>
     */
    private function validateDom(DOMDocument $document): array
    {
        $errors = [];
        $xpath = new DOMXPath($document);
        $xpath->registerNamespace('vr', ListingDataFeed::NAMESPACE);

        if ($xpath->query('//vr:ListingDataFeed')->length === 0) {
            $errors[] = 'Elemento ListingDataFeed ausente ou namespace inválido.';
        }

        if ($xpath->query('//vr:Header/vr:Provider')->length === 0) {
            $errors[] = 'Header.Provider ausente no XML.';
        }

        if ($xpath->query('//vr:Listing')->length === 0) {
            $errors[] = 'Nenhum Listing encontrado no XML.';
        }

        foreach ($xpath->query('//vr:Listing') ?: [] as $index => $listingNode) {
            $listingLabel = 'Listing XML #'.($index + 1);

            if ($this->nodeText($xpath, 'vr:ListingID', $listingNode) === '') {
                $errors[] = "{$listingLabel}: ListingID ausente.";
            }

            if ($this->nodeText($xpath, 'vr:Title', $listingNode) === '') {
                $errors[] = "{$listingLabel}: Title ausente.";
            }

            $transactionType = $this->nodeText($xpath, 'vr:TransactionType', $listingNode);

            if ($transactionType === '' || ! VrSyncEnumerations::isTransactionType($transactionType)) {
                $errors[] = "{$listingLabel}: TransactionType inválido.";
            }

            $currency = $this->attributeValue($xpath, 'vr:Details/vr:ListPrice', 'currency', $listingNode) ?? 'BRL';

            if (! VrSyncEnumerations::isCurrency($currency)) {
                $errors[] = "{$listingLabel}: Currency inválida.";
            }

            $detailUrl = $this->nodeText($xpath, 'vr:DetailUrl', $listingNode);

            if ($detailUrl !== '' && ! $this->isValidUrl($detailUrl)) {
                $errors[] = "{$listingLabel}: DetailUrl inválida.";
            }

            $errors = array_merge($errors, $this->validateCoordinates(
                $this->optionalNodeText($xpath, 'vr:Location/vr:Latitude', $listingNode),
                $this->optionalNodeText($xpath, 'vr:Location/vr:Longitude', $listingNode),
                $listingLabel,
            ));

            foreach ($xpath->query('vr:Media/vr:Item', $listingNode) ?: [] as $mediaIndex => $mediaNode) {
                $url = trim((string) $mediaNode->textContent);

                if (! $this->isValidUrl($url)) {
                    $errors[] = "{$listingLabel} Media #".($mediaIndex + 1).': URL de imagem inválida.';
                }
            }
        }

        return $errors;
    }

    /**
     * @return list<string>
     */
    private function validateXsd(DOMDocument $document): array
    {
        if (! (bool) config('publishing.xml.validate_xsd', true)) {
            return [];
        }

        $path = $this->xsdPath();

        if ($path === null) {
            return [];
        }

        $previous = libxml_use_internal_errors(true);

        if (! $document->schemaValidate($path)) {
            $message = $this->firstLibXmlError() ?? 'Falha na validação XSD.';
            libxml_clear_errors();
            libxml_use_internal_errors($previous);

            return ['XSD: '.$message];
        }

        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        return [];
    }

    private function xsdPath(): ?string
    {
        $configured = config('publishing.xml.xsd_path');

        if (is_string($configured) && $configured !== '' && is_file($configured)) {
            return $configured;
        }

        $default = dirname(__DIR__, 2).'/resources/schemas/vrsync-minimal.xsd';

        return is_file($default) ? $default : null;
    }

    private function isValidUrl(string $url): bool
    {
        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }

    private function firstLibXmlError(): ?string
    {
        $error = libxml_get_last_error();

        return $error !== false ? trim($error->message) : null;
    }

    private function nodeText(DOMXPath $xpath, string $query, \DOMNode $context): string
    {
        $node = $xpath->query($query, $context)?->item(0);

        return $node !== null ? trim((string) $node->textContent) : '';
    }

    private function optionalNodeText(DOMXPath $xpath, string $query, \DOMNode $context): ?string
    {
        $value = $this->nodeText($xpath, $query, $context);

        return $value === '' ? null : $value;
    }

    private function attributeValue(DOMXPath $xpath, string $query, string $attribute, \DOMNode $context): ?string
    {
        $node = $xpath->query($query, $context)?->item(0);

        if (! $node instanceof \DOMElement || ! $node->hasAttribute($attribute)) {
            return null;
        }

        return trim($node->getAttribute($attribute));
    }
}
