<?php

namespace Dalten\RuianToUir\Command;

use Dalten\RuianToUir\Converter\ConverterInterface;
use Dalten\RuianToUir\Converter\RemovedItem;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class PareDownXml extends BaseCommand
{
    private const LIMIT_PER_TYPE = 3;

    /**
     * @param ConverterInterface[] $converters
     */
    public function __construct(
        private readonly array $converters,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this->setName('pare-down-xml');
        $this->setDescription('Stream-extract first N items per present RUIAN type into a minimal XML while preserving wrappers.');
        $this->addArgument(
            'input-file',
            InputArgument::REQUIRED,
            'Zdrojovy XML soubor (cesta k souboru).'
        );
    }

    public function execute(InputInterface $input, OutputInterface $output): int
    {
        /** @var string $inputFile */
        $inputFile = $input->getArgument('input-file');
        $io = new SymfonyStyle($input, $output);

        if (!is_file($inputFile) || !preg_match('/\\.xml$/i', $inputFile)) {
            $io->error(\sprintf('Zadejte cestu k XML souboru. "%s" neni XML soubor.', $inputFile));

            return 1;
        }

        $source = $inputFile;
        $target = $this->buildTargetPath($source);

        try {
            $io->writeln(['# '.$source, ' -> '.$target]);
            $this->processFile($source, $target, $io);
        } catch (\Throwable $e) {
            $io->error($e->getMessage());

            return 1;
        }

        return 0;
    }

    private function buildTargetPath(string $source): string
    {
        $dotPos = strrpos($source, '.');
        if (false !== $dotPos) {
            return substr($source, 0, $dotPos).'-mini'.substr($source, $dotPos);
        }

        return $source.'-mini.xml';
    }

    private function processFile(string $source, string $target, SymfonyStyle $io): void
    {
        $vfNsUri = 'urn:vf';
        $rootName = 'vf:VymennyFormat';
        /** @var array<string, string> $nsDecl */
        $nsDecl = [];

        $reader = new \XMLReader();
        if (!$reader->open($source)) {
            throw new \RuntimeException(\sprintf('Soubor %s nelze otevrit!', $source));
        }

        while ($reader->read() && \XMLReader::ELEMENT !== $reader->nodeType) {
            // skip
        }

        if (\XMLReader::ELEMENT === $reader->nodeType) {
            $vfNsUri = $reader->namespaceURI ?: $vfNsUri;
            $rootName = $reader->name ?: $rootName;

            if ($reader->moveToFirstAttribute()) {
                do {
                    if (str_starts_with((string) $reader->name, 'xmlns')) {
                        $nsDecl[$reader->name] = $reader->value;
                    }
                } while ($reader->moveToNextAttribute());

                $reader->moveToElement();
            }
        }

        $reader->close();

        $doc = new \DOMDocument('1.0', 'UTF-8');
        $doc->formatOutput = true;
        $root = $doc->createElementNS($vfNsUri, $rootName);
        if (!$root instanceof \DOMElement) {
            throw new \RuntimeException('Nelze vytvorit root element vystupniho XML.');
        }

        $doc->appendChild($root);

        foreach ($nsDecl as $attr => $value) {
            $root->setAttribute($attr, $value);
        }

        $reader = new \XMLReader();
        if (!$reader->open($source)) {
            throw new \RuntimeException(\sprintf('Soubor %s nelze otevrit!', $source));
        }

        /** @var list<ConverterInterface> $activeConverters */
        $activeConverters = array_values(array_filter(
            $this->converters,
            static fn (ConverterInterface $converter): bool => !($converter instanceof RemovedItem)
        ));

        /** @var array<string, int> $counters */
        $counters = [];
        foreach ($activeConverters as $converter) {
            $counters[$converter->getName()] = 0;
        }

        /** @var array<int, string> $pathByDepth */
        $pathByDepth = [];
        /** @var array<string, \DOMElement> $nodeByPath */
        $nodeByPath = [];
        $headerImported = false;
        $totalTypes = \count($counters);

        while ($reader->read()) {
            if (\XMLReader::ELEMENT !== $reader->nodeType) {
                continue;
            }

            $pathByDepth[$reader->depth] = $reader->name;
            $d = $reader->depth + 1;
            while (isset($pathByDepth[$d])) {
                unset($pathByDepth[$d]);
                ++$d;
            }

            if (0 === $reader->depth) {
                continue;
            }

            if (1 === $reader->depth && !$headerImported && 'vf:Hlavicka' === $reader->name) {
                $expanded = $reader->expand();
                if ($expanded instanceof \DOMNode) {
                    $root->appendChild($doc->importNode($expanded, true));
                }

                $headerImported = true;
                continue;
            }

            if ($reader->depth <= 1) {
                continue;
            }

            foreach ($activeConverters as $converter) {
                $name = $converter->getName();
                if (($counters[$name] ?? 0) >= self::LIMIT_PER_TYPE) {
                    continue;
                }

                if (!$converter->accepts($reader)) {
                    continue;
                }

                // Konvertujeme jen skutecne zaznamy, ne obalove kontejnery.
                if (!$this->hasGmlId($reader)) {
                    continue;
                }

                $expanded = $reader->expand();
                if ($expanded instanceof \DOMNode) {
                    $imported = $doc->importNode($expanded, true);

                    $fullPath = [];
                    for ($i = 0; $i <= $reader->depth; ++$i) {
                        if (isset($pathByDepth[$i])) {
                            $fullPath[] = $pathByDepth[$i];
                        }
                    }

                    $wrapperPath = \array_slice($fullPath, 1, \count($fullPath) - 2);
                    $parent = $this->ensureWrapperChain($doc, $vfNsUri, $root, $nodeByPath, $wrapperPath);
                    $parent->appendChild($imported);
                    $counters[$name] = ($counters[$name] ?? 0) + 1;
                }

                $reached = 0;
                foreach ($counters as $count) {
                    if ($count >= self::LIMIT_PER_TYPE) {
                        ++$reached;
                    }
                }

                if ($reached >= $totalTypes) {
                    break 2;
                }

                break;
            }
        }

        $reader->close();

        if (false === $doc->save($target)) {
            throw new \RuntimeException(\sprintf('Nepodarilo se zapsat vystupni soubor: %s', $target));
        }

        $io->success(\sprintf('Vytvoren mini XML soubor: %s', $target));
    }

    private function hasGmlId(\XMLReader $reader): bool
    {
        if (!$reader->hasAttributes) {
            return false;
        }

        if (!$reader->moveToFirstAttribute()) {
            return false;
        }

        do {
            $name = $reader->name;
            $local = $reader->localName;
            $ns = $reader->namespaceURI ?: '';

            if ('gml:id' === $name || ('id' === $local && str_contains($ns, 'opengis.net/gml'))) {
                $reader->moveToElement();

                return true;
            }
        } while ($reader->moveToNextAttribute());

        $reader->moveToElement();

        return false;
    }

    /**
     * @param array<string, \DOMElement> $nodeByPath
     * @param array<int, string>         $wrapperPath
     */
    private function ensureWrapperChain(\DOMDocument $doc, string $vfNsUri, \DOMElement $root, array &$nodeByPath, array $wrapperPath): \DOMElement
    {
        $parent = $root;
        $built = [];

        foreach ($wrapperPath as $segment) {
            $built[] = $segment;
            $key = implode('/', $built);

            if (!isset($nodeByPath[$key])) {
                $created = $doc->createElementNS($vfNsUri, $segment);
                if (!$created instanceof \DOMElement) {
                    throw new \RuntimeException(\sprintf('Nelze vytvorit wrapper element %s.', $segment));
                }

                $nodeByPath[$key] = $created;
                $parent->appendChild($nodeByPath[$key]);
            }

            $parent = $nodeByPath[$key];
        }

        return $parent;
    }
}
