<?php

declare(strict_types=1);

final class MyronNavigationDesignRepository
{
    private const DEFAULTS = [
        'desktop_alignment' => 'split',
        'menu_alignment' => 'right',
        'brand_display' => 'title',
        'brand_subtitle' => '',
        'logo_url' => '',
        'logo_light_url' => '',
        'logo_dark_url' => '',
        'logo_alt' => '',
        'utility_actions_display' => 'auto',
        'appearance_mode_policy' => 'auto',
        'density' => 'comfortable',
        'mobile_mode' => 'takeover',
        'surface_treatment' => 'bordered',
        'position_behavior' => 'static',
    ];

    private const OPTIONS = [
        'desktop_alignment' => ['split', 'centered', 'end'],
        'menu_alignment' => ['left', 'center', 'right'],
        'brand_display' => ['title', 'title-subtitle', 'logo', 'lockup'],
        'utility_actions_display' => ['auto', 'show', 'hide'],
        'appearance_mode_policy' => ['auto', 'light', 'dark'],
        'density' => ['compact', 'comfortable', 'spacious'],
        'mobile_mode' => ['takeover'],
        'surface_treatment' => ['plain', 'bordered', 'elevated'],
        'position_behavior' => ['static', 'sticky'],
    ];

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

    /**
     * @return array<string, list<string>>
     */
    public function optionSets(): array
    {
        return self::OPTIONS;
    }

    /**
     * @return array<string, string>
     */
    public function defaults(): array
    {
        return self::DEFAULTS;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listVersions(): array
    {
        $this->ensurePrimaryRow();
        $statement = $this->pdo->query('SELECT * FROM navigation_designs ORDER BY name ASC, id ASC');
        $rows = $statement ? $statement->fetchAll(PDO::FETCH_ASSOC) : [];
        return array_values(array_map(fn (array $row): array => $this->hydrate($row), array_filter($rows, 'is_array')));
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listActivatedSorted(): array
    {
        return array_values(array_filter($this->listVersions(), static fn (array $row): bool => (bool) ($row['is_activated'] ?? false)));
    }

    /**
     * @return array<string, mixed>
     */
    public function getDefault(): array
    {
        $this->ensurePrimaryRow();
        $statement = $this->pdo->query('SELECT * FROM navigation_designs WHERE is_default = 1 LIMIT 1');
        $row = $statement ? $statement->fetch(PDO::FETCH_ASSOC) : false;
        if (is_array($row)) {
            return $this->hydrate($row);
        }

        $first = $this->listVersions()[0] ?? $this->createVersion('Primary', [], '');
        $this->setDefault((string) $first['id']);
        return $this->getById((string) $first['id']) ?? $first;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function getById(string $id): ?array
    {
        $this->ensurePrimaryRow();
        $statement = $this->pdo->prepare('SELECT * FROM navigation_designs WHERE id = :id LIMIT 1');
        $statement->execute([':id' => trim($id)]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $this->hydrate($row) : null;
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    public function createVersion(string $name, array $settings = [], string $userId = ''): array
    {
        $name = $this->uniqueName($name !== '' ? $name : 'Untitled Navigation');
        $source = $settings !== [] ? $settings : $this->getDefault();
        $settings = $this->normalizeSettings($source, true);
        $id = myronGenerateId('navd');
        $now = myronNow();
        $statement = $this->pdo->prepare(
            'INSERT INTO navigation_designs (
                id, name, is_activated, is_default,
                desktop_alignment, menu_alignment, brand_display, brand_subtitle,
                logo_url, logo_light_url, logo_dark_url, logo_alt,
                utility_actions_display, appearance_mode_policy, density, mobile_mode,
                surface_treatment, position_behavior,
                created_at, updated_at, created_by_user_id, updated_by_user_id
             ) VALUES (
                :id, :name, 1, 0,
                :desktop_alignment, :menu_alignment, :brand_display, :brand_subtitle,
                :logo_url, :logo_light_url, :logo_dark_url, :logo_alt,
                :utility_actions_display, :appearance_mode_policy, :density, :mobile_mode,
                :surface_treatment, :position_behavior,
                :created_at, :updated_at, :created_by_user_id, :updated_by_user_id
             )'
        );
        $statement->execute(array_merge([
            ':id' => $id,
            ':name' => $name,
            ':created_at' => $now,
            ':updated_at' => $now,
            ':created_by_user_id' => $userId,
            ':updated_by_user_id' => $userId,
        ], $this->boundSettings($settings)));

        return $this->getById($id) ?? [];
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    public function saveVersion(string $id, array $settings, string $userId = ''): array
    {
        $existing = $this->getById($id);
        if (!is_array($existing)) {
            throw new RuntimeException('Navigation version not found.');
        }

        $normalized = $this->normalizeSettings(array_merge($existing, $settings), true);
        $name = trim((string) ($settings['name'] ?? $existing['name'] ?? 'Primary'));
        if ($name === '') {
            throw new RuntimeException('Navigation version name is required.');
        }

        $statement = $this->pdo->prepare(
            'UPDATE navigation_designs
             SET name = :name,
                 desktop_alignment = :desktop_alignment,
                 menu_alignment = :menu_alignment,
                 brand_display = :brand_display,
                 brand_subtitle = :brand_subtitle,
                 logo_url = :logo_url,
                 logo_light_url = :logo_light_url,
                 logo_dark_url = :logo_dark_url,
                 logo_alt = :logo_alt,
                 utility_actions_display = :utility_actions_display,
                 appearance_mode_policy = :appearance_mode_policy,
                 density = :density,
                 mobile_mode = :mobile_mode,
                 surface_treatment = :surface_treatment,
                 position_behavior = :position_behavior,
                 updated_by_user_id = :updated_by_user_id,
                 updated_at = :updated_at
             WHERE id = :id'
        );
            $statement->execute(array_merge([
                ':id' => $id,
                ':name' => $name,
                ':updated_by_user_id' => $userId,
                ':updated_at' => myronNow(),
            ], $this->boundSettings($normalized)));

        myronInvalidatePublicPageCache('navigation design changed');

            return $this->getById($id) ?? [];
        }

    public function saveActivation(string $id, bool $isActivated, string $userId = ''): void
    {
        $version = $this->getById($id);
        if (!is_array($version)) {
            throw new RuntimeException('Navigation version not found.');
        }
        if (!$isActivated && (bool) ($version['is_default'] ?? false)) {
            throw new RuntimeException('Set a new default before deactivating this version.');
        }

        $statement = $this->pdo->prepare(
            'UPDATE navigation_designs
             SET is_activated = :is_activated,
                 updated_by_user_id = :updated_by_user_id,
                 updated_at = :updated_at
             WHERE id = :id'
        );
            $statement->execute([
                ':is_activated' => $isActivated ? 1 : 0,
                ':updated_by_user_id' => $userId,
                ':updated_at' => myronNow(),
                ':id' => $id,
            ]);
        myronInvalidatePublicPageCache('navigation design activation changed');
        }

    public function setDefault(string $id, string $userId = ''): void
    {
        $version = $this->getById($id);
        if (!is_array($version) || !(bool) ($version['is_activated'] ?? false)) {
            throw new RuntimeException('Only activated navigation versions can be set as default.');
        }

        $now = myronNow();
        $this->pdo->beginTransaction();
        try {
            $this->pdo->exec('UPDATE navigation_designs SET is_default = 0');
            $statement = $this->pdo->prepare(
                'UPDATE navigation_designs
                 SET is_default = 1,
                     updated_by_user_id = :updated_by_user_id,
                     updated_at = :updated_at
                 WHERE id = :id'
            );
            $statement->execute([
                ':updated_by_user_id' => $userId,
                ':updated_at' => $now,
                ':id' => $id,
            ]);
            $this->pdo->commit();
            } catch (Throwable $exception) {
                if ($this->pdo->inTransaction()) {
                    $this->pdo->rollBack();
                }
                throw $exception;
            }
        myronInvalidatePublicPageCache('navigation design default changed');
        }

    /**
     * @return array<string, mixed>
     */
    public function duplicateVersion(string $id, string $userId = ''): array
    {
        $version = $this->getById($id);
        if (!is_array($version)) {
            throw new RuntimeException('Navigation version not found.');
        }

        return $this->createVersion((string) ($version['name'] ?? 'Navigation') . ' copy', $version, $userId);
    }

    public function deleteVersion(string $id): void
    {
        $version = $this->getById($id);
        if (!is_array($version)) {
            return;
        }
        if ((bool) ($version['is_default'] ?? false)) {
            throw new RuntimeException('The current default cannot be deleted. Change default first.');
        }
        if ($this->isReferencedByTemplate($id)) {
            throw new RuntimeException('Templates use this version. Reassign before deleting.');
        }

            $statement = $this->pdo->prepare('DELETE FROM navigation_designs WHERE id = :id');
            $statement->execute([':id' => $id]);
        myronInvalidatePublicPageCache('navigation design deleted');
        }

    public function isReferencedByTemplate(string $id): bool
    {
        $statement = $this->pdo->prepare('SELECT COUNT(*) FROM template_chrome_assignments WHERE navigation_design_id = :id');
        $statement->execute([':id' => trim($id)]);
        return (int) $statement->fetchColumn() > 0;
    }

    private function ensurePrimaryRow(): void
    {
        $count = (int) ($this->pdo->query('SELECT COUNT(*) FROM navigation_designs')?->fetchColumn() ?: 0);
        if ($count > 0) {
            return;
        }
        $this->createPrimaryRow(self::DEFAULTS);
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function createPrimaryRow(array $settings): void
    {
        $settings = $this->normalizeSettings($settings);
        $now = myronNow();
        $statement = $this->pdo->prepare(
            'INSERT INTO navigation_designs (
                id, name, is_activated, is_default,
                desktop_alignment, menu_alignment, brand_display, brand_subtitle,
                logo_url, logo_light_url, logo_dark_url, logo_alt,
                utility_actions_display, appearance_mode_policy, density, mobile_mode,
                surface_treatment, position_behavior,
                created_at, updated_at, created_by_user_id, updated_by_user_id
             )
             SELECT :id, :name, 1, 1,
                :desktop_alignment, :menu_alignment, :brand_display, :brand_subtitle,
                :logo_url, :logo_light_url, :logo_dark_url, :logo_alt,
                :utility_actions_display, :appearance_mode_policy, :density, :mobile_mode,
                :surface_treatment, :position_behavior,
                :created_at, :updated_at, "", ""
             WHERE NOT EXISTS (SELECT 1 FROM navigation_designs WHERE id = :id_existing)'
        );
        $statement->execute(array_merge([
            ':id' => 'nav_primary',
            ':id_existing' => 'nav_primary',
            ':name' => 'Primary',
            ':created_at' => $now,
            ':updated_at' => $now,
        ], $this->boundSettings($settings)));
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function hydrate(array $row): array
    {
        return array_merge([
            'id' => (string) ($row['id'] ?? ''),
            'name' => (string) ($row['name'] ?? ''),
            'is_activated' => (bool) ((int) ($row['is_activated'] ?? 0)),
            'is_default' => (bool) ((int) ($row['is_default'] ?? 0)),
            'created_at' => (string) ($row['created_at'] ?? ''),
            'updated_at' => (string) ($row['updated_at'] ?? ''),
        ], $this->normalizeSettings($row));
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, string>
     */
    private function normalizeSettings(array $row, bool $strict = false): array
    {
        $settings = [];
        foreach (self::DEFAULTS as $key => $default) {
            $value = trim((string) ($row[$key] ?? $default));
            if (isset(self::OPTIONS[$key]) && !in_array($value, self::OPTIONS[$key], true)) {
                if ($strict) {
                    throw new InvalidArgumentException('Navigation setting `' . $key . '` is invalid.');
                }
                $value = $default;
            }
            if (in_array($key, ['brand_subtitle', 'logo_url', 'logo_light_url', 'logo_dark_url', 'logo_alt'], true)) {
                $value = $this->normalizeTextSetting($key, $value, $strict);
            }
            $settings[$key] = $value;
        }
        if ($settings['logo_url'] === '' && $settings['logo_light_url'] !== '') {
            $settings['logo_url'] = $settings['logo_light_url'];
        }
        if ($settings['logo_light_url'] === '' && $settings['logo_url'] !== '') {
            $settings['logo_light_url'] = $settings['logo_url'];
        }
        if ($settings['logo_dark_url'] === '' && $settings['logo_url'] !== '') {
            $settings['logo_dark_url'] = $settings['logo_url'];
        }
        return $settings;
    }

    private function normalizeTextSetting(string $key, string $value, bool $strict): string
    {
        $limits = [
            'brand_subtitle' => 120,
            'logo_url' => 500,
            'logo_light_url' => 500,
            'logo_dark_url' => 500,
            'logo_alt' => 160,
        ];
        $limit = $limits[$key] ?? 255;
        if (strlen($value) > $limit) {
            if ($strict) {
                throw new InvalidArgumentException('Navigation setting `' . $key . '` is too long.');
            }
            $value = substr($value, 0, $limit);
        }

        if (in_array($key, ['logo_url', 'logo_light_url', 'logo_dark_url'], true) && $value !== '' && !$this->isAllowedLogoUrl($value)) {
            if ($strict) {
                throw new InvalidArgumentException('Logo URL must be a site-relative path or http(s) image URL.');
            }
            return '';
        }

        return $value;
    }

    private function isAllowedLogoUrl(string $value): bool
    {
        if (str_starts_with($value, '/') && !str_starts_with($value, '//')) {
            return true;
        }

        $parts = parse_url($value);
        if (!is_array($parts)) {
            return false;
        }

        $scheme = strtolower((string) ($parts['scheme'] ?? ''));
        return in_array($scheme, ['http', 'https'], true) && trim((string) ($parts['host'] ?? '')) !== '';
    }

    /**
     * @param array<string, string> $settings
     * @return array<string, string>
     */
    private function boundSettings(array $settings): array
    {
        $bound = [];
        foreach (array_keys(self::DEFAULTS) as $key) {
            $bound[':' . $key] = (string) ($settings[$key] ?? self::DEFAULTS[$key]);
        }
        return $bound;
    }

    private function uniqueName(string $name): string
    {
        $base = trim($name) !== '' ? trim($name) : 'Untitled Navigation';
        $candidate = $base;
        $suffix = 2;
        $statement = $this->pdo->prepare('SELECT COUNT(*) FROM navigation_designs WHERE name = :name');
        while (true) {
            $statement->execute([':name' => $candidate]);
            if ((int) $statement->fetchColumn() === 0) {
                return $candidate;
            }
            $candidate = $base . ' ' . $suffix;
            $suffix++;
        }
    }
}
