<?php

declare(strict_types=1);

namespace ImovelZapAi\Importing\Application\Services;

use ImovelZapAi\Importing\Application\Data\ParsedMedia;
use ImovelZapAi\Importing\Application\Data\ParsedProperty;
use ImovelZapAi\Importing\Application\Data\ParseIssue;
use ImovelZapAi\Importing\Application\Data\ParseResult;
use ImovelZapAi\Importing\Application\Ports\VrSyncParserInterface;
use ImovelZapAi\Importing\Domain\Enums\ImportIssueSeverity;
use ImovelZapAi\Importing\Domain\Exceptions\ImportParseException;
use XMLReader;

final class XmlReaderVrSyncParser implements VrSyncParserInterface
{
    public function parse(string $absolutePath): ParseResult
    {
        if (! is_file($absolutePath) || ! is_readable($absolutePath)) {
            throw ImportParseException::malformed('arquivo inacessível');
        }

        $this->rejectUnsafeProlog($absolutePath);

        $maxListings = (int) config('importing.parser.max_listings', 5000);
        $maxMedia = (int) config('importing.parser.max_media_per_listing', 50);
        $maxText = (int) config('importing.parser.max_text_length', 20000);
        $maxErrors = (int) config('importing.parser.max_errors', 500);

        $reader = new XMLReader;
        $opened = @$reader->open($absolutePath, 'UTF-8', LIBXML_NONET | LIBXML_COMPACT | LIBXML_NOERROR | LIBXML_NOWARNING);

        // @codeCoverageIgnoreStart
        if ($opened !== true) {
            throw ImportParseException::malformed('não foi possível abrir o XML');
        }
        // @codeCoverageIgnoreEnd

        $properties = [];
        $issues = [];
        $index = 0;
        $bytesRead = (int) filesize($absolutePath);

        try {
            while ($reader->read()) {
                if ($reader->nodeType === XMLReader::ELEMENT && $this->localName($reader) === 'Listing') {
                    if ($index >= $maxListings) {
                        throw ImportParseException::limitExceeded("max_listings={$maxListings}");
                    }

                    $listingXml = $reader->readOuterXml();
                    $parsed = $this->parseListing($listingXml, $index, $maxMedia, $maxText, $issues);

                    if ($parsed !== null) {
                        $properties[] = $parsed;
                    }

                    $index++;

                    if (count($issues) >= $maxErrors) {
                        $issues[] = new ParseIssue(
                            ImportIssueSeverity::Error,
                            'too_many_issues',
                            "Limite de erros de parse atingido ({$maxErrors}).",
                        );
                        break;
                    }
                }
            }
        } finally {
            $reader->close();
        }

        if ($properties === [] && $issues === []) {
            $issues[] = new ParseIssue(
                ImportIssueSeverity::Error,
                'no_listings',
                'Nenhum Listing encontrado no XML VRSync.',
            );
        }

        return new ParseResult($properties, $issues, $bytesRead);
    }

    private function rejectUnsafeProlog(string $absolutePath): void
    {
        $handle = fopen($absolutePath, 'rb');
        // @codeCoverageIgnoreStart
        if ($handle === false) {
            throw ImportParseException::malformed('arquivo inacessível');
        }
        // @codeCoverageIgnoreEnd

        try {
            $head = (string) fread($handle, 8192);
        } finally {
            fclose($handle);
        }

        if (preg_match('/<!DOCTYPE/i', $head) === 1) {
            throw ImportParseException::unsafeXml('DOCTYPE não permitido');
        }

        if (preg_match('/<!ENTITY/i', $head) === 1) {
            throw ImportParseException::unsafeXml('ENTITY não permitido');
        }
    }

    /**
     * @param  list<ParseIssue>  $issues
     */
    private function parseListing(
        string $listingXml,
        int $index,
        int $maxMedia,
        int $maxText,
        array &$issues,
    ): ?ParsedProperty {
        $previous = libxml_use_internal_errors(true);
        $document = simplexml_load_string($listingXml, 'SimpleXMLElement', LIBXML_NONET | LIBXML_NOENT);
        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        // @codeCoverageIgnoreStart
        if ($document === false) {
            $issues[] = new ParseIssue(
                ImportIssueSeverity::Error,
                'malformed_listing',
                'Listing XML inválido.',
                listingIndex: $index,
                path: 'Listing',
            );

            return null;
        }
        // @codeCoverageIgnoreEnd

        $raw = [];
        $text = static function (string $name) use ($document, &$raw, $maxText): ?string {
            $aliases = match ($name) {
                'DetailViewUrl' => ['DetailViewUrl', 'DetailUrl'],
                default => [$name],
            };

            foreach ($aliases as $alias) {
                if (! isset($document->{$alias})) {
                    continue;
                }

                $value = trim((string) $document->{$alias});
                if ($value === '') {
                    continue;
                }

                if (mb_strlen($value) > $maxText) {
                    $value = mb_substr($value, 0, $maxText);
                }

                $raw[$alias] = $value;

                return $value;
            }

            return null;
        };

        $media = [];
        if (isset($document->Media->Item)) {
            $order = 0;
            foreach ($document->Media->Item as $item) {
                if ($order >= $maxMedia) {
                    $issues[] = new ParseIssue(
                        ImportIssueSeverity::Warning,
                        'media_limit',
                        "Listing excedeu o limite de mídia ({$maxMedia}).",
                        externalId: $text('ListingID'),
                        listingIndex: $index,
                        path: 'Listing/Media',
                    );
                    break;
                }

                $url = trim((string) ($item['url'] ?? $item));
                if ($url === '') {
                    continue;
                }

                $caption = isset($item['caption']) ? trim((string) $item['caption']) : null;
                $primary = strtolower((string) ($item['primary'] ?? '')) === 'true';

                $media[] = new ParsedMedia($url, $caption !== '' ? $caption : null, $order, $primary);
                $order++;
            }
        }

        $externalId = $text('ListingID');
        $title = $text('Title');
        $transactionType = $text('TransactionType');
        $propertyType = $text('PropertyType');
        $publicationType = $text('PublicationType');
        $listPrice = $text('ListPrice');
        $rentalPrice = $text('RentalPrice');
        $description = $text('Description');
        $detailUrl = $text('DetailViewUrl');

        $location = $document->Location ?? null;
        $country = $location !== null ? trim((string) ($location->Country ?? '')) ?: null : null;
        $state = $location !== null ? trim((string) ($location->State ?? '')) ?: null : null;
        $city = $location !== null ? trim((string) ($location->City ?? '')) ?: null : null;
        $neighborhood = null;
        $address = null;
        $streetNumber = null;
        $complement = null;
        $postalCode = null;
        $latitude = null;
        $longitude = null;

        if ($location !== null) {
            foreach (['Neighborhood', 'Zone'] as $alias) {
                if (isset($location->{$alias}) && trim((string) $location->{$alias}) !== '') {
                    $neighborhood = trim((string) $location->{$alias});
                    $raw["Location/{$alias}"] = $neighborhood;
                    break;
                }
            }

            $address = isset($location->Address) ? trim((string) $location->Address) ?: null : null;
            if ($address !== null) {
                $raw['Location/Address'] = $address;
            }

            foreach (['StreetNumber', 'Number'] as $alias) {
                if (isset($location->{$alias}) && trim((string) $location->{$alias}) !== '') {
                    $streetNumber = trim((string) $location->{$alias});
                    $raw["Location/{$alias}"] = $streetNumber;
                    break;
                }
            }

            $complement = isset($location->Complement) ? trim((string) $location->Complement) ?: null : null;
            $postalCode = isset($location->PostalCode) ? trim((string) $location->PostalCode) ?: null : null;
            $latitude = isset($location->Latitude) ? trim((string) $location->Latitude) ?: null : null;
            $longitude = isset($location->Longitude) ? trim((string) $location->Longitude) ?: null : null;

            if ($country !== null) {
                $raw['Location/Country'] = $country;
            }
            if ($state !== null) {
                $raw['Location/State'] = $state;
            }
            if ($city !== null) {
                $raw['Location/City'] = $city;
            }
            if ($complement !== null) {
                $raw['Location/Complement'] = $complement;
            }
            if ($postalCode !== null) {
                $raw['Location/PostalCode'] = $postalCode;
            }
            if ($latitude !== null) {
                $raw['Location/Latitude'] = $latitude;
            }
            if ($longitude !== null) {
                $raw['Location/Longitude'] = $longitude;
            }
        }

        $details = $document->Details ?? null;
        $bedrooms = $details !== null && isset($details->Bedrooms) ? trim((string) $details->Bedrooms) ?: null : null;
        $bathrooms = $details !== null && isset($details->Bathrooms) ? trim((string) $details->Bathrooms) ?: null : null;
        $suites = $details !== null && isset($details->Suites) ? trim((string) $details->Suites) ?: null : null;
        $garage = $details !== null && isset($details->Garage) ? trim((string) $details->Garage) ?: null : null;
        $livingArea = $details !== null && isset($details->LivingArea) ? trim((string) $details->LivingArea) ?: null : null;
        $lotArea = $details !== null && isset($details->LotArea) ? trim((string) $details->LotArea) ?: null : null;

        foreach ([
            'Bedrooms' => $bedrooms,
            'Bathrooms' => $bathrooms,
            'Suites' => $suites,
            'Garage' => $garage,
            'LivingArea' => $livingArea,
            'LotArea' => $lotArea,
        ] as $key => $value) {
            if ($value !== null) {
                $raw["Details/{$key}"] = $value;
            }
        }

        return new ParsedProperty(
            index: $index,
            externalId: $externalId,
            title: $title,
            transactionType: $transactionType,
            propertyType: $propertyType,
            publicationType: $publicationType,
            listPrice: $listPrice,
            rentalPrice: $rentalPrice,
            description: $description,
            detailUrl: $detailUrl,
            country: $country,
            state: $state,
            city: $city,
            neighborhood: $neighborhood,
            address: $address,
            streetNumber: $streetNumber,
            complement: $complement,
            postalCode: $postalCode,
            latitude: $latitude,
            longitude: $longitude,
            bedrooms: $bedrooms,
            bathrooms: $bathrooms,
            suites: $suites,
            garage: $garage,
            livingArea: $livingArea,
            lotArea: $lotArea,
            media: $media,
            raw: $raw,
        );
    }

    private function localName(XMLReader $reader): string
    {
        $name = $reader->localName !== '' ? $reader->localName : $reader->name;

        if (str_contains($name, ':')) {
            return substr($name, (int) strrpos($name, ':') + 1);
        }

        return $name;
    }
}
