<?php

declare(strict_types=1);

/**
 * APO-196 Card 5 — Identity action service.
 *
 * 4 admin verbs (covered by 4 service methods, registered with adminAliases
 * that match BOTH profile.php (self-service) and users.php (admin-managing)
 * uses — the substrate audit's cross-domain alias matching makes one action
 * cover both occurrences):
 *
 *   - save-profile      (self OR admin-managing another user via userId param)
 *   - save-password     (self OR admin-managing — exposeToMcp:false)
 *   - save-scopes       (admin-only; sets explicit user scopes)
 *   - save-registration-settings (admin-only)
 *
 * Self-vs-admin distinction is enforced at the route gate. The service-layer
 * takes a userId; the route decides whether the caller can act on it.
 */
final class MyronIdentityActionService
{
    public function __construct(
        private readonly MyronAuthRepository $auth,
    ) {
    }

    /**
     * @param array<string, mixed> $input
     * @return array{userId: string}
     */
    public function updateProfile(string $userId, array $input, MyronActionCallerContext $caller): array
    {
        $userId = trim($userId);
        if ($userId === '') {
            throw new InvalidArgumentException('userId is required.');
        }
        $saved = (array) $this->auth->updateProfile($userId, $input);
        return ['userId' => (string) ($saved['id'] ?? $userId)];
    }

    /**
     * @return array{userId: string}
     */
    public function updatePassword(string $userId, string $password, string $confirmation, MyronActionCallerContext $caller): array
    {
        $userId = trim($userId);
        if ($userId === '') {
            throw new InvalidArgumentException('userId is required.');
        }
        if ($password === '' || $password !== $confirmation) {
            throw new InvalidArgumentException('passwords must match and be non-empty.');
        }
        $this->auth->updatePassword($userId, $password);
        return ['userId' => $userId];
    }

    /**
     * @param list<string> $grantedScopes
     * @return array{user_id: string, prior_scopes: list<string>, granted_scopes: list<string>, role_id: string}
     */
    public function updateUserScopes(
        string $userId,
        array $grantedScopes,
        MyronActionCallerContext $caller
    ): array {
        $userId = trim($userId);
        if ($userId === '') {
            throw new InvalidArgumentException('userId is required.');
        }
        $prior = $this->auth->findUserById($userId);
        if (!is_array($prior)) {
            throw new RuntimeException('User not found.');
        }
        $normalized = MyronAuthRepository::normalizeGrantedScopes($grantedScopes);
        foreach ($normalized as $scope) {
            if (!$caller->holdsScope($scope)) {
                throw new RuntimeException('Caller cannot grant a scope they do not hold: ' . $scope . '.');
            }
        }
        $saved = (array) $this->auth->updateUserScopes($userId, $normalized);
        return [
            'user_id' => (string) ($saved['id'] ?? $userId),
            'prior_scopes' => MyronAuthRepository::normalizeGrantedScopes($prior['granted_scopes'] ?? []),
            'granted_scopes' => MyronAuthRepository::normalizeGrantedScopes($saved['granted_scopes'] ?? []),
            'role_id' => (string) ($saved['role_id'] ?? ''),
        ];
    }

    /**
     * @return array{user_id: string, role_id: string, role_key: string, granted_scopes: list<string>}
     */
    public function updateUserRole(string $userId, string $roleKey, MyronActionCallerContext $caller): array
    {
        $userId = trim($userId);
        $roleKey = trim($roleKey);
        if ($userId === '') {
            throw new InvalidArgumentException('userId is required.');
        }
        if ($roleKey === '') {
            throw new InvalidArgumentException('role_key is required.');
        }
        $targetScopes = MyronAuthRepository::scopesForRoleKey($roleKey);
        if ($targetScopes === []) {
            throw new InvalidArgumentException('Unknown role_key.');
        }
        foreach ($targetScopes as $scope) {
            if (!$caller->holdsScope($scope)) {
                throw new RuntimeException('Caller cannot assign a role containing a scope they do not hold: ' . $scope . '.');
            }
        }
        $saved = (array) $this->auth->updateUserRole($userId, $roleKey);
        return [
            'user_id' => (string) ($saved['id'] ?? $userId),
            'role_id' => (string) ($saved['role_id'] ?? ''),
            'role_key' => (string) ($saved['role_key'] ?? $roleKey),
            'granted_scopes' => MyronAuthRepository::normalizeGrantedScopes($saved['granted_scopes'] ?? []),
        ];
    }

    /**
     * @param array<int, string> $allowedDomains
     * @return array<string, mixed>
     */
    public function saveRegistrationSettings(array $allowedDomains, MyronActionCallerContext $caller): array
    {
        return (array) $this->auth->saveRegistrationSettings($allowedDomains);
    }
}
