<?php
declare(strict_types=1);

namespace App\Theme;

use App\Security\UrlPolicy;
use PDO;
use Throwable;

/**
 * Managed navigation menus.
 *
 * The header links used to be hardcoded in the layout template, so an operator
 * could not add, reorder or remove one without editing PHP. Items are stored
 * per menu key and may carry either a literal label or a translation key, so
 * the seeded defaults stay translated in every enabled language while
 * administrator-authored items use their own wording.
 */
final class MenuRepository
{
    public const AUDIENCES = ['any', 'guest', 'member', 'admin'];

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

    /**
     * Items for one menu, already filtered for the current viewer.
     *
     * Filtering happens here rather than in the template so a restricted item
     * is absent from the HTML entirely, not merely hidden with CSS.
     *
     * @return array<int, array<string, mixed>>
     */
    public function visible(string $menuKey, ?array $currentUser, string $locale): array
    {
        try {
            $statement = $this->pdo->prepare(
                'SELECT id, menu_key, label, translation_key, url, audience, locales, opens_new_tab, sort_order
                 FROM menu_items
                 WHERE menu_key = :menu_key AND is_enabled = 1
                 ORDER BY sort_order ASC, id ASC'
            );
            $statement->execute(['menu_key' => $menuKey]);
            $rows = $statement->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            // Before migration 026 the table does not exist; the caller falls
            // back to its built-in links.
            return [];
        }

        $isMember = is_array($currentUser) && (int) ($currentUser['id'] ?? 0) > 0;
        $isAdmin = quaeriva_user_has_role($currentUser, 'admin', 'moderator');
        $locale = strtolower(trim($locale));

        $visible = [];

        foreach ($rows as $row) {
            $audience = (string) ($row['audience'] ?? 'any');

            if ($audience === 'guest' && $isMember) continue;
            if ($audience === 'member' && !$isMember) continue;
            if ($audience === 'admin' && !$isAdmin) continue;

            $locales = array_values(array_filter(array_map(
                static fn (string $code): string => strtolower(trim($code)),
                explode(',', (string) ($row['locales'] ?? ''))
            )));

            if ($locales !== [] && !in_array($locale, $locales, true)) {
                continue;
            }

            $visible[] = $row;
        }

        return $visible;
    }

    /** @return array<int, array<string, mixed>> */
    public function all(string $menuKey = 'primary'): array
    {
        try {
            $statement = $this->pdo->prepare(
                'SELECT id, menu_key, label, translation_key, url, audience, locales, is_enabled, opens_new_tab, sort_order
                 FROM menu_items WHERE menu_key = :menu_key ORDER BY sort_order ASC, id ASC'
            );
            $statement->execute(['menu_key' => $menuKey]);
            return $statement->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return [];
        }
    }

    /** @param array<string, mixed> $input */
    public function save(array $input): void
    {
        $id = (int) ($input['id'] ?? 0);
        $label = trim((string) ($input['label'] ?? ''));
        $url = trim((string) ($input['url'] ?? ''));
        $audience = in_array((string) ($input['audience'] ?? 'any'), self::AUDIENCES, true)
            ? (string) $input['audience']
            : 'any';

        if ($url === '' || !UrlPolicy::isAllowed($url)) {
            throw new \InvalidArgumentException('Menu URL is invalid.');
        }

        if ($label === '' && $id <= 0) {
            throw new \InvalidArgumentException('Menu label is required.');
        }

        $params = [
            'menu_key' => 'primary',
            'label' => $label === '' ? null : $label,
            'url' => $url,
            'audience' => $audience,
            'locales' => trim((string) ($input['locales'] ?? '')),
            'is_enabled' => empty($input['is_enabled']) ? 0 : 1,
            'opens_new_tab' => empty($input['opens_new_tab']) ? 0 : 1,
            'sort_order' => (int) ($input['sort_order'] ?? 0),
        ];

        if ($id > 0) {
            // translation_key is intentionally left alone on update: a seeded
            // item keeps its translated label until an administrator types one.
            $params['id'] = $id;
            $statement = $this->pdo->prepare(
                'UPDATE menu_items SET label=:label, url=:url, audience=:audience, locales=:locales,
                        is_enabled=:is_enabled, opens_new_tab=:opens_new_tab, sort_order=:sort_order, updated_at=NOW()
                 WHERE id=:id'
            );
            unset($params['menu_key']);
            $statement->execute($params);
            return;
        }

        $statement = $this->pdo->prepare(
            'INSERT INTO menu_items (menu_key, label, translation_key, url, audience, locales, is_enabled, opens_new_tab, sort_order, created_at, updated_at)
             VALUES (:menu_key, :label, NULL, :url, :audience, :locales, :is_enabled, :opens_new_tab, :sort_order, NOW(), NOW())'
        );
        $statement->execute($params);
    }

    public function delete(int $id): void
    {
        $statement = $this->pdo->prepare('DELETE FROM menu_items WHERE id = :id');
        $statement->execute(['id' => $id]);
    }

    /**
     * Persist an administrator's complete visual order for one menu.
     *
     * Unknown, duplicate and omitted IDs are ignored or appended in their
     * existing order so a stale browser cannot make another item disappear.
     *
     * @param array<int, mixed> $order
     */
    public function reorder(array $order, string $menuKey = 'primary'): void
    {
        $submittedIds = [];
        foreach ($order as $value) {
            $id = (int) $value;
            if ($id > 0 && !in_array($id, $submittedIds, true)) {
                $submittedIds[] = $id;
            }
        }

        $existingStatement = $this->pdo->prepare(
            'SELECT id FROM menu_items WHERE menu_key = :menu_key ORDER BY sort_order ASC, id ASC'
        );
        $existingStatement->execute(['menu_key' => $menuKey]);
        $existingIds = array_map('intval', $existingStatement->fetchAll(PDO::FETCH_COLUMN));
        if ($existingIds === []) {
            return;
        }

        $orderedIds = array_values(array_intersect($submittedIds, $existingIds));
        foreach ($existingIds as $existingId) {
            if (!in_array($existingId, $orderedIds, true)) {
                $orderedIds[] = $existingId;
            }
        }

        $startedTransaction = !$this->pdo->inTransaction();
        if ($startedTransaction) {
            $this->pdo->beginTransaction();
        }

        try {
            $statement = $this->pdo->prepare(
                'UPDATE menu_items SET sort_order = :sort_order, updated_at = NOW()
                 WHERE id = :id AND menu_key = :menu_key'
            );
            foreach ($orderedIds as $position => $id) {
                $statement->execute([
                    'sort_order' => ($position + 1) * 10,
                    'id' => $id,
                    'menu_key' => $menuKey,
                ]);
            }

            if ($startedTransaction) {
                $this->pdo->commit();
            }
        } catch (Throwable $exception) {
            if ($startedTransaction && $this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $exception;
        }
    }

}
