<?php
declare(strict_types=1);

/**
 * 语言包扫描工具
 * 扫描指定目录里的 lg('中文原文') / lg("中文原文") 字面量调用，生成 scan.php，
 * 再和语言包目录下其它语言文件做差异同步（新增缺失的 key，删除不再使用的 key）
 */
final class LangScanner
{
    private const SCAN_FILE = 'scan.php';

    /**
     * 扫描指定目录，返回去重排序后的中文原文 key 列表
     * @param string[] $dirs 绝对路径目录列表
     * @return string[]
     */
    public static function scan(array $dirs): array
    {
        $found = [];
        foreach ($dirs as $dir) {
            self::scanDir($dir, $found);
        }

        $keys = array_values(array_unique($found));
        sort($keys);

        return $keys;
    }

    /**
     * 执行一次完整的扫描 + 写 scan.php + 同步其它语言文件
     * @return array{keys:int, files:array<string,array{added:int,removed:int}>}
     */
    public static function run(array $dirs, string $langDir): array
    {
        $keys = self::scan($dirs);
        self::writeScanFile($langDir, $keys);

        $summary = ['keys' => count($keys), 'files' => []];
        foreach (self::editableLangFiles($langDir, includeZhCn: true) as $file) {
            $summary['files'][$file] = self::syncLangFile($langDir, $file, $keys);
        }

        return $summary;
    }

    /**
     * 递归扫描单个目录，把匹配到的 lg() 字面量参数收集进 $found
     */
    private static function scanDir(string $dir, array &$found): void
    {
        if (!is_dir($dir)) {
            return;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            if (!preg_match('/\.(php|js)$/i', $file->getFilename())) {
                continue;
            }

            $content = file_get_contents($file->getPathname());
            if (preg_match_all('/\blg\(\s*([\'"])((?:(?!\1).)*)\1\s*\)/u', $content, $matches)) {
                foreach ($matches[2] as $key) {
                    $found[] = $key;
                }
            }
        }
    }

    /**
     * 写 pdc/lang/scan.php（纯列表，自动生成，不要手动编辑）
     */
    private static function writeScanFile(string $langDir, array $keys): void
    {
        $lines = array_map(
            static fn(string $key): string => "    '" . self::escape($key) . "',",
            $keys
        );

        $content = "<?php\ndeclare(strict_types=1);\n\n"
            . "// 由语言包扫描工具自动生成，不要手动编辑\nreturn [\n"
            . implode("\n", $lines) . "\n];\n";

        file_put_contents($langDir . '/' . self::SCAN_FILE, $content);
    }

    /**
     * 同步一个语言文件：补齐 scan 出来但文件里没有的 key（默认值=key本身），
     * 删掉文件里有但 scan 不出来的 key
     * @return array{added:int,removed:int}
     */
    private static function syncLangFile(string $langDir, string $file, array $keys): array
    {
        $path = $langDir . '/' . $file;
        $existing = is_file($path) ? (require $path) : [];
        if (!is_array($existing)) {
            $existing = [];
        }

        $keySet = array_flip($keys);
        $added = 0;
        $removed = 0;
        $result = [];

        foreach ($keys as $key) {
            if (array_key_exists($key, $existing)) {
                $result[$key] = $existing[$key];
            } else {
                $result[$key] = $key; // 新 key 默认值用中文原文占位，等翻译
                $added++;
            }
        }

        foreach ($existing as $key => $value) {
            if (!isset($keySet[$key])) {
                $removed++; // 不再使用的 key，不放进 $result，相当于删除
            }
        }

        self::writeLangFile($path, $result);

        return ['added' => $added, 'removed' => $removed];
    }

    /**
     * 写一个 key=>value 形式的语言文件
     */
    private static function writeLangFile(string $path, array $data): void
    {
        $lines = [];
        foreach ($data as $key => $value) {
            $lines[] = "    '" . self::escape((string) $key) . "' => '" . self::escape((string) $value) . "',";
        }

        $content = "<?php\ndeclare(strict_types=1);\n\nreturn [\n" . implode("\n", $lines) . "\n];\n";

        file_put_contents($path, $content);
    }

    /**
     * 语言包目录下，除 scan.php（以及可选排除 zh-CN.php）外的可编辑语言文件名
     * @return string[]
     */
    public static function editableLangFiles(string $langDir, bool $includeZhCn = false): array
    {
        $files = [];
        foreach (glob($langDir . '/*.php') ?: [] as $path) {
            $name = basename($path);
            if ($name === self::SCAN_FILE) {
                continue;
            }
            if (!$includeZhCn && $name === 'zh-CN.php') {
                continue;
            }
            $files[] = $name;
        }
        sort($files);

        return $files;
    }

    /**
     * 取某个语言文件的内容，转成 [{key,value}] 形式供 dxDataGrid 显示
     * @return array<int,array{key:string,value:string}>
     */
    public static function loadFileRows(string $langDir, string $file): array
    {
        $path = self::assertEditableFile($langDir, $file);
        $data = is_file($path) ? (require $path) : [];

        $rows = [];
        foreach ($data as $key => $value) {
            $rows[] = ['key' => (string) $key, 'value' => (string) $value];
        }

        return $rows;
    }

    /**
     * 更新某个语言文件里指定 key 的翻译值（key 必须已存在，不允许新增/删除）
     */
    public static function updateFileValue(string $langDir, string $file, string $key, string $value): void
    {
        $path = self::assertEditableFile($langDir, $file);
        $data = is_file($path) ? (require $path) : [];

        if (!array_key_exists($key, $data)) {
            throw new MvcException("语言文件 {$file} 中不存在 key: {$key}");
        }

        $data[$key] = $value;
        self::writeLangFile($path, $data);
    }

    /**
     * 校验文件名在允许编辑的语言文件列表内，防止任意文件读写
     */
    private static function assertEditableFile(string $langDir, string $file): string
    {
        $file = basename($file);
        if (!in_array($file, self::editableLangFiles($langDir, includeZhCn: false), true)) {
            throw new MvcException("不允许访问的语言文件: {$file}");
        }

        return $langDir . '/' . $file;
    }

    /**
     * 字符串里的单引号/反斜杠转义，用于安全写进单引号 PHP 字面量
     */
    private static function escape(string $value): string
    {
        return str_replace(['\\', "'"], ['\\\\', "\\'"], $value);
    }
}
