from __future__ import annotations

import asyncio
import os
import re
from pathlib import Path
from typing import Any

import httpx
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
from fastapi.responses import JSONResponse

from app.json_store import JsonStore
from app.models import (
    ChannelSettings,
    BackendCredential,
    BackendRecord,
    IntrospectRequest,
    IntrospectResponse,
    MinIOSettings,
    OAuthTokenRequest,
    OAuthTokenResponse,
    OutlookSettings,
    PortalRegisterRequest,
    RegisterBackendRequest,
    RegisterBackendResponse,
    RegisterUserRequest,
    RegisterUserResponse,
    RegisterUserBackendResult,
    RotateSecretResponse,
    UpdateBackendRequest,
    UserRecord,
    utcnow_iso,
)
from app.minio_provisioning import (
    MinIODeprovisioningError,
    MinIOProvisioningError,
    deprovision_user_file_minio_resources,
    provision_user_file_minio_settings,
)
from app.security import JwtSigner, generate_client_secret, hash_secret, verify_secret

DATA_DIR = Path(os.getenv("AUTHZ_DATA_DIR", Path(__file__).resolve().parents[1] / "data"))
ISSUER = os.getenv("AUTHZ_ISSUER", "http://127.0.0.1:19090").rstrip("/")
INTERNAL_TOKEN = os.getenv("AUTHZ_INTERNAL_TOKEN", "dev-internal-token")
ACCESS_TOKEN_TTL_SECONDS = int(os.getenv("AUTHZ_ACCESS_TOKEN_TTL_SECONDS", "3600"))
PRIVATE_KEY_PATH = Path(os.getenv("AUTHZ_PRIVATE_KEY_PATH", DATA_DIR / "signing_key.pem"))
DEPLOY_API_BASE_URL = os.getenv("DEPLOY_API_BASE_URL", "http://127.0.0.1:8090").rstrip("/")
DEPLOY_API_TOKEN = os.getenv("DEPLOY_API_TOKEN", "").strip()
UPSTREAM_TIMEOUT_SECONDS = float(os.getenv("AUTHZ_UPSTREAM_TIMEOUT_SECONDS", "90"))
MCP_PERMISSIVE_DEFAULT = os.getenv("AUTHZ_MCP_PERMISSIVE_DEFAULT", "1").strip() not in {"0", "false", "False"}

store = JsonStore(DATA_DIR)
signer = JwtSigner(PRIVATE_KEY_PATH, ISSUER, ACCESS_TOKEN_TTL_SECONDS)
app = FastAPI(title="AuthZ Service", version="0.1.0")


def _slugify(value: str) -> str:
    lowered = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return lowered or "backend"


def _next_backend_id(name: str) -> str:
    base = _slugify(name)
    existing = {item.backend_id for item in store.list_backends()}
    candidate = base
    index = 1
    while candidate in existing:
        index += 1
        candidate = f"{base}-{index:03d}"
    return candidate


def _ensure_backend(backend_id: str) -> BackendRecord:
    backend = store.get_backend(backend_id)
    if backend is None:
        raise HTTPException(status_code=404, detail="Backend not found")
    return backend


def _clean_optional(value: str | None) -> str | None:
    cleaned = (value or "").strip()
    return cleaned or None


def _as_object(value: Any) -> dict[str, Any]:
    return value if isinstance(value, dict) else {}


def _as_string(value: Any) -> str:
    return value.strip() if isinstance(value, str) else ""


def _http_error_detail(response: httpx.Response) -> str:
    try:
        payload = response.json()
    except ValueError:
        payload = {}
    detail = _as_string(_as_object(payload).get("detail"))
    return detail or response.text.strip() or f"upstream request failed with status {response.status_code}"


async def _request_json(
    method: str,
    url: str,
    *,
    json_body: dict[str, Any] | None = None,
    headers: dict[str, str] | None = None,
) -> dict[str, Any]:
    try:
        async with httpx.AsyncClient(
            timeout=UPSTREAM_TIMEOUT_SECONDS,
            follow_redirects=True,
            trust_env=False,
        ) as client:
            response = await client.request(
                method,
                url,
                json=json_body,
                headers=headers,
            )
    except httpx.TimeoutException as exc:
        raise HTTPException(status_code=504, detail="upstream request timed out") from exc
    except httpx.HTTPError as exc:
        raise HTTPException(status_code=502, detail=str(exc)) from exc

    if response.is_error:
        raise HTTPException(status_code=response.status_code, detail=_http_error_detail(response))

    if not response.content:
        return {}
    try:
        payload = response.json()
    except ValueError as exc:
        raise HTTPException(status_code=502, detail="upstream response was not valid JSON") from exc
    if not isinstance(payload, dict):
        raise HTTPException(status_code=502, detail="upstream response must be a JSON object")
    return payload


async def _call_deploy_control(path: str, payload: dict[str, Any]) -> dict[str, Any]:
    headers: dict[str, str] = {}
    if DEPLOY_API_TOKEN:
        headers["Authorization"] = f"Bearer {DEPLOY_API_TOKEN}"
    return await _request_json(
        "POST",
        f"{DEPLOY_API_BASE_URL}{path}",
        json_body=payload,
        headers=headers,
    )


async def _call_instance_api(base_url: str, path: str, payload: dict[str, Any]) -> dict[str, Any]:
    normalized_base_url = base_url.rstrip("/")
    if not normalized_base_url:
        raise HTTPException(status_code=502, detail="instance api base url is missing")
    return await _request_json(
        "POST",
        f"{normalized_base_url}{path}",
        json_body=payload,
    )


async def _sync_instance_backend_binding(username: str, response: dict[str, Any]) -> dict[str, Any] | None:
    local_backend = _as_object(response.get("local_backend"))
    backend_id = _as_string(local_backend.get("backend_id"))
    if not username.strip() or not backend_id:
        return None

    payload: dict[str, Any] = {
        "username": username.strip(),
        "backend_id": backend_id,
        "authz_base_url": ISSUER,
    }
    backend_name = _as_string(local_backend.get("name"))
    if backend_name:
        payload["backend_name"] = backend_name

    try:
        return await _call_deploy_control("/api/instances/bind-backend", payload)
    except HTTPException:
        # Registration should not fail only because registry metadata sync failed.
        return None


def _normalize_portal_token_response(
    response: dict[str, Any],
    routing: dict[str, Any],
) -> dict[str, Any]:
    frontend_base_url = _as_string(routing.get("frontend_base_url"))
    api_base_url = _as_string(routing.get("api_base_url")) or _as_string(routing.get("public_url"))
    public_url = _as_string(routing.get("public_url")) or api_base_url
    backend_connection = _as_object(response.get("backend_connection"))

    merged_backend_connection = {
        **backend_connection,
        "frontend_base_url": _as_string(backend_connection.get("frontend_base_url")) or frontend_base_url or public_url or None,
        "api_base_url": _as_string(backend_connection.get("api_base_url")) or api_base_url or public_url or None,
        "public_base_url": _as_string(backend_connection.get("public_base_url")) or public_url or api_base_url or None,
    }

    return {
        **response,
        "backend_connection": merged_backend_connection,
    }


def _require_internal(authorization: str | None = Header(default=None)) -> None:
    token = ""
    if authorization and authorization.lower().startswith("bearer "):
        token = authorization[7:].strip()
    if token != INTERNAL_TOKEN:
        raise HTTPException(status_code=401, detail="Invalid internal token")


def _allowed_scopes_for_audience(backend_id: str, audience: str) -> set[str]:
    permissions = store.get_permissions(backend_id)
    allowed: set[str] = set()

    if audience.startswith("mcp:"):
        server_id = audience.split(":", 1)[1]
        mcp_config = permissions.get("mcp", {}).get(server_id, {})
        if not isinstance(mcp_config, dict) or not mcp_config.get("enabled"):
            return set()
        allowed.add("list_tools")
        for tool_name in mcp_config.get("tools", []) or []:
            if isinstance(tool_name, str) and tool_name.strip():
                allowed.add(f"tool:{tool_name.strip()}")
        return allowed

    if audience.startswith("a2a:"):
        agent_id = audience.split(":", 1)[1]
        a2a_config = permissions.get("a2a", {})
        if not isinstance(a2a_config, dict) or not a2a_config.get("enabled"):
            return set()
        agents = a2a_config.get("agents", []) or []
        if agent_id in agents:
            allowed.add("run_task")
        return allowed

    if audience.startswith("channel:"):
        channel_id = audience.split(":", 1)[1]
        channel_config = permissions.get("channels", {}).get(channel_id, {})
        if not isinstance(channel_config, dict) or not channel_config.get("enabled"):
            return set()
        # `invoke` is the baseline permission for a protected channel connector.
        allowed.add("invoke")
        for scope_name in channel_config.get("scopes", []) or []:
            if isinstance(scope_name, str) and scope_name.strip():
                allowed.add(scope_name.strip())
        return allowed

    return set()


def _issue_token(payload: OAuthTokenRequest) -> OAuthTokenResponse:
    if payload.grant_type != "client_credentials":
        raise HTTPException(status_code=400, detail="Unsupported grant_type")

    credential = store.get_credential_by_client_id(payload.client_id)
    if credential is None or not verify_secret(payload.client_secret, credential.client_secret_hash):
        raise HTTPException(status_code=401, detail="Invalid client credentials")

    backend = _ensure_backend(credential.backend_id)
    if backend.status != "active":
        raise HTTPException(status_code=403, detail="Backend is disabled")

    allowed = _allowed_scopes_for_audience(credential.backend_id, payload.aud)
    requested = {item.strip() for item in (payload.scopes or []) if isinstance(item, str) and item.strip()}
    if payload.aud.startswith("mcp:") and MCP_PERMISSIVE_DEFAULT:
        allowed = set(allowed)
        allowed.add("list_tools")
        allowed.update(requested)
    if not allowed:
        raise HTTPException(status_code=403, detail="Audience is not enabled for this backend")

    if requested:
        if not requested.issubset(allowed):
            raise HTTPException(status_code=403, detail="Requested scopes exceed backend permissions")
        scopes = sorted(requested)
    else:
        scopes = sorted(allowed)

    token, expires_in = signer.issue_access_token(
        credential=credential,
        audience=payload.aud,
        scopes=scopes,
    )
    return OAuthTokenResponse(access_token=token, expires_in=expires_in)


def _parse_token_request_from_form(form: dict[str, Any]) -> OAuthTokenRequest:
    raw_scopes = form.get("scope", "")
    scopes = [item for item in str(raw_scopes).split() if item]
    return OAuthTokenRequest(
        grant_type=str(form.get("grant_type") or "client_credentials"),
        client_id=str(form.get("client_id") or ""),
        client_secret=str(form.get("client_secret") or ""),
        aud=str(form.get("aud") or form.get("resource") or ""),
        scopes=scopes,
    )


def _create_backend(
    *,
    backend_id: str,
    name: str,
    base_url: str,
    frontend_base_url: str | None,
) -> tuple[BackendRecord, str]:
    client_secret = generate_client_secret()
    backend = BackendRecord(
        backend_id=backend_id,
        name=name,
        base_url=base_url,
        frontend_base_url=frontend_base_url,
    )
    credential = BackendCredential(
        backend_id=backend_id,
        client_id=backend_id,
        client_secret_hash=hash_secret(client_secret),
    )
    store.save_backend(backend)
    store.save_credential(credential)
    return backend, client_secret


def _update_backend_record(
    backend: BackendRecord,
    *,
    name: str | None = None,
    base_url: str | None = None,
    frontend_base_url: str | None = None,
) -> BackendRecord:
    updated = backend.model_copy(deep=True)
    if name is not None:
        updated.name = name
    if base_url is not None:
        updated.base_url = base_url
    if frontend_base_url is not None:
        updated.frontend_base_url = frontend_base_url
    updated.updated_at = utcnow_iso()
    store.save_backend(updated)
    return updated


def _upsert_user(
    *,
    username: str,
    email: str | None,
    default_backend_id: str | None,
) -> UserRecord:
    existing = store.get_user(username)
    if existing is None:
        user = UserRecord(
            username=username,
            email=email,
            default_backend_id=default_backend_id,
        )
    else:
        user = existing.model_copy(deep=True)
        if email is not None:
            user.email = email
        if default_backend_id is not None:
            user.default_backend_id = default_backend_id
        user.updated_at = utcnow_iso()
    store.save_user(user)
    return user


async def _ensure_user_file_storage_settings(backend_id: str) -> MinIOSettings | None:
    try:
        settings = await asyncio.to_thread(
            provision_user_file_minio_settings,
            backend_id=backend_id,
            existing=store.get_minio_settings(backend_id),
        )
    except MinIOProvisioningError as exc:
        raise HTTPException(status_code=502, detail=str(exc)) from exc
    if settings is not None:
        store.save_minio_settings(backend_id, settings)
        store.touch_backend(backend_id)
    return settings


async def _deprovision_user_file_storage(backend_id: str, *, best_effort: bool = False) -> dict[str, Any]:
    existing = store.get_minio_settings(backend_id)
    try:
        result = await asyncio.to_thread(
            deprovision_user_file_minio_resources,
            backend_id=backend_id,
            existing=existing,
            best_effort=best_effort,
        )
    except MinIODeprovisioningError as exc:
        raise HTTPException(status_code=502, detail=str(exc)) from exc

    settings_removed = False
    if result.get("ok") is True:
        settings_removed = store.delete_minio_settings(backend_id)
        if settings_removed:
            store.touch_backend(backend_id)
    result["settings"] = {"status": "removed" if settings_removed else "absent"}
    return result


def _resolve_register_backend_payload(req: RegisterBackendRequest) -> tuple[str, str, str, str | None]:
    backend_name = req.name.strip() or "backend"
    backend_id = _clean_optional(req.backend_id)
    base_url = req.base_url.strip()
    if not base_url:
        raise HTTPException(status_code=400, detail="base_url is required")
    frontend_base_url = _clean_optional(req.frontend_base_url)
    return backend_name, backend_id or _next_backend_id(backend_name), base_url, frontend_base_url


def _resolve_register_user_backend_payload(req: RegisterUserRequest) -> tuple[str, str | None, str, str | None]:
    nested = req.backend
    backend_name = _clean_optional(
        (nested.name if nested else None)
        or req.backend_name
        or req.name
    ) or req.username.strip()
    backend_id = _clean_optional((nested.backend_id if nested else None) or req.backend_id)
    base_url = _clean_optional(
        (nested.base_url if nested else None)
        or req.public_base_url
        or req.base_url
    )
    if not base_url:
        raise HTTPException(status_code=400, detail="base_url is required")
    frontend_base_url = _clean_optional(
        (nested.frontend_base_url if nested else None)
        or req.frontend_base_url
    )
    return backend_name, backend_id, base_url, frontend_base_url


@app.get("/healthz")
async def healthz() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/.well-known/oauth-authorization-server")
async def oauth_metadata() -> dict[str, Any]:
    return {
        "issuer": ISSUER,
        "token_endpoint": f"{ISSUER}/oauth/token",
        "introspection_endpoint": f"{ISSUER}/oauth/introspect",
        "jwks_uri": f"{ISSUER}/.well-known/jwks.json",
        "grant_types_supported": ["client_credentials"],
        "token_endpoint_auth_methods_supported": ["client_secret_post"],
    }


@app.get("/.well-known/jwks.json")
async def jwks() -> dict[str, Any]:
    return signer.build_jwks()


@app.post("/portal/register")
async def portal_register(req: PortalRegisterRequest) -> dict[str, Any]:
    username = req.username.strip()
    if not username:
        raise HTTPException(status_code=400, detail="username is required")
    if not req.password:
        raise HTTPException(status_code=400, detail="password is required")

    deploy_payload: dict[str, Any] = {
        "username": username,
        "password": req.password,
        "authz_base_url": ISSUER,
    }
    email = _clean_optional(req.email)
    if email is not None:
        deploy_payload["email"] = email

    optional_fields = {
        "instance_id": _clean_optional(req.instance_id),
        "backend_name": _clean_optional(req.backend_name),
        "image_name": _clean_optional(req.image_name),
    }
    for key, value in optional_fields.items():
        if value is not None:
            deploy_payload[key] = value
    if req.replace:
        deploy_payload["replace"] = True

    routing = await _call_deploy_control("/api/instances/register", deploy_payload)
    api_base_url = _as_string(routing.get("api_base_url")) or _as_string(routing.get("public_url"))
    instance_payload: dict[str, Any] = {
        "username": username,
        "password": req.password,
        "authz_base_url": ISSUER,
    }
    if email is not None:
        instance_payload["email"] = email
    backend_name = _clean_optional(req.backend_name)
    if backend_name is not None:
        instance_payload["backend_name"] = backend_name

    response = await _call_instance_api(api_base_url, "/api/auth/register", instance_payload)
    bound_instance = await _sync_instance_backend_binding(username, response)
    if isinstance(bound_instance, dict):
        synced_record = _as_object(bound_instance.get("instance"))
        if synced_record:
            routing["instance"] = synced_record
    return _normalize_portal_token_response(response, routing)


@app.post("/backends/register", response_model=RegisterBackendResponse)
async def register_backend(req: RegisterBackendRequest) -> RegisterBackendResponse:
    backend_name, backend_id, base_url, frontend_base_url = _resolve_register_backend_payload(req)
    if store.get_backend(backend_id) is not None:
        raise HTTPException(status_code=409, detail="Backend already exists")
    backend, client_secret = _create_backend(
        backend_id=backend_id,
        name=backend_name,
        base_url=base_url,
        frontend_base_url=frontend_base_url,
    )
    await _ensure_user_file_storage_settings(backend.backend_id)
    return RegisterBackendResponse(
        backend_id=backend.backend_id,
        client_id=backend.backend_id,
        client_secret=client_secret,
        created_at=backend.created_at,
        frontend_base_url=backend.frontend_base_url,
    )


@app.get("/backends")
async def list_backends() -> list[dict[str, Any]]:
    return [item.model_dump(mode="json") for item in store.list_backends()]


@app.get("/backends/{backend_id}")
async def get_backend(backend_id: str) -> dict[str, Any]:
    return _ensure_backend(backend_id).model_dump(mode="json")


@app.put("/backends/{backend_id}")
async def update_backend(backend_id: str, req: UpdateBackendRequest) -> dict[str, Any]:
    backend = _ensure_backend(backend_id)
    updated = _update_backend_record(
        backend,
        name=_clean_optional(req.name),
        base_url=_clean_optional(req.base_url),
        frontend_base_url=_clean_optional(req.frontend_base_url),
    )
    return updated.model_dump(mode="json")


@app.post("/backends/{backend_id}/disable")
async def disable_backend(backend_id: str) -> dict[str, Any]:
    backend = store.disable_backend(backend_id)
    if backend is None:
        raise HTTPException(status_code=404, detail="Backend not found")
    return backend.model_dump(mode="json")


@app.post("/backends/{backend_id}/enable")
async def enable_backend(backend_id: str) -> dict[str, Any]:
    backend = store.enable_backend(backend_id)
    if backend is None:
        raise HTTPException(status_code=404, detail="Backend not found")
    return backend.model_dump(mode="json")


@app.post("/backends/{backend_id}/rotate-secret", response_model=RotateSecretResponse)
async def rotate_secret(backend_id: str) -> RotateSecretResponse:
    backend = _ensure_backend(backend_id)
    client_secret = generate_client_secret()
    credential = BackendCredential(
        backend_id=backend_id,
        client_id=backend_id,
        client_secret_hash=hash_secret(client_secret),
        created_at=utcnow_iso(),
        rotated_at=utcnow_iso(),
    )
    store.save_credential(credential)
    return RotateSecretResponse(
        backend_id=backend_id,
        client_id=backend_id,
        client_secret=client_secret,
        rotated_at=credential.rotated_at or credential.created_at,
    )


@app.get("/backends/{backend_id}/permissions")
async def get_permissions(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    return store.get_permissions(backend_id)


@app.post("/backends/{backend_id}/permissions")
async def save_permissions(backend_id: str, payload: dict[str, Any]) -> dict[str, Any]:
    _ensure_backend(backend_id)
    saved = store.save_permissions(backend_id, payload)
    store.touch_backend(backend_id)
    return saved


@app.get("/backends/{backend_id}/settings/outlook")
async def get_outlook_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    settings = store.get_outlook_settings(backend_id)
    if settings is None:
        return {"configured": False}
    return settings.masked_dict()


@app.post("/backends/{backend_id}/settings/outlook")
async def save_outlook_settings(backend_id: str, payload: OutlookSettings) -> dict[str, Any]:
    _ensure_backend(backend_id)
    existing = store.get_outlook_settings(backend_id)
    if not payload.password:
        if existing is None or not existing.password:
            raise HTTPException(status_code=400, detail="Password is required for initial Outlook setup")
        payload = payload.model_copy(update={"password": existing.password, "updated_at": utcnow_iso()})
    else:
        payload = payload.model_copy(update={"updated_at": utcnow_iso()})
    store.save_outlook_settings(backend_id, payload)
    store.touch_backend(backend_id)
    return payload.masked_dict()


@app.delete("/backends/{backend_id}/settings/outlook")
async def delete_outlook_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    removed = store.delete_outlook_settings(backend_id)
    if removed:
        store.touch_backend(backend_id)
    return {"ok": removed}


@app.get("/backends/{backend_id}/settings/minio")
async def get_minio_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    settings = store.get_minio_settings(backend_id)
    if settings is None:
        return {"configured": False}
    return settings.masked_dict()


@app.post("/backends/{backend_id}/settings/minio")
async def save_minio_settings(backend_id: str, payload: MinIOSettings) -> dict[str, Any]:
    _ensure_backend(backend_id)
    existing = store.get_minio_settings(backend_id)
    if not payload.secret_key:
        if existing is None or not existing.secret_key:
            raise HTTPException(status_code=400, detail="Secret key is required for initial MinIO setup")
        payload = payload.model_copy(update={"secret_key": existing.secret_key, "updated_at": utcnow_iso()})
    else:
        payload = payload.model_copy(update={"updated_at": utcnow_iso()})
    store.save_minio_settings(backend_id, payload)
    store.touch_backend(backend_id)
    return payload.masked_dict()


@app.delete("/backends/{backend_id}/settings/minio")
async def delete_minio_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    removed = store.delete_minio_settings(backend_id)
    if removed:
        store.touch_backend(backend_id)
    return {"ok": removed}


@app.get("/backends/{backend_id}/settings/channels")
async def list_channel_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    return {
        channel_id: settings.masked_dict()
        for channel_id, settings in store.list_channel_settings(backend_id).items()
    }


@app.get("/backends/{backend_id}/settings/channels/{channel_id}")
async def get_channel_settings(backend_id: str, channel_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    settings = store.get_channel_settings(backend_id, channel_id)
    if settings is None:
        return {"configured": False}
    return settings.masked_dict()


@app.post("/backends/{backend_id}/settings/channels/{channel_id}")
async def save_channel_settings(backend_id: str, channel_id: str, payload: ChannelSettings) -> dict[str, Any]:
    _ensure_backend(backend_id)
    existing = store.get_channel_settings(backend_id, channel_id)
    if not payload.secrets and existing is not None and existing.secrets:
        payload = payload.model_copy(update={"secrets": existing.secrets, "updated_at": utcnow_iso()})
    else:
        payload = payload.model_copy(update={"updated_at": utcnow_iso()})
    store.save_channel_settings(backend_id, channel_id, payload)
    store.touch_backend(backend_id)
    return payload.masked_dict()


@app.delete("/backends/{backend_id}/settings/channels/{channel_id}")
async def delete_channel_settings(backend_id: str, channel_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    removed = store.delete_channel_settings(backend_id, channel_id)
    if removed:
        store.touch_backend(backend_id)
    return {"ok": removed}


@app.post("/oauth/register", response_model=RegisterUserResponse)
async def oauth_register(req: RegisterUserRequest) -> RegisterUserResponse:
    username = req.username.strip()
    if not username:
        raise HTTPException(status_code=400, detail="username is required")
    if not req.password:
        raise HTTPException(status_code=400, detail="password is required")

    backend_name, requested_backend_id, base_url, frontend_base_url = _resolve_register_user_backend_payload(req)
    backend_id = requested_backend_id or _next_backend_id(backend_name)
    existing_backend = store.get_backend(backend_id)

    client_secret: str | None = None
    if existing_backend is None:
        backend, client_secret = _create_backend(
            backend_id=backend_id,
            name=backend_name,
            base_url=base_url,
            frontend_base_url=frontend_base_url,
        )
    else:
        backend = _update_backend_record(
            existing_backend,
            name=backend_name,
            base_url=base_url,
            frontend_base_url=frontend_base_url,
        )

    user = _upsert_user(
        username=username,
        email=_clean_optional(req.email),
        default_backend_id=backend.backend_id,
    )
    await _ensure_user_file_storage_settings(backend.backend_id)
    return RegisterUserResponse(
        user=user,
        backend=RegisterUserBackendResult(
            backend_id=backend.backend_id,
            client_id=backend.backend_id,
            client_secret=client_secret,
            name=backend.name,
            base_url=backend.base_url,
            frontend_base_url=backend.frontend_base_url,
            status=backend.status,
            created_at=backend.created_at,
        ),
    )


@app.get("/internal/backends/{backend_id}/settings/outlook", dependencies=[Depends(_require_internal)])
async def get_internal_outlook_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    settings = store.get_outlook_settings(backend_id)
    if settings is None:
        raise HTTPException(status_code=404, detail="Outlook settings not configured")
    return settings.model_dump(mode="json")


@app.get("/internal/backends/{backend_id}/settings/minio", dependencies=[Depends(_require_internal)])
async def get_internal_minio_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    settings = store.get_minio_settings(backend_id)
    if settings is None:
        raise HTTPException(status_code=404, detail="MinIO settings not configured")
    return settings.model_dump(mode="json")


@app.delete("/internal/backends/{backend_id}/user-files", dependencies=[Depends(_require_internal)])
async def delete_internal_user_files(
    backend_id: str,
    best_effort: bool = Query(default=False),
) -> dict[str, Any]:
    return await _deprovision_user_file_storage(backend_id, best_effort=best_effort)


@app.get("/internal/backends/{backend_id}/settings/channels", dependencies=[Depends(_require_internal)])
async def list_internal_channel_settings(backend_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    return {
        channel_id: settings.model_dump(mode="json")
        for channel_id, settings in store.list_channel_settings(backend_id).items()
    }


@app.get("/internal/backends/{backend_id}/settings/channels/{channel_id}", dependencies=[Depends(_require_internal)])
async def get_internal_channel_settings(backend_id: str, channel_id: str) -> dict[str, Any]:
    _ensure_backend(backend_id)
    settings = store.get_channel_settings(backend_id, channel_id)
    if settings is None:
        raise HTTPException(status_code=404, detail="Channel settings not configured")
    return settings.model_dump(mode="json")


@app.post("/oauth/token", response_model=OAuthTokenResponse)
async def oauth_token(request: Request) -> OAuthTokenResponse:
    content_type = request.headers.get("content-type", "")
    if "application/json" in content_type:
        payload = OAuthTokenRequest.model_validate(await request.json())
    else:
        form = await request.form()
        payload = _parse_token_request_from_form(dict(form))
    return _issue_token(payload)


@app.post("/oauth/introspect", response_model=IntrospectResponse, dependencies=[Depends(_require_internal)])
async def oauth_introspect(req: IntrospectRequest) -> IntrospectResponse:
    try:
        payload = signer.decode(req.token)
    except Exception:
        return IntrospectResponse(active=False)
    return IntrospectResponse(
        active=True,
        client_id=str(payload.get("client_id") or ""),
        backend_id=str(payload.get("backend_id") or ""),
        aud=str(payload.get("aud") or ""),
        scp=[str(item) for item in payload.get("scp", []) if str(item).strip()],
        exp=int(payload.get("exp")) if payload.get("exp") is not None else None,
    )


@app.exception_handler(HTTPException)
async def http_error_handler(_: Request, exc: HTTPException):
    return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
