"""Matrix user account management for agents."""

import hashlib
import hmac
import secrets
from dataclasses import dataclass
from functools import cached_property

import httpx
import nio
from nio import crypto

from mindroom.constants import RuntimePaths, runtime_matrix_homeserver, runtime_matrix_ssl_verify
from mindroom.logging_config import get_logger
from mindroom.matrix import appservice, provisioning
from mindroom.matrix.client_session import (
    DEFAULT_MATRIX_SYNC_STORAGE,
    MatrixSyncStorage,
    login,
    matrix_client,
    matrix_startup_error,
    olm_store_exists,
    restore_login,
)
from mindroom.matrix.cross_signing import ensure_agent_cross_signing
from mindroom.matrix.identity import MatrixID, managed_account_key, parse_current_matrix_user_id
from mindroom.matrix.state import MatrixState, matrix_state_for_runtime
from mindroom.matrix_identifiers import agent_username_localpart, extract_server_name_from_homeserver

logger = get_logger(__name__)

_INVALID_REGISTRATION_TOKEN_MESSAGE = (
    "Matrix registration failed: MATRIX_REGISTRATION_TOKEN is invalid. "  # noqa: S105
    "Generate/issue a valid token for bot provisioning and try again."
)


INTERNAL_USER_AGENT_NAME = "user"
INTERNAL_USER_ACCOUNT_KEY = managed_account_key(INTERNAL_USER_AGENT_NAME)


@dataclass
class AgentMatrixUser:
    """Represents a Matrix user account for an agent."""

    agent_name: str
    user_id: str
    display_name: str
    password: str | None
    device_id: str | None = None
    access_token: str | None = None

    @cached_property
    def matrix_id(self) -> MatrixID:
        """MatrixID object from user_id."""
        return MatrixID.parse(self.user_id)


@dataclass(frozen=True)
class ManagedAccountProvisioningRequest:
    """One managed Matrix account that may be created during account preparation."""

    entity_name: str
    username: str | None = None


def _account_key_for_agent(entity_name: str) -> str:
    if entity_name == INTERNAL_USER_AGENT_NAME:
        return INTERNAL_USER_ACCOUNT_KEY
    return managed_account_key(entity_name)


def _account_label(entity_name: str) -> str:
    if entity_name == INTERNAL_USER_AGENT_NAME:
        return "internal user"
    return f"entity {entity_name!r}"


def _creation_username(request: ManagedAccountProvisioningRequest, runtime_paths: RuntimePaths) -> str:
    return request.username or agent_username_localpart(request.entity_name, runtime_paths=runtime_paths)


def preflight_managed_account_provisioning(
    requests: list[ManagedAccountProvisioningRequest],
    runtime_paths: RuntimePaths,
) -> None:
    """Reject localpart collisions before creating any missing managed account."""
    state = matrix_state_for_runtime(runtime_paths)
    unique_requests = {_account_key_for_agent(request.entity_name): request for request in requests}
    existing_owners_by_username = {
        account.username: _account_label(account_key.removeprefix("agent_"))
        for account_key, account in state.accounts.items()
    }
    pending_owners_by_username: dict[str, str] = {}

    for account_key, request in unique_requests.items():
        existing_account = state.get_account(account_key)
        if existing_account is not None:
            _validate_existing_internal_user_request(
                agent_name=request.entity_name,
                requested_username=request.username,
                existing_creds={
                    "username": existing_account.username,
                    "requested_username": existing_account.requested_username,
                },
            )
            continue

        username = _creation_username(request, runtime_paths)
        label = _account_label(request.entity_name)
        existing_owner = existing_owners_by_username.get(username)
        if existing_owner is not None:
            msg = (
                "Matrix account localpart collision before provisioning: "
                f"{label} would create {username!r}, already used by {existing_owner}."
            )
            raise matrix_startup_error(msg, permanent=True)

        pending_owner = pending_owners_by_username.get(username)
        if pending_owner is not None:
            msg = (
                "Matrix account localpart collision before provisioning: "
                f"{label} and {pending_owner} would both create {username!r}."
            )
            raise matrix_startup_error(msg, permanent=True)
        pending_owners_by_username[username] = label


def _get_agent_credentials(
    agent_name: str,
    runtime_paths: RuntimePaths,
) -> dict[str, str | None] | None:
    """Get credentials for a specific agent from matrix_state.yaml.

    Args:
        agent_name: The agent name
        runtime_paths: Explicit runtime context for matrix state lookup

    Returns:
        Dictionary with username and password, or None if not found

    """
    state = matrix_state_for_runtime(runtime_paths)
    agent_key = managed_account_key(agent_name)
    account = state.get_account(agent_key)
    if account:
        return {
            "username": account.username,
            "password": account.password,
            "requested_username": account.requested_username,
            "domain": account.domain,
            "device_id": account.device_id,
            "access_token": account.access_token,
        }
    return None


def _save_agent_credentials(
    agent_name: str,
    username: str,
    password: str | None,
    runtime_paths: RuntimePaths,
    *,
    domain: str | None = None,
    requested_username: str | None = None,
    device_id: str | None = None,
    access_token: str | None = None,
) -> None:
    """Save credentials for a specific agent to matrix_state.yaml.

    Args:
        agent_name: The agent name
        username: The Matrix username
        password: The Matrix password
        runtime_paths: Explicit runtime context for matrix state persistence
        domain: Optional Matrix domain to persist with the account
        requested_username: Optional Matrix username originally requested at account creation
        device_id: Optional Matrix device ID to persist with the account
        access_token: Optional Matrix access token to persist with the account

    """
    state = MatrixState.load(runtime_paths=runtime_paths)
    agent_key = managed_account_key(agent_name)
    server_name = domain or extract_server_name_from_homeserver(
        runtime_matrix_homeserver(runtime_paths),
        runtime_paths=runtime_paths,
    )
    state.add_account(
        agent_key,
        username,
        password,
        requested_username=requested_username,
        domain=server_name,
        device_id=device_id,
        access_token=access_token,
    )
    state.save(runtime_paths=runtime_paths)
    logger.info("agent_credentials_saved", agent=agent_name)


def _persist_agent_session(
    agent_name: str,
    username: str,
    password: str | None,
    *,
    domain: str | None = None,
    device_id: str | None,
    access_token: str | None,
    runtime_paths: RuntimePaths,
    requested_username: str | None = None,
) -> None:
    """Persist one agent session so restarts can reuse the same Matrix device."""
    _save_agent_credentials(
        agent_name,
        username,
        password,
        runtime_paths,
        domain=domain,
        requested_username=requested_username,
        device_id=device_id,
        access_token=access_token,
    )
    logger.info(
        "agent_session_persisted",
        agent=agent_name,
        device_id=device_id,
        has_access_token=bool(access_token),
    )


def _validated_expected_agent_user_id(agent_user: AgentMatrixUser) -> str:
    return _validated_returned_user_id(agent_user.user_id, source="Managed Matrix account")


def _validated_authenticated_agent_matrix_id(
    client: nio.AsyncClient,
    *,
    expected_user_id: str,
    source: str,
) -> MatrixID:
    actual_user_id = _validated_returned_user_id(client.user_id, source=source)
    if actual_user_id != expected_user_id:
        msg = f"{source} returned {actual_user_id} for managed Matrix account {expected_user_id}"
        raise matrix_startup_error(msg, permanent=True)
    return MatrixID.parse(expected_user_id)


def _persist_authenticated_agent_session(
    agent_user: AgentMatrixUser,
    client: nio.AsyncClient,
    runtime_paths: RuntimePaths,
    *,
    matrix_id: MatrixID,
) -> None:
    _persist_agent_session(
        agent_user.agent_name,
        matrix_id.username,
        agent_user.password,
        domain=matrix_id.domain,
        device_id=client.device_id,
        access_token=client.access_token,
        runtime_paths=runtime_paths,
    )
    agent_user.device_id = client.device_id
    agent_user.access_token = client.access_token


async def _homeserver_requires_registration_token(
    homeserver: str,
    runtime_paths: RuntimePaths,
) -> bool:
    """Check whether the homeserver advertises registration-token flow."""
    url = f"{homeserver.rstrip('/')}/_matrix/client/v3/register"
    try:
        async with httpx.AsyncClient(
            timeout=5,
            verify=runtime_matrix_ssl_verify(runtime_paths=runtime_paths),
        ) as client:
            response = await client.post(url, json={})
            data = response.json()
    except (httpx.HTTPError, ValueError):
        return False

    flows = data.get("flows")
    if not isinstance(flows, list):
        return False
    for flow in flows:
        if not isinstance(flow, dict):
            continue
        stages = flow.get("stages")
        if isinstance(stages, list) and "m.login.registration_token" in stages:
            return True
    return False


async def _registration_failure_message(
    response: nio.ErrorResponse,
    homeserver: str,
    registration_token: str | None,
    runtime_paths: RuntimePaths,
) -> str | None:
    if (
        response.status_code == "M_FORBIDDEN"
        and registration_token
        and "Invalid registration token" in (response.message or "")
    ):
        return _INVALID_REGISTRATION_TOKEN_MESSAGE

    if (
        response.message == "unknown error"
        and not registration_token
        and await _homeserver_requires_registration_token(homeserver, runtime_paths)
    ):
        return (
            "Matrix homeserver requires registration tokens for account creation. "
            "Set MATRIX_REGISTRATION_TOKEN and retry."
        )

    return None


async def _register_user_with_token(
    *,
    homeserver: str,
    user_id: str,
    username: str,
    password: str,
    display_name: str,
    registration_token: str,
    runtime_paths: RuntimePaths,
) -> str:
    """Register a user with token auth, supporting both direct and UIAA flows."""
    register_url = f"{homeserver.rstrip('/')}/_matrix/client/v3/register"
    request_payload = {
        "username": username,
        "password": password,
        "device_name": "mindroom_agent",
        "auth": {
            "type": "m.login.registration_token",
            "token": registration_token,
        },
    }

    try:
        async with httpx.AsyncClient(
            timeout=10,
            verify=runtime_matrix_ssl_verify(runtime_paths=runtime_paths),
        ) as client:
            response = await client.post(register_url, json=request_payload)
    except httpx.HTTPError as exc:
        msg = f"Could not reach Matrix homeserver ({homeserver}) during registration: {exc}"
        raise matrix_startup_error(msg) from exc

    detail, errcode = _registration_http_error_details(response)
    if response.is_success:
        user_id = _direct_registration_success_user_id(response)
        logger.info("matrix_user_registered_with_token", user_id=user_id)
    elif errcode == "M_USER_IN_USE":
        logger.info("matrix_user_already_exists", user_id=user_id)
    else:
        permanent_error = _direct_token_registration_error(username=username, errcode=errcode, detail=detail)
        if permanent_error is not None:
            raise permanent_error
        logger.info(
            "Direct token registration failed; falling back to matrix-nio interactive registration",
            user_id=user_id,
            status_code=response.status_code,
            errcode=errcode,
            detail=detail,
        )
        return await _register_user_with_token_via_nio(
            homeserver=homeserver,
            user_id=user_id,
            username=username,
            password=password,
            display_name=display_name,
            registration_token=registration_token,
            runtime_paths=runtime_paths,
        )

    return await _login_existing_user_or_raise_collision(
        homeserver=homeserver,
        user_id=user_id,
        password=password,
        display_name=display_name,
        runtime_paths=runtime_paths,
    )


def _synapse_shared_secret_registration_mac(
    *,
    shared_secret: str,
    nonce: str,
    username: str,
    password: str,
    admin: bool = False,
) -> str:
    mac = hmac.new(key=shared_secret.encode("utf-8"), digestmod=hashlib.sha1)
    for index, value in enumerate((nonce, username, password, "admin" if admin else "notadmin")):
        if index:
            mac.update(b"\x00")
        mac.update(value.encode("utf-8"))
    return mac.hexdigest()


def _synapse_shared_secret_nonce(response: httpx.Response) -> str:
    if not response.is_success:
        detail, _ = _registration_http_error_details(response)
        msg = f"Synapse shared-secret registration nonce request failed: HTTP {response.status_code}: {detail}"
        raise matrix_startup_error(msg, permanent=response.status_code in {400, 401, 403, 404})
    try:
        body = response.json()
    except ValueError as exc:
        msg = "Synapse shared-secret registration nonce response missing valid JSON."
        raise matrix_startup_error(msg, permanent=True) from exc
    if not isinstance(body, dict):
        msg = "Synapse shared-secret registration nonce response payload must be an object."
        raise matrix_startup_error(msg, permanent=True)
    nonce = body.get("nonce")
    if not isinstance(nonce, str) or not nonce.strip():
        msg = "Synapse shared-secret registration nonce response missing nonce."
        raise matrix_startup_error(msg, permanent=True)
    return nonce.strip()


async def _register_user_with_synapse_shared_secret(
    *,
    homeserver: str,
    user_id: str,
    username: str,
    password: str,
    display_name: str,
    shared_secret: str,
    runtime_paths: RuntimePaths,
) -> str:
    """Register a user with Synapse's shared-secret admin registration API."""
    register_url = f"{homeserver.rstrip('/')}/_synapse/admin/v1/register"
    try:
        async with httpx.AsyncClient(
            timeout=10,
            verify=runtime_matrix_ssl_verify(runtime_paths=runtime_paths),
        ) as client:
            nonce = _synapse_shared_secret_nonce(await client.get(register_url))
            response = await client.post(
                register_url,
                json={
                    "nonce": nonce,
                    "username": username,
                    "password": password,
                    "admin": False,
                    "mac": _synapse_shared_secret_registration_mac(
                        shared_secret=shared_secret,
                        nonce=nonce,
                        username=username,
                        password=password,
                    ),
                    "displayname": display_name,
                },
            )
    except httpx.HTTPError as exc:
        msg = f"Could not reach Synapse shared-secret registration endpoint ({homeserver}): {exc}"
        raise matrix_startup_error(msg) from exc

    detail, errcode = _registration_http_error_details(response)
    if response.is_success:
        actual_user_id = _direct_registration_success_user_id(response)
        logger.info("matrix_user_registered_with_synapse_shared_secret", user_id=actual_user_id)
    elif errcode == "M_USER_IN_USE":
        actual_user_id = user_id
        logger.info("matrix_user_already_exists", user_id=user_id)
    else:
        msg = f"Synapse shared-secret registration failed for user {username}: {detail}"
        raise matrix_startup_error(msg, permanent=response.status_code in {400, 401, 403, 404})

    return await _login_existing_user_or_raise_collision(
        homeserver=homeserver,
        user_id=actual_user_id,
        password=password,
        display_name=display_name,
        runtime_paths=runtime_paths,
    )


def _validated_returned_user_id(raw_user_id: object, *, source: str) -> str:
    if not isinstance(raw_user_id, str) or not raw_user_id.strip():
        msg = f"{source} response missing user_id"
        raise matrix_startup_error(msg, permanent=True)
    try:
        return parse_current_matrix_user_id(raw_user_id.strip())
    except ValueError as exc:
        msg = f"{source} response returned invalid user_id {raw_user_id!r}"
        raise matrix_startup_error(msg, permanent=True) from exc


def _validated_login_user_id(login_response: nio.LoginResponse, *, expected_user_id: str) -> str:
    actual_user_id = _validated_returned_user_id(login_response.user_id, source="Matrix login")
    expected = _validated_returned_user_id(expected_user_id, source="Matrix login request")
    if actual_user_id != expected:
        msg = f"Matrix login returned {actual_user_id} while provisioning expected {expected}"
        raise matrix_startup_error(msg, permanent=True)
    return actual_user_id


def _direct_registration_success_user_id(response: httpx.Response) -> str:
    try:
        body = response.json()
    except ValueError as exc:
        msg = "Matrix registration response missing valid JSON."
        raise matrix_startup_error(msg, permanent=True) from exc
    if not isinstance(body, dict):
        msg = "Matrix registration response payload must be an object."
        raise matrix_startup_error(msg, permanent=True)
    return _validated_returned_user_id(body.get("user_id"), source="Matrix registration")


def _registration_http_error_details(response: httpx.Response) -> tuple[str, str | None]:
    """Extract a human-readable error detail and errcode from an HTTP response."""
    detail = response.text.strip() or "unknown error"
    errcode = None
    try:
        body = response.json()
    except ValueError:
        return detail, errcode

    if not isinstance(body, dict):
        return detail, errcode
    raw_errcode = body.get("errcode")
    if isinstance(raw_errcode, str) and raw_errcode:
        errcode = raw_errcode
    raw_error = body.get("error")
    if raw_error is not None:
        detail = str(raw_error)
    return detail, errcode


def _direct_token_registration_error(
    *,
    username: str,
    errcode: str | None,
    detail: str,
) -> ValueError | None:
    """Return a permanent startup error for terminal direct token registration failures."""
    if errcode == "M_FORBIDDEN" and "Invalid registration token" in detail:
        return matrix_startup_error(_INVALID_REGISTRATION_TOKEN_MESSAGE, permanent=True)
    if errcode == "M_INVALID_USERNAME":
        msg = f"Failed to register user {username}: {errcode}"
        return matrix_startup_error(msg, permanent=True)
    return None


def _validate_existing_internal_user_request(
    *,
    agent_name: str,
    requested_username: str | None,
    existing_creds: dict[str, str | None],
) -> None:
    if agent_name != INTERNAL_USER_AGENT_NAME or requested_username is None:
        return
    stored_request = existing_creds.get("requested_username") or existing_creds.get("username")
    if stored_request == requested_username:
        return
    msg = (
        "mindroom_user.username cannot be changed after the internal Matrix account has been created. "
        f"Configured username {requested_username!r} does not match the original requested username "
        f"{stored_request!r}."
    )
    raise matrix_startup_error(msg, permanent=True)


async def _register_user_with_token_via_nio(
    *,
    homeserver: str,
    user_id: str,
    username: str,
    password: str,
    display_name: str,
    registration_token: str,
    runtime_paths: RuntimePaths,
) -> str:
    """Fallback to matrix-nio's interactive registration for spec-strict homeservers."""

    async def _register_with_client(client: nio.AsyncClient) -> str:
        response = await client.register_with_token(
            username=username,
            password=password,
            registration_token=registration_token,
            device_name="mindroom_agent",
        )
        return await _handle_register_response(
            response=response,
            client=client,
            homeserver=homeserver,
            user_id=user_id,
            username=username,
            password=password,
            display_name=display_name,
            registration_token=registration_token,
            runtime_paths=runtime_paths,
        )

    async with matrix_client(homeserver, user_id=user_id, runtime_paths=runtime_paths) as client:
        return await _register_with_client(client)


def _account_collision_error(user_id: str, login_response: object) -> ValueError:
    msg = (
        f"Matrix account collision for {user_id}: the user already exists but login with the configured password failed "
        f"({login_response}). Set a unique MINDROOM_NAMESPACE (or choose different names) and retry."
    )
    return matrix_startup_error(msg, permanent=True)


async def _login_and_sync_display_name(
    *,
    client: nio.AsyncClient,
    password: str,
    display_name: str,
) -> nio.LoginResponse | nio.LoginError:
    """Login with known password and keep display name synchronized."""
    login_response = await client.login(password)
    if isinstance(login_response, nio.LoginResponse):
        display_response = await client.set_displayname(display_name)
        if isinstance(display_response, nio.ErrorResponse):
            logger.warning(
                "matrix_user_display_name_sync_failed",
                user_id=client.user_id,
                error=str(display_response),
            )
    return login_response


async def _login_existing_user(
    *,
    homeserver: str,
    user_id: str,
    password: str,
    display_name: str,
    runtime_paths: RuntimePaths,
) -> nio.LoginResponse | nio.LoginError:
    """Login an existing user with a fresh client and keep the display name synchronized."""

    async def _login_with_client(client: nio.AsyncClient) -> nio.LoginResponse | nio.LoginError:
        return await _login_and_sync_display_name(
            client=client,
            password=password,
            display_name=display_name,
        )

    async with matrix_client(homeserver, user_id=user_id, runtime_paths=runtime_paths) as client:
        return await _login_with_client(client)


async def _login_existing_user_or_raise_collision(
    *,
    homeserver: str,
    user_id: str,
    password: str,
    display_name: str,
    runtime_paths: RuntimePaths,
) -> str:
    """Login an existing user, sync display name, and fail permanently on collisions."""
    login_response = await _login_existing_user(
        homeserver=homeserver,
        user_id=user_id,
        password=password,
        display_name=display_name,
        runtime_paths=runtime_paths,
    )
    if not isinstance(login_response, nio.LoginResponse):
        raise _account_collision_error(user_id, login_response)
    return _validated_login_user_id(login_response, expected_user_id=user_id)


async def _login_existing_user_with_client_or_raise_collision(
    *,
    client: nio.AsyncClient,
    user_id: str,
    password: str,
    display_name: str,
) -> str:
    """Login an existing user with a provided client, sync display name, and fail on collisions."""
    login_response = await _login_and_sync_display_name(
        client=client,
        password=password,
        display_name=display_name,
    )
    if not isinstance(login_response, nio.LoginResponse):
        raise _account_collision_error(user_id, login_response)
    return _validated_login_user_id(login_response, expected_user_id=user_id)


async def _handle_register_response(
    *,
    response: nio.RegisterResponse | nio.ErrorResponse,
    client: nio.AsyncClient,
    homeserver: str,
    user_id: str,
    username: str,
    password: str,
    display_name: str,
    registration_token: str | None,
    runtime_paths: RuntimePaths,
) -> str:
    """Handle a matrix-nio register response and finalize account setup."""
    if isinstance(response, nio.RegisterResponse):
        actual_user_id = _validated_returned_user_id(response.user_id, source="Matrix registration")
        logger.info("matrix_user_registered", user_id=actual_user_id)
        client.user_id = actual_user_id
        client.access_token = response.access_token
        client.device_id = response.device_id

        display_response = await client.set_displayname(display_name)
        if isinstance(display_response, nio.ErrorResponse):
            logger.warning(
                "matrix_user_display_name_set_failed",
                user_id=actual_user_id,
                error=str(display_response),
            )

        return actual_user_id
    if isinstance(response, nio.ErrorResponse) and response.status_code == "M_USER_IN_USE":
        logger.info("matrix_user_already_exists", user_id=user_id)
        return await _login_existing_user_with_client_or_raise_collision(
            client=client,
            user_id=user_id,
            password=password,
            display_name=display_name,
        )

    if not isinstance(response, nio.ErrorResponse):
        msg = f"Failed to register user {username}: {response}"
        raise matrix_startup_error(msg)
    failure_message = await _registration_failure_message(
        response,
        homeserver,
        registration_token,
        runtime_paths,
    )
    if failure_message:
        raise matrix_startup_error(failure_message, permanent=True)
    msg = f"Failed to register user {username}: {response}"
    raise matrix_startup_error(msg, response=response)


async def _register_user(
    homeserver: str,
    username: str,
    password: str,
    display_name: str,
    runtime_paths: RuntimePaths,
) -> str:
    """Register a new Matrix user account.

    Args:
        homeserver: The Matrix homeserver URL
        username: The username for the Matrix account (without domain)
        password: The password for the account
        display_name: The display name for the user
        runtime_paths: Optional explicit runtime context for env and SSL resolution

    Returns:
        The full Matrix user ID (e.g., @user:localhost)

    Raises:
        ValueError: If registration fails

    """
    server_name = extract_server_name_from_homeserver(homeserver, runtime_paths=runtime_paths)
    user_id = MatrixID.from_username(username, server_name).full_id
    registration_token = provisioning.registration_token_from_env(runtime_paths=runtime_paths)
    registration_shared_secret = provisioning.registration_shared_secret_from_env(runtime_paths=runtime_paths)

    provisioning_result = await _register_user_via_provisioning_if_configured(
        homeserver=homeserver,
        username=username,
        password=password,
        display_name=display_name,
        registration_token=registration_token,
        runtime_paths=runtime_paths,
    )
    if provisioning_result is not None:
        return provisioning_result
    if registration_shared_secret:
        return await _register_user_with_synapse_shared_secret(
            homeserver=homeserver,
            user_id=user_id,
            username=username,
            password=password,
            display_name=display_name,
            shared_secret=registration_shared_secret,
            runtime_paths=runtime_paths,
        )
    if registration_token:
        return await _register_user_with_token(
            homeserver=homeserver,
            user_id=user_id,
            username=username,
            password=password,
            display_name=display_name,
            registration_token=registration_token,
            runtime_paths=runtime_paths,
        )
    return await _register_user_without_token(
        homeserver=homeserver,
        user_id=user_id,
        username=username,
        password=password,
        display_name=display_name,
        runtime_paths=runtime_paths,
    )


async def _register_user_via_provisioning_if_configured(
    *,
    homeserver: str,
    username: str,
    password: str,
    display_name: str,
    registration_token: str | None,
    runtime_paths: RuntimePaths,
) -> str | None:
    """Register through the provisioning service when local client creds are configured."""
    provisioning_url = provisioning.provisioning_url_from_env(runtime_paths=runtime_paths)
    creds = provisioning.required_local_provisioning_client_credentials_for_registration(
        provisioning_url=provisioning_url,
        registration_token=registration_token,
        runtime_paths=runtime_paths,
    )
    if not (creds and provisioning_url):
        return None

    client_id, client_secret = creds
    provisioning_result = await provisioning.register_user_via_provisioning_service(
        provisioning_url=provisioning_url,
        client_id=client_id,
        client_secret=client_secret,
        homeserver=homeserver,
        username=username,
        password=password,
        display_name=display_name,
        runtime_paths=runtime_paths,
    )
    provisioning_user_id = _validated_returned_user_id(
        provisioning_result.user_id,
        source="Provisioning service",
    )
    if provisioning_result.status == "created":
        logger.info("matrix_user_registered_via_provisioning", user_id=provisioning_user_id)
        return provisioning_user_id

    logger.info("matrix_user_already_exists_via_provisioning", user_id=provisioning_user_id)
    return await _login_existing_user_or_raise_collision(
        homeserver=homeserver,
        user_id=provisioning_user_id,
        password=password,
        display_name=display_name,
        runtime_paths=runtime_paths,
    )


async def _register_user_without_token(
    *,
    homeserver: str,
    user_id: str,
    username: str,
    password: str,
    display_name: str,
    runtime_paths: RuntimePaths,
) -> str:
    """Register directly against the Matrix homeserver without token auth."""

    async def _register_with_client(client: nio.AsyncClient) -> str:
        response = await client.register(
            username=username,
            password=password,
            device_name="mindroom_agent",
        )
        return await _handle_register_response(
            response=response,
            client=client,
            homeserver=homeserver,
            user_id=user_id,
            username=username,
            password=password,
            display_name=display_name,
            registration_token=None,
            runtime_paths=runtime_paths,
        )

    async with matrix_client(homeserver, user_id=user_id, runtime_paths=runtime_paths) as client:
        return await _register_with_client(client)


async def create_agent_user(
    homeserver: str,
    agent_name: str,
    agent_display_name: str,
    runtime_paths: RuntimePaths,
    username: str | None = None,
) -> AgentMatrixUser:
    """Create or retrieve a Matrix user account for an agent.

    Args:
        homeserver: The Matrix homeserver URL
        agent_name: The internal agent name (e.g., 'calculator')
        agent_display_name: The display name for the agent (e.g., 'CalculatorAgent')
        username: Optional Matrix username localpart to request when creating a missing account
        runtime_paths: Optional explicit runtime context for env and SSL resolution

    Returns:
        AgentMatrixUser object with account details

    """
    auth = appservice.resolve_managed_account_auth(runtime_paths)

    # Check if credentials already exist in matrix_state.yaml
    existing_creds = _get_agent_credentials(agent_name, runtime_paths)
    preferred_username = username
    requested_username: str | None = None

    if existing_creds:
        _validate_existing_internal_user_request(
            agent_name=agent_name,
            requested_username=preferred_username,
            existing_creds=existing_creds,
        )
        username_value = existing_creds["username"]
        password_value = existing_creds["password"]
        if username_value is None or (auth.mode == "password" and password_value is None):
            msg = f"Stored Matrix credentials for {agent_name} are incomplete"
            raise matrix_startup_error(msg, permanent=True)
        matrix_username = username_value
        password = password_value if auth.mode == "password" else None
        # Older persisted credentials may not include session fields yet.
        existing_device_id = existing_creds.get("device_id")
        existing_access_token = existing_creds.get("access_token")
        existing_domain = existing_creds.get("domain")
        logger.info("agent_credentials_loaded", agent=agent_name)
        registration_needed = False
    else:
        # Generate new credentials
        matrix_username = preferred_username or agent_username_localpart(agent_name, runtime_paths=runtime_paths)
        requested_username = matrix_username
        password = secrets.token_urlsafe(24) if auth.mode == "password" else None
        existing_device_id = None
        existing_access_token = None
        existing_domain = None
        logger.info("agent_credentials_generated", agent=agent_name)
        registration_needed = True

    # Extract server name from homeserver URL
    server_name = extract_server_name_from_homeserver(homeserver, runtime_paths=runtime_paths)
    user_id = MatrixID.from_username(matrix_username, existing_domain or server_name).full_id

    if registration_needed:
        if auth.mode == "appservice":
            assert auth.appservice_token is not None
            user_id = await appservice.register_appservice_user(
                homeserver,
                username=matrix_username,
                expected_user_id=user_id,
                token=auth.appservice_token,
                runtime_paths=runtime_paths,
            )
        else:
            assert password is not None
            user_id = await _register_user(
                homeserver=homeserver,
                username=matrix_username,
                password=password,
                display_name=agent_display_name,
                runtime_paths=runtime_paths,
            )
        actual_matrix_id = MatrixID.parse(user_id)
        matrix_username = actual_matrix_id.username
        server_name = actual_matrix_id.domain

    # Save credentials only after registration/verification succeeds.
    if registration_needed:
        _save_agent_credentials(
            agent_name,
            matrix_username,
            password,
            runtime_paths,
            domain=server_name,
            requested_username=requested_username,
        )
        logger.info("agent_credentials_saved_after_registration", agent=agent_name)

    return AgentMatrixUser(
        agent_name=agent_name,
        user_id=user_id,
        display_name=agent_display_name,
        password=password,
        device_id=existing_device_id,
        access_token=existing_access_token,
    )


async def _login_agent_with_configured_auth(
    homeserver: str,
    agent_user: AgentMatrixUser,
    expected_user_id: str,
    auth: appservice.ManagedAccountAuth,
    runtime_paths: RuntimePaths,
    sync_storage: MatrixSyncStorage,
) -> tuple[nio.AsyncClient, str]:
    if auth.mode == "appservice":
        assert auth.appservice_token is not None
        client = await appservice.login_appservice_user(
            homeserver,
            user_id=expected_user_id,
            token=auth.appservice_token,
            runtime_paths=runtime_paths,
            sync_storage=sync_storage,
        )
        return client, "Matrix application-service login"

    if agent_user.password is None:
        msg = f"Stored Matrix password for {agent_user.agent_name} is missing"
        raise matrix_startup_error(msg, permanent=True)
    client = await login(
        homeserver,
        expected_user_id,
        agent_user.password,
        runtime_paths=runtime_paths,
        sync_storage=sync_storage,
    )
    return client, "Matrix password login"


async def login_agent_user(
    homeserver: str,
    agent_user: AgentMatrixUser,
    runtime_paths: RuntimePaths,
    sync_storage: MatrixSyncStorage = DEFAULT_MATRIX_SYNC_STORAGE,
) -> nio.AsyncClient:
    """Login an agent user and return the authenticated client.

    Args:
        homeserver: The Matrix homeserver URL
        agent_user: The agent user to login
        runtime_paths: Explicit runtime context for environment and SSL resolution
        sync_storage: Selects which sync state nio persists for this client

    Returns:
        Authenticated AsyncClient instance

    Raises:
        ValueError: If login fails

    """
    auth = appservice.resolve_managed_account_auth(runtime_paths)
    expected_user_id = _validated_expected_agent_user_id(agent_user)

    store_intact = not crypto.ENCRYPTION_ENABLED or (
        agent_user.device_id is None or olm_store_exists(expected_user_id, agent_user.device_id, runtime_paths)
    )
    if not store_intact:
        logger.warning(
            "matrix_olm_store_missing_fresh_device_login",
            agent=agent_user.agent_name,
            user_id=expected_user_id,
            device_id=agent_user.device_id,
            hint=(
                "The encryption store for this device is gone; restoring the session would wedge "
                "its crypto identity. Logging in as a fresh device instead. Messages encrypted "
                "only to the lost device stay undecryptable."
            ),
        )

    if store_intact and agent_user.access_token and agent_user.device_id:
        try:
            restored_client = await restore_login(
                homeserver,
                expected_user_id,
                agent_user.device_id,
                agent_user.access_token,
                runtime_paths=runtime_paths,
                sync_storage=sync_storage,
            )
        except ValueError:
            logger.warning(
                "matrix_login_restore_failed_falling_back_to_configured_auth",
                agent=agent_user.agent_name,
                user_id=agent_user.user_id,
                device_id=agent_user.device_id,
            )
        else:
            try:
                matrix_id = _validated_authenticated_agent_matrix_id(
                    restored_client,
                    expected_user_id=expected_user_id,
                    source="Matrix session restore",
                )
            except ValueError as exc:
                await restored_client.close()
                logger.warning(
                    "matrix_login_restore_identity_mismatch_falling_back_to_configured_auth",
                    agent=agent_user.agent_name,
                    expected_user_id=expected_user_id,
                    returned_user_id=restored_client.user_id,
                    device_id=agent_user.device_id,
                    error=str(exc),
                )
            else:
                _persist_authenticated_agent_session(
                    agent_user,
                    restored_client,
                    runtime_paths,
                    matrix_id=matrix_id,
                )
                await ensure_agent_cross_signing(restored_client, agent_user)
                return restored_client

    client, login_source = await _login_agent_with_configured_auth(
        homeserver,
        agent_user,
        expected_user_id,
        auth,
        runtime_paths,
        sync_storage,
    )
    try:
        matrix_id = _validated_authenticated_agent_matrix_id(
            client,
            expected_user_id=expected_user_id,
            source=login_source,
        )
    except ValueError:
        await client.close()
        raise

    if auth.mode == "appservice":
        display_response = await client.set_displayname(agent_user.display_name)
        if isinstance(display_response, nio.ErrorResponse):
            logger.warning(
                "matrix_user_display_name_sync_failed",
                user_id=expected_user_id,
                error=str(display_response),
            )

    _persist_authenticated_agent_session(
        agent_user,
        client,
        runtime_paths,
        matrix_id=matrix_id,
    )
    await ensure_agent_cross_signing(client, agent_user)
    return client
