<?php

declare(strict_types=1);

namespace NoahVet\Reef\Command\OpenAPI;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
    name: 'reef:open-api:format',
    description: 'Adds blank lines between component section items and before comments, and sorts properties keys alphabetically',
)]
class FormatCommand extends Command implements FormatOpenApiCommandInterface
{
    protected function configure(): void
    {
        $this
            ->addArgument(
                'path',
                InputArgument::REQUIRED,
                'File or directory to process (e.g., config/, config/openapi.yaml)',
            )
            ->addOption(
                'dry-run',
                null,
                InputOption::VALUE_NONE,
                'Displays which files would be modified without actually writing them',
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $path = $input->getArgument('path');
        $dryRun = $input->getOption('dry-run');

        if (!\file_exists($path)) {
            $output->writeln("<error>The path '{$path}' does not exist.</error>");

            return Command::FAILURE;
        }

        if (\is_file($path)) {
            $this->processFile($path, $dryRun, $output);
        } else {
            $this->processDirectory($path, $dryRun, $output);
        }

        return Command::SUCCESS;
    }

    private function processDirectory(string $directory, bool $dryRun, OutputInterface $output): void
    {
        $dirIterator = new \RecursiveDirectoryIterator($directory);
        $iterator = new \RecursiveIteratorIterator($dirIterator);
        $yamlFiles = new \RegexIterator($iterator, '/^.+\.(ya?ml)$/i', \RegexIterator::GET_MATCH);

        foreach ($yamlFiles as $files) {
            $file = $files[0];
            $this->processFile($file, $dryRun, $output);
        }
    }

    private function processFile(string $filePath, bool $dryRun, OutputInterface $output): void
    {
        $contents = \file_get_contents($filePath);

        if (false === $contents) {
            $output->writeln("<error>Unable to read file: {$filePath}</error>");

            return;
        }

        $lines = \preg_split("/\r\n|\n|\r/", $contents);

        $inComponents = false;
        $componentsIndent = null;
        $inSection = false;
        $sectionIndent = null;
        $hasSeenFirstItem = false;
        $result = [];
        /** @var array<int, string> $lines */
        foreach ($lines as $line) {
            $raw = $line;
            $trimmed = \ltrim($raw);
            $indent = $this->getIndent($raw);

            if (\preg_match('/^components:\s*$/', $trimmed)) {
                $inComponents = true;
                $componentsIndent = $indent;
                $inSection = false;
                $sectionIndent = null;
                $hasSeenFirstItem = false;
            } else {
                if ($inComponents && $indent <= $componentsIndent && '' !== $trimmed) {
                    $inComponents = false;
                    $componentsIndent = null;
                    $inSection = false;
                    $sectionIndent = null;
                    $hasSeenFirstItem = false;
                }
            }

            if ($inComponents) {
                if (
                    $indent === $componentsIndent + 2
                    && \preg_match('/^[^#\s][^:]*:\s*$/', $trimmed)
                ) {
                    $inSection = true;
                    $sectionIndent = $indent;
                    $hasSeenFirstItem = false;
                } elseif (
                    $inSection
                    && null !== $sectionIndent
                    && $indent <= $sectionIndent
                    && '' !== $trimmed
                ) {
                    $inSection = false;
                    $sectionIndent = null;
                    $hasSeenFirstItem = false;
                }
            }

            $isItemHeader = false;

            if ($inSection && null !== $sectionIndent) {
                if (
                    $indent === $sectionIndent + 2
                    && \preg_match('/^[^#\s][^:]*:\s*$/', $trimmed)
                ) {
                    $isItemHeader = true;
                }
            }

            if ($isItemHeader) {
                if ($hasSeenFirstItem) {
                    $lastLine = \end($result);
                    if ('' !== $lastLine) {
                        $result[] = '';
                    }
                }
                $hasSeenFirstItem = true;
            }

            $isComment = 1 === \preg_match('/^#/', $trimmed);

            if ($isComment) {
                if (!empty($result)) {
                    $lastLine = \end($result);
                    if ('' !== $lastLine) {
                        $result[] = '';
                    }
                }
            }

            $result[] = $raw;
        }

        // Trim trailing empty lines
        while (!empty($result) && '' === \trim(\end($result))) {
            \array_pop($result);
        }

        // 🔹 NOUVEAU : tri récursif des clés sous tous les blocs "properties:"
        $result = $this->sortAllProperties($result);

        $newContent = \implode("\n", $result);

        if ($newContent === $contents) {
            return;
        }

        if ($dryRun) {
            $output->writeln("<comment>[DRY-RUN]</comment> {$filePath} would be modified.</comment>");
        } else {
            \file_put_contents($filePath, $newContent."\n");
            $output->writeln("<info>Formatted:</info> {$filePath}");
        }
    }

    /**
     * Retourne l'indentation (nombre d'espaces) d'une ligne.
     */
    private function getIndent(string $line): int
    {
        $trimmed = \ltrim($line);

        return \strlen($line) - \strlen($trimmed);
    }

    /**
     * Trie récursivement tous les blocs "properties:" trouvés dans le fichier.
     *
     * @param array<int, string> $lines
     *
     * @return array<int, string>
     */
    private function sortAllProperties(array $lines): array
    {
        $this->sortPropertiesInRange($lines, 0, \count($lines));

        return $lines;
    }

    /**
     * Parcourt une plage de lignes et trie les enfants de chaque "properties:" trouvé,
     * puis applique récursivement le même traitement dans les sous-blocs.
     *
     * @param array<int, string> $lines
     */
    private function sortPropertiesInRange(array &$lines, int $start, int $end): void
    {
        $i = $start;

        while ($i < $end) {
            $line = $lines[$i] ?? '';
            $trimmed = \ltrim($line);

            if ('' !== $trimmed && \preg_match('/^properties:\s*$/', $trimmed)) {
                $propertiesIndent = $this->getIndent($line);
                $n = \count($lines);

                // Délimite le bloc couvert par ce "properties:"
                $j = $i + 1;
                while ($j < $n && $j < $end) {
                    $lineJ = $lines[$j];
                    $trimJ = \trim($lineJ);

                    if ('' === $trimJ) {
                        ++$j;

                        continue;
                    }

                    $indentJ = $this->getIndent($lineJ);
                    if ($indentJ <= $propertiesIndent) {
                        break;
                    }

                    ++$j;
                }

                $blockEnd = $j;

                // Trie les propriétés directement sous ce bloc
                $this->sortDirectProperties($lines, $i, $blockEnd);

                // Après le tri, on retravaille dans le bloc trié pour gérer les sous-blocs
                $propertiesChildIndent = $propertiesIndent + 2;
                $k = $i + 1;

                while ($k < $blockEnd) {
                    $lineK = $lines[$k];
                    $trimK = \ltrim($lineK);

                    if ('' === $trimK) {
                        ++$k;

                        continue;
                    }

                    $indentK = $this->getIndent($lineK);

                    // On repère les lignes "clé:" (propriétés directes)
                    if (
                        $indentK === $propertiesChildIndent
                        && \preg_match('/^[^#\s][^:]*:\s*$/', $trimK)
                    ) {
                        $propStart = $k;
                        $m = $k + 1;

                        while ($m < $blockEnd) {
                            $cur = $lines[$m];
                            $curTrim = \ltrim($cur);

                            if ('' === $curTrim) {
                                ++$m;

                                continue;
                            }

                            $curIndent = $this->getIndent($cur);

                            if ($curIndent <= $propertiesChildIndent && \str_starts_with($curTrim, '#')) {
                                break;
                            }

                            ++$m;
                        }

                        // Appel récursif dans le bloc de cette propriété
                        $this->sortPropertiesInRange($lines, $propStart + 1, $m);
                        $k = $m;

                        continue;
                    }

                    ++$k;
                }

                $i = $blockEnd;

                continue;
            }

            ++$i;
        }
    }

    /**
     * Trie les blocs "clé: ... (sous-lignes)" directement sous un bloc "properties:".
     *
     * @param array<int, string> $lines
     */
    private function sortDirectProperties(array &$lines, int $propertiesIndex, int $blockEnd): void
    {
        $propertiesIndent = $this->getIndent($lines[$propertiesIndex]);
        $propsStart = $propertiesIndex + 1;

        if ($propsStart >= $blockEnd) {
            return;
        }

        $entries = [];
        $i = $propsStart;
        $n = \count($lines);
        $propertyIndent = $propertiesIndent + 2;

        while ($i < $blockEnd && $i < $n) {
            $line = $lines[$i];
            $trimmed = \ltrim($line);

            if ('' === $trimmed) {
                ++$i;

                continue;
            }

            $indent = $this->getIndent($line);

            // On repère les lignes "clé:" au bon niveau d'indentation
            if (
                $indent === $propertyIndent
                && \preg_match('/^([^\s#][^:]*):\s*$/', $trimmed, $m)
            ) {
                $key = $m[1];

                // On inclut d'éventuels commentaires/blank lines juste au-dessus,
                // d'indentation >= au niveau des propriétés.
                $blockStart = $i;
                $k = $i - 1;
                while ($k >= $propsStart) {
                    $prevLine = $lines[$k];
                    $prevTrim = \ltrim($prevLine);
                    $prevIndent = $this->getIndent($prevLine);

                    if (
                        ('' === $prevTrim || \str_starts_with($prevTrim, '#'))
                        && $prevIndent >= $propertyIndent
                    ) {
                        $blockStart = $k;
                        --$k;

                        continue;
                    }

                    break;
                }

                // On cherche la fin du bloc de cette propriété
                $j = $i + 1;
                while ($j < $blockEnd) {
                    $cur = $lines[$j];
                    $curTrim = \ltrim($cur);

                    if ('' === $curTrim) {
                        ++$j;

                        continue;
                    }

                    $curIndent = $this->getIndent($cur);

                    // Si on rencontre un autre header de propriété (ou un bloc au même niveau non-commentaire), on s'arrête
                    if ($curIndent <= $propertyIndent && \str_starts_with($curTrim, '#')) {
                        break;
                    }

                    ++$j;
                }

                $entries[] = [
                    'key' => $key,
                    'start' => $blockStart,
                    'end' => $j,
                ];

                $i = $j;

                continue;
            }

            ++$i;
        }

        if (1 >= \count($entries)) {
            return;
        }

        // Tri alphabétique insensible à la casse
        \usort($entries, static fn (array $a, array $b): int => \strcasecmp($a['key'], $b['key']));

        // Reconstruit le segment [propsStart, blockEnd) avec les blocs triés
        $fixed = [];

        $minStart = \min(\array_column($entries, 'start'));
        $maxEnd = \max(\array_column($entries, 'end'));

        // On garde tel quel ce qu'il y a entre "properties:" et le premier bloc
        if ($minStart > $propsStart) {
            $fixed = \array_merge(
                $fixed,
                \array_slice($lines, $propsStart, $minStart - $propsStart),
            );
        }

        // On ajoute les blocs triés
        foreach ($entries as $entry) {
            $fixed = \array_merge(
                $fixed,
                \array_slice($lines, $entry['start'], $entry['end'] - $entry['start']),
            );
        }

        // On garde la fin du bloc (après la dernière propriété)
        if ($maxEnd < $blockEnd) {
            $fixed = \array_merge(
                $fixed,
                \array_slice($lines, $maxEnd, $blockEnd - $maxEnd),
            );
        }

        // Remplacement dans le tableau de lignes
        \array_splice($lines, $propsStart, $blockEnd - $propsStart, $fixed);
    }
}
