<?php
declare(strict_types=1);

namespace Celena\Module\Translations;

use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Security\Auth;
use Celena\Core\Security\Csrf;
use Celena\Core\Template\Engine;
use Celena\Module\AdminBase\AdminController;
use Celena\Module\Settings\Options;

/**
 * UI-редактор JSON-файлов локалей. Сканирует:
 *   - core/languages/{locale}.json
 *   - plugins/<slug>/languages/{locale}.json
 *
 * Считает «дыры» — ключи, которые есть в одном языке, но отсутствуют в других.
 * Сохранение пишет в тот же файл (с pretty-print), создаёт *.bak копию рядом.
 */
final class TranslationsController extends AdminController
{
    public function __construct(
        Engine $engine,
        Auth $auth,
        Options $options,
        private readonly string $rootDir,
    ) {
        parent::__construct($engine, $auth, $options);
    }

    public function index(Request $request): Response
    {
        if ($deny = $this->deny('translations.manage')) return $deny;
        $files = $this->scan();
        $locale = $this->sanitizeLocale((string) $request->query('locale', 'ru'));
        $scope  = $this->sanitizeScope((string) $request->query('scope', 'core'));

        $current = $this->loadFile($scope, $locale, $files);
        $allLocales = $this->localesFor($scope, $files);

        // Считаем покрытие: для каждого locale в этом scope — % переведённых.
        $coverage = [];
        $allKeys = [];
        foreach ($allLocales as $loc) {
            $data = $this->loadFile($scope, $loc, $files);
            foreach (array_keys($data) as $k) $allKeys[$k] = true;
        }
        foreach ($allLocales as $loc) {
            $data = $this->loadFile($scope, $loc, $files);
            $filled = 0;
            foreach ($allKeys as $k => $_) {
                if (isset($data[$k]) && trim((string) $data[$k]) !== '') $filled++;
            }
            $total = max(1, count($allKeys));
            $coverage[] = [
                'locale' => $loc,
                'pct'    => (int) round(($filled / $total) * 100),
                'filled' => $filled,
                'total'  => $total,
            ];
        }

        // Развёрнутый список ключ → значение для редактирования.
        $rows = [];
        foreach (array_keys($allKeys) as $key) {
            $rows[] = [
                'key'   => $key,
                'value' => (string) ($current[$key] ?? ''),
                'empty' => !isset($current[$key]) || trim((string) $current[$key]) === '',
            ];
        }
        usort($rows, fn ($a, $b) => strcmp($a['key'], $b['key']));

        return $this->view('admin/translations/index', [
            'scopes'   => array_keys($files),
            'locales'  => $allLocales,
            'scope'    => $scope,
            'locale'   => $locale,
            'coverage' => $coverage,
            'rows'     => $rows,
            'total'    => count($rows),
        ], 'translations', 'Переводы');
    }

    public function save(Request $request): Response
    {
        if ($deny = $this->deny('translations.manage')) return $deny;
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/translations', 'CSRF.', 'error');
        }
        // Строгая валидация scope/locale ДО построения пути — иначе
        // '../../..' в locale/slug = запись JSON в произвольное место (path traversal).
        $scope  = $this->sanitizeScope((string) $request->input('scope', 'core'));
        $locale = $this->sanitizeLocale((string) $request->input('locale', 'ru'));
        if ($scope === '' || $locale === '') {
            return $this->redirect('/admin/translations', 'Недопустимый язык или раздел.', 'error');
        }
        $values = (array) $request->input('values', []);

        $files = $this->scan();
        $path = $files[$scope][$locale] ?? null;
        if ($path === null) {
            // Файл не существует — создадим, если scope валидный.
            if (!isset($files[$scope])) {
                return $this->redirect('/admin/translations', 'Неизвестный scope.', 'error');
            }
            $path = $this->resolvePath($scope, $locale);
        }

        // Считаем существующее как плоский dot-словарь, патчим, потом раскатываем обратно
        // в иерархию (вложенные секции, как в исходных JSON-локалях ядра).
        $rawExisting = is_file($path) ? (json_decode((string) file_get_contents($path), true) ?: []) : [];
        $flat = is_array($rawExisting) ? $this->flatten($rawExisting) : [];
        foreach ($values as $key => $value) {
            $key   = (string) $key;
            $value = (string) $value;
            if ($value === '') {
                unset($flat[$key]);
            } else {
                $flat[$key] = $value;
            }
        }
        ksort($flat);
        $existing = $this->unflatten($flat);

        // Бэкап перед перезаписью.
        if (is_file($path)) {
            @copy($path, $path . '.bak');
        }
        @mkdir(dirname($path), 0775, true);
        file_put_contents(
            $path,
            json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?: '{}'
        );

        return $this->redirect('/admin/translations?scope=' . rawurlencode($scope) . '&locale=' . rawurlencode($locale), 'Сохранено.');
    }

    /** @return array<string, array<string, string>> scope → locale → file path */
    private function scan(): array
    {
        $out = ['core' => []];
        foreach (glob($this->rootDir . '/core/languages/*.json') ?: [] as $f) {
            $out['core'][basename($f, '.json')] = $f;
        }
        foreach (glob($this->rootDir . '/plugins/*/languages', GLOB_ONLYDIR) ?: [] as $dir) {
            $slug = basename(dirname($dir));
            $out['plugin:' . $slug] = [];
            foreach (glob($dir . '/*.json') ?: [] as $f) {
                $out['plugin:' . $slug][basename($f, '.json')] = $f;
            }
        }
        return $out;
    }

    /** @return array<string, string> Плоский dot-словарь: вложенные секции расплющиваются. */
    private function loadFile(string $scope, string $locale, array $files): array
    {
        $path = $files[$scope][$locale] ?? null;
        if ($path === null || !is_file($path)) return [];
        $data = json_decode((string) file_get_contents($path), true);
        return is_array($data) ? $this->flatten($data) : [];
    }

    private function flatten(array $data, string $prefix = ''): array
    {
        $out = [];
        foreach ($data as $k => $v) {
            $key = $prefix === '' ? (string) $k : $prefix . '.' . $k;
            if (is_array($v)) {
                $out += $this->flatten($v, $key);
            } else {
                $out[$key] = is_scalar($v) ? (string) $v : '';
            }
        }
        return $out;
    }

    /** Обратная операция: { 'admin.save' => '...' } → { admin: { save: '...' } }. */
    private function unflatten(array $flat): array
    {
        $out = [];
        foreach ($flat as $key => $value) {
            $segments = explode('.', (string) $key);
            $cursor = &$out;
            foreach ($segments as $i => $seg) {
                if ($i === count($segments) - 1) {
                    $cursor[$seg] = $value;
                } else {
                    if (!isset($cursor[$seg]) || !is_array($cursor[$seg])) {
                        $cursor[$seg] = [];
                    }
                    $cursor = &$cursor[$seg];
                }
            }
            unset($cursor);
        }
        return $out;
    }

    /** @return string[] */
    private function localesFor(string $scope, array $files): array
    {
        $known = array_keys($files[$scope] ?? []);
        // Доступные локали ядра — стандартный список + всё что нашли.
        $baseLocales = array_keys($files['core'] ?? []);
        return array_values(array_unique(array_merge($baseLocales, $known)));
    }

    private function resolvePath(string $scope, string $locale): string
    {
        // scope/locale уже прошли sanitize*, но подстрахуемся ещё раз перед ФС-операцией.
        $locale = $this->sanitizeLocale($locale);
        if ($locale === '') {
            $locale = 'ru';
        }
        if (str_starts_with($scope, 'plugin:')) {
            $slug = substr($scope, 7);
            if (preg_match('/^[a-z0-9_-]+$/', $slug)) {
                return $this->rootDir . '/plugins/' . $slug . '/languages/' . $locale . '.json';
            }
        }
        return $this->rootDir . '/core/languages/' . $locale . '.json';
    }

    /** Код локали: ll или ll-CC. Пустая строка = невалидный ввод. */
    private function sanitizeLocale(string $locale): string
    {
        return preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $locale) ? $locale : '';
    }

    /** Раздел: 'core' или 'plugin:<slug>' (slug — латиница/цифры/дефис). Пустая = невалидный. */
    private function sanitizeScope(string $scope): string
    {
        if ($scope === 'core') {
            return 'core';
        }
        if (str_starts_with($scope, 'plugin:') && preg_match('/^[a-z0-9_-]+$/', substr($scope, 7))) {
            return $scope;
        }
        return '';
    }
}
