# built-ins
 from abc import ABC, abstractmethod
 from base64 import b64encode
-from dataclasses import dataclass, InitVar
+from dataclasses import dataclass, asdict as dc_asdict, InitVar
 from enum import Enum, auto
 from getpass import getuser
 from threading import Thread
 from typing import Any, Callable, NamedTuple, Optional, Self
+from uuid import uuid4
 # ourselves
 from ircplom.events import (
         AffectiveEvent, CrashingException, ExceptionEvent, QueueMixin)
 
 ClientsDb = dict[str, 'Client']
 _NAMES_DESIRED_SERVER_CAPS = ('sasl',)
+_ILLEGAL_NICK_FIRSTCHARS = '~&@+# '
 
 
 class _MsgTok(Enum):
 _EXPECTATIONS += [
     _MsgParseExpectation(_MsgTok.SERVER,
                          '001',  # RPL_WELCOME
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '002',  # RPL_YOURHOST
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '003',  # RPL_CREATED
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '004',  # RPL_MYINFO
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY,
                           _MsgTok.ANY,
                           _MsgTok.ANY,
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '250',  # RPL_STATSDLINE / RPL_STATSCONN
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '251',  # RPL_LUSERCLIENT
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '252',  # RPL_LUSEROP
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY,
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '253',  # RPL_LUSERUNKNOWN
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY,
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '254',  # RPL_LUSERCHANNELS
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY,
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '255',  # RPL_LUSERME
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '265',  # RPL_LOCALUSERS
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
-                         '265',  # RPL_GLOBALUSERS
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         '265',  # RPL_LOCALUSERS
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY,
                           _MsgTok.ANY,
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
-                         '266',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         '266',  # RPL_GLOBALUSERS
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
-                         '266',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         '266',  # RPL_GLOBALUSERS
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY,
                           _MsgTok.ANY,
                           _MsgTok.ANY)),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '375',  # RPL_MOTDSTART already implied by 1st 372
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),
 ]
 
 _EXPECTATIONS += [
     _MsgParseExpectation(_MsgTok.SERVER,
                          '005',  # RPL_ISUPPORT
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, ':isupports'),
                           _MsgTok.ANY),  # comment
                          idx_into_list=1),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '372',  # RPL_MOTD
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, ':line'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '376',  # RPL_ENDOFMOTD
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           _MsgTok.ANY)),  # comment
     _MsgParseExpectation(_MsgTok.SERVER,
                          '396',  # RPL_VISIBLEHOST
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
-                          (_MsgTok.SERVER, 'set_db_attr:client_host'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
+                          (_MsgTok.SERVER, 'set_me_attr:host'),
                           _MsgTok.ANY)),  # comment
 ]
 
 _EXPECTATIONS += [
     _MsgParseExpectation(_MsgTok.SERVER,
                          '900',  # RPL_LOGGEDIN
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
-                          (_MsgTok.NICK_USER_HOST,
-                           'set_db_attr:_nick_user_host'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
+                          (_MsgTok.NICK_USER_HOST, 'set_me_attr:nickuserhost'),
                           (_MsgTok.ANY, 'set_db_attr:sasl_account'),
                           _MsgTok.ANY)),  # comment
     _MsgParseExpectation(_MsgTok.SERVER,
                          '903',  # RPL_SASLSUCCESS
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, 'set_db_attr:sasl_auth_state'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '904',  # ERR_SASLFAIL
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, 'set_db_attr:sasl_auth_state'))),
     _MsgParseExpectation(_MsgTok.NONE,
                          'AUTHENTICATE',
 _EXPECTATIONS += [
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('NEW', ':subverb'),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('DEL', ':subverb'),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('ACK', ':subverb'),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('NAK', ':subverb'),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('LS', ':subverb'),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('LS', ':subverb'),
                           ('*', ':tbc'),
                           (_MsgTok.LIST, ':items'))),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('LIST', ':subverb'),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'CAP',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           ('LIST', ':subverb'),
                           ('*', ':tbc'),
                           (_MsgTok.LIST, ':items'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '432',  # ERR_ERRONEOUSNICKNAME
                          ('*',
-                          _MsgTok.NICKNAME,
+                          _MsgTok.NICKNAME,  # no need to re-use the bad one
                           _MsgTok.ANY)),  # comment
     _MsgParseExpectation(_MsgTok.SERVER,
                          '432',  # ERR_ERRONEOUSNICKNAME
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
-                          _MsgTok.NICKNAME,
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
+                          _MsgTok.NICKNAME,  # no need to re-use the bad one
                           _MsgTok.ANY)),  # comment
     _MsgParseExpectation(_MsgTok.SERVER,
                          '433',  # ERR_NICKNAMEINUSE
-                         (_MsgTok.NICKNAME,
+                         (_MsgTok.NICKNAME,  # we rather go for incrementation
                           (_MsgTok.NICKNAME, ':used'),
                           _MsgTok.ANY)),  # comment
     _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':named'),
                          'NICK',
-                         ((_MsgTok.NICKNAME, ':nickname'),)),
+                         ((_MsgTok.NICKNAME, ':nick'),)),
 ]
 
 # joining/leaving
 _EXPECTATIONS += [
     _MsgParseExpectation(_MsgTok.SERVER,
                          '353',  # RPL_NAMREPLY
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           '=',
                           (_MsgTok.CHANNEL, ':channel'),
                           (_MsgTok.LIST, ':names'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          '366',  # RPL_ENDOFNAMES
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.CHANNEL, ':channel'),
                           _MsgTok.ANY)),  # comment
     _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':joiner'),
 _EXPECTATIONS += [
     _MsgParseExpectation(_MsgTok.SERVER,
                          '401',  # ERR_NOSUCKNICK
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.NICKNAME, ':target'),
                           _MsgTok.ANY)),  # comment
     _MsgParseExpectation(_MsgTok.SERVER,
                           (_MsgTok.ANY, ':message'))),
     _MsgParseExpectation(_MsgTok.SERVER,
                          'NOTICE',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, ':message'))),
-    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
                          'NOTICE',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, ':message'))),
-    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
                          'NOTICE',
                          ((_MsgTok.CHANNEL, ':channel'),
                           (_MsgTok.ANY, ':message'))),
-    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
                          'PRIVMSG',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, ':message'))),
-    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':sender'),
+    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_user:sender'),
                          'PRIVMSG',
                          ((_MsgTok.CHANNEL, ':channel'),
                           (_MsgTok.ANY, ':message'))),
     _MsgParseExpectation(_MsgTok.NONE,
                          'ERROR',
                          ((_MsgTok.ANY, 'set_db_attr:connection_state'),)),
-    _MsgParseExpectation(_MsgTok.NICKNAME,
+    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, 'set_me_attr:nickuserhost'),
                          'MODE',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, 'set_db_attr:user_modes'))),
-    _MsgParseExpectation(_MsgTok.NICK_USER_HOST,
+    _MsgParseExpectation(_MsgTok.NICKNAME,
                          'MODE',
-                         ((_MsgTok.NICKNAME, 'set_db_attr:nickname'),
+                         ((_MsgTok.NICKNAME, 'set_me_attr:nick'),
                           (_MsgTok.ANY, 'set_db_attr:user_modes'))),
     _MsgParseExpectation(_MsgTok.NONE,
                          'PING',
     def __init__(self) -> None:
         self._dict: dict[str, Any] = {}
 
+    @property
+    def keys(self) -> tuple[str, ...]:
+        'Keys of item registrations.'
+        return tuple(self._dict.keys())
+
     def set_on_update(self, name: str, on_update: Callable) -> None:
         'Caller of on_update with path= set to name.'
         self._on_update = lambda k: on_update(name, k)
         return self._dict[key]
 
     def __setitem__(self, key: str, val: Any) -> None:
+        if isinstance(val, _NickUserHost):
+            val.set_on_update(lambda: self._on_update(key))
         self._dict[key] = val
         self._on_update(key)
 
 
 
 class _ChannelDb(_Db):
-    _completable_users: _CompletableStringsList
+    _completable_user_ids: _CompletableStringsList
     # topic: str
     # channel_modes: str
 
 class SharedClientDbFields(IrcConnSetup):
     'API for fields shared directly in name and type with TUI.'
     connection_state: str
-    client_host: str
-    nickname: str
     sasl_account: str
     sasl_auth_state: str
     user_modes: str
-    username: str
+    users: Any
+    _channels: dict[str, Any]
+
+    def _purge_users(self) -> None:
+        to_keep = {'me'}
+        for chan in self._channels.values():
+            to_keep |= set(chan.user_ids)
+        for user_id in [id_ for id_ in self.users.keys if id_ not in to_keep]:
+            del self.users[user_id]
 
+    def chans_of_user(self, user_id: str) -> tuple[str, ...]:
+        'Return names of channels user of user_id currently participates in.'
+        return tuple(k for k, v in self._channels.items()
+                     if user_id in v.user_ids)
 
-class _NickUserHost(NamedTuple):
-    nick: str
-    user: str
-    host: str
+
+@dataclass
+class NickUserHost:
+    'Combination of nickname, username on host, and host.'
+    nick: str = '?'
+    user: str = '?'
+    host: str = '?'
+
+    def copy(self) -> Self:
+        'Produce copy not subject to later attribute changes on original.'
+        return self.__class__(**dc_asdict(self))
+
+
+class _NickUserHost(NickUserHost):
+    _on_update: Callable
 
     def __str__(self) -> str:
         return f'{self.nick}!{self.user}@{self.host}'
         assert len(toks) == 3
         return cls(*toks)
 
+    def set_on_update(self, on_update: Callable) -> None:
+        'Caller of on_update with path= set to name.'
+        self._on_update = on_update
+
+    def __setattr__(self, key: str, value: Any) -> None:
+        if key == 'nickuserhost' and isinstance(value, _NickUserHost):
+            self.nick = value.nick
+            self.user = value.user
+            self.host = value.host
+        else:
+            super().__setattr__(key, value)
+            if key != '_on_update' and hasattr(self, '_on_update'):
+                self._on_update()
+
 
 class _ClientDb(_Db, SharedClientDbFields):
     caps: _UpdatingDict
     isupports: _UpdatingDict
+    users: _UpdatingDict
     _completable_motd: _CompletableStringsList
     _channels: dict[str, _ChannelDb]
 
-    def __init__(self, **kwargs) -> None:
-        super().__init__(**kwargs)
-        self._types['_nick_user_host'] = _NickUserHost
-
-    def __setattr__(self, key: str, value) -> None:
-        super().__setattr__(key, value)
-        if key == 'nickname':
-            self.nick_wanted = value
-
-    @property
-    def _nick_user_host(self) -> _NickUserHost:
-        return _NickUserHost(self.nickname, self.username, self.client_host)
-
-    @_nick_user_host.setter
-    def _nick_user_host(self, nick_user_host: _NickUserHost) -> None:
-        self.nickname = nick_user_host.nick
-        self.username = nick_user_host.user
-        self.client_host = nick_user_host.host
+    def user_id(self, query: str | _NickUserHost) -> str:
+        'Return user_id for nickname of entire NickUserHost, create if none.'
+        nick = query if isinstance(query, str) else query.nick
+        matches = [id_ for id_ in self.users.keys
+                   if self.users[id_].nick == nick]
+        assert len(matches) < 2
+        id_ = matches[0] if matches else str(uuid4())
+        if isinstance(query, _NickUserHost):
+            self.users[id_] = query
+        elif not matches:
+            self.users[id_] = _NickUserHost(query)
+        return id_
+
+    def remove_user_from_channel(self, user_id: str, chan_name: str) -> None:
+        'Remove user from channel, check that user deleted if that was last.'
+        self.chan(chan_name).remove_completable('user_ids', user_id, True)
+        if user_id == 'me':
+            self.del_chan(chan_name)
+        self._purge_users()
+
+    def remove_user(self, user_id: str) -> tuple[str, ...]:
+        'Run remove_user_from_channel on all channels user is in.'
+        affected_chans = self.chans_of_user(user_id)
+        for chan_name in affected_chans:
+            self.remove_user_from_channel(user_id, chan_name)
+        return affected_chans
 
     def needs_arg(self, key: str) -> bool:
         'Reply if attribute of key may reasonably be addressed without an arg.'
     def del_chan(self, name: str) -> None:
         'Remove DB for channel of name.'
         del self._channels[name]
+        self._purge_users()
         self._on_update(name)
 
     def chan(self, name: str) -> _ChannelDb:
                 on_update=lambda k: self._on_update(name, k))
         return self._channels[name]
 
-    def chans_of_user(self, user: str) -> dict[str, _ChannelDb]:
-        'Return part of channels dictionary for channels user is currently in.'
-        return {k: v for k, v in self._channels.items() if user in v.users}
-
 
 class Client(ABC, ClientQueueMixin):
     'Abstracts socket connection, loop over it, and handling messages from it.'
         self.client_id = conn_setup.hostname
         super().__init__(client_id=self.client_id, **kwargs)
         self._db = _ClientDb(on_update=self._on_update)
-        self._db.username = getuser()
+        self._db.users['me'] = _NickUserHost('?', getuser(), '?')
         self._caps = _CapsManager(self.send, self._db.caps)
         for k in conn_setup.__annotations__:
             setattr(self._db, k, getattr(conn_setup, k))
         assert self.conn is not None
         self._db.connection_state = 'connected'
         self._caps.start_negotation()
-        self.send(IrcMessage(verb='USER', params=(self._db.username, '0', '*',
-                                                  self._db.realname)))
+        self.send(IrcMessage(verb='USER', params=(
+            self._db.users['me'].user.lstrip('~'),
+            '0', '*', self._db.realname)))
         self.send(IrcMessage(verb='NICK', params=(self._db.nick_wanted,)))
 
     @abstractmethod
         for name in self._db.chan_names:
             self._db.del_chan(name)
         self._db.isupports.clear()
-        self._db.nickname = ''
+        self._db.users['me'].nick = '?'
         self._db.sasl_auth_state = ''
 
     def on_handled_loop_exception(self, e: IrcConnAbortException) -> None:
                 return {'id': msg_tok, 'db': self._db.chan(msg_tok)
                         } if msg_tok[0] == '#' else None
             if ex_tok is _MsgTok.NICKNAME:
-                return msg_tok if msg_tok[0] not in '~&@%+# ' else None
+                return (msg_tok if msg_tok[0] not in _ILLEGAL_NICK_FIRSTCHARS
+                        else None)
             if ex_tok is _MsgTok.NICK_USER_HOST:
                 try:
                     return _NickUserHost.from_str(msg_tok)
             for idx, ex_tok in enumerate(ex_tok_fields):
                 ex_tok, key = ((ex_tok[0], ex_tok[1])
                                if isinstance(ex_tok, tuple) else (ex_tok, ''))
-                task, key = key.split(':', maxsplit=1) if key else ('', '')
-                if task:
+                tasks_, key = key.split(':', maxsplit=1) if key else ('', '')
+                for task in tasks_.split(','):
                     tasks[task] = tasks.get(task, []) + [key]
                 to_return[key] = param_match(ex_tok, msg_tok_fields[idx])
                 if to_return[key] is None:
             for arg in args:
                 if task == 'set_db_attr':
                     setattr(self._db, arg, ret[arg])
+                if task == 'set_me_attr':
+                    setattr(self._db.users['me'], arg, ret[arg])
+                if task == 'set_user':
+                    self._db.user_id(ret[arg])
         if ret['verb'] == '005':   # RPL_ISUPPORT
             for item in ret['isupports']:
                 toks = item.split('=', maxsplit=1)
                     self._db.isupports[toks[0]] = (toks[1] if len(toks) > 1
                                                    else '')
         elif ret['verb'] == '353':  # RPL_NAMREPLY
-            for name in ret['names']:
-                ret['channel']['db'].append_completable('users',
-                                                        name.lstrip('~&@%+'))
+            for id_ in [self._db.user_id(name.lstrip(_ILLEGAL_NICK_FIRSTCHARS))
+                        for name in ret['names']]:
+                ret['channel']['db'].append_completable('user_ids', id_)
         elif ret['verb'] == '366':  # RPL_ENDOFNAMES
-            ret['channel']['db'].declare_complete('users')
+            ret['channel']['db'].declare_complete('user_ids')
         elif ret['verb'] == '372':  # RPL_MOTD
             self._db.append_completable('motd', ret['line'])
         elif ret['verb'] == '376':  # RPL_ENDOFMOTD
                       target=ret['target'], alert=True)
         elif ret['verb'] == '432':  # ERR_ERRONEOUSNICKNAME
             alert = 'nickname refused for bad format'
-            if 'nickname' not in ret:
+            if 'nick' not in ret:
                 alert += ', giving up'
                 self.close()
             self._log(alert, alert=True)
                     self._caps.end_negotiation()
         elif ret['verb'] == 'ERROR':
             self.close()
-        elif ret['verb'] == 'JOIN' and ret['joiner'].nick != self._db.nickname:
+        elif ret['verb'] == 'JOIN' and ret['joiner'] != self._db.users['me']:
             ret['channel']['db'].append_completable(
-                    'users', ret['joiner'].nick, True)
+                    'user_ids', self._db.user_id(ret['joiner']), True)
         elif ret['verb'] == 'NICK':
-            if ret['named'].nick == self._db.nickname:
-                self._db.nickname = ret['nickname']
-            else:
-                for id_, ch in self._db.chans_of_user(ret['named'].nick
-                                                      ).items():
-                    ch.remove_completable('users', ret['named'].nick, True)
-                    ch.append_completable('users', ret['nickname'], True)
-                    self._log(f'{ret["named"]} becomes {ret["nickname"]}',
-                              scope=LogScope.CHAT, target=id_)
+            user_id = self._db.user_id(ret['named'])
+            self._db.users[user_id].nick = ret['nick']
+            if user_id == 'me':
+                self._db.nick_wanted = ret['nick']
         elif ret['verb'] in {'NOTICE', 'PRIVMSG'}:
             kw: dict[str, bool | str | LogScope] = {
                     'as_notice': msg.verb == 'NOTICE'}
             if 'sender' in ret:  # not just server message
                 kw |= {'sender': ret['sender'].nick, 'scope': LogScope.CHAT,
-                       'target': (ret['sender'].nick if 'nickname' in ret
+                       'target': (ret['sender'].nick if 'nick' in ret
                                   else ret['channel']['id'])}
             self._log(ret['message'], out=False, **kw)
         elif ret['verb'] == 'PART':
-            if ret['parter'].nick == self._db.nickname:
-                self._db.del_chan(ret['channel']['id'])
-            else:
-                ret['channel']['db'].remove_completable(
-                        'users', ret['parter'].nick, True)
+            self._db.remove_user_from_channel(self._db.user_id(ret['parter']),
+                                              ret['channel']['id'])
         elif ret['verb'] == 'PING':
             self.send(IrcMessage(verb='PONG', params=(ret['reply'],)))
         elif ret['verb'] == 'QUIT':
-            for id_, ch in self._db.chans_of_user(ret['quitter'].nick).items():
-                ch.remove_completable('users', ret['quitter'].nick, True)
+            for chan in self._db.remove_user(self._db.user_id(ret['quitter'])):
                 self._log(f'{ret["quitter"]} quits: {ret["message"]}',
-                          LogScope.CHAT, target=id_)
+                          LogScope.CHAT, target=chan)
 
 
 @dataclass