<?php
declare(strict_types=1);

namespace App\Languages;

use InvalidArgumentException;
use PDO;

final class LanguageRepository
{
    public function __construct(private readonly PDO $pdo)
    {
    }

    /** @return list<array<string, mixed>> */
    public function all(): array
    {
        $statement = $this->pdo->query(
            'SELECT l.code, l.name, l.native_name, l.direction, l.fallback_locale, l.is_enabled, l.sort_order,
                    l.created_at, l.updated_at, COUNT(t.translation_key) AS overrides_count
             FROM languages l
             LEFT JOIN translation_overrides t ON t.locale = l.code
             GROUP BY l.code
             ORDER BY l.sort_order ASC, l.native_name ASC, l.code ASC'
        );

        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    /** @return list<array<string, mixed>> */
    public function enabled(): array
    {
        $statement = $this->pdo->query(
            'SELECT code, name, native_name, direction, fallback_locale, is_enabled, sort_order
             FROM languages
             WHERE is_enabled = 1
             ORDER BY sort_order ASC, native_name ASC, code ASC'
        );

        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    /** @return array<string, string> */
    public function directions(): array
    {
        $directions = [];
        foreach ($this->enabled() as $language) {
            $code = (string) ($language['code'] ?? '');
            if ($code !== '') {
                $directions[$code] = (string) ($language['direction'] ?? 'ltr');
            }
        }
        return $directions;
    }

    public function exists(string $code): bool
    {
        $statement = $this->pdo->prepare('SELECT 1 FROM languages WHERE code = :code LIMIT 1');
        $statement->execute(['code' => $this->normalizeCode($code)]);
        return (bool) $statement->fetchColumn();
    }

    /** @return array<string, mixed>|null */
    public function find(string $code): ?array
    {
        $statement = $this->pdo->prepare(
            'SELECT code, name, native_name, direction, fallback_locale, is_enabled, sort_order
             FROM languages WHERE code = :code LIMIT 1'
        );
        $statement->execute(['code' => $this->normalizeCode($code)]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $row : null;
    }

    public function save(array $input): string
    {
        $code = $this->normalizeCode((string) ($input['code'] ?? ''));
        $name = trim((string) ($input['name'] ?? ''));
        $nativeName = trim((string) ($input['native_name'] ?? ''));
        $direction = strtolower(trim((string) ($input['direction'] ?? 'ltr')));
        $fallback = trim((string) ($input['fallback_locale'] ?? ''));
        $fallback = $fallback === '' ? null : $this->normalizeCode($fallback);
        $enabled = !empty($input['is_enabled']);
        $sortOrder = (int) ($input['sort_order'] ?? 0);

        if ($name === '' || $nativeName === '') {
            throw new InvalidArgumentException('Language names are required.');
        }
        if (!in_array($direction, ['rtl', 'ltr'], true)) {
            throw new InvalidArgumentException('Language direction is invalid.');
        }
        if ($fallback === $code) {
            throw new InvalidArgumentException('A language cannot fall back to itself.');
        }
        if ($fallback !== null) {
            $fallbackRow = $this->find($fallback);
            if (!is_array($fallbackRow)) {
                throw new InvalidArgumentException('Fallback language does not exist.');
            }
            if (empty($fallbackRow['is_enabled'])) {
                throw new InvalidArgumentException('Fallback language must be enabled.');
            }
            $this->assertFallbackChain($code, $fallback);
        }

        $statement = $this->pdo->prepare(
            'INSERT INTO languages (code, name, native_name, direction, fallback_locale, is_enabled, sort_order, created_at, updated_at)
             VALUES (:code, :name, :native_name, :direction, :fallback_locale, :is_enabled, :sort_order, NOW(), NOW())
             ON DUPLICATE KEY UPDATE
                name = VALUES(name), native_name = VALUES(native_name), direction = VALUES(direction),
                fallback_locale = VALUES(fallback_locale), is_enabled = VALUES(is_enabled),
                sort_order = VALUES(sort_order), updated_at = NOW()'
        );
        $statement->execute([
            'code' => $code,
            'name' => $name,
            'native_name' => $nativeName,
            'direction' => $direction,
            'fallback_locale' => $fallback,
            'is_enabled' => $enabled ? 1 : 0,
            'sort_order' => $sortOrder,
        ]);

        return $code;
    }

    public function fallbackLocaleFor(string $code, string $defaultLocale): string
    {
        $code = $this->normalizeCode($code);
        $defaultLocale = $this->normalizeCode($defaultLocale);
        $language = $this->find($code);
        $fallback = is_array($language) ? trim((string) ($language['fallback_locale'] ?? '')) : '';

        if ($fallback !== '') {
            $fallbackRow = $this->find($fallback);
            if (is_array($fallbackRow) && !empty($fallbackRow['is_enabled'])) {
                return (string) $fallbackRow['code'];
            }
        }

        if ($defaultLocale !== $code) {
            $defaultRow = $this->find($defaultLocale);
            if (is_array($defaultRow) && !empty($defaultRow['is_enabled'])) {
                return (string) $defaultRow['code'];
            }
        }

        return $code;
    }

    /** @return list<string> */
    public function fallbackDependents(string $code): array
    {
        $statement = $this->pdo->prepare(
            'SELECT code FROM languages WHERE fallback_locale = :code AND is_enabled = 1 ORDER BY code ASC'
        );
        $statement->execute(['code' => $this->normalizeCode($code)]);
        return array_values(array_map('strval', $statement->fetchAll(PDO::FETCH_COLUMN)));
    }

    /** @return array{categories:int,tags:int} */
    public function taxonomyCountsOutsideLocale(string $locale): array
    {
        $locale = $this->normalizeCode($locale);
        $categories = $this->pdo->prepare('SELECT COUNT(*) FROM categories WHERE content_locale <> :locale OR content_locale IS NULL');
        $categories->execute(['locale' => $locale]);
        $tags = $this->pdo->prepare('SELECT COUNT(*) FROM tags WHERE content_locale <> :locale OR content_locale IS NULL');
        $tags->execute(['locale' => $locale]);
        return ['categories' => (int) $categories->fetchColumn(), 'tags' => (int) $tags->fetchColumn()];
    }

    /** @return array{categories:int,tags:int,questions:int} */
    public function contentReferences(string $locale): array
    {
        $locale = $this->normalizeCode($locale);
        $counts = [];
        foreach (['categories', 'tags', 'questions'] as $table) {
            $statement = $this->pdo->prepare('SELECT COUNT(*) FROM ' . $table . ' WHERE content_locale = :locale');
            $statement->execute(['locale' => $locale]);
            $counts[$table] = (int) $statement->fetchColumn();
        }
        return $counts;
    }

    public function delete(string $code, string $defaultLocale, string $adminLocale): void
    {
        $code = $this->normalizeCode($code);
        $language = $this->find($code);
        if (!is_array($language)) {
            throw new InvalidArgumentException('Language does not exist.');
        }
        if ($code === $this->normalizeCode($defaultLocale) || $code === $this->normalizeCode($adminLocale)) {
            throw new InvalidArgumentException('The default language cannot be deleted.');
        }
        if ($this->fallbackDependents($code) !== []) {
            throw new InvalidArgumentException('A language with active fallback dependents cannot be deleted.');
        }
        $references = $this->contentReferences($code);
        if (($references['categories'] ?? 0) > 0 || ($references['tags'] ?? 0) > 0 || ($references['questions'] ?? 0) > 0) {
            throw new InvalidArgumentException('A language referenced by content cannot be deleted.');
        }
        if (!empty($language['is_enabled'])) {
            $enabledCount = (int) $this->pdo->query('SELECT COUNT(*) FROM languages WHERE is_enabled = 1')->fetchColumn();
            if ($enabledCount <= 1) {
                throw new InvalidArgumentException('The last enabled language cannot be deleted.');
            }
        }

        $this->pdo->beginTransaction();
        try {
            $this->pdo->prepare('DELETE FROM translation_overrides WHERE locale = :locale')->execute(['locale' => $code]);
            $this->pdo->prepare('UPDATE users SET preferred_locale = :replacement WHERE preferred_locale = :locale')->execute(['replacement' => $this->normalizeCode($defaultLocale), 'locale' => $code]);
            $this->reassignAdminLocale($code, $adminLocale);
            $this->pdo->prepare('DELETE FROM languages WHERE code = :code')->execute(['code' => $code]);
            $this->pdo->commit();
        } catch (\Throwable $throwable) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
    }

    /** @return array<string, string> */
    public function overrides(string $locale): array
    {
        $statement = $this->pdo->prepare(
            'SELECT translation_key, value FROM translation_overrides WHERE locale = :locale ORDER BY translation_key ASC'
        );
        $statement->execute(['locale' => $this->normalizeCode($locale)]);
        $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
        $values = [];
        foreach ($rows as $row) {
            $key = (string) ($row['translation_key'] ?? '');
            if ($key !== '') {
                $values[$key] = (string) ($row['value'] ?? '');
            }
        }
        return $values;
    }

    public function saveOverride(string $locale, string $key, string $value, int $actorId): void
    {
        $locale = $this->normalizeCode($locale);
        $key = trim($key);
        if (!$this->exists($locale)) {
            throw new InvalidArgumentException('Language does not exist.');
        }
        if ($key === '' || strlen($key) > 191 || preg_match('/^[A-Za-z0-9_.-]+$/', $key) !== 1) {
            throw new InvalidArgumentException('Translation key is invalid.');
        }

        $statement = $this->pdo->prepare(
            'INSERT INTO translation_overrides (locale, translation_key, value, updated_by, created_at, updated_at)
             VALUES (:locale, :translation_key, :value, :updated_by, NOW(), NOW())
             ON DUPLICATE KEY UPDATE value = VALUES(value), updated_by = VALUES(updated_by), updated_at = NOW()'
        );
        $statement->execute([
            'locale' => $locale,
            'translation_key' => $key,
            'value' => $value,
            'updated_by' => $actorId > 0 ? $actorId : null,
        ]);
    }

    public function deleteOverride(string $locale, string $key): void
    {
        $statement = $this->pdo->prepare(
            'DELETE FROM translation_overrides WHERE locale = :locale AND translation_key = :translation_key'
        );
        $statement->execute([
            'locale' => $this->normalizeCode($locale),
            'translation_key' => trim($key),
        ]);
    }

    public function adminLocaleForUser(int $userId): ?string
    {
        $statement = $this->pdo->prepare('SELECT admin_locale FROM user_preferences WHERE user_id = :user_id LIMIT 1');
        $statement->execute(['user_id' => $userId]);
        $value = $statement->fetchColumn();
        if (!is_string($value) || trim($value) === '') {
            return null;
        }
        $code = $this->normalizeCode($value);
        $language = $this->find($code);
        return is_array($language) && !empty($language['is_enabled']) ? $code : null;
    }

    public function setAdminLocaleForUser(int $userId, string $locale): void
    {
        $locale = $this->normalizeCode($locale);
        $language = $this->find($locale);
        if (!is_array($language) || empty($language['is_enabled'])) {
            throw new InvalidArgumentException('Admin locale must be enabled.');
        }
        $statement = $this->pdo->prepare(
            'INSERT INTO user_preferences (user_id, admin_locale, updated_at)
             VALUES (:user_id, :admin_locale, NOW())
             ON DUPLICATE KEY UPDATE admin_locale = VALUES(admin_locale), updated_at = NOW()'
        );
        $statement->execute(['user_id' => $userId, 'admin_locale' => $locale]);
    }

    public function reassignAdminLocale(string $locale, string $replacement): void
    {
        $statement = $this->pdo->prepare('UPDATE user_preferences SET admin_locale = :replacement WHERE admin_locale = :locale');
        $statement->execute([
            'replacement' => $this->normalizeCode($replacement),
            'locale' => $this->normalizeCode($locale),
        ]);
    }

    private function assertFallbackChain(string $code, string $fallback): void
    {
        $visited = [];
        $current = $fallback;
        while ($current !== '') {
            if ($current === $code) {
                throw new InvalidArgumentException('The selected fallback would create a language fallback cycle.');
            }
            if (isset($visited[$current])) {
                throw new InvalidArgumentException('The language fallback chain already contains a cycle.');
            }
            $visited[$current] = true;
            $row = $this->find($current);
            if (!is_array($row)) {
                return;
            }
            $current = trim((string) ($row['fallback_locale'] ?? ''));
        }
    }

    private function normalizeCode(string $code): string
    {
        $code = strtolower(str_replace('_', '-', trim($code)));
        if ($code === '' || strlen($code) > 16 || preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/', $code) !== 1) {
            throw new InvalidArgumentException('Language code is invalid.');
        }
        return $code;
    }
}
