<?php

/**
 * This file is part of the Nette Framework (https://nette.org)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

declare(strict_types=1);

namespace Nette\Bridges\SecurityDI;

use Nette;
use Nette\Schema\Expect;
use Tracy;


/**
 * Security extension for Nette DI.
 */
class SecurityExtension extends Nette\DI\CompilerExtension
{
	private bool $debugMode;


	public function __construct(bool $debugMode = false)
	{
		$this->debugMode = $debugMode;
	}


	public function getConfigSchema(): Nette\Schema\Schema
	{
		return Expect::structure([
			'debugger' => Expect::bool(),
			'users' => Expect::arrayOf(
				Expect::anyOf(
					Expect::string(), // user => password
					Expect::structure([ // user => password + roles + data
						'password' => Expect::string(),
						'roles' => Expect::anyOf(Expect::string(), Expect::listOf('string')),
						'data' => Expect::array(),
					])->castTo('array'),
				),
			),
			'roles' => Expect::arrayOf('string|array|null'), // role => parent(s)
			'resources' => Expect::arrayOf('string|null'), // resource => parent
			'authentication' => Expect::structure([
				'storage' => Expect::anyOf('session', 'cookie')->default('session'),
				'expiration' => Expect::string()->dynamic(),
				'cookieName' => Expect::string(),
				'cookieDomain' => Expect::string(),
				'cookieSamesite' => Expect::anyOf('Lax', 'Strict', 'None'),
			]),
		]);
	}


	public function loadConfiguration(): void
	{
		/** @var object{debugger: bool, users: array, roles: array, resources: array, authentication: \stdClass} $config */
		$config = $this->config;
		$builder = $this->getContainerBuilder();

		$builder->addDefinition($this->prefix('passwords'))
			->setFactory(Nette\Security\Passwords::class);

		$auth = $config->authentication;
		$storage = $builder->addDefinition($this->prefix('userStorage'))
			->setType(Nette\Security\UserStorage::class)
			->setFactory([
				'session' => Nette\Bridges\SecurityHttp\SessionStorage::class,
				'cookie' => Nette\Bridges\SecurityHttp\CookieStorage::class,
			][$auth->storage]);

		if ($auth->storage === 'cookie') {
			if ($auth->cookieDomain === 'domain') {
				$auth->cookieDomain = $builder::literal('$this->getByType(Nette\Http\IRequest::class)->getUrl()->getDomain(2)');
			}

			$storage->addSetup('setCookieParameters', [$auth->cookieName, $auth->cookieDomain, $auth->cookieSamesite]);
		}

		$user = $builder->addDefinition($this->prefix('user'))
			->setFactory(Nette\Security\User::class);

		if ($auth->expiration) {
			$user->addSetup('setExpiration', [$auth->expiration]);
		}

		if ($config->users) {
			$usersList = $usersRoles = $usersData = [];
			foreach ($config->users as $username => $data) {
				$data = is_array($data) ? $data : ['password' => $data];
				$usersList[$username] = $data['password'];
				$usersRoles[$username] = $data['roles'] ?? null;
				$usersData[$username] = $data['data'] ?? [];
			}

			$builder->addDefinition($this->prefix('authenticator'))
				->setType(Nette\Security\IAuthenticator::class)
				->setFactory(Nette\Security\SimpleAuthenticator::class, [$usersList, $usersRoles, $usersData]);

			if ($this->name === 'security') {
				$builder->addAlias('nette.authenticator', $this->prefix('authenticator'));
			}
		}

		if ($config->roles || $config->resources) {
			$authorizator = $builder->addDefinition($this->prefix('authorizator'))
				->setType(Nette\Security\Authorizator::class)
				->setFactory(Nette\Security\Permission::class);

			foreach ($config->roles as $role => $parents) {
				$authorizator->addSetup('addRole', [$role, $parents]);
			}

			foreach ($config->resources as $resource => $parents) {
				$authorizator->addSetup('addResource', [$resource, $parents]);
			}

			if ($this->name === 'security') {
				$builder->addAlias('nette.authorizator', $this->prefix('authorizator'));
			}
		}

		if ($this->name === 'security') {
			$builder->addAlias('user', $this->prefix('user'));
			$builder->addAlias('nette.userStorage', $this->prefix('userStorage'));
		}
	}


	public function beforeCompile(): void
	{
		$builder = $this->getContainerBuilder();

		if (
			$this->debugMode &&
			($this->config->debugger ?? $builder->getByType(Tracy\Bar::class))
		) {
			$builder->getDefinition($this->prefix('user'))->addSetup('@Tracy\Bar::addPanel', [
				new Nette\DI\Definitions\Statement(Nette\Bridges\SecurityTracy\UserPanel::class),
			]);
		}
	}
}
