<?php

namespace XF\Import\Data;

use XF\Entity\UserAuth;
use XF\Entity\UserOption;
use XF\Entity\UserPrivacy;
use XF\Entity\UserProfile;
use XF\Import\DataHelper\Avatar;
use XF\Import\DataHelper\Permission;

use function is_null, is_string;

/**
 * @mixin \XF\Entity\User
 */
class User extends AbstractData
{
	/**
	 * @var EntityEmulator[]
	 */
	protected $tables = [];

	protected $regIp;

	protected $avatarPath;

	/**
	 * @var Admin|null
	 */
	protected $admin;

	/**
	 * @var UserBan|null
	 */
	protected $ban;

	protected $permissions = [];

	protected $rejection = [
		'date' => null,
		'user_id' => 0,
		'reason' => '',
	];

	public function getImportType()
	{
		return 'user';
	}

	protected function init()
	{
		$em = $this->em();
		$vf = $em->getValueFormatter();

		$this->tables = [
			'user' => new EntityEmulator($this, $em->getEntityStructure(\XF\Entity\User::class), $vf),
			'option' => new EntityEmulator($this, $em->getEntityStructure(UserOption::class), $vf),
			'privacy' => new EntityEmulator($this, $em->getEntityStructure(UserPrivacy::class), $vf),
			'profile' => new EntityEmulator($this, $em->getEntityStructure(UserProfile::class), $vf),
			'auth' => new EntityEmulator($this, $em->getEntityStructure(UserAuth::class), $vf),
		];
	}

	public function setPasswordData($scheme, array $data)
	{
		$auth = $this->tables['auth'];
		$auth->scheme_class = $scheme;
		$auth->data = $data;
	}

	public function setRegistrationIp($ip)
	{
		$this->regIp = $ip;
	}

	public function setRejectionDetails(array $details)
	{
		foreach ($details AS $k => $v)
		{
			if (substr($k, 0, 7) == 'reject_')
			{
				$k = substr($k, 7);
			}
			if (isset($this->rejection[$k]))
			{
				$this->rejection[$k] = $v;
			}
		}
	}

	public function setAdmin(array $data)
	{
		if (!$this->admin)
		{
			$this->admin = $this->dataManager->newHandler(Admin::class, false);
		}

		$this->admin->bulkSet($data);
	}

	public function setBan(array $data)
	{
		if (!$this->ban)
		{
			$this->ban = $this->dataManager->newHandler(UserBan::class, false);
		}

		$this->ban->bulkSet($data);
	}

	public function setPermissions(array $permissions)
	{
		$this->permissions = $permissions;
	}

	public function setCustomFields(array $customFields)
	{
		foreach ($customFields AS $k => &$fieldValue)
		{
			if (is_string($fieldValue))
			{
				$fieldValue = $this->convertToUtf8($fieldValue);
			}
			else if (is_null($fieldValue))
			{
				unset($customFields[$k]);
			}
		}

		$this->tables['profile']->custom_fields = $customFields;
	}

	public function setAvatarPath($path)
	{
		$this->avatarPath = $path;
	}

	public function set($field, $value, array $options = [])
	{
		$set = false;
		foreach ($this->tables AS $table)
		{
			if ($table->exists($field))
			{
				$table->set($field, $value, $options);
				$set = true;
			}
		}

		if (!$set)
		{
			throw new \InvalidArgumentException("Unknown column '$field'");
		}
	}

	public function bulkSetDirect($part, array $fields, array $options = [])
	{
		foreach ($fields AS $field => $value)
		{
			$this->setDirect($part, $field, $value, $options);
		}
	}

	public function setDirect($part, $field, $value, array $options = [])
	{
		if (!isset($this->tables[$part]))
		{
			throw new \InvalidArgumentException("Invalid table '$part'");
		}

		$this->tables[$part]->set($field, $value, $options);
	}

	public function get($field)
	{
		foreach ($this->tables AS $table)
		{
			if ($table->exists($field))
			{
				return $table->get($field);
			}
		}

		throw new \InvalidArgumentException("Unknown column '$field'");
	}

	protected function write($oldId)
	{
		$db = $this->db();
		$user = $this->tables['user'];

		if ($oldId == 1 && $this->retainIds())
		{
			// We need to keep admin access for user 1 if retaining IDs as otherwise, we might not be able to continue.
			// For safety, keep the email and password that was registered with the install to prevent someone from
			// being able to gain access to this.

			$user->remove(['last_activity', 'is_admin', 'email']);
			$this->admin = null;
			$this->tables['profile']->remove('password_date');
			$this->tables['auth']->remove(['scheme_class', 'data']);

			foreach ($this->tables AS $table)
			{
				$table->update($oldId, $db);
			}

			$newId = $oldId;
		}
		else
		{
			$newId = $user->insert($oldId, $db);

			foreach ($this->tables AS $type => $table)
			{
				if ($type == 'user')
				{
					continue;
				}

				$table->set('user_id', $newId);
				$table->insert(false, $db);
			}
		}

		return $newId;
	}

	protected function importedIdFound($oldId, $newId)
	{
		foreach ($this->tables AS $table)
		{
			$table->set('user_id', $newId);
		}
	}

	protected function preSave($oldId)
	{
		$user = $this->tables['user'];

		if (strpos($user->username, ',') !== false)
		{
			throw new \LogicException("Username cannot contain a comma");
		}

		if ($oldId == 1 && $this->retainIds())
		{
			// we're just doing an update so don't do any other work we'd be doing for an insert
			return;
		}

		if (!$user->secret_key)
		{
			$user->secret_key = \XF::generateRandomString(32);
		}

		if (!$user->language_id)
		{
			// unless a language ID has been set, set the default (as XF doesn't use 0).
			$user->language_id = \XF::options()->defaultLanguageId;
		}

		// if primary user_group_id is also in secondary_group_ids then exclude it.
		$secondaryGroupIds = $user->secondary_group_ids;
		$user->secondary_group_ids = array_values(array_diff($secondaryGroupIds, [$user->user_group_id]));

		$privacy = $this->tables['privacy'];

		// we commonly have imported this as 'everyone' but we don't expose that as an option
		// in the UI so it can cause the value to be overwritten to 'none' just set that here
		if ($privacy->allow_send_personal_conversation == 'everyone')
		{
			$privacy->allow_send_personal_conversation = 'members';
		}

		if ($privacy->allow_post_profile == 'everyone')
		{
			$privacy->allow_post_profile = 'members';
		}

		// TODO: check DoB
	}

	protected function postSave($oldId, $newId)
	{
		$isMainUser = ($oldId == 1 && $this->retainIds());

		$user = $this->tables['user'];
		$groupInserts = [
			[
				'user_id' => $newId,
				'user_group_id' => $user->user_group_id,
				'is_primary' => 1,
			],
		];
		foreach ($user->secondary_group_ids AS $groupId)
		{
			$groupInserts[] = [
				'user_id' => $newId,
				'user_group_id' => $groupId,
				'is_primary' => 0,
			];
		}
		$this->db()->insertBulk('xf_user_group_relation', $groupInserts, false, 'is_primary = VALUES(is_primary)');

		$profile = $this->tables['profile'];
		if ($profile->custom_fields)
		{
			$this->insertCustomFieldValues('xf_user_field_value', 'user_id', $newId, $profile->custom_fields);
		}

		$option = $this->tables['option'];
		if ($option->alert_optout)
		{
			$optOutInserts = [];
			foreach ($option->alert_optout AS $optOut)
			{
				$optOutInserts[] = [
					'user_id' => $newId,
					'alert' => $optOut,
				];
			}
			$this->db()->insertBulk('xf_user_alert_optout', $optOutInserts, false, false, 'IGNORE');
		}

		if ($this->user_state == 'moderated')
		{
			$this->db()->insert('xf_approval_queue', [
				'content_type' => 'user',
				'content_id' => $newId,
				'content_date' => $this->register_date,
			], true);
		}
		else if ($this->user_state == 'rejected')
		{
			$this->db()->insert('xf_user_reject', [
				'user_id' => $newId,
				'reject_date' => $this->rejection['date'] ?: $this->register_date,
				'reject_user_id' => $this->rejection['user_id'] ?: 0,
				'reject_reason' => $this->convertToUtf8($this->rejection['reason']) ?: '',
			], true);
		}

		if ($this->regIp)
		{
			$this->importRawIp($newId, 'user', $newId, 'register', $this->regIp, $this->register_date);
		}

		if ($this->permissions)
		{
			/** @var Permission $permissionHelper */
			$permissionHelper = $this->dataManager->helper(Permission::class);
			$permissionHelper->insertUserPermissions($newId, $this->permissions);
		}

		if ($this->admin && !$isMainUser)
		{
			$this->admin->user_id = $newId;
			$this->admin->save($oldId);
		}
		if ($this->ban)
		{
			$this->ban->user_id = $newId;
			$this->ban->save($oldId);
		}

		/** @var \XF\Entity\User $user */
		$user = $this->em()->find(\XF\Entity\User::class, $newId);
		$user->rebuildPermissionCombination();

		if ($this->avatarPath)
		{
			/** @var Avatar $avatarHelper */
			$avatarHelper = $this->dataManager->helper(Avatar::class);
			$avatarHelper->setAvatarFromFile($this->avatarPath, $user);
		}

		$this->em()->detachEntity($user);
	}

	public function mergeFromInto($oldUserId, $targetUserId)
	{
		$mappedId = $this->dataManager->lookup($this->getImportType(), $oldUserId);
		if ($mappedId !== false)
		{
			return $mappedId;
		}

		$db = $this->db();

		$db->beginTransaction();

		foreach ($this->getMergeUpdateSql() AS $table => $updates)
		{
			$db->query("
				UPDATE `{$table}` SET
					" . implode(",\n", $updates) . "
				WHERE user_id = ?
			", $targetUserId);
		}

		$this->dataManager->log($this->getImportType(), $oldUserId, $targetUserId);

		$db->commit();

		return $targetUserId;
	}

	protected function getMergeUpdateSql()
	{
		$updates = [];
		$db = $this->db();

		if ($this->message_count)
		{
			$updates['xf_user']['message_count'] = 'message_count = message_count + ' . $db->quote($this->message_count);
		}
		if ($this->register_date)
		{
			$updates['xf_user']['register_date'] = 'register_date = LEAST(register_date, ' . $db->quote($this->register_date) . ')';
		}

		return $updates;
	}

}
