<?php
declare(strict_types=1);

/**
 * Copyright 2010 - 2026, Cake Development Corporation (https://www.cakedc.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2010 - 2026, Cake Development Corporation (https://www.cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

namespace CakeDC\Users\Loader;

use Cake\Core\Configure;
use CakeDC\Auth\Authentication\AuthenticationService;
use CakeDC\Auth\Authentication\TwoFactorProcessorLoader;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Class AuthenticationServiceLoader
 *
 * @package CakeDC\Users\Loader
 */
class AuthenticationServiceLoader
{
    /**
     * Load the authentication service with authenticators from config Auth.Authenticators,
     * and identifiers from config Auth.Identifiers.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request The request.
     * @return \CakeDC\Auth\Authentication\AuthenticationService
     */
    public function __invoke(ServerRequestInterface $request)
    {
        $processors = TwoFactorProcessorLoader::processors();
        $service = new AuthenticationService(['processors' => $processors]);
        $this->loadAuthenticators($service);
        $this->loadTwoFactorAuthenticator($service, $processors);

        return $service;
    }

    /**
     * Load the indentifiers defined at config Auth.Identifiers
     *
     * @param \CakeDC\Auth\Authentication\AuthenticationService $service Authentication service to load identifiers
     * @return void
     */
    protected function loadIdentifiers($service)
    {
        $identifiers = Configure::read('Auth.Identifiers');

        if (empty($identifiers)) {
            return;
        }

        deprecationWarning(
            '15.2.0',
            'Configuring identifiers globally via `Auth.Identifiers` is deprecated. ' .
                'Please move each identifier\'s configuration into the `identifier` key within its specific authenticator under `Auth.Authenticators`. ' .
                'For example, the `Auth.Identifiers.Password` configuration should now be placed inside `Auth.Authenticators.Form.identifier`.',
        );

        foreach ($identifiers as $key => $item) {
            [$identifier, $options] = $this->_getItemLoadData($item, $key);

            $service->identifiers()->load($identifier, $options);
        }
    }

    /**
     * Load the authenticators defined at config Auth.Authenticators
     *
     * @param \CakeDC\Auth\Authentication\AuthenticationService $service Authentication service to load identifiers
     * @return void
     */
    protected function loadAuthenticators($service)
    {
        $authenticators = Configure::read('Auth.Authenticators');

        foreach ($authenticators as $key => $item) {
            [$authenticator, $options] = $this->_getItemLoadData($item, $key);

            $service->loadAuthenticator($authenticator, $options);
        }
    }

    /**
     * Load the CakeDC/Auth.TwoFactor based on config OneTimePasswordAuthenticator.login
     *
     * @param \CakeDC\Auth\Authentication\AuthenticationService $service Authentication service to load identifiers
     * @param \CakeDC\Auth\Authentication\TwoFactorProcessorCollection $processors TwoFactorProcessors collection
     * @return void
     */
    protected function loadTwoFactorAuthenticator($service, $processors)
    {
        if (collection($processors)->some(fn($processor) => $processor->enabled())) {
            $service->loadAuthenticator('CakeDC/Auth.TwoFactor', [
                'skipTwoFactorVerify' => true,
            ]);
        }
    }

    /**
     * @param mixed $item Item configuration or className
     * @param string $key Item array key.
     * @return array
     */
    protected function _getItemLoadData($item, $key)
    {
        $options = [];
        if (!is_array($item)) {
            return [$item, $options];
        }
        $options = $item;
        if (!isset($options['className'])) {
            throw new \InvalidArgumentException(
                __d('cake_d_c/users', 'Property  {0}.className should be defined', $key),
            );
        }
        $className = $options['className'];
        unset($options['className']);

        return [$className, $options];
    }
}
