"""Provisioning helpers for hosted local-MindRoom registration flows."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal, NoReturn

import httpx

from mindroom.constants import RuntimePaths, runtime_env_path, runtime_matrix_ssl_verify
from mindroom.http_error_detail import error_detail_from_response
from mindroom.matrix.client_session import matrix_startup_error
from mindroom.matrix.identity import parse_current_matrix_user_id


def provisioning_url_from_env(runtime_paths: RuntimePaths) -> str | None:
    """Get hosted provisioning API base URL from environment if configured."""
    url = (runtime_paths.env_value("MINDROOM_PROVISIONING_URL") or "").strip()
    return url.rstrip("/") or None


def registration_token_from_env(runtime_paths: RuntimePaths) -> str | None:
    """Get MATRIX_REGISTRATION_TOKEN from environment if configured."""
    token = (runtime_paths.env_value("MATRIX_REGISTRATION_TOKEN") or "").strip()
    return token or None


def registration_shared_secret_from_env(runtime_paths: RuntimePaths) -> str | None:
    """Get Synapse shared-secret registration credentials from env or file."""
    secret = (runtime_paths.env_value("MATRIX_REGISTRATION_SHARED_SECRET") or "").strip()
    if secret:
        return secret

    file_path = runtime_env_path(runtime_paths, "MATRIX_REGISTRATION_SHARED_SECRET_FILE")
    if file_path is None:
        return None
    try:
        return file_path.read_text(encoding="utf-8").strip() or None
    except OSError as exc:
        msg = f"MATRIX_REGISTRATION_SHARED_SECRET_FILE is not readable: {file_path}"
        raise matrix_startup_error(msg, permanent=True) from exc


def _local_provisioning_client_credentials_from_env(
    runtime_paths: RuntimePaths,
) -> tuple[str, str] | None:
    """Get local provisioning client credentials from environment if configured."""
    client_id = (runtime_paths.env_value("MINDROOM_LOCAL_CLIENT_ID") or "").strip()
    client_secret = (runtime_paths.env_value("MINDROOM_LOCAL_CLIENT_SECRET") or "").strip()
    if not client_id and not client_secret:
        return None
    if not client_id or not client_secret:
        msg = (
            "Provisioning credentials are incomplete. "
            "Set both MINDROOM_LOCAL_CLIENT_ID and MINDROOM_LOCAL_CLIENT_SECRET, "
            "or run `mindroom connect --pair-code ...` again."
        )
        raise matrix_startup_error(msg, permanent=True)
    return client_id, client_secret


def required_local_provisioning_client_credentials_for_registration(
    *,
    provisioning_url: str | None,
    registration_token: str | None,
    runtime_paths: RuntimePaths,
) -> tuple[str, str] | None:
    """Resolve required local provisioning credentials when using hosted registration."""
    if registration_token or not provisioning_url:
        return None

    creds = _local_provisioning_client_credentials_from_env(runtime_paths)
    if creds is None:
        msg = (
            "MINDROOM_PROVISIONING_URL is set but local client credentials are missing. "
            "Run `mindroom connect --pair-code ...` first."
        )
        raise matrix_startup_error(msg, permanent=True)
    return creds


@dataclass(frozen=True)
class _ProvisioningRegisterResult:
    """Result returned by the provisioning register-agent endpoint."""

    status: Literal["created", "user_in_use"]
    user_id: str


# Kept in sync with scripts/local_mindroom_provisioning_service.py by a contract
# test. The service's other credential failures ("Missing/Invalid local client
# credentials") always use HTTP 401, so only the revoked detail matters for 403.
_CONNECTION_REVOKED_DETAIL = "Connection revoked"
_NAMESPACE_MISMATCH_DETAIL = "Requested username is outside this local connection namespace"


def _raise_for_register_agent_error(response: httpx.Response, *, username: str) -> NoReturn:
    """Raise the appropriate error for a failed register-agent response."""
    detail = error_detail_from_response(response)
    if response.status_code == 401 or (response.status_code == 403 and detail == _CONNECTION_REVOKED_DETAIL):
        msg = (
            f"Provisioning credentials are invalid or revoked (server said: {detail}). "
            "Run `mindroom connect --pair-code ...` again."
        )
        raise matrix_startup_error(msg, permanent=True)
    if response.status_code == 403:
        msg = f"Provisioning service refused to register agent user {username!r}: {detail}"
        if detail == _NAMESPACE_MISMATCH_DETAIL:
            msg += (
                ". Usernames must match mindroom_<entity>_<namespace>; check that MINDROOM_NAMESPACE "
                "matches this connection's namespace or re-run `mindroom connect --pair-code ...`."
            )
        raise matrix_startup_error(msg, permanent=True)
    if response.status_code == 404:
        msg = "Provisioning service does not support /register-agent yet. Deploy the latest local provisioning service."
        raise matrix_startup_error(msg, permanent=True)
    if response.status_code in {400, 422}:
        msg = f"Provisioning service rejected the register-agent request (HTTP {response.status_code}): {detail}"
        raise matrix_startup_error(msg, permanent=True)
    if 300 <= response.status_code < 400:
        location = response.headers.get("location", "unknown")
        msg = (
            f"Provisioning service URL redirects (HTTP {response.status_code} to {location}). "
            "Update MINDROOM_PROVISIONING_URL to the final URL."
        )
        raise matrix_startup_error(msg, permanent=True)
    msg = f"Provisioning service returned HTTP {response.status_code}: {detail}"
    raise ValueError(msg)


async def register_user_via_provisioning_service(
    *,
    provisioning_url: str,
    client_id: str,
    client_secret: str,
    homeserver: str,
    username: str,
    password: str,
    display_name: str,
    runtime_paths: RuntimePaths,
) -> _ProvisioningRegisterResult:
    """Register an agent account via provisioning service server-side flow."""
    url = f"{provisioning_url}/v1/local-mindroom/register-agent"
    headers = {
        "X-Local-MindRoom-Client-Id": client_id,
        "X-Local-MindRoom-Client-Secret": client_secret,
    }
    payload = {
        "homeserver": homeserver.rstrip("/"),
        "username": username,
        "password": password,
        "display_name": display_name,
    }
    try:
        async with httpx.AsyncClient(
            timeout=10,
            verify=runtime_matrix_ssl_verify(runtime_paths=runtime_paths),
        ) as client:
            response = await client.post(url, json=payload, headers=headers)
    except httpx.HTTPError as exc:
        msg = f"Could not reach provisioning service ({provisioning_url}): {exc}"
        raise ValueError(msg) from exc

    if not response.is_success:
        _raise_for_register_agent_error(response, username=username)

    try:
        body = response.json()
    except ValueError as exc:
        msg = "Provisioning service returned invalid JSON while registering agent."
        raise matrix_startup_error(msg, permanent=True) from exc

    if not isinstance(body, dict):
        msg = "Provisioning service returned invalid register-agent payload."
        raise matrix_startup_error(msg, permanent=True)

    status = body.get("status")
    user_id = body.get("user_id")
    if status not in {"created", "user_in_use"}:
        msg = "Provisioning service response missing valid status for register-agent."
        raise matrix_startup_error(msg, permanent=True)
    if not isinstance(user_id, str) or not user_id.strip():
        msg = "Provisioning service response missing user_id for register-agent."
        raise matrix_startup_error(msg, permanent=True)
    try:
        parsed_user_id = parse_current_matrix_user_id(user_id.strip())
    except ValueError as exc:
        msg = "Provisioning service response returned invalid user_id for register-agent."
        raise matrix_startup_error(msg, permanent=True) from exc

    return _ProvisioningRegisterResult(status=status, user_id=parsed_user_id)
