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

    public function __construct($realm) {
        $this->realm = NexoraRealm::validate($realm);
        $this->platform = Connection::Database('Platform', $this->realm);
        $this->ttl = max(5, (int)NexoraSecurity::config('password_reset_ttl_minutes', 20));
    }

    public function request($email, $ip) {
        if(!Validator::Email($email) || !Validator::Ip($ip)) return true; // generic response prevents enumeration
        $this->requirePlatform();
        $accountDb = Connection::Database('Me_MuOnline', $this->realm);
        $account = $accountDb->query_fetch_single('SELECT TOP 1 * FROM '._TBL_MI_.' WHERE '._CLMN_EMAIL_.' = ?', array($email));
        if(!is_array($account)) return true;

        $max = max(1, (int)NexoraSecurity::config('password_reset_max_requests_per_hour', 5));
        $rate = $this->platform->query_fetch_single('SELECT COUNT(*) AS qty FROM NEXORA_PASSWORD_RESETS WHERE requested_ip = ? AND created_at >= DATEADD(HOUR,-1,SYSUTCDATETIME())', array($ip));
        if(is_array($rate) && (int)$rate['qty'] >= $max) throw new RuntimeException('Se alcanzó el límite temporal de solicitudes. Intenta más tarde.');

        $raw = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
        $hash = hash('sha256', $raw);
        $userId = (int)$account[_CLMN_MEMBID_];
        $username = (string)$account[_CLMN_USERNM_];
        $accountEmail = (string)$account[_CLMN_EMAIL_];

        if(!$this->platform->beginTransaction()) throw new RuntimeException('No se pudo iniciar la solicitud de recuperación.');
        try {
            $this->platform->query('UPDATE NEXORA_PASSWORD_RESETS SET used_at = SYSUTCDATETIME() WHERE realm = ? AND account_name = ? AND used_at IS NULL', array($this->realm, $username));
            $ok = $this->platform->query_affected(
                'INSERT INTO NEXORA_PASSWORD_RESETS (realm, user_id, account_name, email, token_hash, requested_ip, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, SYSUTCDATETIME(), DATEADD(MINUTE, ?, SYSUTCDATETIME()))',
                array($this->realm, $userId, $username, $accountEmail, $hash, $ip, $this->ttl)
            );
            if($ok !== 1) throw new RuntimeException('No se pudo guardar el token de recuperación.');
            if(!$this->platform->commit()) throw new RuntimeException('No se pudo confirmar la solicitud de recuperación.');
        } catch(Throwable $e) {
            if($this->platform->inTransaction()) $this->platform->rollBack();
            throw $e;
        }

        $link = __BASE_URL__.'forgotpassword/?token='.rawurlencode($raw).'&realm='.rawurlencode($this->realm);
        $mail = new Email();
        $mail->setTemplate('PASSWORD_RECOVERY_REQUEST');
        $mail->addVariable('{USERNAME}', $username);
        $mail->addVariable('{DATE}', date('Y-m-d @ h:i a'));
        $mail->addVariable('{IP_ADDRESS}', $ip);
        $mail->addVariable('{LINK}', $link);
        $mail->addAddress($accountEmail);
        $mail->send();
        NexoraAudit::log('identity.password_reset_requested', 'account', $username, null, array('email'=>$this->maskEmail($accountEmail)), $this->realm);
        return true;
    }

    public function tokenInfo($rawToken) {
        $this->requirePlatform();
        $hash = $this->tokenHash($rawToken);
        $row = $this->platform->query_fetch_single(
            'SELECT TOP 1 id, realm, user_id, account_name, email, expires_at FROM NEXORA_PASSWORD_RESETS WHERE realm = ? AND token_hash = ? AND used_at IS NULL AND expires_at >= SYSUTCDATETIME() ORDER BY id DESC',
            array($this->realm, $hash)
        );
        return is_array($row) ? $row : null;
    }

    public function complete($rawToken, $newPassword, $confirmPassword) {
        $this->requirePlatform();
        if(!Validator::PasswordLength($newPassword) || $newPassword !== $confirmPassword) throw new RuntimeException('La nueva contraseña no cumple los requisitos o no coincide.');
        $hash = $this->tokenHash($rawToken);

        if(!$this->platform->beginTransaction()) throw new RuntimeException('No se pudo iniciar el cambio de contraseña.');
        try {
            $reset = $this->platform->query_fetch_single(
                'SELECT TOP 1 * FROM NEXORA_PASSWORD_RESETS 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($reset)) throw new RuntimeException('El enlace de recuperación no es válido o ya expiró.');

            $username = (string)$reset['account_name'];
            $updated = 0;
            $targets = NexoraRealm::sharedRegistrationEnabled() ? NexoraRealm::registrationRealms() : array($this->realm);
            foreach($targets as $targetRealm) {
                if(!$this->sameSqlInstance($targetRealm)) throw new RuntimeException('Platform y realms deben compartir instancia SQL para completar el cambio de contraseña de forma atómica.');
                $updated += $this->updateRealmPassword($targetRealm, $username, $newPassword);
            }
            if($updated < 1) throw new RuntimeException('No se encontró una cuenta válida para actualizar.');

            $used = $this->platform->query_affected('UPDATE NEXORA_PASSWORD_RESETS SET used_at = SYSUTCDATETIME() WHERE id = ? AND used_at IS NULL', array((int)$reset['id']));
            if($used !== 1) throw new RuntimeException('El token ya fue utilizado.');
            if(!$this->platform->commit()) throw new RuntimeException('No se pudo confirmar el cambio de contraseña.');

            $mail = new Email();
            $mail->setTemplate('PASSWORD_RECOVERY_COMPLETED_SAFE');
            $mail->addVariable('{USERNAME}', $username);
            $mail->addVariable('{DATE}', date('Y-m-d @ h:i a'));
            $mail->addAddress((string)$reset['email']);
            $mail->send();
            NexoraAudit::log('identity.password_reset_completed', 'account', $username, null, array('realms_updated'=>$updated), $this->realm);
            return true;
        } catch(Throwable $e) {
            if($this->platform->inTransaction()) $this->platform->rollBack();
            throw $e;
        }
    }

    private function updateRealmPassword($realm, $username, $password) {
        $conn = NexoraRealm::connection('Me_MuOnline', $realm);
        $dbName = $this->qi($conn['database']);
        $table = $dbName.'.[dbo].'.$this->qi(_TBL_MI_);
        $userCol = $this->qi(_CLMN_USERNM_);
        $passCol = $this->qi(_CLMN_PASSWD_);
        $encryption = strtolower((string)config('SQL_PASSWORD_ENCRYPTION', true));
        $data = array();
        switch($encryption) {
            case 'wzmd5':
                $fn = $dbName.'.[dbo].[fn_md5]';
                $sql = 'UPDATE '.$table.' SET '.$passCol.' = '.$fn.'(?, '.$userCol.') WHERE '.$userCol.' = ?';
                $data = array($password, $username);
                break;
            case 'phpmd5':
                $sql = 'UPDATE '.$table.' SET '.$passCol.' = ? WHERE '.$userCol.' = ?';
                $data = array(md5($password), $username);
                break;
            case 'sha256':
                $salt = (string)config('SQL_SHA256_SALT', true);
                $hex = '0x'.hash('sha256', $password.$username.$salt);
                $sql = 'UPDATE '.$table.' SET '.$passCol.' = CONVERT(binary(32), ?, 1) WHERE '.$userCol.' = ?';
                $data = array($hex, $username);
                break;
            default:
                $sql = 'UPDATE '.$table.' SET '.$passCol.' = ? WHERE '.$userCol.' = ?';
                $data = array($password, $username);
        }
        $result = $this->platform->query_affected($sql, $data);
        return $result === false ? 0 : (int)$result;
    }

    private function sameSqlInstance($realm) {
        $platform = NexoraRealm::connection('Platform', $this->realm);
        $target = NexoraRealm::connection('Me_MuOnline', $realm);
        foreach(array('host','port','username','driver') as $key) if((string)$platform[$key] !== (string)$target[$key]) return false;
        return true;
    }

    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 de recuperación no es válido.');
        return hash('sha256', $token);
    }

    private function requirePlatform() {
        if(!NexoraRealm::platformEnabled()) throw new RuntimeException('La recuperación segura requiere activar Nexora Platform en el entorno de prueba.');
    }

    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 maskEmail($email) {
        $parts = explode('@', (string)$email, 2);
        if(count($parts) !== 2) return '***';
        return substr($parts[0],0,1).'***@'.$parts[1];
    }
}
