"""cloud_infra domain — business logic for CF account pool and workspace routing."""
from __future__ import annotations

import json
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional
from uuid import UUID, uuid4

import structlog

from src.core.exceptions import ConflictError, NotFoundError, ValidationError
from src.core.security.encryption import decrypt_value, encrypt_value
from src.domain.cloud_infra.models import (
    CFAccount,
    CFAccountStatus,
    CFAccountTier,
    CloudInstance,
    CloudInstanceStatus,
    CloudShard,
    CloudShardMigrationStatus,
    CloudShardStatus,
    ProjectShardStatus,
    WorkspaceCloudAssignment,
)
from src.domain.cloud_infra.repository import (
    CFAccountRepository,
    CloudInstanceRepository,
    CloudShardMigrationStatusRepository,
    CloudShardRepository,
    ProjectCloudAssignmentRepository,
    WorkspaceCloudAssignmentRepository,
)
from src.domain.cloud_infra.schemas import (
    AssignProjectToShardRequest,
    AssignWorkspaceRequest,
    CFAccountCredentials,
    CFAccountResponse,
    CloudInstanceHeartbeatResponse,
    CloudInstanceResponse,
    CloudShardMigrationStatusResponse,
    CloudShardResponse,
    CreateCFAccountRequest,
    CreateCloudShardRequest,
    ProjectCloudAssignmentResponse,
    RegisterCloudInstanceRequest,
    ReportShardMigrationRequest,
    ShardFleetMigrationRow,
    UpdateCFAccountRequest,
    UpdateCloudInstanceRequest,
    UpdateCloudShardRequest,
    WorkspaceCloudAssignmentResponse,
    WorkspaceRoutingInfo,
)

logger = structlog.get_logger()

# ---------------------------------------------------------------------------
# Module-level credentials cache (60 s TTL, per-process)
# ---------------------------------------------------------------------------
_creds_cache: dict[str, tuple["CfCredentials", float]] = {}
_CREDS_TTL = 60.0


def _get_cached(key: str) -> Optional["CfCredentials"]:
    entry = _creds_cache.get(key)
    if entry and (time.monotonic() - entry[1]) < _CREDS_TTL:
        return entry[0]
    return None


def _set_cached(key: str, creds: "CfCredentials") -> None:
    _creds_cache[key] = (creds, time.monotonic())


@dataclass
class CfCredentials:
    """Decrypted, ready-to-use CF credentials resolved for a specific account."""

    account_id: str          # CF account ID (plaintext from DB)
    api_token: str
    zone_id: str
    w4p_namespace_production: str
    w4p_namespace_preview: str
    w4p_namespace_staging: str
    dispatch_kv_namespace_id: str
    w4p_trusted_workers: bool
    worker_compatibility_date: str


class CloudInfraService:
    def __init__(
        self,
        *,
        cf_account_repo: CFAccountRepository,
        instance_repo: CloudInstanceRepository,
        assignment_repo: WorkspaceCloudAssignmentRepository,
        shard_repo: Optional[CloudShardRepository] = None,
        project_shard_repo: Optional[ProjectCloudAssignmentRepository] = None,
        shard_migration_repo: Optional[CloudShardMigrationStatusRepository] = None,
        cloud_client=None,  # Optional[CloudControlClient] — avoids circular import
    ) -> None:
        self._cf = cf_account_repo
        self._inst = instance_repo
        self._assign = assignment_repo
        self._shard = shard_repo
        self._proj_shard = project_shard_repo
        self._shard_mig = shard_migration_repo
        self._cloud = cloud_client

    # ------------------------------------------------------------------
    # CF Accounts (admin only)
    # ------------------------------------------------------------------

    async def create_cf_account(self, body: CreateCFAccountRequest) -> CFAccountResponse:
        existing = await self._cf.get_by_cf_account_id(body.account_id)
        if existing:
            raise ConflictError(f"CF account {body.account_id!r} already registered")
        config_enc = _encrypt_credentials(body.credentials)
        row = await self._cf.create(
            account_id=body.account_id,
            account_name=body.account_name,
            tier=body.tier,
            max_workspaces=body.max_workspaces,
            config_enc=config_enc,
            metadata=body.metadata,
        )
        logger.info("cf_account_created", account_id=body.account_id, tier=body.tier)
        return _cf_account_response(row)

    async def update_cf_account(
        self, account_id: UUID, body: UpdateCFAccountRequest
    ) -> CFAccountResponse:
        row = await self._cf.get_by_id(account_id)
        if not row:
            raise NotFoundError("CF account not found")
        fields: dict = {}
        if body.account_name is not None:
            fields["account_name"] = body.account_name
        if body.tier is not None:
            fields["tier"] = body.tier
        if body.status is not None:
            fields["status"] = body.status
        if body.max_workspaces is not None:
            fields["max_workspaces"] = body.max_workspaces
        if body.credentials is not None:
            fields["config_enc"] = _encrypt_credentials(body.credentials)
        if body.metadata is not None:
            fields["metadata_"] = body.metadata
        row = await self._cf.update(row, **fields)
        return _cf_account_response(row)

    async def list_cf_accounts(self) -> list[CFAccountResponse]:
        rows = await self._cf.list_all()
        return [_cf_account_response(r) for r in rows]

    async def get_cf_account(self, account_id: UUID) -> CFAccountResponse:
        row = await self._cf.get_by_id(account_id)
        if not row:
            raise NotFoundError("CF account not found")
        return _cf_account_response(row)

    async def get_credentials(self, cf_account_id: UUID) -> CfCredentials:
        """Decrypt and return CF credentials for a specific CF account row."""
        row = await self._cf.get_by_id(cf_account_id)
        if not row:
            raise NotFoundError("CF account not found")
        if not row.config_enc:
            raise ValueError(
                f"CF account {row.account_id!r} has no credentials configured"
            )
        return _decrypt_credentials(row)

    async def get_credentials_for_workspace(self, workspace_id: UUID) -> CfCredentials:
        """Return decrypted CF credentials for the CF account assigned to a workspace.

        Resolves: workspace_id → workspace_cloud_assignment → cloud_instance → cf_account.
        Result is cached per workspace_id for 60 seconds.
        """
        cache_key = f"ws:{workspace_id}"
        cached = _get_cached(cache_key)
        if cached:
            return cached

        assignment = await self._assign.get_active_for_workspace(workspace_id)
        if not assignment:
            raise NotFoundError(f"No active cloud assignment for workspace {workspace_id}")
        instance = await self._inst.get_by_id(assignment.cloud_instance_id)
        if not instance:
            raise NotFoundError("Cloud instance for workspace assignment not found")
        row = await self._cf.get_by_id(instance.cf_account_id)
        if not row:
            raise NotFoundError("CF account for workspace not found")
        if not row.config_enc:
            raise ValueError(f"CF account {row.account_id!r} has no credentials configured")

        creds = _decrypt_credentials(row)
        _set_cached(cache_key, creds)
        return creds

    async def get_primary_credentials(self) -> CfCredentials:
        """Return decrypted CF credentials for the primary (first active) CF account.

        Used for platform-level CF operations (zone management, CF SaaS) that are
        not scoped to a specific workspace. Cached for 60 seconds.
        """
        cached = _get_cached("primary")
        if cached:
            return cached

        rows = await self._cf.list_active()
        if not rows:
            raise NotFoundError("No active CF accounts configured")
        if not rows[0].config_enc:
            raise ValueError("Primary CF account has no credentials configured")

        creds = _decrypt_credentials(rows[0])
        _set_cached("primary", creds)
        return creds

    async def delete_cf_account(self, account_id: UUID) -> None:
        """Soft-delete a CF account by marking it suspended.

        Raises ConflictError if the account has active workspace assignments.
        """
        row = await self._cf.get_by_id(account_id)
        if not row:
            raise NotFoundError("CF account not found")
        if row.workspace_count > 0:
            raise ConflictError(
                f"CF account has {row.workspace_count} active workspace(s) — "
                "reassign them before deleting this account"
            )
        await self._cf.update(row, status=CFAccountStatus.suspended)
        logger.info("cf_account_deleted", account_id=str(account_id))

    # ------------------------------------------------------------------
    # Cloud Instances
    # ------------------------------------------------------------------

    async def register_instance(
        self, body: RegisterCloudInstanceRequest
    ) -> CloudInstanceResponse:
        """Called by a Cloud instance on startup to register or refresh its record."""
        existing = await self._inst.get_by_instance_id(body.instance_id)
        token = (body.internal_token or "").strip()
        token_enc = encrypt_value(token) if token else None

        if existing:
            # Re-registration — update gateway_url and token, set active
            fields: dict = {
                "gateway_url": body.gateway_url,
                "status": CloudInstanceStatus.active,
                "metadata_": body.metadata or existing.metadata_,
            }
            if token_enc:
                fields["internal_token_enc"] = token_enc
            await self._inst.update(existing, **fields)
            await self._inst.touch_heartbeat(body.instance_id)
            logger.info("cloud_instance_reregistered", instance_id=body.instance_id)
            return _instance_response(existing)

        if not token_enc:
            raise ValidationError("internal_token is required on first Cloud instance register")

        # Resolve CF account
        cf_account_id: UUID
        if body.cf_account_id:
            cf_row = await self._cf.get_by_id(body.cf_account_id)
            if not cf_row:
                raise NotFoundError("CF account not found")
            cf_account_id = cf_row.id
        else:
            # Auto-resolve: pick any active CF account of matching tier
            candidates = await self._cf.list_active(tier=body.tier)
            if not candidates:
                raise NotFoundError(
                    f"No active CF account available for tier {body.tier!r}"
                )
            cf_account_id = candidates[0].id

        row = await self._inst.create(
            instance_id=body.instance_id,
            cf_account_id=cf_account_id,
            tier=body.tier,
            gateway_url=body.gateway_url,
            internal_token_enc=token_enc,
            metadata=body.metadata,
        )
        await self._inst.touch_heartbeat(body.instance_id)
        logger.info(
            "cloud_instance_registered",
            instance_id=body.instance_id,
            tier=body.tier,
            gateway_url=body.gateway_url,
        )
        return _instance_response(row)

    async def heartbeat(self, instance_id: str) -> CloudInstanceHeartbeatResponse:
        """Touch last_heartbeat_at for a running instance."""
        row = await self._inst.get_by_instance_id(instance_id)
        if not row:
            raise NotFoundError("Cloud instance not found")
        await self._inst.touch_heartbeat(instance_id)
        return CloudInstanceHeartbeatResponse(ok=True, instance_id=instance_id)

    async def update_instance(
        self, instance_id: UUID, body: UpdateCloudInstanceRequest
    ) -> CloudInstanceResponse:
        row = await self._inst.get_by_id(instance_id)
        if not row:
            raise NotFoundError("Cloud instance not found")
        fields: dict = {}
        if body.gateway_url is not None:
            fields["gateway_url"] = body.gateway_url
        if body.status is not None:
            fields["status"] = body.status
        if body.metadata is not None:
            fields["metadata_"] = body.metadata
        row = await self._inst.update(row, **fields)
        return _instance_response(row)

    async def list_instances(self) -> list[CloudInstanceResponse]:
        rows = await self._inst.list_all()
        return [_instance_response(r) for r in rows]

    # ------------------------------------------------------------------
    # Workspace Assignment + Routing
    # ------------------------------------------------------------------

    async def assign_workspace(
        self, workspace_id: UUID, body: AssignWorkspaceRequest
    ) -> WorkspaceCloudAssignmentResponse:
        """Admin override — assign workspace to a specific Cloud instance."""
        instance = await self._inst.get_by_id(body.cloud_instance_id)
        if not instance:
            raise NotFoundError("Cloud instance not found")

        existing = await self._assign.get_active_for_workspace(workspace_id)
        if existing:
            if existing.cloud_instance_id == body.cloud_instance_id:
                # Already assigned to the same instance — idempotent
                return _assignment_response(existing)
            # workspace_id is unique on the assignment table, so do not
            # release then insert.  Reassign the existing row in place and
            # adjust the counters for the old and new CF accounts.
            old_instance = await self._inst.get_by_id(existing.cloud_instance_id)
            if not old_instance:
                raise NotFoundError("Cloud instance for workspace assignment not found")
            await self._assign.reassign(
                existing,
                cloud_instance_id=body.cloud_instance_id,
            )
            await self._cf.increment_workspace_count(old_instance.cf_account_id, delta=-1)
            await self._cf.increment_workspace_count(instance.cf_account_id, delta=1)
            _creds_cache.pop(f"ws:{workspace_id}", None)
            row = existing
        else:
            row = await self._assign.create(
                workspace_id=workspace_id,
                cloud_instance_id=body.cloud_instance_id,
            )
            await self._cf.increment_workspace_count(instance.cf_account_id, delta=1)
        logger.info(
            "workspace_assigned_to_instance",
            workspace_id=str(workspace_id),
            instance_id=str(body.cloud_instance_id),
        )
        return _assignment_response(row)

    async def auto_assign_workspace(self, workspace_id: UUID) -> WorkspaceCloudAssignment:
        """Auto-assign workspace to the least-loaded active Cloud instance.

        Called lazily when a workspace needs routing but has no assignment yet.
        """
        existing = await self._assign.get_active_for_workspace(workspace_id)
        if existing:
            return existing

        instances = await self._inst.list_active()
        if not instances:
            raise NotFoundError("No active Cloud instances available — contact ops")

        # Pick first active instance (later: load-balance by workspace_count)
        instance = instances[0]
        row = await self._assign.create(
            workspace_id=workspace_id,
            cloud_instance_id=instance.id,
        )
        await self._cf.increment_workspace_count(instance.cf_account_id, delta=1)
        logger.info(
            "workspace_auto_assigned",
            workspace_id=str(workspace_id),
            instance_id=str(instance.id),
        )
        return row

    async def get_routing(self, workspace_id: UUID) -> Optional[WorkspaceRoutingInfo]:
        """Return resolved gateway URL + decrypted token for per-workspace routing.

        Returns None if no assignment exists (caller may auto-assign or fall back to global).
        """
        assignment = await self._assign.get_active_for_workspace(workspace_id)
        if not assignment:
            return None
        instance = await self._inst.get_by_id(assignment.cloud_instance_id)
        if not instance or instance.status == CloudInstanceStatus.offline:
            return None
        return WorkspaceRoutingInfo(
            workspace_id=workspace_id,
            cloud_instance_id=instance.id,
            gateway_url=instance.gateway_url.rstrip("/"),
            internal_token=decrypt_value(instance.internal_token_enc),
        )

    async def get_assignment(
        self, workspace_id: UUID
    ) -> Optional[WorkspaceCloudAssignmentResponse]:
        row = await self._assign.get_active_for_workspace(workspace_id)
        if not row:
            return None
        return _assignment_response(row)

    # ------------------------------------------------------------------
    # Cloud Shards (admin only)
    # ------------------------------------------------------------------

    def _require_shard_repos(self) -> None:
        if not self._shard or not self._proj_shard:
            raise RuntimeError("CloudInfraService instantiated without shard repositories")

    async def create_shard(self, body: CreateCloudShardRequest) -> CloudShardResponse:
        self._require_shard_repos()
        assert self._shard is not None
        existing = await self._shard.get_by_key(body.shard_key)
        if existing:
            raise ConflictError(f"Shard key {body.shard_key!r} already exists")
        cf_row = await self._cf.get_by_id(body.cf_account_id)
        if not cf_row:
            raise NotFoundError("CF account not found")
        row = await self._shard.create(
            id=uuid4(),
            shard_key=body.shard_key,
            region=body.region,
            shard_type=body.shard_type,
            cf_account_id=body.cf_account_id,
            d1_database_id=body.d1_database_id,
            r2_bucket_name=body.r2_bucket_name,
            dispatch_namespace=body.dispatch_namespace,
            gateway_base_url=body.gateway_base_url,
            worker_name=body.worker_name,
            d1_database_name=body.d1_database_name,
            max_projects=body.max_projects,
        )
        logger.info("cloud_shard_created", shard_key=body.shard_key, shard_type=body.shard_type)
        return _shard_response(row)

    async def update_shard(self, shard_id: UUID, body: UpdateCloudShardRequest) -> CloudShardResponse:
        self._require_shard_repos()
        assert self._shard is not None
        row = await self._shard.get_by_id(shard_id)
        if not row:
            raise NotFoundError("Cloud shard not found")
        fields: dict = {}
        if body.region is not None:
            fields["region"] = body.region
        if body.cf_account_id is not None:
            cf_row = await self._cf.get_by_id(body.cf_account_id)
            if not cf_row:
                raise NotFoundError("CF account not found")
            fields["cf_account_id"] = body.cf_account_id
        if body.d1_database_id is not None:
            fields["d1_database_id"] = body.d1_database_id
        if body.r2_bucket_name is not None:
            fields["r2_bucket_name"] = body.r2_bucket_name
        if body.dispatch_namespace is not None:
            fields["dispatch_namespace"] = body.dispatch_namespace
        if body.gateway_base_url is not None:
            fields["gateway_base_url"] = body.gateway_base_url
        if body.worker_name is not None:
            fields["worker_name"] = body.worker_name
        if body.d1_database_name is not None:
            fields["d1_database_name"] = body.d1_database_name
        if body.status is not None:
            fields["status"] = body.status
        if body.is_accepting is not None:
            fields["is_accepting"] = body.is_accepting
        if body.max_projects is not None:
            fields["max_projects"] = body.max_projects
        row = await self._shard.update(row, **fields)
        return _shard_response(row)

    async def list_shards(self) -> list[CloudShardResponse]:
        self._require_shard_repos()
        assert self._shard is not None
        rows = await self._shard.list_all()
        return [_shard_response(r) for r in rows]

    async def get_shard(self, shard_id: UUID) -> CloudShardResponse:
        self._require_shard_repos()
        assert self._shard is not None
        row = await self._shard.get_by_id(shard_id)
        if not row:
            raise NotFoundError("Cloud shard not found")
        return _shard_response(row)

    # ------------------------------------------------------------------
    # Project Shard Assignments
    # ------------------------------------------------------------------

    async def auto_assign_project_shard(self, project_id: UUID) -> ProjectCloudAssignmentResponse:
        """Auto-assign a project to the default accepting shared shard.

        Idempotent — if an assignment already exists, returns it unchanged.
        Called from the project bootstrap worker (sync_project_to_cloud).
        """
        self._require_shard_repos()
        assert self._shard is not None
        assert self._proj_shard is not None

        existing = await self._proj_shard.get_for_project(project_id)
        if existing:
            shard = await self._shard.get_by_id(existing.shard_id)
            return _project_assignment_response(existing, shard)

        shard = await self._shard.get_default_shared()
        if not shard:
            raise NotFoundError(
                "No accepting shared shard available — ops must create one via "
                "POST /api/admin/cloud-infra/shards before projects can be bootstrapped"
            )
        row = await self._proj_shard.create(project_id=project_id, shard_id=shard.id)
        logger.info(
            "project_shard_auto_assigned",
            project_id=str(project_id),
            shard_key=shard.shard_key,
        )
        return _project_assignment_response(row, shard)

    async def assign_project_to_shard(
        self, project_id: UUID, body: AssignProjectToShardRequest
    ) -> ProjectCloudAssignmentResponse:
        """Admin override — move a project to a specific shard.

        Pushes a KV routing cache invalidation to Cloud so the ingress router
        stops serving the old shard mapping for this project immediately.
        """
        self._require_shard_repos()
        assert self._shard is not None
        assert self._proj_shard is not None

        shard = await self._shard.get_by_id(body.shard_id)
        if not shard:
            raise NotFoundError("Cloud shard not found")

        existing = await self._proj_shard.get_for_project(project_id)
        if existing:
            if existing.shard_id == body.shard_id:
                return _project_assignment_response(existing, shard)
            await self._proj_shard.update(existing, shard_id=body.shard_id, status=ProjectShardStatus.active)
            logger.info(
                "project_shard_reassigned",
                project_id=str(project_id),
                shard_key=shard.shard_key,
            )
            await self._push_routing_cache_invalidation(project_id)
            return _project_assignment_response(existing, shard)

        row = await self._proj_shard.create(project_id=project_id, shard_id=body.shard_id)
        return _project_assignment_response(row, shard)

    async def _push_routing_cache_invalidation(self, project_id: UUID) -> None:
        """Ask Cloud's ingress to drop the KV routing cache entry for this project."""
        if not self._cloud:
            return
        try:
            await self._cloud.invalidate_project_routing_cache(str(project_id))
        except Exception as exc:
            logger.warning(
                "routing_cache_invalidation_failed",
                project_id=str(project_id),
                error=str(exc),
            )

    async def get_project_shard_assignment(
        self, project_id: UUID
    ) -> Optional[ProjectCloudAssignmentResponse]:
        self._require_shard_repos()
        assert self._shard is not None
        assert self._proj_shard is not None

        row = await self._proj_shard.get_for_project(project_id)
        if not row:
            return None
        shard = await self._shard.get_by_id(row.shard_id)
        return _project_assignment_response(row, shard)

    # ------------------------------------------------------------------
    # Shard Migration Status
    # ------------------------------------------------------------------

    async def report_shard_migration(
        self, body: ReportShardMigrationRequest
    ) -> CloudShardMigrationStatusResponse:
        """Called by a shard gateway after wrangler d1 migrations apply."""
        if not self._shard_mig:
            raise RuntimeError("Shard migration repo not initialised")
        shard = await self._shard.get_by_id(body.shard_id) if self._shard else None
        if not shard:
            raise NotFoundError("Cloud shard not found")
        row = await self._shard_mig.upsert(
            shard_id=body.shard_id,
            migration_version=body.migration_version,
            status=body.status,
            applied_at=datetime.now(timezone.utc),
            error=body.error,
            reported_by=body.reported_by,
        )
        logger.info(
            "shard_migration_reported",
            shard_id=str(body.shard_id),
            version=body.migration_version,
            status=body.status,
        )
        return _shard_migration_response(row)

    async def list_shard_migrations(
        self, shard_id: UUID
    ) -> list[CloudShardMigrationStatusResponse]:
        if not self._shard_mig:
            raise RuntimeError("Shard migration repo not initialised")
        rows = await self._shard_mig.list_for_shard(shard_id)
        return [_shard_migration_response(r) for r in rows]

    async def get_fleet_migration_status(
        self, expected_head: Optional[str] = None
    ) -> list[ShardFleetMigrationRow]:
        """Fleet-wide view: one row per shard showing its latest applied migration.

        Flags shards that lag behind expected_head (if provided).
        """
        if not self._shard_mig or not self._shard:
            raise RuntimeError("Shard migration repo not initialised")
        all_shards = await self._shard.list_all()
        latest_per_shard = {
            row.shard_id: row
            for row in await self._shard_mig.get_latest_per_shard()
        }
        result: list[ShardFleetMigrationRow] = []
        for shard in all_shards:
            latest = latest_per_shard.get(shard.id)
            lags = (
                expected_head is not None
                and latest is not None
                and latest.migration_version != expected_head
            ) or (expected_head is not None and latest is None)
            result.append(ShardFleetMigrationRow(
                shard_id=shard.id,
                shard_key=shard.shard_key,
                latest_migration_version=latest.migration_version if latest else None,
                latest_status=latest.status if latest else None,
                applied_at=latest.applied_at if latest else None,
                lags_head=lags,
            ))
        return result
        self._require_shard_repos()
        assert self._shard is not None
        assert self._proj_shard is not None

        row = await self._proj_shard.get_for_project(project_id)
        if not row:
            return None
        shard = await self._shard.get_by_id(row.shard_id)
        return _project_assignment_response(row, shard)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _encrypt_credentials(creds: CFAccountCredentials) -> str:
    payload = json.dumps(creds.model_dump())
    return encrypt_value(payload)


def _decrypt_credentials(row: CFAccount) -> CfCredentials:
    raw = decrypt_value(row.config_enc)  # type: ignore[arg-type]
    data = json.loads(raw)
    return CfCredentials(
        account_id=row.account_id,
        api_token=data.get("api_token", ""),
        zone_id=data.get("zone_id", ""),
        w4p_namespace_production=data.get("w4p_namespace_production", ""),
        w4p_namespace_preview=data.get("w4p_namespace_preview", ""),
        w4p_namespace_staging=data.get("w4p_namespace_staging", ""),
        dispatch_kv_namespace_id=data.get("dispatch_kv_namespace_id", ""),
        w4p_trusted_workers=data.get("w4p_trusted_workers", False),
        worker_compatibility_date=data.get("worker_compatibility_date", ""),
    )


def _cf_account_response(row: CFAccount) -> CFAccountResponse:
    return CFAccountResponse(
        id=row.id,
        account_id=row.account_id,
        account_name=row.account_name,
        tier=row.tier,
        status=row.status,
        max_workspaces=row.max_workspaces,
        workspace_count=row.workspace_count,
        credentials_configured=bool(row.config_enc),
        created_at=row.created_at,
        updated_at=row.updated_at,
        metadata=row.metadata_,
    )


def _instance_response(row: CloudInstance) -> CloudInstanceResponse:
    return CloudInstanceResponse(
        id=row.id,
        instance_id=row.instance_id,
        cf_account_id=row.cf_account_id,
        tier=row.tier,
        gateway_url=row.gateway_url,
        status=row.status,
        last_heartbeat_at=row.last_heartbeat_at,
        registered_at=row.registered_at,
        metadata=row.metadata_,
    )


def _assignment_response(row: WorkspaceCloudAssignment) -> WorkspaceCloudAssignmentResponse:
    return WorkspaceCloudAssignmentResponse(
        id=row.id,
        workspace_id=row.workspace_id,
        cloud_instance_id=row.cloud_instance_id,
        status=row.status,
        assigned_at=row.assigned_at,
        released_at=row.released_at,
    )


def _shard_response(row: CloudShard) -> CloudShardResponse:
    from src.domain.cloud_infra.schemas import CloudShardResponse
    return CloudShardResponse(
        id=row.id,
        shard_key=row.shard_key,
        region=row.region,
        shard_type=row.shard_type,
        cf_account_id=row.cf_account_id,
        d1_database_id=row.d1_database_id,
        r2_bucket_name=row.r2_bucket_name,
        dispatch_namespace=row.dispatch_namespace,
        gateway_base_url=row.gateway_base_url,
        worker_name=row.worker_name,
        d1_database_name=row.d1_database_name,
        status=row.status,
        is_accepting=row.is_accepting,
        max_projects=row.max_projects,
        created_at=row.created_at,
        updated_at=row.updated_at,
    )


def _shard_migration_response(row: CloudShardMigrationStatus) -> CloudShardMigrationStatusResponse:
    return CloudShardMigrationStatusResponse(
        id=row.id,
        shard_id=row.shard_id,
        migration_version=row.migration_version,
        applied_at=row.applied_at,
        status=row.status,
        error=row.error,
        reported_by=row.reported_by,
        created_at=row.created_at,
        updated_at=row.updated_at,
    )


def _project_assignment_response(
    row, shard: Optional[CloudShard]
) -> ProjectCloudAssignmentResponse:
    return ProjectCloudAssignmentResponse(
        id=row.id,
        project_id=row.project_id,
        shard_id=row.shard_id,
        status=row.status,
        assigned_at=row.assigned_at,
        shard=_shard_response(shard) if shard else None,
    )
