<?php

declare(strict_types=1);
/**
 * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OCA\Circles\Command;

use OC\Core\Command\Base;
use OCA\Circles\CirclesManager;
use OCA\Circles\IFederatedUser;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MigrateCustomGroups extends Base {
	private OutputInterface $output;
	/** @var array<string, null|IFederatedUser> */
	private array $fedList = [];

	public function __construct(
		private CirclesManager $circlesManager,
		protected IDBConnection $connection,
		protected IConfig $config,
		private LoggerInterface $logger,
	) {
		parent::__construct();
	}

	protected function configure() {
		parent::configure();
		$this->setName('circles:migrate:customgroups');
	}

	protected function execute(InputInterface $input, OutputInterface $output): int {
		$this->output = $output;
		if (!$this->shouldRun()) {
			$this->output->writeln('migration already done or table \'custom_group\' not found');
			return 0;
		}

		$this->migrateTeams();
		$this->config->setAppValue('circles', 'imported_custom_groups', 'true');

		return 0;
	}

	public function migrateTeams(): void {
		$this->output->writeln('Migrating custom groups to Teams');

		$owners = $this->extractCustomGroupsAndOwners();

		// we get list of all custom groups
		$queryCustomGroups = $this->connection->getQueryBuilder();
		$queryCustomGroups->select('group_id', 'display_name', 'uri')
			->from('custom_group')
			->orderBy('group_id');

		$resultCustomGroups = $queryCustomGroups->executeQuery();

		// we cycle for each custom group
		while ($rowCG = $resultCustomGroups->fetch()) {
			$groupId = $rowCG['group_id'] ?? 0;
			$groupUri = $rowCG['uri'] ?? '';
			$ownerId = $owners[$groupId] ?? '';
			if ($ownerId === '' || $groupId === 0) {
				continue; // if group or owner is not know, we ignore the entry.
			}

			$name = $rowCG['display_name'];
			while (strlen($name) < 3) {
				$name = '_' . $name;
			}

			$this->output->writeln('+ New Team <info>' . $name . '</info>, owned by <info>' . $ownerId . '</info>');

			// based on owner's userid, we create federateduser and a new circle
			$owner = $this->cachedFed($ownerId);
			if ($owner === null) {
				$this->output->writeln('<error>unknown user</error> ' . $ownerId);
				continue;
			}

			$this->circlesManager->startSession($owner);

			try {
				$circle = $this->circlesManager->createCircle($name);
			} catch (\Exception $e) {
				$this->output->writeln('<error>' . get_class($e) . ' ' . $e->getMessage() . '</error> with data ' . json_encode($rowCG));
				$this->logger->log(2, 'error while creating circle', ['exception' => $e]);
				$this->circlesManager->stopSession();
				continue;
			}

			// we get all members for this custom group
			$queryMembers = $this->connection->getQueryBuilder();
			$queryMembers->select('user_id', 'role')
				->from('custom_group_member')
				->where($queryMembers->expr()->eq('group_id', $queryMembers->createNamedParameter($groupId)));

			$members = [$ownerId];
			$resultMembers = $queryMembers->executeQuery();
			while ($rowM = $resultMembers->fetch()) {
				$userId = $rowM['user_id'];
				// if admin, ignore
				if ($userId === '') {
					continue;
				}

				try {
					$members[] = $userId;
					if ($userId === $ownerId) {
						continue; // owner is already in the circles
					}

					$fedUser = $this->cachedFed($userId);
					if ($fedUser === null) {
						$this->output->writeln('<error>unknown user</error> ' . $userId);
						continue;
					}
					$this->output->writeln(' - new member <info>' . $userId . '</info>');

					$member = $this->circlesManager->addMember($circle->getSingleId(), $fedUser);
					if ($rowM['role'] === '1') {
						$this->circlesManager->levelMember($member->getId(), Member::LEVEL_ADMIN);
					}
				} catch (\Exception $e) {
					$this->output->writeln('<error>' . get_class($e) . ' ' . $e->getMessage() . '</error>');
					$this->logger->log(2, 'error while migrating custom group member', ['exception' => $e]);
				}
			}

			$this->circlesManager->stopSession();
			$resultMembers->closeCursor();

			$this->updateShares($groupUri, $circle->getSingleId(), $members);
			$this->output->writeln('');
		}

		$resultCustomGroups->closeCursor();
	}

	/**
	 * - type 7 instead of 1
	 * - with circle ID instead of `customgroup_` + group URI
	 * - update children using memberIds
	 *
	 * @param string $groupUri
	 * @param string $circleId
	 * @param array $memberIds
	 *
	 * @throws Exception
	 */
	public function updateShares(string $groupUri, string $circleId, array $memberIds): void {
		$shareIds = $this->getSharedIds($groupUri);

		$update = $this->connection->getQueryBuilder();
		$update->update('share')
			->set('share_type', $update->createNamedParameter(IShare::TYPE_CIRCLE))
			->set('share_with', $update->createNamedParameter($circleId))
			->where($update->expr()->in('id', $update->createNamedParameter($shareIds, IQueryBuilder::PARAM_INT_ARRAY)));

		$count = $update->executeStatement();
		$this->output->writeln('> ' . ((string)$count) . ' shares updated');

		$this->fixShareChildren($shareIds, $memberIds);
	}

	/**
	 * manage local cache FederatedUser
	 *
	 * @param string $userId
	 * @return null|FederatedUser
	 */
	private function cachedFed(string $userId): ?FederatedUser {
		if (!array_key_exists($userId, $this->fedList)) {
			try {
				$this->fedList[$userId] = $this->circlesManager->getLocalFederatedUser($userId);
			} catch (\Exception $e) {
				$this->logger->warning('unknown local user ' . $userId, ['exception' => $e]);
				$this->fedList[$userId] = null;
			}
		}

		return $this->fedList[$userId];
	}

	/**
	 * update share children using the correct member id
	 *
	 * @param string $shareId
	 * @param array $memberIds
	 */
	private function fixShareChildren(array $shareIds, array $memberIds): void {
		$update = $this->connection->getQueryBuilder();
		$update->update('share')
			->set('share_type', $update->createNamedParameter(IShare::TYPE_CIRCLE))
			->set('share_with', $update->createParameter('new_recipient'))
			->where($update->expr()->in('parent', $update->createNamedParameter($shareIds, IQueryBuilder::PARAM_INT_ARRAY)))
			->andWhere($update->expr()->eq('share_with', $update->createParameter('old_recipient')));

		$count = 0;
		foreach ($memberIds as $memberId) {
			$fedUser = $this->cachedFed($memberId);
			if ($fedUser === null) {
				// we dont update, user does not exist anymore
				continue;
			}
			$update->setParameter('old_recipient', $memberId);
			$update->setParameter('new_recipient', $fedUser->getSingleId());
			$count += $update->executeStatement();
		}

		$this->output->writeln('> ' . ((string)$count) . ' children shares updated');
	}


	private function getSharedIds(string $groupUri): array {
		$select = $this->connection->getQueryBuilder();
		$select->select('*')
			->from('share')
			->where($select->expr()->eq('share_type', $select->createNamedParameter(IShare::TYPE_GROUP)));

		$shareIds = [];
		$result = $select->execute();
		while ($row = $result->fetch()) {
			$with = $row['share_with'];
			if (!str_starts_with($with, 'customgroup_')
				|| substr($with, strlen('customgroup_')) !== $groupUri) {
				// not a custom group, or not the one we're looking for
				continue;
			}

			$shareIds[] = $row['id'];
		}

		return $shareIds;
	}

	protected function shouldRun(): bool {
		$alreadyImported = $this->config->getAppValue('circles', 'imported_custom_groups', 'false');
		return $alreadyImported === 'false' && $this->connection->tableExists('custom_group') && $this->connection->tableExists('custom_group_member');
	}

	/**
	 * returns owners for each custom groups
	 *
	 * @return array<string, string> [groupId => userId]
	 * @throws Exception
	 */
	private function extractCustomGroupsAndOwners(): array {
		$queryOwners = $this->connection->getQueryBuilder();
		$queryOwners->select('group_id', 'user_id')
			->from('custom_group_member')
			->where($queryOwners->expr()->eq('role', $queryOwners->createNamedParameter('1')));

		$resultOwners = $queryOwners->executeQuery();
		$owners = [];
		while ($rowO = $resultOwners->fetch()) {
			// no idea if custom groups in owncloud can hold multiple 'owner'
			$owners[$rowO['group_id']] = $owners[$rowO['group_id']] ?? $rowO['user_id'];
		}
		$resultOwners->closeCursor();

		return $owners;
	}
}
