<?php

declare(strict_types=1);

require_once __DIR__ . '/auth/AuthAccountTrait.php';
require_once __DIR__ . '/users/UserManagementTrait.php';
require_once __DIR__ . '/registration/RegistrationPolicyTrait.php';
require_once __DIR__ . '/RoleScopeProjector.php';
require_once dirname(__DIR__, 2) . '/system/api/Scopes.php';

final class MyronAuthRepository
{
    use MyronAuthAccountTrait;
    use MyronIdentityUserManagementTrait;
    use MyronIdentityRegistrationPolicyTrait;

    private const SOCIAL_FIELDS = [
        'facebook_url',
        'x_url',
        'pinterest_url',
        'linkedin_url',
        'youtube_url',
        'instagram_url',
        'tiktok_url',
    ];

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

    /**
     * @return list<array{input: string, normalized: string, runtimeRoles: list<string>, lossy: bool, notes: string}>
     */
    public function roleProjectionCatalog(): array
    {
        return MyronRoleScopeProjector::catalog();
    }

    /**
     * @return list<string>
     */
    public function projectRoleTerm(string $role): array
    {
        return MyronRoleScopeProjector::projectRole($role);
    }

    /**
     * @param array<string, mixed>|null $roleScope
     * @return array<string, list<string>>|null
     */
    public function projectRoleScope(?array $roleScope): ?array
    {
        return MyronRoleScopeProjector::projectRoleScope($roleScope);
    }

    /**
     * @param list<string> $roleTerms
     */
    public function userMatchesRoleTerms(array $user, array $roleTerms): bool
    {
        return MyronRoleScopeProjector::userMatchesRoles($user, $roleTerms);
    }

    private function baseUserSelect(): string
    {
        return 'SELECT
                users.*,
                roles.role_key,
                roles.name AS role_name
            FROM users
            INNER JOIN roles ON roles.id = users.role_id';
    }

    private function decorateUserRecord(array $user): array
    {
        $roleKey = (string) ($user['role_key'] ?? '');
        $roleName = (string) ($user['role_name'] ?? '');
        $scopes = self::normalizeGrantedScopes($user['granted_scopes_json'] ?? null);

        $user['granted_scopes'] = $scopes;
        $user['effective_role_key'] = $roleKey !== '' ? $roleKey : 'custom';
        $user['effective_role_name'] = $roleName !== '' ? $roleName : 'Custom';
        $user['permission_level_label'] = self::permissionLevelLabel($roleKey, $scopes);

        $user['is_active'] = (int) ($user['is_active'] ?? 0) === 1;

        return $user;
    }

    private function requireRoleByKey(string $roleKey): array
    {
        $statement = $this->pdo->prepare('SELECT * FROM roles WHERE role_key = :role_key LIMIT 1');
        $statement->execute([':role_key' => $roleKey]);
        $role = $statement->fetch();

        if (!is_array($role)) {
            throw new RuntimeException('Required role `' . $roleKey . '` is missing.');
        }

        return $role;
    }

    private function generateUniqueUsername(string $email, ?string $excludeUserId = null): string
    {
        $localPart = explode('@', myronNormalizeEmail($email), 2)[0] ?? 'user';
        $base = myronNormalizeUsername($localPart);
        $candidate = $base;
        $suffix = 2;

        while ($this->usernameExists($candidate, $excludeUserId)) {
            $candidate = $base . '-' . $suffix;
            $suffix++;
        }

        return $candidate;
    }

    private function usernameExists(string $username, ?string $excludeUserId = null): bool
    {
        $sql = 'SELECT id FROM users WHERE username = :username';
        $params = [':username' => $username];
        if ($excludeUserId !== null) {
            $sql .= ' AND id != :exclude_user_id';
            $params[':exclude_user_id'] = $excludeUserId;
        }

        $sql .= ' LIMIT 1';
        $statement = $this->pdo->prepare($sql);
        $statement->execute($params);

        return $statement->fetchColumn() !== false;
    }

    private function assertEmailAvailable(string $email, ?string $excludeUserId = null): void
    {
        $sql = 'SELECT id FROM users WHERE email = :email';
        $params = [':email' => $email];
        if ($excludeUserId !== null) {
            $sql .= ' AND id != :exclude_user_id';
            $params[':exclude_user_id'] = $excludeUserId;
        }
        $sql .= ' LIMIT 1';

        $statement = $this->pdo->prepare($sql);
        $statement->execute($params);
        if ($statement->fetchColumn() !== false) {
            throw new InvalidArgumentException('That email address is already in use.');
        }
    }

    private function assertPasswordPolicy(string $password): void
    {
        if (strlen($password) < 8) {
            throw new InvalidArgumentException('Passwords must be at least 8 characters long.');
        }
    }

    private function activeAdministratorCountExcluding(string $excludeUserId): int
    {
        $statement = $this->pdo->prepare(
            'SELECT id, granted_scopes_json
             FROM users
             WHERE is_active = 1
               AND id != :exclude_user_id'
        );
        $statement->execute([':exclude_user_id' => $excludeUserId]);
        $count = 0;
        foreach ($statement->fetchAll() ?: [] as $row) {
            if (!is_array($row)) {
                continue;
            }
            if (MyronApiScopes::covers(self::normalizeGrantedScopes($row['granted_scopes_json'] ?? null), MyronApiScopes::ADMINISTRATION_DESTRUCTIVE)) {
                $count++;
            }
        }

        return $count;
    }

    private function normalizeUrlField(string $value, string $label): string
    {
        $trimmed = trim($value);
        if ($trimmed === '') {
            return '';
        }

        if (!filter_var($trimmed, FILTER_VALIDATE_URL)) {
            throw new InvalidArgumentException($label . ' URL must be a valid URL.');
        }

        return $trimmed;
    }

    private function normalizeAllowedDomains(array $domains): array
    {
        $normalized = [];
        foreach ($domains as $domain) {
            $value = strtolower(trim((string) $domain));
            $value = ltrim($value, '@');
            if ($value === '') {
                continue;
            }

            if (!preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/', $value)) {
                throw new InvalidArgumentException('Allowed domains must be valid email domains.');
            }

            $normalized[$value] = true;
        }

        return array_keys($normalized);
    }

    private function decodeAllowedDomains(string $json): array
    {
        $decoded = json_decode($json, true);
        if (!is_array($decoded)) {
            return [];
        }

        return $this->normalizeAllowedDomains($decoded);
    }

    private function emailDomainAllowed(string $email, array $allowedDomains): bool
    {
        $domain = strtolower((string) (explode('@', myronNormalizeEmail($email), 2)[1] ?? ''));
        if ($domain === '') {
            return false;
        }

        return in_array($domain, $allowedDomains, true);
    }

    private function expireOldRegistrationTokens(string $userId): void
    {
        $statement = $this->pdo->prepare(
            'UPDATE registration_tokens
             SET consumed_at = :consumed_at
             WHERE user_id = :user_id
               AND consumed_at IS NULL'
        );
        $statement->execute([
            ':consumed_at' => myronNow(),
            ':user_id' => $userId,
        ]);
    }

    /**
     * @return array{0: array<string, mixed>, 1: array<string, mixed>}
     */
    private function requireActiveRegistrationToken(string $token): array
    {
        $parts = explode('.', trim($token), 2);
        if (count($parts) !== 2 || trim($parts[0]) === '' || trim($parts[1]) === '') {
            throw new InvalidArgumentException('That registration link is invalid.');
        }

        [$selector, $verifier] = $parts;
        $statement = $this->pdo->prepare(
            'SELECT * FROM registration_tokens WHERE selector = :selector LIMIT 1'
        );
        $statement->execute([':selector' => $selector]);
        $record = $statement->fetch();
        if (!is_array($record)) {
            throw new InvalidArgumentException('That registration link is invalid.');
        }

        if ((string) ($record['consumed_at'] ?? '') !== '') {
            throw new InvalidArgumentException('That registration link has already been used.');
        }

        if (strtotime((string) ($record['expires_at'] ?? '')) < time()) {
            throw new InvalidArgumentException('That registration link has expired.');
        }

        if (!hash_equals((string) ($record['verifier_hash'] ?? ''), hash('sha256', $verifier))) {
            throw new InvalidArgumentException('That registration link is invalid.');
        }

        $user = $this->findUserById((string) $record['user_id']);
        if (!is_array($user)) {
            throw new InvalidArgumentException('That registration link is invalid.');
        }

        return [$record, $user];
    }

    /**
     * @return array<string, array{name: string, description: string, granted_scopes: list<string>}>
     */
    public static function roleCatalog(): array
    {
        return [
            'viewer' => [
                'name' => 'Viewer',
                'description' => 'Read-only access across Design, Content, and Administration surfaces.',
                'granted_scopes' => [
                    MyronApiScopes::DESIGN_READ,
                    MyronApiScopes::CONTENT_READ,
                    MyronApiScopes::ADMINISTRATION_READ,
                ],
            ],
            'designer' => [
                'name' => 'Designer',
                'description' => 'Design-context write access; read access elsewhere.',
                'granted_scopes' => [
                    MyronApiScopes::DESIGN_READ,
                    MyronApiScopes::DESIGN_WRITE,
                    MyronApiScopes::CONTENT_READ,
                    MyronApiScopes::ADMINISTRATION_READ,
                ],
            ],
            'editor' => [
                'name' => 'Editor',
                'description' => 'Content-context write access; read access elsewhere.',
                'granted_scopes' => [
                    MyronApiScopes::DESIGN_READ,
                    MyronApiScopes::CONTENT_READ,
                    MyronApiScopes::CONTENT_WRITE,
                    MyronApiScopes::ADMINISTRATION_READ,
                ],
            ],
            'administrator' => [
                'name' => 'Administrator',
                'description' => 'Full access including destructive operations like snapshot restore and site type changes.',
                'granted_scopes' => [
                    MyronApiScopes::DESIGN_READ,
                    MyronApiScopes::DESIGN_WRITE,
                    MyronApiScopes::CONTENT_READ,
                    MyronApiScopes::CONTENT_WRITE,
                    MyronApiScopes::ADMINISTRATION_READ,
                    MyronApiScopes::ADMINISTRATION_WRITE,
                    MyronApiScopes::ADMINISTRATION_DESTRUCTIVE,
                ],
            ],
        ];
    }

    /**
     * @return list<string>
     */
    public static function scopesForRoleKey(string $roleKey): array
    {
        $role = self::roleCatalog()[$roleKey] ?? null;
        return is_array($role) ? array_values($role['granted_scopes']) : [];
    }

    /**
     * @return list<string>
     */
    public static function normalizeGrantedScopes(mixed $raw): array
    {
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            $raw = is_array($decoded) ? $decoded : [];
        }
        if (!is_array($raw)) {
            return [];
        }
        $allowed = array_fill_keys(self::allKnownScopes(), true);
        $scopes = [];
        foreach ($raw as $scope) {
            $scope = trim((string) $scope);
            if ($scope === '' || !isset($allowed[$scope])) {
                continue;
            }
            $scopes[$scope] = true;
        }
        return array_values(array_keys($scopes));
    }

    /**
     * @return list<string>
     */
    public static function allKnownScopes(): array
    {
        return [
            MyronApiScopes::DESIGN_READ,
            MyronApiScopes::DESIGN_WRITE,
            MyronApiScopes::CONTENT_READ,
            MyronApiScopes::CONTENT_WRITE,
            MyronApiScopes::ADMINISTRATION_READ,
            MyronApiScopes::ADMINISTRATION_WRITE,
            MyronApiScopes::ADMINISTRATION_DESTRUCTIVE,
        ];
    }

    /**
     * @param list<string> $scopes
     */
    private static function encodeGrantedScopes(array $scopes): string
    {
        return json_encode(self::normalizeGrantedScopes($scopes), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
    }

    /**
     * @param list<string> $scopes
     */
    public static function permissionLevelLabel(string $roleKey, array $scopes): string
    {
        $catalog = self::roleCatalog();
        $role = $catalog[$roleKey] ?? null;
        $roleName = is_array($role) ? (string) $role['name'] : 'Custom';
        $normalized = self::normalizeGrantedScopes($scopes);
        $template = is_array($role) ? array_values($role['granted_scopes']) : [];

        sort($normalized);
        sort($template);

        if ($roleKey === 'administrator') {
            $full = self::scopesForRoleKey('administrator');
            $operational = array_values(array_filter(
                $full,
                static fn (string $scope): bool => $scope !== MyronApiScopes::ADMINISTRATION_DESTRUCTIVE
            ));
            sort($full);
            sort($operational);
            if ($normalized === $full) {
                return 'Administrator · Full';
            }
            if ($normalized === $operational) {
                return 'Administrator · Operational';
            }
            return 'Administrator · Custom';
        }

        return $normalized === $template ? $roleName : $roleName . ' · Custom';
    }
}
