<?php
declare(strict_types=1);

namespace Autogedal\FrontendI18n\Model;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use FilesystemIterator;

class PhraseCollector
{
    private const ALLOWED_EXTENSIONS = ['php', 'phtml', 'html', 'xml', 'js'];
    private const IGNORED_PATH_SEGMENTS = [
        '/generated/',
        '/var/',
        '/pub/',
        '/static/',
        '/vendor/',
        '/node_modules/',
        '/.git/',
    ];
    private const IGNORED_PHRASE_PATTERNS = [
        '/stores\s*->\s*configuration/i',
        '/content\s*->\s*elements/i',
    ];

    public function __construct(
        private readonly AreaPathsResolver $areaPathsResolver
    ) {
    }

    /**
     * @return string[]
     */
    public function collect(string $area, bool $includeVendor = false): array
    {
        $phrases = [];

        foreach ($this->areaPathsResolver->resolve($area, $includeVendor) as $root) {
            foreach ($this->iterateFiles($root, $area, $includeVendor) as $file) {
                foreach ($this->extractFromFile($file) as $phrase) {
                    $phrases[$phrase] = true;
                }
            }
        }

        return array_keys($phrases);
    }

    /**
     * @return string[]
     */
    private function extractFromFile(string $path): array
    {
        $content = @file_get_contents($path);
        if ($content === false || $content === '') {
            return [];
        }

        $phrases = [];
        $patterns = [
            '~__\(\s*\'((?:[^\'\\\\]|\\\\.)*)\'(?=\s*(?:,|\)))~s',
            '~__\(\s*"((?:[^"\\\\]|\\\\.)*)"\s*(?=\s*(?:,|\)))~s',
            '~\{\{\s*trans\s+\'((?:[^\'\\\\]|\\\\.)*)\'~s',
            '~\{\{\s*trans\s+"((?:[^"\\\\]|\\\\.)*)"~s',
            '~i18n:\s*\'((?:[^\'\\\\]|\\\\.)*)\'~s',
            '~i18n:\s*"((?:[^"\\\\]|\\\\.)*)"~s',
        ];

        foreach ($patterns as $pattern) {
            if (preg_match_all($pattern, $content, $matches)) {
                foreach ($matches[1] as $phrase) {
                    $this->addPhrase($phrases, $phrase);
                }
            }
        }

        if (str_ends_with($path, '.xml')) {
            $this->collectXmlNodeText($content, $phrases);
        }

        return array_keys($phrases);
    }

    /**
     * @param array<string,bool> $phrases
     */
    private function collectXmlNodeText(string $content, array &$phrases): void
    {
        $pattern = '~<([a-zA-Z0-9:_-]+)([^>]*\btranslate="[^"]+"[^>]*)>(.*?)</\\1>~si';
        if (!preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
            return;
        }

        foreach ($matches as $match) {
            $text = trim(html_entity_decode(strip_tags($match[3]), ENT_QUOTES | ENT_XML1, 'UTF-8'));
            $this->addPhrase($phrases, $text);
        }
    }

    /**
     * @param array<string,bool> $phrases
     */
    private function addPhrase(array &$phrases, string $phrase): void
    {
        $phrase = trim(stripcslashes($phrase));
        if ($phrase === '') {
            return;
        }

        if ($this->isIgnoredPhrase($phrase)) {
            return;
        }

        $phrases[$phrase] = true;
    }

    /**
     * @return \Generator<string>
     */
    private function iterateFiles(string $root, string $area, bool $includeVendor = false): \Generator
    {
        $matches = glob($root, GLOB_ONLYDIR) ?: [];
        if (!$matches) {
            return;
        }

        foreach ($matches as $directory) {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS)
            );

            foreach ($iterator as $fileInfo) {
                if (!$fileInfo->isFile()) {
                    continue;
                }

                $extension = strtolower(pathinfo($fileInfo->getFilename(), PATHINFO_EXTENSION));
                if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
                    continue;
                }

                $path = str_replace('\\', '/', $fileInfo->getPathname());
                if ($this->isIgnoredPath($path, $includeVendor)) {
                    continue;
                }

                $isAdminhtml = stripos($path, '/adminhtml/') !== false || stripos($path, '/Adminhtml/') !== false;
                if ($area === AreaPathsResolver::AREA_FRONTEND && $isAdminhtml) {
                    continue;
                }

                if ($area === AreaPathsResolver::AREA_BACKEND && !$isAdminhtml) {
                    continue;
                }

                yield $fileInfo->getPathname();
            }
        }
    }

    private function isIgnoredPath(string $path, bool $includeVendor = false): bool
    {
        foreach (self::IGNORED_PATH_SEGMENTS as $segment) {
            if ($includeVendor && $segment === '/vendor/') {
                continue;
            }

            if (stripos($path, $segment) !== false) {
                return true;
            }
        }

        return false;
    }

    private function isIgnoredPhrase(string $phrase): bool
    {
        foreach (self::IGNORED_PHRASE_PATTERNS as $pattern) {
            if (preg_match($pattern, $phrase) === 1) {
                return true;
            }
        }

        return false;
    }
}
