<?php

namespace App\Helpers;

use Illuminate\Support\Facades\Cache;

/**
 * TranslationHelper — read / write / diff Laravel's `lang/*.json` files
 * from the admin Translations editor.
 *
 * Why we keep using JSON files instead of moving translations into the DB:
 * - The whole codebase already calls `__()` / `@lang()` which resolves
 *   against lang/{locale}.json natively. Moving to the DB would require
 *   either reimplementing the resolver or shimming through a custom
 *   loader — both higher-risk than just editing the files.
 * - It plays nicely with the existing 80+ pre-translated language files
 *   that ship in this repo.
 *
 * Concurrency: writes go through `flock(LOCK_EX)` so two admins editing
 * the same language at once can't clobber each other's saves.
 */
class TranslationHelper
{
    /**
     * Source-of-truth file. Every other language's keys must exist here.
     */
    protected const SOURCE_LOCALE = 'en';

    public static function langPath(string $code): string
    {
        return base_path('lang/' . $code . '.json');
    }

    /**
     * Return the full English source as an associative array (key → value).
     * Cached for 60s so the editor page doesn't re-read a 3500-line file
     * on every Livewire update.
     */
    public static function getSourceStrings(): array
    {
        return Cache::remember('lang_source_strings', 60, function () {
            $path = self::langPath(self::SOURCE_LOCALE);
            if (!file_exists($path)) return [];
            $json = file_get_contents($path) ?: '{}';
            $data = json_decode($json, true);
            return is_array($data) ? $data : [];
        });
    }

    /**
     * Return a target locale's saved translations. Missing file → empty.
     */
    public static function getTranslations(string $code): array
    {
        $path = self::langPath($code);
        if (!file_exists($path)) return [];
        $json = file_get_contents($path) ?: '{}';
        $data = json_decode($json, true);
        return is_array($data) ? $data : [];
    }

    /**
     * Combined view used by the editor: each row has the source string,
     * the current translation (or empty), and a status flag.
     *
     * status:
     *   - translated: non-empty AND different from source
     *   - missing:    key not present in target file
     *   - same:       translation equals the source (likely untranslated)
     */
    public static function buildEditorRows(string $code): array
    {
        $source = self::getSourceStrings();
        $target = self::getTranslations($code);
        $rows = [];

        foreach ($source as $key => $sourceValue) {
            $hasKey = array_key_exists($key, $target);
            $value  = $hasKey ? (string) $target[$key] : '';

            $status = 'missing';
            if ($hasKey && $value !== '' && $value !== (string) $sourceValue) {
                $status = 'translated';
            } elseif ($hasKey && $value === (string) $sourceValue) {
                $status = 'same';
            }

            $rows[] = [
                'key'    => (string) $key,
                'source' => (string) $sourceValue,
                'value'  => $value,
                'status' => $status,
            ];
        }

        return $rows;
    }

    /**
     * High-level progress stats for the language picker / progress bar.
     */
    public static function getStats(string $code): array
    {
        $source = self::getSourceStrings();
        $target = self::getTranslations($code);
        $total = count($source);
        $translated = 0;

        foreach ($source as $key => $sourceValue) {
            if (array_key_exists($key, $target)
                && $target[$key] !== ''
                && $target[$key] !== $sourceValue) {
                $translated++;
            }
        }

        return [
            'total' => $total,
            'translated' => $translated,
            'missing' => $total - $translated,
            'percent' => $total > 0 ? (int) round(($translated / $total) * 100) : 0,
        ];
    }

    /**
     * Persist edits. $partial is `[key => translation]` — only keys present
     * here are touched, the rest of the file is preserved.
     *
     * Empty translations are stored as the empty string (not removed) so the
     * editor's "missing" filter still picks them up next time.
     */
    public static function saveTranslations(string $code, array $partial): array
    {
        if (!self::isValidLocale($code)) {
            throw new \InvalidArgumentException('Invalid locale code.');
        }

        $path = self::langPath($code);

        // Acquire exclusive lock on a sentinel file (works even if the JSON
        // doesn't exist yet — flock needs an existing fd).
        $lockPath = $path . '.lock';
        $lock = fopen($lockPath, 'c');
        if (!$lock) {
            throw new \RuntimeException('Could not acquire translation lock.');
        }
        flock($lock, LOCK_EX);

        try {
            $current = self::getTranslations($code);

            // Backup once per save so admins can roll back one step
            if (file_exists($path)) {
                @copy($path, $path . '.bak');
            }

            // Merge partial edits on top of the existing data
            foreach ($partial as $key => $value) {
                $current[$key] = (string) $value;
            }

            // Sort keys to match en.json ordering for clean diffs
            $source = self::getSourceStrings();
            $ordered = [];
            foreach ($source as $k => $_) {
                if (array_key_exists($k, $current)) {
                    $ordered[$k] = $current[$k];
                }
            }
            // Append any extra (orphan) keys at the end so we don't silently
            // drop them — these surface in the editor as "orphan" rows.
            foreach ($current as $k => $v) {
                if (!array_key_exists($k, $ordered)) $ordered[$k] = $v;
            }

            $json = json_encode($ordered, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if ($json === false) {
                throw new \RuntimeException('Failed to encode translations as JSON.');
            }

            file_put_contents($path, $json);
            Cache::forget('lang_source_strings');
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
            @unlink($lockPath);
        }

        return self::getStats($code);
    }

    /**
     * Replace the entire target file from an imported JSON payload.
     * Returns [imported_count, ignored_count] so the UI can show a summary.
     *
     * Strategy: only keys present in en.json are accepted (no orphans),
     * non-string values are coerced or skipped.
     */
    public static function importJson(string $code, string $json): array
    {
        if (!self::isValidLocale($code)) {
            throw new \InvalidArgumentException('Invalid locale code.');
        }

        $incoming = json_decode($json, true);
        if (!is_array($incoming)) {
            throw new \InvalidArgumentException('Uploaded file is not valid JSON.');
        }

        $source = self::getSourceStrings();
        $clean = [];
        $imported = 0;
        $ignored = 0;

        foreach ($incoming as $key => $value) {
            if (!array_key_exists($key, $source)) {
                $ignored++;
                continue;
            }
            $clean[$key] = is_scalar($value) ? (string) $value : '';
            $imported++;
        }

        self::saveTranslations($code, $clean);
        return ['imported' => $imported, 'ignored' => $ignored];
    }

    /**
     * Locale code sanitizer — letters, digits, hyphen, underscore only.
     * Stops a malicious "code" like `../../.env` from being used as a file
     * path. Always paired with langPath() which prepends base_path('lang/').
     */
    public static function isValidLocale(string $code): bool
    {
        return (bool) preg_match('/^[a-zA-Z][a-zA-Z0-9_-]{0,15}$/', $code);
    }
}
