<?php

declare(strict_types=1);

trait MyronIdentityUserManagementTrait
{
    /**
     * @return list<array<string, mixed>>
     */
    public function listUsers(): array
    {
        $statement = $this->pdo->query(
            $this->baseUserSelect()
            . ' ORDER BY users.created_at ASC, users.email ASC'
        );

        $users = $statement->fetchAll();
        if (!is_array($users)) {
            return [];
        }

        return array_map(fn (array $user): array => $this->decorateUserRecord($user), $users);
    }

    /**
     * @param list<string> $grantedScopes
     */
    public function updateUserScopes(string $userId, array $grantedScopes): array
    {
        $existing = $this->findUserById($userId);
        if ($existing === null) {
            throw new RuntimeException('User not found.');
        }

        $scopes = self::normalizeGrantedScopes($grantedScopes);
        $hadDestructive = MyronApiScopes::covers(
            self::normalizeGrantedScopes($existing['granted_scopes'] ?? []),
            MyronApiScopes::ADMINISTRATION_DESTRUCTIVE
        );
        $keepsDestructive = MyronApiScopes::covers($scopes, MyronApiScopes::ADMINISTRATION_DESTRUCTIVE);
        if ($hadDestructive && !$keepsDestructive && $this->activeAdministratorCountExcluding($userId) < 1) {
            throw new RuntimeException('At least one administrator must retain destructive Administration access.');
        }

        $statement = $this->pdo->prepare(
            'UPDATE users
             SET granted_scopes_json = :granted_scopes_json,
                 updated_at = :updated_at
             WHERE id = :user_id'
        );
        $statement->execute([
            ':granted_scopes_json' => self::encodeGrantedScopes($scopes),
            ':updated_at' => myronNow(),
            ':user_id' => $userId,
        ]);

        $user = $this->findUserById($userId);
        if ($user === null) {
            throw new RuntimeException('Updated user could not be reloaded.');
        }

        return $user;
    }

    public function updateUserRole(string $userId, string $roleKey): array
    {
        $existing = $this->findUserById($userId);
        if ($existing === null) {
            throw new RuntimeException('User not found.');
        }

        $roleKey = trim($roleKey);
        $role = $this->requireRoleByKey($roleKey);
        $scopes = self::scopesForRoleKey($roleKey);
        $hadDestructive = MyronApiScopes::covers(
            self::normalizeGrantedScopes($existing['granted_scopes'] ?? []),
            MyronApiScopes::ADMINISTRATION_DESTRUCTIVE
        );
        $keepsDestructive = MyronApiScopes::covers($scopes, MyronApiScopes::ADMINISTRATION_DESTRUCTIVE);
        if ($hadDestructive && !$keepsDestructive && $this->activeAdministratorCountExcluding($userId) < 1) {
            throw new RuntimeException('At least one administrator must retain destructive Administration access.');
        }

        $statement = $this->pdo->prepare(
            'UPDATE users
             SET role_id = :role_id,
                 granted_scopes_json = :granted_scopes_json,
                 updated_at = :updated_at
             WHERE id = :user_id'
        );
        $statement->execute([
            ':role_id' => (string) $role['id'],
            ':granted_scopes_json' => self::encodeGrantedScopes($scopes),
            ':updated_at' => myronNow(),
            ':user_id' => $userId,
        ]);

        $user = $this->findUserById($userId);
        if ($user === null) {
            throw new RuntimeException('Updated user could not be reloaded.');
        }

        return $user;
    }

    public function updateUserAccess(string $userId, bool $design, bool $content, bool $administration): array
    {
        $roleKey = match (true) {
            $administration => 'administrator',
            $design && !$content => 'designer',
            $content && !$design => 'editor',
            default => 'viewer',
        };
        return $this->updateUserRole($userId, $roleKey);
    }

    public function updateProfile(string $userId, array $input): array
    {
        $existing = $this->findUserById($userId);
        if ($existing === null) {
            throw new RuntimeException('User not found.');
        }

        $email = myronNormalizeEmail((string) ($input['email'] ?? ''));
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('A valid email address is required.');
        }

        $displayName = trim((string) ($input['display_name'] ?? ''));
        if ($displayName === '') {
            throw new InvalidArgumentException('Display Name is required.');
        }

        $this->assertEmailAvailable($email, $userId);

        $firstName = trim((string) ($input['first_name'] ?? ''));
        $lastName = trim((string) ($input['last_name'] ?? ''));
        $bio = trim((string) ($input['bio'] ?? ''));
        $avatarUrl = $this->normalizeUrlField((string) ($input['avatar_url'] ?? ''), 'Avatar');
        $jobTitle = trim((string) ($input['job_title'] ?? ''));
        $sameAsUrls = $this->normalizeSeoSameAsUrls($input['same_as_urls'] ?? '');
        $socialUrls = [];
        foreach (self::SOCIAL_FIELDS as $field) {
            $label = ucwords(str_replace(['_url', '_'], ['', ' '], $field));
            $socialUrls[$field] = $this->normalizeUrlField((string) ($input[$field] ?? ''), $label);
        }

        $statement = $this->pdo->prepare(
            'UPDATE users
             SET email = :email,
                 display_name = :display_name,
                 first_name = :first_name,
                 last_name = :last_name,
                 bio = :bio,
                 avatar_url = :avatar_url,
                 facebook_url = :facebook_url,
                 x_url = :x_url,
                 pinterest_url = :pinterest_url,
                 linkedin_url = :linkedin_url,
                 youtube_url = :youtube_url,
                 instagram_url = :instagram_url,
                 tiktok_url = :tiktok_url,
                 job_title = :job_title,
                 same_as_urls_json = :same_as_urls_json,
                 updated_at = :updated_at
             WHERE id = :user_id'
        );
        $statement->execute([
            ':email' => $email,
            ':display_name' => $displayName,
            ':first_name' => $firstName,
            ':last_name' => $lastName,
            ':bio' => $bio,
            ':avatar_url' => $avatarUrl,
            ':facebook_url' => $socialUrls['facebook_url'],
            ':x_url' => $socialUrls['x_url'],
            ':pinterest_url' => $socialUrls['pinterest_url'],
            ':linkedin_url' => $socialUrls['linkedin_url'],
            ':youtube_url' => $socialUrls['youtube_url'],
            ':instagram_url' => $socialUrls['instagram_url'],
            ':tiktok_url' => $socialUrls['tiktok_url'],
            ':job_title' => $jobTitle,
            ':same_as_urls_json' => json_encode($sameAsUrls, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES),
            ':updated_at' => myronNow(),
            ':user_id' => $userId,
        ]);

        $user = $this->findUserById($userId);
        if ($user === null) {
            throw new RuntimeException('Updated profile could not be reloaded.');
        }

        return $user;
    }

    /**
     * @return list<string>
     */
    private function normalizeSeoSameAsUrls(mixed $value): array
    {
        $lines = is_array($value) ? $value : preg_split('/\R+/', (string) $value);
        $urls = [];
        foreach ($lines ?: [] as $line) {
            $url = trim((string) $line);
            if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
                continue;
            }
            $urls[$url] = true;
        }
        return array_keys($urls);
    }
}
