<?php
/**
 * Nexora VE Networks — alta compartida de cuentas.
 *
 * Crea únicamente la identidad base (MEMB_INFO) en Legado y Seasonal. Todas
 * las demás operaciones continúan usando la conexión del reino seleccionado.
 *
 * Las dos inserciones se ejecutan desde la conexión principal y dentro de una
 * sola transacción SQL Server. De esta forma nunca queda una cuenta creada en
 * un solo reino si la segunda inserción falla.
 */

class NexoraRegistrationConflictException extends Exception {}

class NexoraAccountProvisioner {

	private $_realms = array();
	private $_primaryRealm;
	private $_database;
	private $_connections = array();
	private $_passwordEncryption;
	private $_sha256salt;
	private $_defaultAccountSerial;

	public function __construct($passwordEncryption, $sha256salt, $defaultAccountSerial) {
		if(!class_exists('NexoraRealm') || !NexoraRealm::sharedRegistrationEnabled()) {
			throw new RuntimeException('El registro compartido de Nexora no está habilitado.');
		}

		$this->_realms = NexoraRealm::registrationRealms();
		$this->_primaryRealm = NexoraRealm::registrationPrimaryRealm();
		$this->_passwordEncryption = strtolower((string)$passwordEncryption);
		$this->_sha256salt = (string)$sha256salt;
		$this->_defaultAccountSerial = (string)$defaultAccountSerial;

		foreach($this->_realms as $realm) {
			$this->_connections[$realm] = NexoraRealm::connection('Me_MuOnline', $realm);
		}

		$this->_assertSingleSqlInstance();
		$this->_database = Connection::Database('Me_MuOnline', $this->_primaryRealm);
	}

	/** Verifica usuario y correo en todos los reinos antes de iniciar el alta. */
	public function assertAvailable($username, $email) {
		$this->_assertAvailableOnAllRealms($username, $email, false);
		return true;
	}

	/**
	 * Inserta la cuenta en todos los reinos configurados dentro de una única
	 * transacción SQL Server. No modifica sesiones ni el reino activo.
	 */
	public function createAccounts($username, $password, $email) {
		if(!$this->_database->beginTransaction()) {
			$this->_logFailure('No se pudo iniciar la transacción SQL del registro compartido.');
			throw new RuntimeException($this->_safeFailureMessage());
		}

		try {
			// HOLDLOCK evita que dos solicitudes simultáneas creen la misma identidad.
			$this->_assertAvailableOnAllRealms($username, $email, true);

			foreach($this->_realms as $realm) {
				// Legacy databases may still contain TR_Copy_WebAccount_To_muonlinetemp.
				// If the primary insert provisioned a secondary realm through that trigger,
				// accept the exact matching row instead of inserting it a second time.
				$existing = $this->_findAccount($realm, $username);
				if(is_array($existing)) {
					$existingEmail = isset($existing[_CLMN_EMAIL_]) ? strtolower(trim((string)$existing[_CLMN_EMAIL_])) : '';
					if($existingEmail !== strtolower(trim((string)$email))) {
						throw new RuntimeException('La cuenta apareció en '.$realm.' durante el alta con datos incompatibles.');
					}
					continue;
				}
				if(!$this->_insertAccount($realm, $username, $password, $email)) {
					throw new RuntimeException('Falló la inserción de MEMB_INFO para el reino '.$realm.'.');
				}
			}

			if(!$this->_database->commit()) {
				throw new RuntimeException('No se pudo confirmar la transacción del registro compartido.');
			}
		} catch(NexoraRegistrationConflictException $ex) {
			if($this->_database->inTransaction()) $this->_database->rollBack();
			throw $ex;
		} catch(Throwable $ex) {
			if($this->_database->inTransaction()) $this->_database->rollBack();
			$this->_logFailure($ex->getMessage());
			throw new RuntimeException($this->_safeFailureMessage());
		}

		$this->_addOldSeasonSupport($username);
		try { if(class_exists('Vote')) Vote::recordAccountCreation($username, $this->_realms); } catch(Throwable $voteMetaError) { $this->_logFailure('Vote Anti-Farm metadata: '.$voteMetaError->getMessage()); }
		if(class_exists('NexoraAudit')) NexoraAudit::log('identity.shared_registration', 'account', $username, null, array('realms'=>$this->_realms), $this->_primaryRealm);
		return true;
	}

	private function _assertAvailableOnAllRealms($username, $email, $lockRows) {
		$hint = $lockRows ? ' WITH (UPDLOCK, HOLDLOCK)' : '';

		foreach($this->_realms as $realm) {
			$table = $this->_qualifiedTable($realm, _TBL_MI_);
			$userColumn = $this->_quoteIdentifier(_CLMN_USERNM_);
			$emailColumn = $this->_quoteIdentifier(_CLMN_EMAIL_);

			$userResult = $this->_database->query_fetch(
				'SELECT TOP 1 '.$userColumn.' FROM '.$table.$hint.' WHERE '.$userColumn.' = ?',
				array($username)
			);
			if($userResult === false) {
				throw new RuntimeException('No se pudo validar el usuario en el reino '.$realm.'.');
			}
			if(is_array($userResult) && isset($userResult[0])) {
				throw new NexoraRegistrationConflictException(lang('error_10', true));
			}

			$emailResult = $this->_database->query_fetch(
				'SELECT TOP 1 '.$emailColumn.' FROM '.$table.$hint.' WHERE '.$emailColumn.' = ?',
				array($email)
			);
			if($emailResult === false) {
				throw new RuntimeException('No se pudo validar el correo en el reino '.$realm.'.');
			}
			if(is_array($emailResult) && isset($emailResult[0])) {
				throw new NexoraRegistrationConflictException(lang('error_11', true));
			}
		}
	}

	private function _findAccount($realm, $username) {
		$table = $this->_qualifiedTable($realm, _TBL_MI_);
		$userColumn = $this->_quoteIdentifier(_CLMN_USERNM_);
		$emailColumn = $this->_quoteIdentifier(_CLMN_EMAIL_);
		return $this->_database->query_fetch_single('SELECT TOP 1 '.$userColumn.', '.$emailColumn.' FROM '.$table.' WHERE '.$userColumn.' = ?', array($username));
	}

	private function _insertAccount($realm, $username, $password, $email) {
		$table = $this->_qualifiedTable($realm, _TBL_MI_);
		$columns = implode(', ', array(
			$this->_quoteIdentifier(_CLMN_USERNM_),
			$this->_quoteIdentifier(_CLMN_PASSWD_),
			$this->_quoteIdentifier(_CLMN_MEMBNAME_),
			$this->_quoteIdentifier(_CLMN_SNONUMBER_),
			$this->_quoteIdentifier(_CLMN_EMAIL_),
			$this->_quoteIdentifier(_CLMN_BLOCCODE_),
			$this->_quoteIdentifier(_CLMN_CTLCODE_),
		));

		$data = array(
			'username' => $username,
			'password' => $password,
			'name' => $username,
			'serial' => $this->_defaultAccountSerial,
			'email' => $email,
		);

		switch($this->_passwordEncryption) {
			case 'wzmd5':
				$passwordValue = $this->_qualifiedFunction($realm, 'fn_md5').'(:password, :username)';
				break;
			case 'phpmd5':
				$data['password'] = md5($password);
				$passwordValue = ':password';
				break;
			case 'sha256':
				$data['password'] = '0x'.hash('sha256', $password.$username.$this->_sha256salt);
				$passwordValue = 'CONVERT(binary(32), :password, 1)';
				break;
			default:
				$passwordValue = ':password';
		}

		$query = 'INSERT INTO '.$table.' ('.$columns.') VALUES '
			.'(:username, '.$passwordValue.', :name, :serial, :email, 0, 0)';

		return $this->_database->query($query, $data);
	}

	private function _assertSingleSqlInstance() {
		$primary = $this->_connections[$this->_primaryRealm];
		$keys = array('host', 'port', 'username', 'password', 'driver');

		foreach($this->_connections as $realm => $connection) {
			foreach($keys as $key) {
				if((string)$connection[$key] !== (string)$primary[$key]) {
					throw new RuntimeException(
						'El registro compartido requiere que Legado y Seasonal usen la misma instancia y credenciales SQL.'
					);
				}
			}
		}
	}

	private function _qualifiedTable($realm, $table) {
		$database = $this->_connections[$realm]['database'];
		return $this->_quoteIdentifier($database).'.[dbo].'.$this->_quoteIdentifier($table);
	}

	private function _qualifiedFunction($realm, $function) {
		$database = $this->_connections[$realm]['database'];
		return $this->_quoteIdentifier($database).'.[dbo].'.$this->_quoteIdentifier($function);
	}

	private function _quoteIdentifier($identifier) {
		$identifier = (string)$identifier;
		if(!preg_match('/^[A-Za-z0-9_]+$/D', $identifier)) {
			throw new RuntimeException('La configuración contiene un identificador SQL no válido.');
		}
		return '['.$identifier.']';
	}

	private function _addOldSeasonSupport($username) {
		if(!config('season_1_support')) return;

		foreach($this->_realms as $realm) {
			$table = $this->_qualifiedTable($realm, 'VI_CURR_INFO');
			@$this->_database->query(
				'INSERT INTO '.$table.' (ends_days, chek_code, used_time, memb___id, memb_name, memb_guid, sno__numb, Bill_Section, Bill_Value, Bill_Hour, Surplus_Point, Surplus_Minute, Increase_Days) '
				."VALUES ('2005', '1', '1234', ?, '', '1', '7', '6', '3', '6', '6', '2020-01-01 00:00:00', '0')",
				array($username)
			);
		}
	}

	private function _safeFailureMessage() {
		return 'No se pudo crear la cuenta en Legado y Seasonal. No se guardó en ningún servidor; inténtalo nuevamente.';
	}

	private function _logFailure($message) {
		if(!defined('WEBENGINE_DATABASE_ERRORLOG')) return;
		@error_log(
			'['.date('Y/m/d H:i:s').'] [Nexora shared registration] '.$message."\r\n",
			3,
			WEBENGINE_DATABASE_ERRORLOG
		);
	}
}

