<?php
class NexoraEmailChange {
    private $realm;
    private $platform;
    private $ttl;

    public function __construct($realm=null) {
        $this->realm = NexoraRealm::validate($realm === null ? NexoraRealm::active() : $realm);
        $this->platform = Connection::Database('Platform', $this->realm);
        $this->ttl = 30;
    }

    public function request($userId, $username, $currentPassword, $newEmail, $ip) {
        if(!NexoraRealm::platformEnabled()) throw new RuntimeException('El cambio seguro de email requiere Nexora Platform.');
        if(!Validator::UnsignedNumber($userId) || !Validator::Email($newEmail) || !Validator::Ip($ip)) throw new RuntimeException('Datos inválidos.');
        $account = new common($this->realm);
        if(!$account->validateUser($username, $currentPassword)) throw new RuntimeException(lang('error_13', true));
        if($account->accountOnline($username)) throw new RuntimeException(lang('error_14', true));
        $info = $account->accountInformation($userId);
        if(!is_array($info) || strcasecmp((string)$info[_CLMN_USERNM_], (string)$username) !== 0) throw new RuntimeException('No se pudo validar la cuenta.');
        $oldEmail = (string)$info[_CLMN_EMAIL_];
        if(strcasecmp($oldEmail, $newEmail) === 0) throw new RuntimeException('El nuevo correo es igual al actual.');
        $this->assertEmailAvailable($newEmail, $username);

        $raw = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
        $hash = hash('sha256', $raw);
        if(!$this->platform->beginTransaction()) throw new RuntimeException('No se pudo iniciar la solicitud.');
        try {
            $this->platform->query('UPDATE NEXORA_EMAIL_CHANGE_REQUESTS SET used_at = SYSUTCDATETIME() WHERE realm = ? AND account_name = ? AND used_at IS NULL', array($this->realm, $username));
            $affected = $this->platform->query_affected(
                'INSERT INTO NEXORA_EMAIL_CHANGE_REQUESTS (realm, user_id, account_name, old_email, new_email, token_hash, requested_ip, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, SYSUTCDATETIME(), DATEADD(MINUTE, ?, SYSUTCDATETIME()))',
                array($this->realm, (int)$userId, $username, $oldEmail, $newEmail, $hash, $ip, $this->ttl)
            );
            if($affected !== 1) throw new RuntimeException('No se pudo crear la solicitud.');
            if(!$this->platform->commit()) throw new RuntimeException('No se pudo confirmar la solicitud.');
        } catch(Throwable $e) {
            if($this->platform->inTransaction()) $this->platform->rollBack();
            throw $e;
        }

        $link = __BASE_URL__.'verifyemail/?op=3&token='.rawurlencode($raw).'&realm='.rawurlencode($this->realm);
        $mail = new Email();
        $mail->setTemplate('CHANGE_EMAIL_VERIFICATION');
        $mail->addVariable('{USERNAME}', $username);
        $mail->addVariable('{IP_ADDRESS}', $ip);
        $mail->addVariable('{NEW_EMAIL}', $newEmail);
        $mail->addVariable('{LINK}', $link);
        $mail->addAddress($oldEmail);
        $mail->send();
        NexoraAudit::log('identity.email_change_requested', 'account', $username, array('email'=>$this->mask($oldEmail)), array('email'=>$this->mask($newEmail)), $this->realm);
        return true;
    }

    public function complete($rawToken) {
        if(!NexoraRealm::platformEnabled()) throw new RuntimeException('El cambio seguro de email requiere Nexora Platform.');
        $hash = $this->tokenHash($rawToken);
        if(!$this->platform->beginTransaction()) throw new RuntimeException('No se pudo iniciar la validación.');
        try {
            $row = $this->platform->query_fetch_single(
                'SELECT TOP 1 * FROM NEXORA_EMAIL_CHANGE_REQUESTS WITH (UPDLOCK, HOLDLOCK) WHERE realm = ? AND token_hash = ? AND used_at IS NULL AND expires_at >= SYSUTCDATETIME() ORDER BY id DESC',
                array($this->realm, $hash)
            );
            if(!is_array($row)) throw new RuntimeException('El enlace no es válido, ya fue utilizado o expiró.');
            $username = (string)$row['account_name'];
            $newEmail = (string)$row['new_email'];
            $this->assertEmailAvailable($newEmail, $username);
            $updated = $this->updateAllRealms($username, $newEmail);
            if($updated < 1) throw new RuntimeException('No se encontró la cuenta para actualizar.');
            $used = $this->platform->query_affected('UPDATE NEXORA_EMAIL_CHANGE_REQUESTS SET used_at = SYSUTCDATETIME() WHERE id = ? AND used_at IS NULL', array((int)$row['id']));
            if($used !== 1) throw new RuntimeException('El enlace ya fue utilizado.');
            if(!$this->platform->commit()) throw new RuntimeException('No se pudo confirmar el cambio.');

            $mail = new Email();
            $mail->setTemplate('ADMIN_CHANGE_EMAIL');
            $mail->addVariable('{USERNAME}', $username);
            $mail->addVariable('{NEW_EMAIL}', $newEmail);
            $mail->addAddress((string)$row['old_email']);
            $mail->send();
            NexoraAudit::log('identity.email_changed', 'account', $username, array('email'=>$this->mask($row['old_email'])), array('email'=>$this->mask($newEmail),'realms_updated'=>$updated), $this->realm);
            return true;
        } catch(Throwable $e) {
            if($this->platform->inTransaction()) $this->platform->rollBack();
            throw $e;
        }
    }

    private function assertEmailAvailable($email, $username) {
        $targets = NexoraRealm::sharedRegistrationEnabled() ? NexoraRealm::registrationRealms() : array($this->realm);
        foreach($targets as $realm) {
            $db = Connection::Database('Me_MuOnline', $realm);
            $row = $db->query_fetch_single('SELECT TOP 1 '._CLMN_USERNM_.' AS username FROM '._TBL_MI_.' WHERE '._CLMN_EMAIL_.' = ?', array($email));
            if(is_array($row) && strcasecmp((string)$row['username'], (string)$username) !== 0) throw new RuntimeException(lang('error_11', true));
        }
    }

    private function updateAllRealms($username, $newEmail) {
        $targets = NexoraRealm::sharedRegistrationEnabled() ? NexoraRealm::registrationRealms() : array($this->realm);
        $platformConn = NexoraRealm::connection('Platform', $this->realm);
        $updated = 0;
        foreach($targets as $realm) {
            $conn = NexoraRealm::connection('Me_MuOnline', $realm);
            foreach(array('host','port','username','driver') as $key) if((string)$conn[$key] !== (string)$platformConn[$key]) throw new RuntimeException('Platform y realms deben compartir instancia SQL para esta operación.');
            $dbName = $this->qi($conn['database']);
            $table = $dbName.'.[dbo].'.$this->qi(_TBL_MI_);
            $affected = $this->platform->query_affected('UPDATE '.$table.' SET '.$this->qi(_CLMN_EMAIL_).' = ? WHERE '.$this->qi(_CLMN_USERNM_).' = ?', array($newEmail, $username));
            if($affected === false) throw new RuntimeException('No se pudo actualizar el email en '.$realm.'.');
            $updated += (int)$affected;
        }
        return $updated;
    }

    private function tokenHash($token) {
        $token = trim((string)$token);
        if(strlen($token) < 32 || strlen($token) > 128 || !preg_match('/^[A-Za-z0-9_-]+$/D', $token)) throw new RuntimeException('El enlace no es válido.');
        return hash('sha256', $token);
    }

    private function qi($value) {
        if(!preg_match('/^[A-Za-z0-9_]+$/D', (string)$value)) throw new RuntimeException('Identificador SQL inválido.');
        return '['.$value.']';
    }

    private function mask($email) {
        $parts = explode('@', (string)$email, 2);
        return count($parts) === 2 ? substr($parts[0],0,1).'***@'.$parts[1] : '***';
    }
}
