<?php

namespace BlueSpice\PermissionManager;

use BlueSpice\PermissionManager\Logging\GroupManagerSpecialLogLogger;
use Exception;
use InvalidArgumentException;
use MediaWiki\Config\Config;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Message\Message;
use MediaWiki\Permissions\Authority;
use MediaWiki\User\User;
use MWStake\MediaWiki\Component\DynamicConfig\DynamicConfigManager;
use Psr\Log\LoggerInterface;
use Wikimedia\Rdbms\ILoadBalancer;

class GroupManager {

	/** @var DynamicConfigManager */
	private DynamicConfigManager $configManager;
	/** @var ILoadBalancer */
	private ILoadBalancer $lb;
	/** @var Config */
	private Config $config;
	/** @var HookContainer */
	private HookContainer $hookContainer;
	/** @var LoggerInterface */
	private LoggerInterface $logger;
	/** @var GroupManagerSpecialLogLogger */
	private GroupManagerSpecialLogLogger $spLogger;

	/**
	 * @param DynamicConfigManager $configManager
	 * @param ILoadBalancer $lb
	 * @param Config $config Main config (wg)
	 * @param HookContainer $hookContainer
	 * @param LoggerInterface $logger
	 * @param GroupManagerSpecialLogLogger $spLogger
	 */
	public function __construct(
		DynamicConfigManager $configManager, ILoadBalancer $lb, Config $config,
		HookContainer $hookContainer, LoggerInterface $logger, GroupManagerSpecialLogLogger $spLogger
	) {
		$this->configManager = $configManager;
		$this->lb = $lb;
		$this->config = $config;
		$this->hookContainer = $hookContainer;
		$this->logger = $logger;
		$this->spLogger = $spLogger;
	}

	/**
	 * @param string $name
	 * @param Authority $actor
	 * @param bool $ignoreExists
	 * @return void
	 * @throws Exception
	 */
	public function addGroup( string $name, Authority $actor, bool $ignoreExists = false ) {
		$this->assertActorCan( 'add', $actor );
		$this->assertValidName( $name, $ignoreExists );
		$current = $this->config->get( 'AdditionalGroups' ) ?? [];
		$current[$name] = true;
		$this->store( $current );
		$this->hookContainer->run( 'BSPermissionManagerGroupAdded', [ $name, $actor ] );
		$this->log( 'create', $actor, [ 'group' => $name ] );
	}

	/**
	 * @param string $oldName
	 * @param string $newName
	 * @param Authority $actor
	 * @return void
	 * @throws Exception
	 */
	public function editGroup( string $oldName, string $newName, Authority $actor ) {
		$this->assertActorCan( 'edit', $actor );
		$this->assertValidName( $newName );
		$this->assertGroupExists( $oldName );
		$current = $this->config->get( 'AdditionalGroups' ) ?? [];
		unset( $current[$oldName] );
		$current[$newName] = true;
		$this->renameGroup( $oldName, $newName );
		$this->store( $current );
		$this->hookContainer->run( 'BSPermissionManagerGroupEdited', [ $oldName, $newName, $actor ] );
		$this->log( 'modify', $actor, [ 'group' => $oldName, 'newGroup' => $newName ] );
	}

	/**
	 * @param string $name
	 * @param Authority $actor
	 * @return void
	 * @throws Exception
	 */
	public function removeGroup( string $name, Authority $actor ) {
		$this->assertActorCan( 'delete', $actor );
		$this->assertGroupExists( $name );
		$current = $this->config->get( 'AdditionalGroups' ) ?? [];
		unset( $current[$name] );
		$this->store( $current );
		$this->unassignUsers( $name );
		$this->hookContainer->run( 'BSPermissionManagerGroupDeleted', [ $name, $actor ] );
		$this->log( 'remove', $actor, [ 'group' => $name ] );
	}

	/**
	 * @param string $name
	 * @param bool $ignoreExists
	 * @return void
	 */
	private function assertValidName( string $name, bool $ignoreExists = false ) {
		$invalidChars = [];
		$name = trim( $name );
		if ( substr_count( $name, '\'' ) > 0 ) {
			$invalidChars[] = '\'';
		}
		if ( substr_count( $name, '"' ) > 0 ) {
			$invalidChars[] = '"';
		}
		if ( !empty( $invalidChars ) ) {
			throw new InvalidArgumentException(
				Message::newFromKey( 'bs-permissionmanager-groupmanager-invalid-name' )
					->numParams( count( $invalidChars ) )
					->params( implode( ',', $invalidChars ) )
					->text()
			);
		} elseif ( preg_match( "/^[0-9]+$/", $name ) ) {
			throw new InvalidArgumentException(
				Message::newFromKey( 'bs-permissionmanager-groupmanager-invalid-name-numeric' )->text()
			);
		} elseif ( strlen( $name ) > 255 ) {
			throw new InvalidArgumentException(
				Message::newFromKey( 'bs-permissionmanager-groupmanager-invalid-name-length' )->text()
			);
		}
		if ( !$ignoreExists && $this->checkGroupExists( $name ) ) {
			throw new InvalidArgumentException(
				Message::newFromKey( 'bs-permissionmanager-group-already-exists' )->text()
			);
		}
	}

	/**
	 * @param array $value
	 * @return void
	 * @throws Exception
	 */
	private function store( array $value ) {
		$config = $this->configManager->getConfigObject( 'bs-groupmanager-groups' );
		if ( !$config ) {
			throw new Exception( 'Config object not found' );
		}
		$this->configManager->storeConfig( $config, $value );
	}

	/**
	 * @param string $oldName
	 * @param string $newName
	 * @return void
	 * @throws Exception
	 */
	private function renameGroup( string $oldName, string $newName ) {
		$db = $this->lb->getConnection( DB_PRIMARY );
		$res = $db->update(
			'user_groups',
			[ 'ug_group' => $newName ],
			[ 'ug_group' => $oldName ],
			__METHOD__
		);
		if ( !$res ) {
			throw new Exception( $db->lastError() );
		}
	}

	/**
	 * @param string $name
	 * @return void
	 */
	private function assertGroupExists( string $name ) {
		if ( !$this->checkGroupExists( $name ) ) {
			throw new InvalidArgumentException(
				Message::newFromKey( 'bs-permissionmanager-group-not-found' )->text()
			);
		}
	}

	/**
	 * @param string $action
	 * @param Authority $actor
	 * @return void
	 */
	private function assertActorCan( string $action, Authority $actor ) {
		if ( $actor instanceof User && $actor->isSystemUser() ) {
			return;
		}
		if ( !$actor->isAllowed( 'wikiadmin' ) ) {
			throw new InvalidArgumentException(
				Message::newFromKey( 'bs-permissionmanager-action-not-allowed' )->text()
			);
		}
	}

	/**
	 * @param string $name
	 * @return bool
	 */
	private function checkGroupExists( string $name ): bool {
		$groupPermissions = $this->config->get( 'GroupPermissions' ) ?? [];
		$existingGroups = array_keys( $groupPermissions );
		return in_array( $name, $existingGroups );
	}

	/**
	 * @param string $type
	 * @param Authority $actor
	 * @param array $params
	 * @return void
	 */
	private function log( string $type, Authority $actor, array $params ) {
		// Special:Log logging
		$this->spLogger->log( $type, $actor, $params );
		// Structured logging
		$this->logger->info( 'New group created', array_merge( [
			'actor' => $actor->getUser()->getName(),
		], $params ) );
	}

	/**
	 * @param string $name
	 * @return void
	 */
	private function unassignUsers( string $name ) {
		$db = $this->lb->getConnection( DB_PRIMARY );
		$db->delete(
			'user_groups',
			[ 'ug_group' => $name ],
			__METHOD__
		);
	}

}
