"""
Orca Cloud API Routes

Device-pairing (RFC 8628) connect/disconnect + profile sync endpoints for the
Orca Cloud external-app surface.

Auth shape (see :mod:`backend.app.services.orca_cloud` for the deep dive):

    POST /orca-cloud/device/start
        Request a device code, persist it server-side (TTL 10 min), return the
        user_code + verification URIs + poll interval.
    POST /orca-cloud/device/poll
        One poll of the token endpoint. Returns an in-progress status while the
        user approves; on approval, persists the token pair and reports
        connected. The frontend calls this every ``interval`` seconds.
    GET  /orca-cloud/status
        Connected/disconnected + user_id.
    POST /orca-cloud/logout
        Clear stored tokens (Bambuddy then has no token to use; the user can
        also disconnect from Orca Cloud's own settings to revoke server-side).
    GET  /orca-cloud/profiles
        List of the user's Orca Cloud profiles, grouped by type. JIT-refreshes
        the access token if it's within the refresh leeway of expiry.
    GET  /orca-cloud/profiles/{id}
        Single profile's full content.

Storage shape mirrors the Bambu Cloud surface: per-user columns on ``users``
when auth is enabled, fallback to global ``settings`` keys when auth is
disabled. The transient pending device-code state (device_code, interval,
started_at) reuses the ``orca_cloud_pending_*`` columns — same dual-mode
pattern; no schema change from the previous PKCE flow.
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from backend.app.api.routes.cloud import _cloud_api_key_gate, cloud_caller
from backend.app.core.database import get_db
from backend.app.core.permissions import Permission
from backend.app.models.settings import Settings
from backend.app.models.user import User
from backend.app.schemas.orca_cloud import (
    OrcaAuthStatusResponse,
    OrcaDevicePollResponse,
    OrcaDeviceStartResponse,
    OrcaProfileDetail,
    OrcaProfileListResponse,
    OrcaProfileMeta,
)
from backend.app.services.orca_cloud import (
    DEVICE_CODE_TTL,
    DevicePoll,
    OrcaCloudAuthError,
    OrcaCloudError,
    OrcaCloudService,
)

logger = logging.getLogger(__name__)

# Router-level dependency: enforce the same API-key cloud-access fence as the
# Bambu Cloud router (rejects ownerless legacy keys, requires the
# ``can_access_cloud`` scope, stashes the owner on ``request.state`` so
# per-route deps can resolve it as the effective ``current_user``).
# Without this gate the kiosk's API-keyed requests sail past with
# ``current_user=None`` → ``_build_authenticated_service`` falls back to
# the global Settings table → no Orca token → 401, no presets surfaced.
# Bambu Cloud works in the same kiosk because its router has this gate.
router = APIRouter(prefix="/orca-cloud", tags=["orca-cloud"], dependencies=[Depends(_cloud_api_key_gate)])

# Orca ``content.type`` values map onto Bambu Cloud's preset type vocabulary.
# Empirically (confirmed against a live account on 2026-06-04): Orca uses
# ``"printer"`` / ``"print"`` / ``"filament"`` — NOT the BambuStudio
# ``"machine"`` / ``"process"`` / ``"filament"`` triplet that lives elsewhere
# in the OrcaSlicer source. The aliases keep us forward-compatible if Orca
# ever flips back to the older naming.
_ORCA_TYPE_TO_BAMBU = {
    "filament": "filament",
    "printer": "printer",
    "machine": "printer",  # alias for the BambuStudio-style naming
    "print": "process",
    "process": "process",  # alias for the BambuStudio-style naming
}


def _orca_to_setting(orca_profile: dict) -> OrcaProfileMeta | None:
    """Normalize one Orca profile (``{id, name, content, ...}``) into a
    ``SlicerSetting``-shaped row. Returns ``None`` if the content isn't a dict
    or the type isn't one we render."""
    content = orca_profile.get("content") or {}
    if not isinstance(content, dict):
        return None
    bambu_type = _ORCA_TYPE_TO_BAMBU.get(str(content.get("type", "")))
    if bambu_type is None:
        return None
    pid = orca_profile.get("id")
    if pid is None:
        return None
    updated = orca_profile.get("updated_time")
    return OrcaProfileMeta(
        setting_id=str(pid),
        name=str(orca_profile.get("name") or pid),
        type=bambu_type,
        version=_str_or_none(content.get("version")),
        # ``from`` distinguishes ``system`` (bundled) from ``User`` (custom),
        # same field the Bambu source-of-truth uses for that distinction.
        user_id=_str_or_none(content.get("user_id") or content.get("from")),
        updated_time=str(updated) if updated is not None else None,
        # Every profile that lives in the user's Orca Cloud account is by
        # definition user-authored; bundled defaults aren't synced.
        is_custom=True,
    )


def _str_or_none(value: object) -> str | None:
    """Cast non-empty scalars to ``str``; pass ``None`` and empty values
    through unchanged. Used to keep the response shape consistent when
    Orca's source data has heterogenous typing for the same field."""
    if value is None:
        return None
    s = str(value)
    return s if s else None


# Settings table keys for the auth-disabled fallback. Mirrors the Bambu Cloud
# pattern (``bambu_cloud_token`` etc.) so administrators inspecting the
# settings table see a consistent prefix. The ``pending_*`` keys hold the
# transient device-code state (device_code / interval / started_at).
_SETTINGS_KEYS = {
    "token": "orca_cloud_token",
    "refresh_token": "orca_cloud_refresh_token",
    "expires_at": "orca_cloud_expires_at",  # ISO 8601 UTC string
    "email": "orca_cloud_email",
    "user_id": "orca_cloud_user_id",
    "pending_device_code": "orca_cloud_pending_verifier",  # reused column
    "pending_interval": "orca_cloud_pending_state",  # reused column
    "pending_at": "orca_cloud_pending_at",  # ISO 8601 UTC string
}


# ---------------------------------------------------------------------------
# Storage helpers — bridge User-row vs Settings-table fallback transparently
# ---------------------------------------------------------------------------


def _iso(dt: datetime | None) -> str | None:
    """Serialize a datetime to ISO 8601 UTC. ``None`` passes through."""
    if dt is None:
        return None
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc).isoformat()


def _as_utc(dt: datetime | None) -> datetime | None:
    """Attach ``tzinfo=UTC`` to a naive datetime that we know was stored as
    UTC. ``None`` passes through. Already-aware datetimes are converted to
    UTC to normalize."""
    if dt is None:
        return None
    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


def _parse_iso(value: str | None) -> datetime | None:
    """Parse an ISO 8601 string back to a UTC datetime. ``None`` passes through."""
    if not value:
        return None
    try:
        dt = datetime.fromisoformat(value)
    except (TypeError, ValueError):
        return None
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt


class _OrcaCredentials:
    """Lightweight bag for stored Orca Cloud credentials. We use a class
    rather than a dataclass so the helpers can mutate it as needed during
    JIT-refresh without rebuilding the whole object.

    ``pending_device_code`` / ``pending_interval`` / ``pending_at`` hold the
    in-flight device-code pairing state (reusing the ``orca_cloud_pending_*``
    columns that the old PKCE flow used for its verifier/state)."""

    __slots__ = (
        "token",
        "refresh_token",
        "expires_at",
        "email",
        "user_id",
        "pending_device_code",
        "pending_interval",
        "pending_at",
    )

    def __init__(self) -> None:
        self.token: str | None = None
        self.refresh_token: str | None = None
        self.expires_at: datetime | None = None
        self.email: str | None = None
        self.user_id: str | None = None
        self.pending_device_code: str | None = None
        self.pending_interval: str | None = None
        self.pending_at: datetime | None = None


async def _load_credentials(db: AsyncSession, user: User | None) -> _OrcaCredentials:
    """Load stored Orca Cloud credentials for the caller (user-row when auth
    is enabled, Settings fallback when auth is disabled).

    Datetimes coming back from the User row are NAIVE on the Postgres side
    (asyncpg strips tzinfo for ``TIMESTAMP WITHOUT TIME ZONE`` columns) but
    represent UTC moments because that's what we stored. We attach
    ``tzinfo=UTC`` here so downstream comparisons against
    ``datetime.now(timezone.utc)`` don't get shifted by the host's local
    offset — ``naive_dt.astimezone(UTC)`` would assume local time, which on
    a UTC+2 host turns a 1-minute-old pending state into a 2h1m one and
    fires the 10-minute TTL guard immediately."""
    creds = _OrcaCredentials()
    if user is not None:
        creds.token = user.orca_cloud_token
        creds.refresh_token = user.orca_cloud_refresh_token
        creds.expires_at = _as_utc(user.orca_cloud_expires_at)
        creds.email = user.orca_cloud_email
        creds.user_id = user.orca_cloud_user_id
        creds.pending_device_code = user.orca_cloud_pending_verifier
        creds.pending_interval = user.orca_cloud_pending_state
        creds.pending_at = _as_utc(user.orca_cloud_pending_at)
        return creds

    result = await db.execute(select(Settings).where(Settings.key.in_(list(_SETTINGS_KEYS.values()))))
    raw = {s.key: s.value for s in result.scalars().all()}
    creds.token = raw.get(_SETTINGS_KEYS["token"])
    creds.refresh_token = raw.get(_SETTINGS_KEYS["refresh_token"])
    creds.expires_at = _parse_iso(raw.get(_SETTINGS_KEYS["expires_at"]))
    creds.email = raw.get(_SETTINGS_KEYS["email"])
    creds.user_id = raw.get(_SETTINGS_KEYS["user_id"])
    creds.pending_device_code = raw.get(_SETTINGS_KEYS["pending_device_code"])
    creds.pending_interval = raw.get(_SETTINGS_KEYS["pending_interval"])
    creds.pending_at = _parse_iso(raw.get(_SETTINGS_KEYS["pending_at"]))
    return creds


async def _persist_pending_device(
    db: AsyncSession,
    user: User | None,
    device_code: str,
    interval: int,
    when: datetime,
) -> None:
    """Store the transient device-code state used by ``/device/start`` ->
    ``/device/poll``. The device_code is a secret kept server-side."""
    if user is not None:
        await db.execute(
            update(User)
            .where(User.id == user.id)
            .values(
                orca_cloud_pending_verifier=device_code,
                orca_cloud_pending_state=str(interval),
                orca_cloud_pending_at=when,
            )
        )
        await db.commit()
        return
    await _upsert_settings(
        db,
        {
            _SETTINGS_KEYS["pending_device_code"]: device_code,
            _SETTINGS_KEYS["pending_interval"]: str(interval),
            _SETTINGS_KEYS["pending_at"]: _iso(when),
        },
    )


async def _clear_pending_device(db: AsyncSession, user: User | None) -> None:
    """Wipe just the pending device-code state (on terminal poll outcomes),
    leaving any existing tokens untouched."""
    if user is not None:
        await db.execute(
            update(User)
            .where(User.id == user.id)
            .values(
                orca_cloud_pending_verifier=None,
                orca_cloud_pending_state=None,
                orca_cloud_pending_at=None,
            )
        )
        await db.commit()
        return
    await _upsert_settings(
        db,
        {
            _SETTINGS_KEYS["pending_device_code"]: None,
            _SETTINGS_KEYS["pending_interval"]: None,
            _SETTINGS_KEYS["pending_at"]: None,
        },
    )


async def _persist_tokens(
    db: AsyncSession,
    user: User | None,
    access_token: str,
    refresh_token: str | None,
    expires_at: datetime | None,
    email: str | None,
    user_id: str | None,
) -> None:
    """Atomically write the new access/refresh pair to whichever backing store
    the deployment uses. Also clears the pending device-code state on the same
    write, since by this point the pairing is complete."""
    if user is not None:
        await db.execute(
            update(User)
            .where(User.id == user.id)
            .values(
                orca_cloud_token=access_token,
                orca_cloud_refresh_token=refresh_token,
                orca_cloud_expires_at=expires_at,
                orca_cloud_email=email,
                orca_cloud_user_id=user_id,
                orca_cloud_pending_verifier=None,
                orca_cloud_pending_state=None,
                orca_cloud_pending_at=None,
            )
        )
        await db.commit()
        return
    await _upsert_settings(
        db,
        {
            _SETTINGS_KEYS["token"]: access_token,
            _SETTINGS_KEYS["refresh_token"]: refresh_token,
            _SETTINGS_KEYS["expires_at"]: _iso(expires_at),
            _SETTINGS_KEYS["email"]: email,
            _SETTINGS_KEYS["user_id"]: user_id,
            _SETTINGS_KEYS["pending_device_code"]: None,
            _SETTINGS_KEYS["pending_interval"]: None,
            _SETTINGS_KEYS["pending_at"]: None,
        },
    )


async def _persist_rotated_tokens(
    db: AsyncSession,
    user: User | None,
    access_token: str,
    refresh_token: str | None,
    expires_at: datetime | None,
) -> None:
    """Persist tokens after a refresh — does NOT touch email/user_id and does
    NOT touch the pending state (refresh happens long after pairing)."""
    if user is not None:
        await db.execute(
            update(User)
            .where(User.id == user.id)
            .values(
                orca_cloud_token=access_token,
                orca_cloud_refresh_token=refresh_token,
                orca_cloud_expires_at=expires_at,
            )
        )
        await db.commit()
        return
    await _upsert_settings(
        db,
        {
            _SETTINGS_KEYS["token"]: access_token,
            _SETTINGS_KEYS["refresh_token"]: refresh_token,
            _SETTINGS_KEYS["expires_at"]: _iso(expires_at),
        },
    )


async def _clear_credentials(db: AsyncSession, user: User | None) -> None:
    """Wipe everything Orca-related (tokens, identity, pending state)."""
    if user is not None:
        await db.execute(
            update(User)
            .where(User.id == user.id)
            .values(
                orca_cloud_token=None,
                orca_cloud_refresh_token=None,
                orca_cloud_expires_at=None,
                orca_cloud_email=None,
                orca_cloud_user_id=None,
                orca_cloud_pending_verifier=None,
                orca_cloud_pending_state=None,
                orca_cloud_pending_at=None,
            )
        )
        await db.commit()
        return
    result = await db.execute(select(Settings).where(Settings.key.in_(list(_SETTINGS_KEYS.values()))))
    for setting in result.scalars().all():
        await db.delete(setting)
    await db.commit()


async def _upsert_settings(db: AsyncSession, values: dict[str, str | None]) -> None:
    """Idempotent upsert into the Settings table. ``None`` values delete the row."""
    keys = [k for k, _ in values.items()]
    result = await db.execute(select(Settings).where(Settings.key.in_(keys)))
    existing = {s.key: s for s in result.scalars().all()}
    for key, value in values.items():
        row = existing.get(key)
        if value is None:
            if row is not None:
                await db.delete(row)
            continue
        if row is not None:
            row.value = value
        else:
            db.add(Settings(key=key, value=value))
    await db.commit()


# ---------------------------------------------------------------------------
# Authenticated service builder with JIT refresh
# ---------------------------------------------------------------------------


async def _build_authenticated_service(
    db: AsyncSession,
    user: User | None,
    clear_on_auth_failure: bool = True,
) -> OrcaCloudService:
    """Construct an :class:`OrcaCloudService` pre-populated with stored
    credentials. If the access token is within the refresh-leeway of expiry,
    proactively refresh and persist the new pair BEFORE returning, so the
    next API call doesn't time out mid-flight on an expired token.

    We don't lock around the refresh: Orca tolerates concurrent refreshes for
    ~60s (each racer gets its own valid pair on the same connection rather than
    a revoke), so a lost race here is harmless — last-write-wins on the stored
    pair, and whichever pair we keep is valid.

    ``clear_on_auth_failure`` controls what happens when the refresh is
    rejected. Routes leave it on: the caller is a person looking at the UI, and
    wiping the dead credentials flips the page to disconnected in front of them
    so they can pair again. Background jobs pass ``False`` — see the caveat
    below.

    Why background callers must not clear: Orca reports every rejection with
    one composite reason (``unknown, expired, revoked, or already used``), so
    a genuine revocation is indistinguishable from a lost refresh-rotation
    race. Acting destructively on a signal that can't be disambiguated is the
    #2562 mistake in a different cloud. It also gains nothing — a route call
    hits the same failure and clears then, at a moment the user can respond to.
    A successful refresh is still persisted either way: by that point the old
    refresh token is consumed, so dropping the new pair would break a working
    pairing for real.
    """
    creds = await _load_credentials(db, user)
    if not creds.token:
        raise HTTPException(status_code=401, detail="Orca Cloud is not connected — sign in first.")

    svc = OrcaCloudService()
    svc.set_tokens(creds.token, creds.refresh_token, creds.expires_at)
    if not svc.is_authenticated:
        if not svc.refresh_token:
            raise HTTPException(
                status_code=401,
                detail="Orca Cloud session expired and no refresh token is stored — sign in again.",
            )
        try:
            await svc.refresh()
        except OrcaCloudAuthError as e:
            # Refresh token was revoked or rotated out from under us. Clear
            # the stale credentials so the UI flips to disconnected — unless
            # the caller is a background job, which must not change sign-in
            # state on its own.
            if clear_on_auth_failure:
                await _clear_credentials(db, user)
            raise HTTPException(status_code=401, detail=f"Orca Cloud session refresh failed: {e}") from e
        except OrcaCloudError as e:
            raise HTTPException(status_code=502, detail=f"Orca Cloud unreachable: {e}") from e
        # Persist new pair BEFORE returning. A crash between here and the
        # downstream API call would still leave the user with valid stored
        # tokens for the next request.
        await _persist_rotated_tokens(db, user, svc.access_token, svc.refresh_token, svc.token_expiry)
    return svc


# ---------------------------------------------------------------------------
# Route handlers
# ---------------------------------------------------------------------------


@router.post("/device/start", response_model=OrcaDeviceStartResponse)
async def device_start(
    request: Request,
    db: AsyncSession = Depends(get_db),
    current_user: User | None = cloud_caller(Permission.ORCA_CLOUD_AUTH),
):
    """Begin device pairing. Requests a device code from Orca, stores it
    server-side (the device_code is a secret and never leaves the backend),
    and returns the user_code + verification URIs + poll interval for the
    frontend to display and poll against."""
    svc = OrcaCloudService()
    # instance_url/label are display-only anti-phishing context on the approval
    # card. base_url may be off behind a reverse proxy, but it's harmless if so.
    instance_url = str(request.base_url).rstrip("/") or None
    try:
        data = await svc.request_device_code(instance_url=instance_url, instance_label="Bambuddy")
    except OrcaCloudAuthError as e:
        # invalid_client etc. — an operator misconfiguration, not user error.
        raise HTTPException(status_code=502, detail=f"Orca Cloud pairing is misconfigured: {e}") from e
    except OrcaCloudError as e:
        raise HTTPException(status_code=502, detail=f"Orca Cloud unreachable: {e}") from e

    device_code = data.get("device_code")
    user_code = data.get("user_code")
    if not device_code or not user_code:
        raise HTTPException(status_code=502, detail="Orca Cloud returned an incomplete device-code response.")

    interval = int(data.get("interval") or 5)
    expires_in = int(data.get("expires_in") or DEVICE_CODE_TTL.total_seconds())
    await _persist_pending_device(db, current_user, device_code, interval, datetime.now(timezone.utc))

    return OrcaDeviceStartResponse(
        user_code=user_code,
        verification_uri=str(data.get("verification_uri") or ""),
        verification_uri_complete=str(data.get("verification_uri_complete") or ""),
        interval=interval,
        expires_in=expires_in,
    )


@router.post("/device/poll", response_model=OrcaDevicePollResponse)
async def device_poll(
    db: AsyncSession = Depends(get_db),
    current_user: User | None = cloud_caller(Permission.ORCA_CLOUD_AUTH),
):
    """Poll the token endpoint once for the in-flight pairing. Returns an
    in-progress status while the user approves; on approval persists the token
    pair (clearing the pending state) and reports connected."""
    creds = await _load_credentials(db, current_user)
    if not creds.pending_device_code or not creds.pending_at:
        raise HTTPException(
            status_code=400,
            detail="No pending Orca Cloud pairing. Click Connect first to start the flow.",
        )

    # creds.pending_at is already tz-aware UTC after _load_credentials' _as_utc
    # normalization. Subtracting two aware UTC datetimes gives a real delta.
    age = datetime.now(timezone.utc) - creds.pending_at
    if age > DEVICE_CODE_TTL:
        await _clear_pending_device(db, current_user)
        return OrcaDevicePollResponse(status=DevicePoll.EXPIRED, connected=False)

    svc = OrcaCloudService()
    try:
        status, token_data = await svc.poll_token(creds.pending_device_code)
    except OrcaCloudError as e:
        raise HTTPException(status_code=502, detail=f"Orca Cloud unreachable: {e}") from e

    if status in DevicePoll.ONGOING:
        return OrcaDevicePollResponse(status=status, connected=False)

    if status in DevicePoll.TERMINAL:
        # access_denied / expired_token — the attempt is dead; clear it so the
        # user starts fresh next time.
        await _clear_pending_device(db, current_user)
        return OrcaDevicePollResponse(status=status, connected=False)

    # COMPLETE — tokens issued and applied to svc. Introspect for the user_id
    # (the external API's /me doesn't return an email, so email stays None).
    user_id: str | None = None
    try:
        info = await svc.introspect()
        if isinstance(info, dict):
            user_id = _str_or_none(info.get("user_id"))
    except OrcaCloudError as e:
        # Don't fail the whole pairing over the side introspection call — we
        # have valid tokens, which is the load-bearing part.
        logger.warning("Orca Cloud introspection failed after successful pairing: %s", e)

    await _persist_tokens(db, current_user, svc.access_token, svc.refresh_token, svc.token_expiry, None, user_id)
    return OrcaDevicePollResponse(status=DevicePoll.COMPLETE, connected=True, email=None, user_id=user_id)


@router.get("/status", response_model=OrcaAuthStatusResponse)
async def get_status(
    db: AsyncSession = Depends(get_db),
    current_user: User | None = cloud_caller(Permission.ORCA_CLOUD_AUTH),
):
    """Return whether the caller has an Orca Cloud session stored, plus
    identifier details for display. Does NOT make a live API call."""
    creds = await _load_credentials(db, current_user)
    return OrcaAuthStatusResponse(
        connected=bool(creds.token),
        email=creds.email,
        user_id=creds.user_id,
    )


@router.post("/logout")
async def logout(
    db: AsyncSession = Depends(get_db),
    current_user: User | None = cloud_caller(Permission.ORCA_CLOUD_AUTH),
):
    """Clear stored Orca Cloud credentials. Does not call Orca's disconnect
    endpoint (the user can revoke server-side from Orca Cloud's own settings;
    Bambuddy will no longer have the token to use either way)."""
    await _clear_credentials(db, current_user)
    return {"success": True}


@router.get("/profiles", response_model=OrcaProfileListResponse)
async def list_profiles(
    db: AsyncSession = Depends(get_db),
    current_user: User | None = cloud_caller(Permission.ORCA_CLOUD_AUTH),
):
    """Return profile metadata grouped by type (``filament`` / ``printer``
    / ``process``), matching the ``SlicerSettingsResponse`` shape the
    Bambu Cloud tab consumes. This lets the frontend render Orca profiles
    with the same visual components — same cards, same filter bar, same
    grouping — without separate UI code paths."""
    svc = await _build_authenticated_service(db, current_user)
    try:
        raw_profiles = await svc.list_profiles()
    except OrcaCloudAuthError as e:
        raise HTTPException(status_code=401, detail=str(e)) from e
    except OrcaCloudError as e:
        raise HTTPException(status_code=502, detail=str(e)) from e
    grouped: dict[str, list[OrcaProfileMeta]] = {"filament": [], "printer": [], "process": []}
    # Log any unknown content.type values we silently drop, so a future
    # change in Orca's type vocabulary surfaces in the logs rather than
    # quietly losing profiles.
    unknown_types: dict[str, int] = {}
    for entry in raw_profiles:
        setting = _orca_to_setting(entry)
        if setting is None:
            content = entry.get("content") if isinstance(entry, dict) else None
            raw_type = (content.get("type") if isinstance(content, dict) else None) or "<missing>"
            unknown_types[str(raw_type)] = unknown_types.get(str(raw_type), 0) + 1
            continue
        grouped[setting.type].append(setting)
    if unknown_types:
        logger.warning(
            "Orca Cloud profile list dropped %d profiles with unmapped content.type values: %s",
            sum(unknown_types.values()),
            unknown_types,
        )
    return OrcaProfileListResponse(**grouped)


@router.get("/profiles/{profile_id}", response_model=OrcaProfileDetail)
async def get_profile(
    profile_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User | None = cloud_caller(Permission.ORCA_CLOUD_AUTH),
):
    """Fetch a single profile's full content, shaped like
    ``SlicerSettingDetail`` so the Bambu Cloud detail modal can render it
    unchanged. The inner ``setting`` field is the raw slicer-format JSON
    Orca stores — same shape Bambu Cloud uses since OrcaSlicer is a
    BambuStudio fork."""
    svc = await _build_authenticated_service(db, current_user)
    try:
        profile = await svc.get_profile(profile_id)
    except OrcaCloudAuthError as e:
        raise HTTPException(status_code=401, detail=str(e)) from e
    except OrcaCloudError as e:
        if "not found" in str(e).lower():
            raise HTTPException(status_code=404, detail=str(e)) from e
        raise HTTPException(status_code=502, detail=str(e)) from e
    content = profile.get("content") if isinstance(profile, dict) else None
    if not isinstance(content, dict):
        content = {}
    orca_type = str(content.get("type", ""))
    bambu_type = _ORCA_TYPE_TO_BAMBU.get(orca_type, orca_type)
    update_time = profile.get("updated_time") if isinstance(profile, dict) else None
    return OrcaProfileDetail(
        setting_id=str(profile_id),
        name=str(profile.get("name") if isinstance(profile, dict) else "") or str(profile_id),
        type=bambu_type,
        version=_str_or_none(content.get("version")),
        base_id=_str_or_none(content.get("inherits") or content.get("base_id")),
        update_time=str(update_time) if update_time is not None else None,
        setting=content,
    )
