use crank_core::{
    AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AsyncJobHandle, AsyncJobId,
    AuthProfile, GraphqlOperationType, HttpMethod, InvitationId, InvitationToken, InvocationLog,
    InvocationLogId, JobStatus, MembershipRole, OperationId, OperationStatus, PlatformApiKey,
    PlatformApiKeyId, PlatformApiKeyStatus, Secret, SecretId, SecretVersion, StreamSession,
    StreamSessionId, StreamStatus, Target, UsageRollup, User, UserId, UserSessionId, Workspace,
    WorkspaceId,
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use sqlx::{
    PgPool, Postgres, Row, Transaction,
    postgres::{PgConnectOptions, PgPoolOptions, PgRow},
    types::Json,
};

use crate::{
    error::RegistryError,
    migrations,
    model::{
        AgentSummary, AgentVersionRecord, AsyncJobFilter, AuthUserRecord, CreateAgentRequest,
        CreateAsyncJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
        CreatePlatformApiKeyRequest, CreateSecretRequest, CreateStreamSessionRequest,
        CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
        DescriptorMetadata, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
        MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
        OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord,
        PublishAgentRequest, PublishRequest, PublishedAgentTool, RegistryOperation,
        RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
        SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SecretRecord,
        SecretVersionRecord, SessionRecord, StreamSessionFilter, UpdateAsyncJobStatusRequest,
        UpdateStreamSessionStateRequest, UpdateWorkspaceRequest, UsageAgentBreakdown,
        UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
        WorkspaceMembershipRecord, WorkspaceRecord, YamlImportJob, YamlImportJobCompletion,
        YamlImportJobId, YamlImportJobStatus,
    },
};

#[derive(Clone, Debug)]
pub struct PostgresRegistry {
    pool: PgPool,
}

impl PostgresRegistry {
    pub async fn connect(database_url: &str) -> Result<Self, RegistryError> {
        Self::connect_in_schema(database_url, None).await
    }

    pub async fn connect_with_options(
        connect_options: PgConnectOptions,
    ) -> Result<Self, RegistryError> {
        let pool = PgPoolOptions::new().connect_with(connect_options).await?;
        migrations::apply_postgres(&pool).await?;
        Ok(Self { pool })
    }

    pub fn pool(&self) -> &PgPool {
        &self.pool
    }

    pub async fn migrate(&self) -> Result<(), RegistryError> {
        migrations::apply_postgres(&self.pool).await?;
        Ok(())
    }

    pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                slug,
                display_name,
                status,
                settings_json,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
             from workspaces
             order by slug asc",
        )
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_workspace_record).collect()
    }

    pub async fn list_workspaces_for_user(
        &self,
        user_id: &UserId,
    ) -> Result<Vec<WorkspaceMembershipRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                w.id,
                w.slug,
                w.display_name,
                w.status,
                w.settings_json,
                to_char(w.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(w.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                m.role
             from memberships m
             join workspaces w on w.id = m.workspace_id
             where m.user_id = $1
             order by w.slug asc",
        )
        .bind(user_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_workspace_membership_record).collect()
    }

    pub async fn upsert_bootstrap_user(
        &self,
        email: &str,
        display_name: &str,
        password_hash: &str,
    ) -> Result<UserId, RegistryError> {
        let row = sqlx::query(
            "insert into users (
                id,
                email,
                display_name,
                password_hash,
                status,
                created_at
             ) values (
                $1, $2, $3, $4, 'active', now()
             )
             on conflict (email) do update
             set display_name = excluded.display_name,
                 password_hash = excluded.password_hash,
                 status = 'active'
             returning id",
        )
        .bind(format!("user_{}", uuid::Uuid::now_v7().simple()))
        .bind(email)
        .bind(display_name)
        .bind(password_hash)
        .fetch_one(&self.pool)
        .await?;

        Ok(UserId::new(row.try_get::<String, _>("id")?))
    }

    pub async fn ensure_membership(
        &self,
        workspace_id: &WorkspaceId,
        user_id: &UserId,
        role: MembershipRole,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into memberships (
                workspace_id,
                user_id,
                role,
                created_at
             ) values (
                $1, $2, $3, now()
             )
             on conflict (workspace_id, user_id) do update
             set role = excluded.role",
        )
        .bind(workspace_id.as_str())
        .bind(user_id.as_str())
        .bind(serialize_enum_text(&role, "role")?)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_auth_user_by_email(
        &self,
        email: &str,
    ) -> Result<Option<AuthUserRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                email,
                display_name,
                password_hash,
                status,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
             from users
             where email = $1
             limit 1",
        )
        .bind(email)
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_auth_user_record).transpose()
    }

    pub async fn get_auth_user_by_id(
        &self,
        user_id: &UserId,
    ) -> Result<Option<AuthUserRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                email,
                display_name,
                password_hash,
                status,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
             from users
             where id = $1
             limit 1",
        )
        .bind(user_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_auth_user_record).transpose()
    }

    pub async fn update_user_profile(
        &self,
        user_id: &UserId,
        email: &str,
        display_name: &str,
    ) -> Result<User, RegistryError> {
        let row = sqlx::query(
            "update users
             set email = $2,
                 display_name = $3
             where id = $1
             returning
                id,
                email,
                display_name,
                status,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at",
        )
        .bind(user_id.as_str())
        .bind(email)
        .bind(display_name)
        .fetch_one(&self.pool)
        .await
        .map_err(|error| map_user_update_error(error, user_id, email))?;

        map_user(&row)
    }

    pub async fn update_user_password(
        &self,
        user_id: &UserId,
        password_hash: &str,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update users
             set password_hash = $2
             where id = $1",
        )
        .bind(user_id.as_str())
        .bind(password_hash)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::UserNotFound {
                user_id: user_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn create_user_session(
        &self,
        session_id: &UserSessionId,
        user_id: &UserId,
        current_workspace_id: Option<&WorkspaceId>,
        secret_hash: &str,
        expires_at: &str,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into user_sessions (
                id,
                user_id,
                current_workspace_id,
                secret_hash,
                status,
                expires_at,
                last_seen_at,
                created_at
             ) values (
                $1,
                $2,
                $3,
                $4,
                'active',
                $5::timestamptz,
                now(),
                now()
             )",
        )
        .bind(session_id.as_str())
        .bind(user_id.as_str())
        .bind(current_workspace_id.map(|id| id.as_str()))
        .bind(secret_hash)
        .bind(expires_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_user_session(
        &self,
        session_id: &UserSessionId,
        secret_hash: &str,
    ) -> Result<Option<SessionRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                s.id,
                s.user_id,
                s.current_workspace_id,
                u.email,
                u.display_name,
                u.status,
                to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
             from user_sessions s
             join users u on u.id = s.user_id
             where s.id = $1
               and s.secret_hash = $2
               and s.status = 'active'
               and s.expires_at > now()
             limit 1",
        )
        .bind(session_id.as_str())
        .bind(secret_hash)
        .fetch_optional(&self.pool)
        .await?;

        let Some(row) = row else {
            return Ok(None);
        };

        let user_id = UserId::new(row.try_get::<String, _>("user_id")?);
        let user = User {
            id: user_id.clone(),
            email: row.try_get("email")?,
            display_name: row.try_get("display_name")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            created_at: row.try_get("created_at")?,
        };
        let memberships = self.list_workspaces_for_user(&user_id).await?;
        let stored_workspace_id = row
            .try_get::<Option<String>, _>("current_workspace_id")?
            .map(WorkspaceId::new);
        let current_workspace_id = stored_workspace_id
            .filter(|workspace_id| {
                memberships
                    .iter()
                    .any(|membership| membership.workspace.id == *workspace_id)
            })
            .or_else(|| {
                memberships
                    .first()
                    .map(|membership| membership.workspace.id.clone())
            });

        Ok(Some(SessionRecord {
            session_id: UserSessionId::new(row.try_get::<String, _>("id")?),
            user,
            memberships,
            current_workspace_id,
        }))
    }

    pub async fn touch_user_session(
        &self,
        session_id: &UserSessionId,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "update user_sessions
             set last_seen_at = now()
             where id = $1",
        )
        .bind(session_id.as_str())
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn revoke_user_session(
        &self,
        session_id: &UserSessionId,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "update user_sessions
             set status = 'revoked'
             where id = $1",
        )
        .bind(session_id.as_str())
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn set_user_session_current_workspace(
        &self,
        session_id: &UserSessionId,
        workspace_id: &WorkspaceId,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "update user_sessions
             set current_workspace_id = $2
             where id = $1",
        )
        .bind(session_id.as_str())
        .bind(workspace_id.as_str())
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn user_has_workspace_access(
        &self,
        user_id: &UserId,
        workspace_id: &WorkspaceId,
    ) -> Result<bool, RegistryError> {
        let row = sqlx::query(
            "select exists(
                select 1
                from memberships
                where user_id = $1
                  and workspace_id = $2
            ) as allowed",
        )
        .bind(user_id.as_str())
        .bind(workspace_id.as_str())
        .fetch_one(&self.pool)
        .await?;

        Ok(row.try_get("allowed")?)
    }

    pub async fn list_memberships(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<MembershipRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                m.workspace_id,
                m.user_id,
                m.role,
                to_char(m.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                u.email,
                u.display_name,
                u.status,
                to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as user_created_at
             from memberships m
             join users u on u.id = m.user_id
             where m.workspace_id = $1
             order by u.email asc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_membership_record).collect()
    }

    pub async fn update_membership_role(
        &self,
        workspace_id: &WorkspaceId,
        user_id: &UserId,
        role: MembershipRole,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update memberships
             set role = $3
             where workspace_id = $1 and user_id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(user_id.as_str())
        .bind(serialize_enum_text(&role, "role")?)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::MembershipNotFound {
                workspace_id: workspace_id.as_str().to_owned(),
                user_id: user_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn delete_membership(
        &self,
        workspace_id: &WorkspaceId,
        user_id: &UserId,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "delete from memberships
             where workspace_id = $1 and user_id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(user_id.as_str())
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::MembershipNotFound {
                workspace_id: workspace_id.as_str().to_owned(),
                user_id: user_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn list_invitations(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<InvitationRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                workspace_id,
                email,
                role,
                status,
                token_hash,
                to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
             from invitation_tokens
             where workspace_id = $1
             order by created_at desc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_invitation_record).collect()
    }

    pub async fn create_invitation(
        &self,
        request: CreateInvitationRequest<'_>,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into invitation_tokens (
                id,
                workspace_id,
                email,
                role,
                status,
                token_hash,
                expires_at,
                created_at
             ) values (
                $1, $2, $3, $4, $5, $6, $7::timestamptz, $8::timestamptz
             )",
        )
        .bind(request.invitation.id.as_str())
        .bind(request.invitation.workspace_id.as_str())
        .bind(&request.invitation.email)
        .bind(serialize_enum_text(&request.invitation.role, "role")?)
        .bind(serialize_enum_text(&request.invitation.status, "status")?)
        .bind(&request.invitation.token_hash)
        .bind(&request.invitation.expires_at)
        .bind(&request.invitation.created_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn delete_invitation(
        &self,
        workspace_id: &WorkspaceId,
        invitation_id: &InvitationId,
    ) -> Result<(), RegistryError> {
        let result =
            sqlx::query("delete from invitation_tokens where workspace_id = $1 and id = $2")
                .bind(workspace_id.as_str())
                .bind(invitation_id.as_str())
                .execute(&self.pool)
                .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::InvitationNotFound {
                invitation_id: invitation_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn delete_workspace(&self, workspace_id: &WorkspaceId) -> Result<(), RegistryError> {
        let result = sqlx::query("delete from workspaces where id = $1")
            .bind(workspace_id.as_str())
            .execute(&self.pool)
            .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::WorkspaceNotFound {
                workspace_id: workspace_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn list_platform_api_keys(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<PlatformApiKeyRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                workspace_id,
                name,
                prefix,
                scopes_json,
                status,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
             from platform_api_keys
             where workspace_id = $1
             order by created_at desc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_platform_api_key_record).collect()
    }

    pub async fn get_platform_api_key_by_secret_for_workspace_slug(
        &self,
        workspace_slug: &str,
        secret_hash: &str,
    ) -> Result<Option<PlatformApiKeyRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                k.id,
                k.workspace_id,
                k.name,
                k.prefix,
                k.scopes_json,
                k.status,
                to_char(k.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(k.last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
             from platform_api_keys k
             join workspaces w on w.id = k.workspace_id
             where w.slug = $1
               and k.secret_hash = $2
               and k.status = 'active'
             limit 1",
        )
        .bind(workspace_slug)
        .bind(secret_hash)
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_platform_api_key_record).transpose()
    }

    pub async fn list_secrets(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<SecretRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                workspace_id,
                name,
                kind,
                status,
                current_version,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
             from secrets
             where workspace_id = $1
             order by name asc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_secret_record).collect()
    }

    pub async fn get_secret(
        &self,
        workspace_id: &WorkspaceId,
        secret_id: &SecretId,
    ) -> Result<Option<SecretRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                workspace_id,
                name,
                kind,
                status,
                current_version,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
             from secrets
             where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(secret_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_secret_record).transpose()
    }

    pub async fn get_current_secret_version(
        &self,
        workspace_id: &WorkspaceId,
        secret_id: &SecretId,
    ) -> Result<Option<SecretVersionRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                sv.secret_id,
                sv.version,
                sv.ciphertext,
                sv.key_version,
                to_char(sv.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                sv.created_by
             from secrets s
             join secret_versions sv
               on sv.secret_id = s.id and sv.version = s.current_version
             where s.workspace_id = $1 and s.id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(secret_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_secret_version_record).transpose()
    }

    pub async fn create_secret(
        &self,
        request: CreateSecretRequest<'_>,
    ) -> Result<(), RegistryError> {
        let mut tx = self.pool.begin().await?;
        let result = sqlx::query(
            "insert into secrets (
                id,
                workspace_id,
                name,
                kind,
                status,
                current_version,
                last_used_at,
                created_at,
                updated_at
             ) values (
                $1, $2, $3, $4, $5, $6, $7::timestamptz, $8::timestamptz, $9::timestamptz
             )",
        )
        .bind(request.secret.id.as_str())
        .bind(request.secret.workspace_id.as_str())
        .bind(&request.secret.name)
        .bind(serialize_enum_text(&request.secret.kind, "kind")?)
        .bind(serialize_enum_text(&request.secret.status, "status")?)
        .bind(to_db_version(request.secret.current_version))
        .bind(request.secret.last_used_at.as_deref())
        .bind(&request.secret.created_at)
        .bind(&request.secret.updated_at)
        .execute(&mut *tx)
        .await;

        match result {
            Ok(_) => {
                sqlx::query(
                    "insert into secret_versions (
                        secret_id,
                        version,
                        ciphertext,
                        key_version,
                        created_at,
                        created_by
                     ) values (
                        $1, $2, $3, $4, $5::timestamptz, $6
                     )",
                )
                .bind(request.secret.id.as_str())
                .bind(to_db_version(request.secret.current_version))
                .bind(request.ciphertext)
                .bind(request.key_version)
                .bind(&request.secret.created_at)
                .bind(request.created_by.map(|value| value.as_str()))
                .execute(&mut *tx)
                .await?;
                tx.commit().await?;
                Ok(())
            }
            Err(sqlx::Error::Database(error))
                if error.constraint() == Some("secrets_workspace_name_idx") =>
            {
                Err(RegistryError::SecretNameAlreadyExists {
                    workspace_id: request.secret.workspace_id.as_str().to_owned(),
                    name: request.secret.name.clone(),
                })
            }
            Err(error) => Err(RegistryError::Storage(error)),
        }
    }

    pub async fn rotate_secret(
        &self,
        request: RotateSecretRequest<'_>,
    ) -> Result<SecretVersionRecord, RegistryError> {
        let existing = self
            .get_secret(request.workspace_id, request.secret_id)
            .await?
            .ok_or_else(|| RegistryError::SecretNotFound {
                secret_id: request.secret_id.as_str().to_owned(),
            })?;
        let next_version = existing.secret.current_version + 1;

        let mut tx = self.pool.begin().await?;
        sqlx::query(
            "insert into secret_versions (
                secret_id,
                version,
                ciphertext,
                key_version,
                created_at,
                created_by
             ) values (
                $1, $2, $3, $4, $5::timestamptz, $6
             )",
        )
        .bind(request.secret_id.as_str())
        .bind(to_db_version(next_version))
        .bind(request.ciphertext)
        .bind(request.key_version)
        .bind(request.created_at)
        .bind(request.created_by.map(|value| value.as_str()))
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            "update secrets
                set current_version = $3,
                    updated_at = $4::timestamptz
              where workspace_id = $1 and id = $2",
        )
        .bind(request.workspace_id.as_str())
        .bind(request.secret_id.as_str())
        .bind(to_db_version(next_version))
        .bind(request.updated_at)
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;

        Ok(SecretVersionRecord {
            secret_version: SecretVersion {
                secret_id: request.secret_id.clone(),
                version: next_version,
                ciphertext: request.ciphertext.to_owned(),
                key_version: request.key_version.to_owned(),
                created_at: request.created_at.to_owned(),
                created_by: request.created_by.cloned(),
            },
        })
    }

    pub async fn delete_secret(
        &self,
        workspace_id: &WorkspaceId,
        secret_id: &SecretId,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "delete from secrets
              where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(secret_id.as_str())
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::SecretNotFound {
                secret_id: secret_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn touch_secret(
        &self,
        workspace_id: &WorkspaceId,
        secret_id: &SecretId,
        used_at: &str,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update secrets
                set last_used_at = $3::timestamptz
              where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(secret_id.as_str())
        .bind(used_at)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::SecretNotFound {
                secret_id: secret_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn create_stream_session(
        &self,
        request: CreateStreamSessionRequest<'_>,
    ) -> Result<StreamSession, RegistryError> {
        sqlx::query(
            "insert into stream_sessions (
                id,
                workspace_id,
                agent_id,
                operation_id,
                protocol,
                mode,
                status,
                cursor_json,
                state_json,
                expires_at,
                last_poll_at,
                created_at,
                closed_at
             ) values (
                $1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10::timestamptz,
                $11::timestamptz, $12::timestamptz, $13::timestamptz
             )",
        )
        .bind(request.session.id.as_str())
        .bind(request.session.workspace_id.as_str())
        .bind(
            request
                .session
                .agent_id
                .as_ref()
                .map(|value| value.as_str()),
        )
        .bind(request.session.operation_id.as_str())
        .bind(serialize_enum_text(&request.session.protocol, "protocol")?)
        .bind(serialize_enum_text(&request.session.mode, "mode")?)
        .bind(serialize_enum_text(&request.session.status, "status")?)
        .bind(request.session.cursor.clone().map(Json))
        .bind(Json(request.session.state.clone()))
        .bind(&request.session.expires_at)
        .bind(request.session.last_poll_at.as_deref())
        .bind(&request.session.created_at)
        .bind(request.session.closed_at.as_deref())
        .execute(&self.pool)
        .await?;

        Ok(request.session.clone())
    }

    pub async fn get_stream_session(
        &self,
        id: &StreamSessionId,
    ) -> Result<Option<StreamSession>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                workspace_id,
                agent_id,
                operation_id,
                protocol,
                mode,
                status,
                cursor_json,
                state_json,
                to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
                to_char(last_poll_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_poll_at,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(closed_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as closed_at
             from stream_sessions
             where id = $1",
        )
        .bind(id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_stream_session).transpose()
    }

    pub async fn update_stream_session_state(
        &self,
        request: UpdateStreamSessionStateRequest<'_>,
    ) -> Result<StreamSession, RegistryError> {
        validate_stream_session_transition(
            request.session_id,
            request.current_status,
            request.next_status,
        )?;

        let row = sqlx::query(
            "update stream_sessions
                set status = $3,
                    cursor_json = $4::jsonb,
                    state_json = $5::jsonb,
                    expires_at = coalesce($6::timestamptz, expires_at),
                    last_poll_at = coalesce($7::timestamptz, last_poll_at),
                    closed_at = case
                        when $8::timestamptz is null then closed_at
                        else $8::timestamptz
                    end
              where id = $1
                and status = $2
            returning
                id,
                workspace_id,
                agent_id,
                operation_id,
                protocol,
                mode,
                status,
                cursor_json,
                state_json,
                to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
                to_char(last_poll_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_poll_at,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(closed_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as closed_at",
        )
        .bind(request.session_id.as_str())
        .bind(serialize_enum_text(&request.current_status, "status")?)
        .bind(serialize_enum_text(&request.next_status, "status")?)
        .bind(request.cursor.cloned().map(Json))
        .bind(Json(request.state.clone()))
        .bind(request.expires_at)
        .bind(request.last_poll_at)
        .bind(request.closed_at)
        .fetch_optional(&self.pool)
        .await?;

        match row.as_ref() {
            Some(row) => map_stream_session(row),
            None => {
                if self.get_stream_session(request.session_id).await?.is_some() {
                    Err(RegistryError::InvalidStreamSessionTransition {
                        session_id: request.session_id.as_str().to_owned(),
                        from: serialize_enum_text(&request.current_status, "status")?,
                        to: serialize_enum_text(&request.next_status, "status")?,
                    })
                } else {
                    Err(RegistryError::StreamSessionNotFound {
                        session_id: request.session_id.as_str().to_owned(),
                    })
                }
            }
        }
    }

    pub async fn close_stream_session(
        &self,
        id: &StreamSessionId,
        now: &str,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update stream_sessions
                set status = 'stopped',
                    last_poll_at = $2::timestamptz,
                    closed_at = $2::timestamptz
              where id = $1
                and status in ('created', 'running')",
        )
        .bind(id.as_str())
        .bind(now)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() > 0 {
            return Ok(());
        }

        match self.get_stream_session(id).await? {
            Some(existing) => Err(RegistryError::InvalidStreamSessionTransition {
                session_id: id.as_str().to_owned(),
                from: serialize_enum_text(&existing.status, "status")?,
                to: "stopped".to_owned(),
            }),
            None => Err(RegistryError::StreamSessionNotFound {
                session_id: id.as_str().to_owned(),
            }),
        }
    }

    pub async fn list_stream_sessions(
        &self,
        filter: StreamSessionFilter<'_>,
    ) -> Result<Page<StreamSession>, RegistryError> {
        let status = filter
            .status
            .as_ref()
            .map(|value| serialize_enum_text(value, "status"))
            .transpose()?;
        let mode = filter
            .mode
            .as_ref()
            .map(|value| serialize_enum_text(value, "mode"))
            .transpose()?;
        let limit = i64::from(filter.limit);

        let rows = sqlx::query(
            "select
                id,
                workspace_id,
                agent_id,
                operation_id,
                protocol,
                mode,
                status,
                cursor_json,
                state_json,
                to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
                to_char(last_poll_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_poll_at,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(closed_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as closed_at
             from stream_sessions
             where workspace_id = $1
               and ($2::text is null or agent_id = $2)
               and ($3::text is null or operation_id = $3)
               and ($4::text is null or status = $4)
               and ($5::text is null or mode = $5)
             order by created_at desc
             limit $6",
        )
        .bind(filter.workspace_id.as_str())
        .bind(filter.agent_id.map(|value| value.as_str()))
        .bind(filter.operation_id.map(|value| value.as_str()))
        .bind(status)
        .bind(mode)
        .bind(limit)
        .fetch_all(&self.pool)
        .await?;

        let items = rows
            .iter()
            .map(map_stream_session)
            .collect::<Result<Vec<_>, _>>()?;
        let total = items.len() as u64;

        Ok(Page { items, total })
    }

    pub async fn delete_expired_stream_sessions(&self, now: &str) -> Result<u64, RegistryError> {
        let result = sqlx::query(
            "delete from stream_sessions
              where expires_at <= $1::timestamptz",
        )
        .bind(now)
        .execute(&self.pool)
        .await?;

        Ok(result.rows_affected())
    }

    pub async fn create_async_job(
        &self,
        request: CreateAsyncJobRequest<'_>,
    ) -> Result<AsyncJobHandle, RegistryError> {
        sqlx::query(
            "insert into async_jobs (
                id,
                workspace_id,
                agent_id,
                operation_id,
                status,
                progress_json,
                result_json,
                error_json,
                expires_at,
                created_at,
                updated_at,
                finished_at
             ) values (
                $1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8::jsonb, $9::timestamptz,
                $10::timestamptz, $11::timestamptz, $12::timestamptz
             )",
        )
        .bind(request.job.id.as_str())
        .bind(request.job.workspace_id.as_str())
        .bind(request.job.agent_id.as_ref().map(|value| value.as_str()))
        .bind(request.job.operation_id.as_str())
        .bind(serialize_enum_text(&request.job.status, "status")?)
        .bind(Json(request.job.progress.clone()))
        .bind(request.job.result.clone().map(Json))
        .bind(request.job.error.clone().map(Json))
        .bind(request.job.expires_at.as_deref())
        .bind(&request.job.created_at)
        .bind(&request.job.updated_at)
        .bind(request.job.finished_at.as_deref())
        .execute(&self.pool)
        .await?;

        Ok(request.job.clone())
    }

    pub async fn get_async_job(
        &self,
        id: &AsyncJobId,
    ) -> Result<Option<AsyncJobHandle>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                workspace_id,
                agent_id,
                operation_id,
                status,
                progress_json,
                result_json,
                error_json,
                to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at
             from async_jobs
             where id = $1",
        )
        .bind(id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_async_job).transpose()
    }

    pub async fn update_async_job_status(
        &self,
        request: UpdateAsyncJobStatusRequest<'_>,
    ) -> Result<AsyncJobHandle, RegistryError> {
        validate_async_job_transition(request.job_id, request.current_status, request.next_status)?;

        let row = sqlx::query(
            "update async_jobs
                set status = $3,
                    progress_json = $4::jsonb,
                    result_json = $5::jsonb,
                    error_json = $6::jsonb,
                    expires_at = coalesce($7::timestamptz, expires_at),
                    updated_at = $8::timestamptz,
                    finished_at = case
                        when $9::timestamptz is null then finished_at
                        else $9::timestamptz
                    end
              where id = $1
                and status = $2
            returning
                id,
                workspace_id,
                agent_id,
                operation_id,
                status,
                progress_json,
                result_json,
                error_json,
                to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at",
        )
        .bind(request.job_id.as_str())
        .bind(serialize_enum_text(&request.current_status, "status")?)
        .bind(serialize_enum_text(&request.next_status, "status")?)
        .bind(Json(request.progress.clone()))
        .bind(request.result.cloned().map(Json))
        .bind(request.error.cloned().map(Json))
        .bind(request.expires_at)
        .bind(request.updated_at)
        .bind(request.finished_at)
        .fetch_optional(&self.pool)
        .await?;

        match row.as_ref() {
            Some(row) => map_async_job(row),
            None => {
                if self.get_async_job(request.job_id).await?.is_some() {
                    Err(RegistryError::InvalidAsyncJobTransition {
                        job_id: request.job_id.as_str().to_owned(),
                        from: serialize_enum_text(&request.current_status, "status")?,
                        to: serialize_enum_text(&request.next_status, "status")?,
                    })
                } else {
                    Err(RegistryError::AsyncJobNotFound {
                        job_id: request.job_id.as_str().to_owned(),
                    })
                }
            }
        }
    }

    pub async fn cancel_async_job(&self, id: &AsyncJobId, now: &str) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update async_jobs
                set status = 'cancelled',
                    updated_at = $2::timestamptz,
                    finished_at = $2::timestamptz
              where id = $1
                and status in ('created', 'running')",
        )
        .bind(id.as_str())
        .bind(now)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() > 0 {
            return Ok(());
        }

        match self.get_async_job(id).await? {
            Some(existing) => Err(RegistryError::InvalidAsyncJobTransition {
                job_id: id.as_str().to_owned(),
                from: serialize_enum_text(&existing.status, "status")?,
                to: "cancelled".to_owned(),
            }),
            None => Err(RegistryError::AsyncJobNotFound {
                job_id: id.as_str().to_owned(),
            }),
        }
    }

    pub async fn list_async_jobs(
        &self,
        filter: AsyncJobFilter<'_>,
    ) -> Result<Page<AsyncJobHandle>, RegistryError> {
        let status = filter
            .status
            .as_ref()
            .map(|value| serialize_enum_text(value, "status"))
            .transpose()?;
        let limit = i64::from(filter.limit);

        let rows = sqlx::query(
            "select
                id,
                workspace_id,
                agent_id,
                operation_id,
                status,
                progress_json,
                result_json,
                error_json,
                to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at
             from async_jobs
             where workspace_id = $1
               and ($2::text is null or agent_id = $2)
               and ($3::text is null or operation_id = $3)
               and ($4::text is null or status = $4)
             order by updated_at desc
             limit $5",
        )
        .bind(filter.workspace_id.as_str())
        .bind(filter.agent_id.map(|value| value.as_str()))
        .bind(filter.operation_id.map(|value| value.as_str()))
        .bind(status)
        .bind(limit)
        .fetch_all(&self.pool)
        .await?;

        let items = rows
            .iter()
            .map(map_async_job)
            .collect::<Result<Vec<_>, _>>()?;
        let total = items.len() as u64;

        Ok(Page { items, total })
    }

    pub async fn delete_expired_async_jobs(&self, now: &str) -> Result<u64, RegistryError> {
        let result = sqlx::query(
            "delete from async_jobs
              where expires_at is not null
                and expires_at <= $1::timestamptz",
        )
        .bind(now)
        .execute(&self.pool)
        .await?;

        Ok(result.rows_affected())
    }

    pub async fn create_platform_api_key(
        &self,
        request: CreatePlatformApiKeyRequest<'_>,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "insert into platform_api_keys (
                id,
                workspace_id,
                name,
                prefix,
                secret_hash,
                scopes_json,
                status,
                created_at,
                last_used_at,
                revoked_at
             ) values (
                $1, $2, $3, $4, $5, $6, $7, $8::timestamptz, $9::timestamptz, $10::timestamptz
             )",
        )
        .bind(request.api_key.id.as_str())
        .bind(request.api_key.workspace_id.as_str())
        .bind(&request.api_key.name)
        .bind(&request.api_key.prefix)
        .bind(request.secret_hash)
        .bind(Json(serialize_json_value(&request.api_key.scopes)?))
        .bind(serialize_enum_text(&request.api_key.status, "status")?)
        .bind(&request.api_key.created_at)
        .bind(request.api_key.last_used_at.as_deref())
        .bind(Option::<&str>::None)
        .execute(&self.pool)
        .await;

        match result {
            Ok(_) => Ok(()),
            Err(sqlx::Error::Database(error))
                if error.constraint() == Some("platform_api_keys_workspace_name_idx") =>
            {
                Err(RegistryError::Storage(sqlx::Error::Database(error)))
            }
            Err(error) => Err(RegistryError::Storage(error)),
        }
    }

    pub async fn revoke_platform_api_key(
        &self,
        workspace_id: &WorkspaceId,
        key_id: &PlatformApiKeyId,
        revoked_at: &str,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update platform_api_keys
                set status = $1,
                    revoked_at = $2::timestamptz
              where workspace_id = $3 and id = $4",
        )
        .bind(serialize_enum_text(
            &PlatformApiKeyStatus::Revoked,
            "status",
        )?)
        .bind(revoked_at)
        .bind(workspace_id.as_str())
        .bind(key_id.as_str())
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::PlatformApiKeyNotFound {
                key_id: key_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn delete_platform_api_key(
        &self,
        workspace_id: &WorkspaceId,
        key_id: &PlatformApiKeyId,
    ) -> Result<(), RegistryError> {
        let result =
            sqlx::query("delete from platform_api_keys where workspace_id = $1 and id = $2")
                .bind(workspace_id.as_str())
                .bind(key_id.as_str())
                .execute(&self.pool)
                .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::PlatformApiKeyNotFound {
                key_id: key_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn touch_platform_api_key(
        &self,
        workspace_id: &WorkspaceId,
        key_id: &PlatformApiKeyId,
        used_at: &str,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update platform_api_keys
                set last_used_at = $3::timestamptz
              where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(key_id.as_str())
        .bind(used_at)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::PlatformApiKeyNotFound {
                key_id: key_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn create_invocation_log(
        &self,
        request: CreateInvocationLogRequest<'_>,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into invocation_logs (
                id,
                workspace_id,
                agent_id,
                operation_id,
                source,
                level,
                status,
                tool_name,
                message,
                request_id,
                status_code,
                duration_ms,
                error_kind,
                request_preview_json,
                response_preview_json,
                created_at
            ) values (
                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16::timestamptz
            )",
        )
        .bind(request.log.id.as_str())
        .bind(request.log.workspace_id.as_str())
        .bind(request.log.agent_id.as_ref().map(|value| value.as_str()))
        .bind(request.log.operation_id.as_str())
        .bind(serialize_enum_text(&request.log.source, "source")?)
        .bind(serialize_enum_text(&request.log.level, "level")?)
        .bind(serialize_enum_text(&request.log.status, "status")?)
        .bind(&request.log.tool_name)
        .bind(&request.log.message)
        .bind(&request.log.request_id)
        .bind(request.log.status_code.map(i32::from))
        .bind(i64::try_from(request.log.duration_ms).map_err(|_| {
            RegistryError::InvalidNumericValue {
                field: "duration_ms",
                value: i64::MAX,
            }
        })?)
        .bind(&request.log.error_kind)
        .bind(Json(request.log.request_preview.clone()))
        .bind(Json(request.log.response_preview.clone()))
        .bind(&request.log.created_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn list_invocation_logs(
        &self,
        query: ListInvocationLogsQuery<'_>,
    ) -> Result<Vec<InvocationLogRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                l.id,
                l.workspace_id,
                l.agent_id,
                l.operation_id,
                l.source,
                l.level,
                l.status,
                l.tool_name,
                l.message,
                l.request_id,
                l.status_code,
                l.duration_ms,
                l.error_kind,
                l.request_preview_json,
                l.response_preview_json,
                to_char(l.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"') as created_at,
                o.name as operation_name,
                o.display_name as operation_display_name,
                a.slug as agent_slug,
                a.display_name as agent_display_name
             from invocation_logs l
             join operations o on o.id = l.operation_id
             left join agents a on a.id = l.agent_id
             where l.workspace_id = $1
               and ($2::text is null or l.level = $2)
               and ($3::text is null or l.source = $3)
               and ($4::text is null or l.operation_id = $4)
               and ($5::text is null or l.agent_id = $5)
               and ($6::timestamptz is null or l.created_at >= $6::timestamptz)
               and (
                    $7::text is null
                    or l.tool_name ilike '%' || $7 || '%'
                    or l.message ilike '%' || $7 || '%'
                    or o.name ilike '%' || $7 || '%'
                    or o.display_name ilike '%' || $7 || '%'
               )
             order by l.created_at desc
             limit $8",
        )
        .bind(query.workspace_id.as_str())
        .bind(
            query
                .level
                .as_ref()
                .map(|value| serialize_enum_text(value, "level"))
                .transpose()?,
        )
        .bind(
            query
                .source
                .as_ref()
                .map(|value| serialize_enum_text(value, "source"))
                .transpose()?,
        )
        .bind(query.operation_id.map(|value| value.as_str()))
        .bind(query.agent_id.map(|value| value.as_str()))
        .bind(query.created_after)
        .bind(query.search_text)
        .bind(i64::from(query.limit))
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_invocation_log_record).collect()
    }

    pub async fn get_invocation_log(
        &self,
        workspace_id: &WorkspaceId,
        log_id: &InvocationLogId,
    ) -> Result<Option<InvocationLogRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                l.id,
                l.workspace_id,
                l.agent_id,
                l.operation_id,
                l.source,
                l.level,
                l.status,
                l.tool_name,
                l.message,
                l.request_id,
                l.status_code,
                l.duration_ms,
                l.error_kind,
                l.request_preview_json,
                l.response_preview_json,
                to_char(l.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"') as created_at,
                o.name as operation_name,
                o.display_name as operation_display_name,
                a.slug as agent_slug,
                a.display_name as agent_display_name
             from invocation_logs l
             join operations o on o.id = l.operation_id
             left join agents a on a.id = l.agent_id
             where l.workspace_id = $1 and l.id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(log_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_invocation_log_record).transpose()
    }

    pub async fn summarize_usage(
        &self,
        query: UsageQuery<'_>,
    ) -> Result<UsageSummary, RegistryError> {
        let row = sqlx::query(
            "select
                count(*)::bigint as calls_total,
                count(*) filter (where status = 'ok')::bigint as calls_ok,
                count(*) filter (where status = 'error')::bigint as calls_error,
                coalesce(percentile_cont(0.5) within group (order by duration_ms), 0)::bigint as p50_ms,
                coalesce(percentile_cont(0.95) within group (order by duration_ms), 0)::bigint as p95_ms,
                coalesce(percentile_cont(0.99) within group (order by duration_ms), 0)::bigint as p99_ms
             from invocation_logs
             where workspace_id = $1
               and created_at >= $2::timestamptz
               and ($3::text is null or source = $3)",
        )
        .bind(query.workspace_id.as_str())
        .bind(query.created_after)
        .bind(
            query
                .source
                .as_ref()
                .map(|value| serialize_enum_text(value, "source"))
                .transpose()?,
        )
        .fetch_one(&self.pool)
        .await?;

        let calls_total = row.try_get::<i64, _>("calls_total")?;
        let calls_ok = row.try_get::<i64, _>("calls_ok")?;
        let calls_error = row.try_get::<i64, _>("calls_error")?;
        let rollup = UsageRollup {
            workspace_id: query.workspace_id.clone(),
            agent_id: None,
            operation_id: None,
            period: query.period,
            calls_total: to_u64(calls_total, "calls_total")?,
            calls_ok: to_u64(calls_ok, "calls_ok")?,
            calls_error: to_u64(calls_error, "calls_error")?,
            p50_ms: to_u64(row.try_get::<i64, _>("p50_ms")?, "p50_ms")?,
            p95_ms: to_u64(row.try_get::<i64, _>("p95_ms")?, "p95_ms")?,
            p99_ms: to_u64(row.try_get::<i64, _>("p99_ms")?, "p99_ms")?,
        };

        let success_rate = if rollup.calls_total == 0 {
            0.0
        } else {
            (rollup.calls_ok as f64 / rollup.calls_total as f64) * 100.0
        };

        Ok(UsageSummary {
            rollup,
            success_rate,
        })
    }

    pub async fn list_usage_timeline(
        &self,
        query: UsageQuery<'_>,
    ) -> Result<Vec<UsageTimelinePoint>, RegistryError> {
        let bucket = usage_bucket_sql(query.bucket);
        let sql = format!(
            "select
                to_char(date_trunc('{bucket}', created_at at time zone 'UTC'), 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as bucket_start,
                count(*) filter (where status = 'ok')::bigint as calls_ok,
                count(*) filter (where status = 'error')::bigint as calls_error
             from invocation_logs
             where workspace_id = $1
               and created_at >= $2::timestamptz
               and ($3::text is null or source = $3)
             group by 1
             order by 1 asc"
        );
        let rows = sqlx::query(&sql)
            .bind(query.workspace_id.as_str())
            .bind(query.created_after)
            .bind(
                query
                    .source
                    .as_ref()
                    .map(|value| serialize_enum_text(value, "source"))
                    .transpose()?,
            )
            .fetch_all(&self.pool)
            .await?;

        rows.iter()
            .map(|row| {
                Ok(UsageTimelinePoint {
                    bucket_start: row.try_get("bucket_start")?,
                    calls_ok: to_u64(row.try_get::<i64, _>("calls_ok")?, "calls_ok")?,
                    calls_error: to_u64(row.try_get::<i64, _>("calls_error")?, "calls_error")?,
                })
            })
            .collect()
    }

    pub async fn list_usage_by_operation(
        &self,
        query: UsageQuery<'_>,
    ) -> Result<Vec<UsageOperationBreakdown>, RegistryError> {
        let rows = sqlx::query(
            "select
                o.id as operation_id,
                o.name as operation_name,
                o.display_name as operation_display_name,
                o.protocol,
                count(*)::bigint as calls_total,
                count(*) filter (where l.status = 'error')::bigint as calls_error,
                coalesce(percentile_cont(0.5) within group (order by l.duration_ms), 0)::bigint as p50_ms,
                coalesce(percentile_cont(0.95) within group (order by l.duration_ms), 0)::bigint as p95_ms,
                coalesce(percentile_cont(0.99) within group (order by l.duration_ms), 0)::bigint as p99_ms
             from invocation_logs l
             join operations o on o.id = l.operation_id
             where l.workspace_id = $1
               and l.created_at >= $2::timestamptz
               and ($3::text is null or l.source = $3)
             group by o.id, o.name, o.display_name, o.protocol
             order by calls_total desc, o.name asc",
        )
        .bind(query.workspace_id.as_str())
        .bind(query.created_after)
        .bind(
            query
                .source
                .as_ref()
                .map(|value| serialize_enum_text(value, "source"))
                .transpose()?,
        )
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_usage_operation_breakdown).collect()
    }

    pub async fn get_usage_for_operation(
        &self,
        query: UsageQuery<'_>,
        operation_id: &OperationId,
    ) -> Result<Option<UsageRollupRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                count(*)::bigint as calls_total,
                count(*) filter (where status = 'ok')::bigint as calls_ok,
                count(*) filter (where status = 'error')::bigint as calls_error,
                coalesce(percentile_cont(0.5) within group (order by duration_ms), 0)::bigint as p50_ms,
                coalesce(percentile_cont(0.95) within group (order by duration_ms), 0)::bigint as p95_ms,
                coalesce(percentile_cont(0.99) within group (order by duration_ms), 0)::bigint as p99_ms
             from invocation_logs
             where workspace_id = $1
               and operation_id = $2
               and created_at >= $3::timestamptz
               and ($4::text is null or source = $4)",
        )
        .bind(query.workspace_id.as_str())
        .bind(operation_id.as_str())
        .bind(query.created_after)
        .bind(
            query
                .source
                .as_ref()
                .map(|value| serialize_enum_text(value, "source"))
                .transpose()?,
        )
        .fetch_one(&self.pool)
        .await?;

        let calls_total = to_u64(row.try_get::<i64, _>("calls_total")?, "calls_total")?;
        if calls_total == 0 {
            return Ok(None);
        }

        Ok(Some(UsageRollupRecord {
            rollup: UsageRollup {
                workspace_id: query.workspace_id.clone(),
                agent_id: None,
                operation_id: Some(operation_id.clone()),
                period: query.period,
                calls_total,
                calls_ok: to_u64(row.try_get::<i64, _>("calls_ok")?, "calls_ok")?,
                calls_error: to_u64(row.try_get::<i64, _>("calls_error")?, "calls_error")?,
                p50_ms: to_u64(row.try_get::<i64, _>("p50_ms")?, "p50_ms")?,
                p95_ms: to_u64(row.try_get::<i64, _>("p95_ms")?, "p95_ms")?,
                p99_ms: to_u64(row.try_get::<i64, _>("p99_ms")?, "p99_ms")?,
            },
        }))
    }

    pub async fn list_usage_by_agent(
        &self,
        query: UsageQuery<'_>,
    ) -> Result<Vec<UsageAgentBreakdown>, RegistryError> {
        let rows = sqlx::query(
            "select
                a.id as agent_id,
                a.slug as agent_slug,
                a.display_name as agent_display_name,
                count(*)::bigint as calls_total,
                count(*) filter (where l.status = 'error')::bigint as calls_error,
                coalesce(percentile_cont(0.5) within group (order by l.duration_ms), 0)::bigint as p50_ms,
                coalesce(percentile_cont(0.95) within group (order by l.duration_ms), 0)::bigint as p95_ms,
                coalesce(percentile_cont(0.99) within group (order by l.duration_ms), 0)::bigint as p99_ms
             from invocation_logs l
             join agents a on a.id = l.agent_id
             where l.workspace_id = $1
               and l.created_at >= $2::timestamptz
               and ($3::text is null or l.source = $3)
             group by a.id, a.slug, a.display_name
             order by calls_total desc, a.slug asc",
        )
        .bind(query.workspace_id.as_str())
        .bind(query.created_after)
        .bind(
            query
                .source
                .as_ref()
                .map(|value| serialize_enum_text(value, "source"))
                .transpose()?,
        )
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_usage_agent_breakdown).collect()
    }

    pub async fn get_usage_for_agent(
        &self,
        query: UsageQuery<'_>,
        agent_id: &AgentId,
    ) -> Result<Option<UsageRollupRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                count(*)::bigint as calls_total,
                count(*) filter (where status = 'ok')::bigint as calls_ok,
                count(*) filter (where status = 'error')::bigint as calls_error,
                coalesce(percentile_cont(0.5) within group (order by duration_ms), 0)::bigint as p50_ms,
                coalesce(percentile_cont(0.95) within group (order by duration_ms), 0)::bigint as p95_ms,
                coalesce(percentile_cont(0.99) within group (order by duration_ms), 0)::bigint as p99_ms
             from invocation_logs
             where workspace_id = $1
               and agent_id = $2
               and created_at >= $3::timestamptz
               and ($4::text is null or source = $4)",
        )
        .bind(query.workspace_id.as_str())
        .bind(agent_id.as_str())
        .bind(query.created_after)
        .bind(
            query
                .source
                .as_ref()
                .map(|value| serialize_enum_text(value, "source"))
                .transpose()?,
        )
        .fetch_one(&self.pool)
        .await?;

        let calls_total = to_u64(row.try_get::<i64, _>("calls_total")?, "calls_total")?;
        if calls_total == 0 {
            return Ok(None);
        }

        Ok(Some(UsageRollupRecord {
            rollup: UsageRollup {
                workspace_id: query.workspace_id.clone(),
                agent_id: Some(agent_id.clone()),
                operation_id: None,
                period: query.period,
                calls_total,
                calls_ok: to_u64(row.try_get::<i64, _>("calls_ok")?, "calls_ok")?,
                calls_error: to_u64(row.try_get::<i64, _>("calls_error")?, "calls_error")?,
                p50_ms: to_u64(row.try_get::<i64, _>("p50_ms")?, "p50_ms")?,
                p95_ms: to_u64(row.try_get::<i64, _>("p95_ms")?, "p95_ms")?,
                p99_ms: to_u64(row.try_get::<i64, _>("p99_ms")?, "p99_ms")?,
            },
        }))
    }

    pub async fn get_workspace(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Option<WorkspaceRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                slug,
                display_name,
                status,
                settings_json,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
             from workspaces
             where id = $1",
        )
        .bind(workspace_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_workspace_record).transpose()
    }

    pub async fn create_workspace(
        &self,
        request: CreateWorkspaceRequest<'_>,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "insert into workspaces (
                id,
                slug,
                display_name,
                status,
                settings_json,
                created_at,
                updated_at
             ) values (
                $1, $2, $3, $4, $5, $6::timestamptz, $7::timestamptz
             )",
        )
        .bind(request.workspace.id.as_str())
        .bind(&request.workspace.slug)
        .bind(&request.workspace.display_name)
        .bind(serialize_enum_text(&request.workspace.status, "status")?)
        .bind(Json(request.workspace.settings.clone()))
        .bind(&request.workspace.created_at)
        .bind(&request.workspace.updated_at)
        .execute(&self.pool)
        .await;

        match result {
            Ok(_) => Ok(()),
            Err(sqlx::Error::Database(error))
                if error.constraint() == Some("workspaces_slug_key") =>
            {
                Err(RegistryError::WorkspaceSlugAlreadyExists {
                    slug: request.workspace.slug.clone(),
                })
            }
            Err(error) => Err(RegistryError::Storage(error)),
        }
    }

    pub async fn update_workspace(
        &self,
        request: UpdateWorkspaceRequest<'_>,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update workspaces
                set slug = $1,
                    display_name = $2,
                    status = $3,
                    settings_json = $4,
                    updated_at = $5::timestamptz
              where id = $6",
        )
        .bind(&request.workspace.slug)
        .bind(&request.workspace.display_name)
        .bind(serialize_enum_text(&request.workspace.status, "status")?)
        .bind(Json(request.workspace.settings.clone()))
        .bind(&request.workspace.updated_at)
        .bind(request.workspace.id.as_str())
        .execute(&self.pool)
        .await;

        match result {
            Ok(result) if result.rows_affected() == 0 => Err(RegistryError::WorkspaceNotFound {
                workspace_id: request.workspace.id.as_str().to_owned(),
            }),
            Ok(_) => Ok(()),
            Err(sqlx::Error::Database(error))
                if error.constraint() == Some("workspaces_slug_key") =>
            {
                Err(RegistryError::WorkspaceSlugAlreadyExists {
                    slug: request.workspace.slug.clone(),
                })
            }
            Err(error) => Err(RegistryError::Storage(error)),
        }
    }

    pub async fn list_agents(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<AgentSummary>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                workspace_id,
                slug,
                display_name,
                description,
                status,
                current_draft_version,
                latest_published_version,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
             from agents
             where workspace_id = $1
             order by slug asc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_agent_summary).collect()
    }

    pub async fn get_agent_summary(
        &self,
        workspace_id: &WorkspaceId,
        agent_id: &AgentId,
    ) -> Result<Option<AgentSummary>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                workspace_id,
                slug,
                display_name,
                description,
                status,
                current_draft_version,
                latest_published_version,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
             from agents
             where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(agent_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_agent_summary).transpose()
    }

    pub async fn create_agent(&self, request: CreateAgentRequest<'_>) -> Result<(), RegistryError> {
        let mut tx = self.pool.begin().await?;

        sqlx::query(
            "insert into agents (
                id,
                workspace_id,
                slug,
                display_name,
                description,
                status,
                current_draft_version,
                latest_published_version,
                created_at,
                updated_at,
                published_at
            ) values (
                $1, $2, $3, $4, $5, $6, $7, $8, $9::timestamptz, $10::timestamptz, $11::timestamptz
            )",
        )
        .bind(request.agent.id.as_str())
        .bind(request.agent.workspace_id.as_str())
        .bind(&request.agent.slug)
        .bind(&request.agent.display_name)
        .bind(&request.agent.description)
        .bind(serialize_enum_text(&request.agent.status, "status")?)
        .bind(to_db_version(request.agent.current_draft_version))
        .bind(request.agent.latest_published_version.map(to_db_version))
        .bind(&request.agent.created_at)
        .bind(&request.agent.updated_at)
        .bind(request.agent.published_at.as_deref())
        .execute(&mut *tx)
        .await?;

        insert_agent_version_row(&mut tx, request.version).await?;
        replace_agent_bindings_rows(
            &mut tx,
            &request.agent.id,
            request.version.version,
            request.bindings,
        )
        .await?;

        tx.commit().await?;
        Ok(())
    }

    pub async fn get_agent_version(
        &self,
        workspace_id: &WorkspaceId,
        agent_id: &AgentId,
        version: u32,
    ) -> Result<Option<AgentVersionRecord>, RegistryError> {
        let summary = match self.get_agent_summary(workspace_id, agent_id).await? {
            Some(value) => value,
            None => return Ok(None),
        };

        let row = sqlx::query(
            "select
                agent_id,
                version,
                status,
                instructions_json,
                tool_selection_policy_json,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
             from agent_versions
             where agent_id = $1 and version = $2",
        )
        .bind(agent_id.as_str())
        .bind(to_db_version(version))
        .fetch_optional(&self.pool)
        .await?;

        let Some(row) = row else {
            return Ok(None);
        };

        let bindings = self.list_agent_bindings(agent_id, version).await?;
        Ok(Some(map_agent_version_record(&summary, &row, bindings)?))
    }

    pub async fn save_agent_bindings(
        &self,
        request: SaveAgentBindingsRequest<'_>,
    ) -> Result<(), RegistryError> {
        if self
            .get_agent_summary(request.workspace_id, request.agent_id)
            .await?
            .is_none()
        {
            return Err(RegistryError::AgentNotFound {
                agent_id: request.agent_id.as_str().to_owned(),
            });
        }

        let mut tx = self.pool.begin().await?;
        replace_agent_bindings_rows(
            &mut tx,
            request.agent_id,
            request.agent_version,
            request.bindings,
        )
        .await?;
        tx.commit().await?;
        Ok(())
    }

    pub async fn update_agent_summary(
        &self,
        workspace_id: &WorkspaceId,
        agent_id: &AgentId,
        slug: &str,
        display_name: &str,
        description: &str,
        updated_at: &str,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update agents
                set slug = $3,
                    display_name = $4,
                    description = $5,
                    updated_at = $6::timestamptz
             where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(agent_id.as_str())
        .bind(slug)
        .bind(display_name)
        .bind(description)
        .bind(updated_at)
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::AgentNotFound {
                agent_id: agent_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn delete_agent(
        &self,
        workspace_id: &WorkspaceId,
        agent_id: &AgentId,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query("delete from agents where workspace_id = $1 and id = $2")
            .bind(workspace_id.as_str())
            .bind(agent_id.as_str())
            .execute(&self.pool)
            .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::AgentNotFound {
                agent_id: agent_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn publish_agent(
        &self,
        request: PublishAgentRequest<'_>,
    ) -> Result<(), RegistryError> {
        if self
            .get_agent_version(request.workspace_id, request.agent_id, request.version)
            .await?
            .is_none()
        {
            return Err(RegistryError::AgentNotFound {
                agent_id: request.agent_id.as_str().to_owned(),
            });
        }

        let mut tx = self.pool.begin().await?;

        sqlx::query(
            "insert into published_agents (
                agent_id,
                version,
                published_at,
                published_by
             ) values ($1, $2, $3::timestamptz, $4)
             on conflict(agent_id) do update set
                version = excluded.version,
                published_at = excluded.published_at,
                published_by = excluded.published_by",
        )
        .bind(request.agent_id.as_str())
        .bind(to_db_version(request.version))
        .bind(request.published_at)
        .bind(request.published_by)
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            "update agent_versions
                set status = $1
              where agent_id = $2 and version = $3",
        )
        .bind(serialize_enum_text(&AgentStatus::Published, "status")?)
        .bind(request.agent_id.as_str())
        .bind(to_db_version(request.version))
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            "update agents
                set status = $1,
                    latest_published_version = $2,
                    published_at = $3::timestamptz,
                    updated_at = $4::timestamptz
              where id = $5 and workspace_id = $6",
        )
        .bind(serialize_enum_text(&AgentStatus::Published, "status")?)
        .bind(to_db_version(request.version))
        .bind(request.published_at)
        .bind(request.published_at)
        .bind(request.agent_id.as_str())
        .bind(request.workspace_id.as_str())
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(())
    }

    pub async fn unpublish_agent(
        &self,
        workspace_id: &WorkspaceId,
        agent_id: &AgentId,
        updated_at: &str,
    ) -> Result<(), RegistryError> {
        let mut tx = self.pool.begin().await?;

        sqlx::query("delete from published_agents where agent_id = $1")
            .bind(agent_id.as_str())
            .execute(&mut *tx)
            .await?;

        sqlx::query(
            "update agent_versions
                set status = $1
              where agent_id = $2
                and version = (
                    select current_draft_version
                    from agents
                    where id = $2 and workspace_id = $3
                )",
        )
        .bind(serialize_enum_text(&AgentStatus::Draft, "status")?)
        .bind(agent_id.as_str())
        .bind(workspace_id.as_str())
        .execute(&mut *tx)
        .await?;

        let result = sqlx::query(
            "update agents
                set status = $1,
                    published_at = null,
                    updated_at = $2::timestamptz
              where id = $3 and workspace_id = $4",
        )
        .bind(serialize_enum_text(&AgentStatus::Draft, "status")?)
        .bind(updated_at)
        .bind(agent_id.as_str())
        .bind(workspace_id.as_str())
        .execute(&mut *tx)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::AgentNotFound {
                agent_id: agent_id.as_str().to_owned(),
            });
        }

        tx.commit().await?;
        Ok(())
    }

    pub async fn archive_agent(
        &self,
        workspace_id: &WorkspaceId,
        agent_id: &AgentId,
        updated_at: &str,
    ) -> Result<(), RegistryError> {
        let mut tx = self.pool.begin().await?;

        sqlx::query("delete from published_agents where agent_id = $1")
            .bind(agent_id.as_str())
            .execute(&mut *tx)
            .await?;

        sqlx::query(
            "update agent_versions
                set status = $1
              where agent_id = $2
                and version = (
                    select current_draft_version
                    from agents
                    where id = $2 and workspace_id = $3
                )",
        )
        .bind(serialize_enum_text(&AgentStatus::Archived, "status")?)
        .bind(agent_id.as_str())
        .bind(workspace_id.as_str())
        .execute(&mut *tx)
        .await?;

        let result = sqlx::query(
            "update agents
                set status = $1,
                    published_at = null,
                    updated_at = $2::timestamptz
              where id = $3 and workspace_id = $4",
        )
        .bind(serialize_enum_text(&AgentStatus::Archived, "status")?)
        .bind(updated_at)
        .bind(agent_id.as_str())
        .bind(workspace_id.as_str())
        .execute(&mut *tx)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::AgentNotFound {
                agent_id: agent_id.as_str().to_owned(),
            });
        }

        tx.commit().await?;
        Ok(())
    }

    pub async fn get_published_agent_tools_by_slug(
        &self,
        workspace_slug: &str,
        agent_slug: &str,
    ) -> Result<Vec<PublishedAgentTool>, RegistryError> {
        let rows = sqlx::query(
            "select
                w.id as workspace_id,
                w.slug as workspace_slug,
                a.id as agent_id,
                a.slug as agent_slug,
                b.tool_name,
                b.tool_title,
                coalesce(b.tool_description_override, ov.tool_description_json->>'description') as tool_description,
                o.id,
                o.name,
                o.display_name,
                o.category,
                o.protocol,
                to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at,
                to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at,
                to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at,
                ov.version,
                ov.status,
                ov.target_json,
                ov.input_schema_json,
                ov.output_schema_json,
                ov.input_mapping_json,
                ov.output_mapping_json,
                ov.execution_config_json,
                ov.tool_description_json,
                ov.samples_json,
                ov.generated_draft_json,
                ov.config_export_json,
                ov.change_note,
                to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                ov.created_by
             from workspaces w
             join agents a on a.workspace_id = w.id
             join published_agents pa on pa.agent_id = a.id
             join agent_operation_bindings b on b.agent_id = a.id and b.agent_version = pa.version
             join operation_versions ov on ov.operation_id = b.operation_id and ov.version = b.operation_version
             join operations o on o.id = ov.operation_id and o.workspace_id = w.id
             where w.slug = $1 and a.slug = $2 and b.enabled = true
             order by b.tool_name asc",
        )
        .bind(workspace_slug)
        .bind(agent_slug)
        .fetch_all(&self.pool)
        .await?;

        if rows.is_empty() {
            let exists = sqlx::query(
                "select 1
                 from workspaces w
                 join agents a on a.workspace_id = w.id
                 join published_agents pa on pa.agent_id = a.id
                 where w.slug = $1 and a.slug = $2",
            )
            .bind(workspace_slug)
            .bind(agent_slug)
            .fetch_optional(&self.pool)
            .await?;

            if exists.is_none() {
                return Err(RegistryError::PublishedAgentNotFound {
                    workspace_slug: workspace_slug.to_owned(),
                    agent_slug: agent_slug.to_owned(),
                });
            }
        }

        rows.iter().map(map_published_agent_tool).collect()
    }

    async fn connect_in_schema(
        database_url: &str,
        schema: Option<&str>,
    ) -> Result<Self, RegistryError> {
        let pool = PgPoolOptions::new()
            .min_connections(0)
            .max_connections(1)
            .idle_timeout(std::time::Duration::from_secs(1))
            .connect(database_url)
            .await?;

        if let Some(schema) = schema {
            sqlx::query(&format!("set search_path to {schema}"))
                .execute(&pool)
                .await?;
        }

        let registry = Self { pool };
        registry.migrate().await?;
        Ok(registry)
    }

    pub async fn create_operation(
        &self,
        workspace_id: &WorkspaceId,
        snapshot: &RegistryOperation,
        created_by: Option<&str>,
    ) -> Result<(), RegistryError> {
        if snapshot.version != 1 {
            return Err(RegistryError::InvalidInitialVersion {
                operation_id: snapshot.id.as_str().to_owned(),
                version: snapshot.version,
            });
        }

        if self
            .get_operation_summary(workspace_id, &snapshot.id)
            .await?
            .is_some()
        {
            return Err(RegistryError::OperationAlreadyExists {
                operation_id: snapshot.id.as_str().to_owned(),
            });
        }

        let mut tx = self.pool.begin().await?;

        sqlx::query(
            "insert into operations (
                id,
                workspace_id,
                name,
                display_name,
                category,
                protocol,
                status,
                current_draft_version,
                latest_published_version,
                created_at,
                updated_at,
                published_at
            ) values (
                $1, $2, $3, $4, $5, $6, $7, $8, $9,
                $10::timestamptz,
                $11::timestamptz,
                $12::timestamptz
            )",
        )
        .bind(snapshot.id.as_str())
        .bind(workspace_id.as_str())
        .bind(&snapshot.name)
        .bind(&snapshot.display_name)
        .bind(&snapshot.category)
        .bind(serialize_enum_text(&snapshot.protocol, "protocol")?)
        .bind(serialize_enum_text(&snapshot.status, "status")?)
        .bind(to_db_version(snapshot.version))
        .bind(
            snapshot
                .published_at
                .as_ref()
                .map(|_| to_db_version(snapshot.version)),
        )
        .bind(&snapshot.created_at)
        .bind(&snapshot.updated_at)
        .bind(snapshot.published_at.as_deref())
        .execute(&mut *tx)
        .await?;

        insert_version_row(&mut tx, snapshot, None, created_by).await?;

        tx.commit().await?;
        Ok(())
    }

    pub async fn create_version(
        &self,
        request: CreateVersionRequest<'_>,
    ) -> Result<(), RegistryError> {
        let Some(summary) = self
            .get_operation_summary(request.workspace_id, &request.snapshot.id)
            .await?
        else {
            return Err(RegistryError::OperationNotFound {
                operation_id: request.snapshot.id.as_str().to_owned(),
            });
        };

        assert_immutable_fields(&summary, request.snapshot)?;

        let expected = summary.current_draft_version + 1;
        if request.snapshot.version != expected {
            return Err(RegistryError::InvalidVersionSequence {
                operation_id: request.snapshot.id.as_str().to_owned(),
                expected,
                actual: request.snapshot.version,
            });
        }

        let mut tx = self.pool.begin().await?;

        insert_version_row(
            &mut tx,
            request.snapshot,
            request.change_note,
            request.created_by,
        )
        .await?;

        sqlx::query(
            "update operations
                set status = $1,
                    current_draft_version = $2,
                    updated_at = $3::timestamptz
              where id = $4",
        )
        .bind(serialize_enum_text(&request.snapshot.status, "status")?)
        .bind(to_db_version(request.snapshot.version))
        .bind(&request.snapshot.updated_at)
        .bind(request.snapshot.id.as_str())
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(())
    }

    pub async fn update_operation_draft(
        &self,
        workspace_id: &WorkspaceId,
        snapshot: &RegistryOperation,
    ) -> Result<(), RegistryError> {
        let Some(summary) = self
            .get_operation_summary(workspace_id, &snapshot.id)
            .await?
        else {
            return Err(RegistryError::OperationNotFound {
                operation_id: snapshot.id.as_str().to_owned(),
            });
        };

        if snapshot.version != summary.current_draft_version {
            return Err(RegistryError::InvalidVersionSequence {
                operation_id: snapshot.id.as_str().to_owned(),
                expected: summary.current_draft_version,
                actual: snapshot.version,
            });
        }

        assert_immutable_fields(&summary, snapshot)?;

        let mut tx = self.pool.begin().await?;

        sqlx::query(
            "update operations
                set display_name = $1,
                    category = $2,
                    status = $3,
                    updated_at = $4::timestamptz
              where workspace_id = $5 and id = $6",
        )
        .bind(&snapshot.display_name)
        .bind(&snapshot.category)
        .bind(serialize_enum_text(&snapshot.status, "status")?)
        .bind(&snapshot.updated_at)
        .bind(workspace_id.as_str())
        .bind(snapshot.id.as_str())
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            "update operation_versions
                set status = $1,
                    target_json = $2,
                    input_schema_json = $3,
                    output_schema_json = $4,
                    input_mapping_json = $5,
                    output_mapping_json = $6,
                    execution_config_json = $7,
                    tool_description_json = $8,
                    samples_json = $9,
                    generated_draft_json = $10,
                    config_export_json = $11
              where operation_id = $12 and version = $13",
        )
        .bind(serialize_enum_text(&snapshot.status, "status")?)
        .bind(Json(serialize_json_value(&snapshot.target)?))
        .bind(Json(serialize_json_value(&snapshot.input_schema)?))
        .bind(Json(serialize_json_value(&snapshot.output_schema)?))
        .bind(Json(serialize_json_value(&snapshot.input_mapping)?))
        .bind(Json(serialize_json_value(&snapshot.output_mapping)?))
        .bind(Json(serialize_json_value(&snapshot.execution_config)?))
        .bind(Json(serialize_json_value(&snapshot.tool_description)?))
        .bind(serialize_option_json_value(&snapshot.samples)?.map(Json))
        .bind(serialize_option_json_value(&snapshot.generated_draft)?.map(Json))
        .bind(serialize_option_json_value(&snapshot.config_export)?.map(Json))
        .bind(snapshot.id.as_str())
        .bind(to_db_version(snapshot.version))
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(())
    }

    pub async fn archive_operation(
        &self,
        workspace_id: &WorkspaceId,
        operation_id: &OperationId,
        archived_at: &str,
    ) -> Result<(), RegistryError> {
        let Some(summary) = self
            .get_operation_summary(workspace_id, operation_id)
            .await?
        else {
            return Err(RegistryError::OperationNotFound {
                operation_id: operation_id.as_str().to_owned(),
            });
        };

        let mut tx = self.pool.begin().await?;

        sqlx::query(
            "update operations
                set status = $1,
                    updated_at = $2::timestamptz
              where workspace_id = $3 and id = $4",
        )
        .bind(serialize_enum_text(&OperationStatus::Archived, "status")?)
        .bind(archived_at)
        .bind(workspace_id.as_str())
        .bind(operation_id.as_str())
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            "update operation_versions
                set status = $1
              where operation_id = $2 and version = $3",
        )
        .bind(serialize_enum_text(&OperationStatus::Archived, "status")?)
        .bind(operation_id.as_str())
        .bind(to_db_version(summary.current_draft_version))
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(())
    }

    pub async fn delete_operation(
        &self,
        workspace_id: &WorkspaceId,
        operation_id: &OperationId,
    ) -> Result<(), RegistryError> {
        if self
            .has_published_agent_bindings_for_operation(workspace_id, operation_id)
            .await?
        {
            return Err(RegistryError::OperationHasPublishedAgentBindings {
                operation_id: operation_id.as_str().to_owned(),
            });
        }

        let deleted = sqlx::query(
            "delete from operations
              where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(operation_id.as_str())
        .execute(&self.pool)
        .await?
        .rows_affected();

        if deleted == 0 {
            return Err(RegistryError::OperationNotFound {
                operation_id: operation_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn has_published_agent_bindings_for_operation(
        &self,
        workspace_id: &WorkspaceId,
        operation_id: &OperationId,
    ) -> Result<bool, RegistryError> {
        let row = sqlx::query(
            "select 1
             from agents a
             join published_agents pa on pa.agent_id = a.id
             join agent_operation_bindings b
               on b.agent_id = a.id and b.agent_version = pa.version
             where a.workspace_id = $1
               and b.operation_id = $2
             limit 1",
        )
        .bind(workspace_id.as_str())
        .bind(operation_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        Ok(row.is_some())
    }

    pub async fn list_operations(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<OperationSummary>, RegistryError> {
        let rows = sqlx::query(
            "select
                operations.id,
                operations.workspace_id,
                operations.name,
                operations.display_name,
                operations.category,
                operations.protocol,
                ov.target_json,
                operations.status,
                operations.current_draft_version,
                operations.latest_published_version,
                to_char(operations.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(operations.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(operations.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
             from operations
             join operation_versions ov
               on ov.operation_id = operations.id
              and ov.version = operations.current_draft_version
             where operations.workspace_id = $1
             order by operations.name asc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_operation_summary).collect()
    }

    pub async fn list_operation_usage_summaries(
        &self,
        workspace_id: &WorkspaceId,
        created_after: &str,
    ) -> Result<Vec<OperationUsageSummary>, RegistryError> {
        let rows = sqlx::query(
            "select
                operation_id,
                count(*)::bigint as calls_today,
                coalesce(
                    round((sum(case when status = 'error' then 1 else 0 end)::numeric / nullif(count(*), 0)) * 100, 2),
                    0
                )::float8 as error_rate_pct,
                coalesce(round(avg(duration_ms))::bigint, 0) as avg_latency_ms
             from invocation_logs
             where workspace_id = $1
               and created_at >= $2::timestamptz
             group by operation_id",
        )
        .bind(workspace_id.as_str())
        .bind(created_after)
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_operation_usage_summary).collect()
    }

    pub async fn list_operation_agent_refs(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<OperationAgentRef>, RegistryError> {
        let rows = sqlx::query(
            "select
                b.operation_id,
                a.id as agent_id,
                a.slug as agent_slug,
                a.display_name
             from agents a
             join published_agents pa on pa.agent_id = a.id
             join agent_operation_bindings b
               on b.agent_id = a.id and b.agent_version = pa.version
             where a.workspace_id = $1
             order by a.display_name asc, b.tool_name asc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_operation_agent_ref).collect()
    }

    pub async fn get_operation_summary(
        &self,
        workspace_id: &WorkspaceId,
        operation_id: &OperationId,
    ) -> Result<Option<OperationSummary>, RegistryError> {
        let row = sqlx::query(
            "select
                operations.id,
                operations.workspace_id,
                operations.name,
                operations.display_name,
                operations.category,
                operations.protocol,
                ov.target_json,
                operations.status,
                operations.current_draft_version,
                operations.latest_published_version,
                to_char(operations.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(operations.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
                to_char(operations.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
             from operations
             join operation_versions ov
               on ov.operation_id = operations.id
              and ov.version = operations.current_draft_version
             where operations.workspace_id = $1 and operations.id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(operation_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_operation_summary).transpose()
    }

    pub async fn get_operation_version(
        &self,
        workspace_id: &WorkspaceId,
        operation_id: &OperationId,
        version: u32,
    ) -> Result<Option<OperationVersionRecord>, RegistryError> {
        let row = sqlx::query(
            "select
                o.id,
                o.workspace_id,
                o.name,
                o.display_name,
                o.category,
                o.protocol,
                to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at,
                to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at,
                to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at,
                ov.version,
                ov.status,
                ov.target_json,
                ov.input_schema_json,
                ov.output_schema_json,
                ov.input_mapping_json,
                ov.output_mapping_json,
                ov.execution_config_json,
                ov.tool_description_json,
                ov.samples_json,
                ov.generated_draft_json,
                ov.config_export_json,
                ov.change_note,
                to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                ov.created_by
             from operation_versions ov
             join operations o on o.id = ov.operation_id
             where o.workspace_id = $1 and ov.operation_id = $2 and ov.version = $3",
        )
        .bind(workspace_id.as_str())
        .bind(operation_id.as_str())
        .bind(to_db_version(version))
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_operation_version_record).transpose()
    }

    pub async fn list_operation_versions(
        &self,
        workspace_id: &WorkspaceId,
        operation_id: &OperationId,
    ) -> Result<Vec<OperationVersionRecord>, RegistryError> {
        let rows = sqlx::query(
            "select
                o.id,
                o.workspace_id,
                o.name,
                o.display_name,
                o.category,
                o.protocol,
                to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at,
                to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at,
                to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at,
                ov.version,
                ov.status,
                ov.target_json,
                ov.input_schema_json,
                ov.output_schema_json,
                ov.input_mapping_json,
                ov.output_mapping_json,
                ov.execution_config_json,
                ov.tool_description_json,
                ov.samples_json,
                ov.generated_draft_json,
                ov.config_export_json,
                ov.change_note,
                to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                ov.created_by
             from operation_versions ov
             join operations o on o.id = ov.operation_id
             where o.workspace_id = $1 and ov.operation_id = $2
             order by ov.version asc",
        )
        .bind(workspace_id.as_str())
        .bind(operation_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_operation_version_record).collect()
    }

    pub async fn publish_operation(
        &self,
        request: PublishRequest<'_>,
    ) -> Result<(), RegistryError> {
        if self
            .get_operation_version(request.workspace_id, request.operation_id, request.version)
            .await?
            .is_none()
        {
            return Err(RegistryError::OperationVersionNotFound {
                operation_id: request.operation_id.as_str().to_owned(),
                version: request.version,
            });
        }

        let mut tx = self.pool.begin().await?;

        sqlx::query(
            "insert into published_operations (
                operation_id,
                version,
                published_at,
                published_by
             ) values ($1, $2, $3::timestamptz, $4)
             on conflict(operation_id) do update set
                version = excluded.version,
                published_at = excluded.published_at,
                published_by = excluded.published_by",
        )
        .bind(request.operation_id.as_str())
        .bind(to_db_version(request.version))
        .bind(request.published_at)
        .bind(request.published_by)
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            "update operation_versions
                set status = $1
              where operation_id = $2 and version = $3",
        )
        .bind(serialize_enum_text(&OperationStatus::Published, "status")?)
        .bind(request.operation_id.as_str())
        .bind(to_db_version(request.version))
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            "update operations
                set status = $1,
                    latest_published_version = $2,
                    published_at = $3::timestamptz,
                    updated_at = $4::timestamptz
              where id = $5",
        )
        .bind(serialize_enum_text(&OperationStatus::Published, "status")?)
        .bind(to_db_version(request.version))
        .bind(request.published_at)
        .bind(request.published_at)
        .bind(request.operation_id.as_str())
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(())
    }

    pub async fn get_published_operation(
        &self,
        operation_id: &OperationId,
    ) -> Result<Option<RegistryOperation>, RegistryError> {
        let row = sqlx::query(
            "select
                o.id,
                o.workspace_id,
                o.name,
                o.display_name,
                o.category,
                o.protocol,
                to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at,
                to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at,
                to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at,
                ov.version,
                ov.status,
                ov.target_json,
                ov.input_schema_json,
                ov.output_schema_json,
                ov.input_mapping_json,
                ov.output_mapping_json,
                ov.execution_config_json,
                ov.tool_description_json,
                ov.samples_json,
                ov.generated_draft_json,
                ov.config_export_json,
                ov.change_note,
                to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                ov.created_by
             from published_operations po
             join operation_versions ov
               on ov.operation_id = po.operation_id and ov.version = po.version
             join operations o on o.id = po.operation_id
             where po.operation_id = $1",
        )
        .bind(operation_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref()
            .map(|value| map_operation_version_record(value).map(|record| record.snapshot))
            .transpose()
    }

    pub async fn list_published_operations(&self) -> Result<Vec<RegistryOperation>, RegistryError> {
        let rows = sqlx::query(
            "select
                o.id,
                o.workspace_id,
                o.name,
                o.display_name,
                o.category,
                o.protocol,
                to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at,
                to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at,
                to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at,
                ov.version,
                ov.status,
                ov.target_json,
                ov.input_schema_json,
                ov.output_schema_json,
                ov.input_mapping_json,
                ov.output_mapping_json,
                ov.execution_config_json,
                ov.tool_description_json,
                ov.samples_json,
                ov.generated_draft_json,
                ov.config_export_json,
                ov.change_note,
                to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                ov.created_by
             from published_operations po
             join operation_versions ov
               on ov.operation_id = po.operation_id and ov.version = po.version
             join operations o on o.id = po.operation_id
             order by o.name asc",
        )
        .fetch_all(&self.pool)
        .await?;

        rows.iter()
            .map(|row| map_operation_version_record(row).map(|record| record.snapshot))
            .collect()
    }

    pub async fn save_auth_profile(
        &self,
        request: SaveAuthProfileRequest<'_>,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into auth_profiles (
                id,
                workspace_id,
                name,
                kind,
                config_json,
                created_at,
                updated_at
             ) values ($1, $2, $3, $4, $5, $6::timestamptz, $7::timestamptz)
             on conflict(id) do update set
                workspace_id = excluded.workspace_id,
                name = excluded.name,
                kind = excluded.kind,
                config_json = excluded.config_json,
                updated_at = excluded.updated_at",
        )
        .bind(request.profile.id.as_str())
        .bind(request.workspace_id.as_str())
        .bind(&request.profile.name)
        .bind(serialize_enum_text(&request.profile.kind, "kind")?)
        .bind(Json(serialize_json_value(&request.profile.config)?))
        .bind(&request.profile.created_at)
        .bind(&request.profile.updated_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn get_auth_profile(
        &self,
        workspace_id: &WorkspaceId,
        auth_profile_id: &crank_core::AuthProfileId,
    ) -> Result<Option<AuthProfile>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                workspace_id,
                name,
                kind,
                config_json,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
             from auth_profiles
             where workspace_id = $1 and id = $2",
        )
        .bind(workspace_id.as_str())
        .bind(auth_profile_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_auth_profile).transpose()
    }

    pub async fn list_auth_profiles(
        &self,
        workspace_id: &WorkspaceId,
    ) -> Result<Vec<AuthProfile>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                workspace_id,
                name,
                kind,
                config_json,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
             from auth_profiles
             where workspace_id = $1
             order by name asc",
        )
        .bind(workspace_id.as_str())
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_auth_profile).collect()
    }

    pub async fn list_auth_profiles_referencing_secret(
        &self,
        workspace_id: &WorkspaceId,
        secret_id: &SecretId,
    ) -> Result<Vec<AuthProfile>, RegistryError> {
        let profiles = self.list_auth_profiles(workspace_id).await?;

        Ok(profiles
            .into_iter()
            .filter(|profile| {
                profile
                    .config
                    .secret_ids()
                    .into_iter()
                    .any(|candidate| candidate == secret_id)
            })
            .collect())
    }

    pub async fn save_sample_metadata(
        &self,
        request: SaveSampleMetadataRequest<'_>,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into operation_samples (
                id,
                operation_id,
                version,
                sample_kind,
                storage_ref,
                content_type,
                file_name,
                created_at
             ) values ($1, $2, $3, $4, $5, $6, $7, $8::timestamptz)
             on conflict(id) do update set
                operation_id = excluded.operation_id,
                version = excluded.version,
                sample_kind = excluded.sample_kind,
                storage_ref = excluded.storage_ref,
                content_type = excluded.content_type,
                file_name = excluded.file_name,
                created_at = excluded.created_at",
        )
        .bind(request.sample.id.as_str())
        .bind(request.sample.operation_id.as_str())
        .bind(to_db_version(request.sample.version))
        .bind(serialize_enum_text(
            &request.sample.sample_kind,
            "sample_kind",
        )?)
        .bind(&request.sample.storage_ref)
        .bind(&request.sample.content_type)
        .bind(request.sample.file_name.as_deref())
        .bind(&request.sample.created_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn list_sample_metadata(
        &self,
        operation_id: &OperationId,
        version: u32,
    ) -> Result<Vec<OperationSampleMetadata>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                operation_id,
                version,
                sample_kind,
                storage_ref,
                content_type,
                file_name,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
             from operation_samples
             where operation_id = $1 and version = $2
             order by created_at asc",
        )
        .bind(operation_id.as_str())
        .bind(to_db_version(version))
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_sample_metadata).collect()
    }

    pub async fn save_descriptor_metadata(
        &self,
        request: SaveDescriptorMetadataRequest<'_>,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into descriptors (
                id,
                operation_id,
                version,
                descriptor_kind,
                storage_ref,
                source_name,
                package_index_json,
                created_at
             ) values ($1, $2, $3, $4, $5, $6, $7, $8::timestamptz)
             on conflict(id) do update set
                operation_id = excluded.operation_id,
                version = excluded.version,
                descriptor_kind = excluded.descriptor_kind,
                storage_ref = excluded.storage_ref,
                source_name = excluded.source_name,
                package_index_json = excluded.package_index_json,
                created_at = excluded.created_at",
        )
        .bind(request.descriptor.id.as_str())
        .bind(
            request
                .descriptor
                .operation_id
                .as_ref()
                .map(|value| value.as_str()),
        )
        .bind(request.descriptor.version.map(to_db_version))
        .bind(serialize_enum_text(
            &request.descriptor.descriptor_kind,
            "descriptor_kind",
        )?)
        .bind(&request.descriptor.storage_ref)
        .bind(request.descriptor.source_name.as_deref())
        .bind(serialize_option_json_value(&request.descriptor.package_index)?.map(Json))
        .bind(&request.descriptor.created_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn list_descriptor_metadata(
        &self,
        operation_id: &OperationId,
        version: u32,
    ) -> Result<Vec<DescriptorMetadata>, RegistryError> {
        let rows = sqlx::query(
            "select
                id,
                operation_id,
                version,
                descriptor_kind,
                storage_ref,
                source_name,
                package_index_json,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
             from descriptors
             where operation_id = $1 and version = $2
             order by created_at asc",
        )
        .bind(operation_id.as_str())
        .bind(to_db_version(version))
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_descriptor_metadata).collect()
    }

    pub async fn create_yaml_import_job(
        &self,
        request: CreateYamlImportJobRequest<'_>,
    ) -> Result<(), RegistryError> {
        sqlx::query(
            "insert into yaml_import_jobs (
                id,
                source_sample_id,
                status,
                format_version,
                mode,
                result_operation_id,
                result_version,
                error_text,
                created_at,
                finished_at
             ) values ($1, $2, $3, $4, $5, null, null, null, $6::timestamptz, null)",
        )
        .bind(request.id.as_str())
        .bind(request.source_sample_id.map(|value| value.as_str()))
        .bind(serialize_enum_text(
            &YamlImportJobStatus::Pending,
            "status",
        )?)
        .bind(request.format_version)
        .bind(serialize_enum_text(&request.mode, "mode")?)
        .bind(request.created_at)
        .execute(&self.pool)
        .await?;

        Ok(())
    }

    pub async fn finish_yaml_import_job(
        &self,
        job_id: &YamlImportJobId,
        completion: &YamlImportJobCompletion,
    ) -> Result<(), RegistryError> {
        let result = sqlx::query(
            "update yaml_import_jobs
                set status = $1,
                    result_operation_id = $2,
                    result_version = $3,
                    error_text = $4,
                    finished_at = $5::timestamptz
              where id = $6",
        )
        .bind(serialize_enum_text(&completion.status, "status")?)
        .bind(
            completion
                .result_operation_id
                .as_ref()
                .map(|value| value.as_str()),
        )
        .bind(completion.result_version.map(to_db_version))
        .bind(completion.error_text.as_deref())
        .bind(&completion.finished_at)
        .bind(job_id.as_str())
        .execute(&self.pool)
        .await?;

        if result.rows_affected() == 0 {
            return Err(RegistryError::YamlImportJobNotFound {
                job_id: job_id.as_str().to_owned(),
            });
        }

        Ok(())
    }

    pub async fn get_yaml_import_job(
        &self,
        job_id: &YamlImportJobId,
    ) -> Result<Option<YamlImportJob>, RegistryError> {
        let row = sqlx::query(
            "select
                id,
                source_sample_id,
                status,
                format_version,
                mode,
                result_operation_id,
                result_version,
                error_text,
                to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
                to_char(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at
             from yaml_import_jobs
             where id = $1",
        )
        .bind(job_id.as_str())
        .fetch_optional(&self.pool)
        .await?;

        row.as_ref().map(map_yaml_import_job).transpose()
    }

    async fn list_agent_bindings(
        &self,
        agent_id: &AgentId,
        version: u32,
    ) -> Result<Vec<AgentOperationBinding>, RegistryError> {
        let rows = sqlx::query(
            "select
                operation_id,
                operation_version,
                tool_name,
                tool_title,
                tool_description_override,
                enabled
             from agent_operation_bindings
             where agent_id = $1 and agent_version = $2
             order by tool_name asc",
        )
        .bind(agent_id.as_str())
        .bind(to_db_version(version))
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_agent_binding).collect()
    }
}

async fn insert_version_row(
    tx: &mut Transaction<'_, Postgres>,
    snapshot: &RegistryOperation,
    change_note: Option<&str>,
    created_by: Option<&str>,
) -> Result<(), RegistryError> {
    sqlx::query(
        "insert into operation_versions (
            operation_id,
            version,
            status,
            target_json,
            input_schema_json,
            output_schema_json,
            input_mapping_json,
            output_mapping_json,
            execution_config_json,
            tool_description_json,
            samples_json,
            generated_draft_json,
            config_export_json,
            change_note,
            created_at,
            created_by
         ) values (
            $1, $2, $3, $4, $5, $6, $7, $8,
            $9, $10, $11, $12, $13, $14, $15::timestamptz, $16
         )",
    )
    .bind(snapshot.id.as_str())
    .bind(to_db_version(snapshot.version))
    .bind(serialize_enum_text(&snapshot.status, "status")?)
    .bind(Json(serialize_json_value(&snapshot.target)?))
    .bind(Json(serialize_json_value(&snapshot.input_schema)?))
    .bind(Json(serialize_json_value(&snapshot.output_schema)?))
    .bind(Json(serialize_json_value(&snapshot.input_mapping)?))
    .bind(Json(serialize_json_value(&snapshot.output_mapping)?))
    .bind(Json(serialize_json_value(&snapshot.execution_config)?))
    .bind(Json(serialize_json_value(&snapshot.tool_description)?))
    .bind(serialize_option_json_value(&snapshot.samples)?.map(Json))
    .bind(serialize_option_json_value(&snapshot.generated_draft)?.map(Json))
    .bind(serialize_option_json_value(&snapshot.config_export)?.map(Json))
    .bind(change_note)
    .bind(&snapshot.updated_at)
    .bind(created_by)
    .execute(&mut **tx)
    .await?;

    Ok(())
}

async fn insert_agent_version_row(
    tx: &mut Transaction<'_, Postgres>,
    version: &AgentVersion,
) -> Result<(), RegistryError> {
    sqlx::query(
        "insert into agent_versions (
            agent_id,
            version,
            status,
            instructions_json,
            tool_selection_policy_json,
            created_at
         ) values (
            $1, $2, $3, $4, $5, $6::timestamptz
         )",
    )
    .bind(version.agent_id.as_str())
    .bind(to_db_version(version.version))
    .bind(serialize_enum_text(&version.status, "status")?)
    .bind(Json(version.instructions.clone()))
    .bind(Json(version.tool_selection_policy.clone()))
    .bind(&version.created_at)
    .execute(&mut **tx)
    .await?;

    Ok(())
}

async fn replace_agent_bindings_rows(
    tx: &mut Transaction<'_, Postgres>,
    agent_id: &AgentId,
    version: u32,
    bindings: &[AgentOperationBinding],
) -> Result<(), RegistryError> {
    sqlx::query(
        "delete from agent_operation_bindings
         where agent_id = $1 and agent_version = $2",
    )
    .bind(agent_id.as_str())
    .bind(to_db_version(version))
    .execute(&mut **tx)
    .await?;

    for binding in bindings {
        sqlx::query(
            "insert into agent_operation_bindings (
                agent_id,
                agent_version,
                operation_id,
                operation_version,
                tool_name,
                tool_title,
                tool_description_override,
                enabled
             ) values ($1, $2, $3, $4, $5, $6, $7, $8)",
        )
        .bind(agent_id.as_str())
        .bind(to_db_version(version))
        .bind(binding.operation_id.as_str())
        .bind(to_db_version(binding.operation_version))
        .bind(&binding.tool_name)
        .bind(&binding.tool_title)
        .bind(binding.tool_description_override.as_deref())
        .bind(binding.enabled)
        .execute(&mut **tx)
        .await?;
    }

    Ok(())
}

fn assert_immutable_fields(
    summary: &OperationSummary,
    snapshot: &RegistryOperation,
) -> Result<(), RegistryError> {
    if summary.name != snapshot.name {
        return Err(RegistryError::ImmutableOperationFieldChanged {
            operation_id: snapshot.id.as_str().to_owned(),
            field: "name",
        });
    }

    if summary.protocol != snapshot.protocol {
        return Err(RegistryError::ImmutableOperationFieldChanged {
            operation_id: snapshot.id.as_str().to_owned(),
            field: "protocol",
        });
    }

    Ok(())
}

fn map_workspace_record(row: &PgRow) -> Result<WorkspaceRecord, RegistryError> {
    Ok(WorkspaceRecord {
        workspace: Workspace {
            id: WorkspaceId::new(row.try_get::<String, _>("id")?),
            slug: row.try_get("slug")?,
            display_name: row.try_get("display_name")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            settings: row.try_get::<Json<Value>, _>("settings_json")?.0,
            created_at: row.try_get("created_at")?,
            updated_at: row.try_get("updated_at")?,
        },
    })
}

fn map_workspace_membership_record(
    row: &PgRow,
) -> Result<WorkspaceMembershipRecord, RegistryError> {
    Ok(WorkspaceMembershipRecord {
        workspace: Workspace {
            id: WorkspaceId::new(row.try_get::<String, _>("id")?),
            slug: row.try_get("slug")?,
            display_name: row.try_get("display_name")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            settings: row.try_get::<Json<Value>, _>("settings_json")?.0,
            created_at: row.try_get("created_at")?,
            updated_at: row.try_get("updated_at")?,
        },
        role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
    })
}

fn map_membership_record(row: &PgRow) -> Result<MembershipRecord, RegistryError> {
    Ok(MembershipRecord {
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        user: User {
            id: UserId::new(row.try_get::<String, _>("user_id")?),
            email: row.try_get("email")?,
            display_name: row.try_get("display_name")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            created_at: row.try_get("user_created_at")?,
        },
        role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
        created_at: row.try_get("created_at")?,
    })
}

fn map_auth_user_record(row: &PgRow) -> Result<AuthUserRecord, RegistryError> {
    Ok(AuthUserRecord {
        user: map_user(row)?,
        password_hash: row.try_get("password_hash")?,
    })
}

fn map_user(row: &PgRow) -> Result<User, RegistryError> {
    Ok(User {
        id: UserId::new(row.try_get::<String, _>("id")?),
        email: row.try_get("email")?,
        display_name: row.try_get("display_name")?,
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        created_at: row.try_get("created_at")?,
    })
}

fn map_user_update_error(error: sqlx::Error, user_id: &UserId, email: &str) -> RegistryError {
    match error {
        sqlx::Error::RowNotFound => RegistryError::UserNotFound {
            user_id: user_id.as_str().to_owned(),
        },
        sqlx::Error::Database(database_error)
            if database_error.code().as_deref() == Some("23505") =>
        {
            RegistryError::UserEmailAlreadyExists {
                email: email.to_owned(),
            }
        }
        other => RegistryError::Storage(other),
    }
}

fn map_invitation_record(row: &PgRow) -> Result<InvitationRecord, RegistryError> {
    Ok(InvitationRecord {
        invitation: InvitationToken {
            id: InvitationId::new(row.try_get::<String, _>("id")?),
            workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
            email: row.try_get("email")?,
            role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            token_hash: row.try_get("token_hash")?,
            expires_at: row.try_get("expires_at")?,
            created_at: row.try_get("created_at")?,
        },
    })
}

fn map_platform_api_key_record(row: &PgRow) -> Result<PlatformApiKeyRecord, RegistryError> {
    Ok(PlatformApiKeyRecord {
        api_key: PlatformApiKey {
            id: PlatformApiKeyId::new(row.try_get::<String, _>("id")?),
            workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
            name: row.try_get("name")?,
            prefix: row.try_get("prefix")?,
            scopes: deserialize_json_value(row.try_get::<Json<Value>, _>("scopes_json")?.0)?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            created_at: row.try_get("created_at")?,
            last_used_at: row.try_get("last_used_at")?,
        },
    })
}

fn map_secret_record(row: &PgRow) -> Result<SecretRecord, RegistryError> {
    Ok(SecretRecord {
        secret: Secret {
            id: SecretId::new(row.try_get::<String, _>("id")?),
            workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
            name: row.try_get("name")?,
            kind: deserialize_enum_text(&row.try_get::<String, _>("kind")?, "kind")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            current_version: from_db_version(
                row.try_get::<i32, _>("current_version")?,
                "current_version",
            )?,
            created_at: row.try_get("created_at")?,
            updated_at: row.try_get("updated_at")?,
            last_used_at: row.try_get("last_used_at")?,
        },
    })
}

fn map_secret_version_record(row: &PgRow) -> Result<SecretVersionRecord, RegistryError> {
    Ok(SecretVersionRecord {
        secret_version: SecretVersion {
            secret_id: SecretId::new(row.try_get::<String, _>("secret_id")?),
            version: from_db_version(row.try_get::<i32, _>("version")?, "version")?,
            ciphertext: row.try_get("ciphertext")?,
            key_version: row.try_get("key_version")?,
            created_at: row.try_get("created_at")?,
            created_by: row
                .try_get::<Option<String>, _>("created_by")?
                .map(UserId::new),
        },
    })
}

fn map_stream_session(row: &PgRow) -> Result<StreamSession, RegistryError> {
    Ok(StreamSession {
        id: StreamSessionId::new(row.try_get::<String, _>("id")?),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        agent_id: row
            .try_get::<Option<String>, _>("agent_id")?
            .map(AgentId::new),
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
        mode: deserialize_enum_text(&row.try_get::<String, _>("mode")?, "mode")?,
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        cursor: row
            .try_get::<Option<Json<Value>>, _>("cursor_json")?
            .map(|value| value.0),
        state: row.try_get::<Json<Value>, _>("state_json")?.0,
        expires_at: row.try_get("expires_at")?,
        last_poll_at: row.try_get("last_poll_at")?,
        created_at: row.try_get("created_at")?,
        closed_at: row.try_get("closed_at")?,
    })
}

fn map_async_job(row: &PgRow) -> Result<AsyncJobHandle, RegistryError> {
    Ok(AsyncJobHandle {
        id: AsyncJobId::new(row.try_get::<String, _>("id")?),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        agent_id: row
            .try_get::<Option<String>, _>("agent_id")?
            .map(AgentId::new),
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        progress: row.try_get::<Json<Value>, _>("progress_json")?.0,
        result: row
            .try_get::<Option<Json<Value>>, _>("result_json")?
            .map(|value| value.0),
        error: row
            .try_get::<Option<Json<Value>>, _>("error_json")?
            .map(|value| value.0),
        expires_at: row.try_get("expires_at")?,
        created_at: row.try_get("created_at")?,
        updated_at: row.try_get("updated_at")?,
        finished_at: row.try_get("finished_at")?,
    })
}

fn map_invocation_log_record(row: &PgRow) -> Result<InvocationLogRecord, RegistryError> {
    Ok(InvocationLogRecord {
        log: InvocationLog {
            id: InvocationLogId::new(row.try_get::<String, _>("id")?),
            workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
            agent_id: row
                .try_get::<Option<String>, _>("agent_id")?
                .map(AgentId::new),
            operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
            source: deserialize_enum_text(&row.try_get::<String, _>("source")?, "source")?,
            level: deserialize_enum_text(&row.try_get::<String, _>("level")?, "level")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            tool_name: row.try_get("tool_name")?,
            message: row.try_get("message")?,
            request_id: row.try_get("request_id")?,
            status_code: match row.try_get::<Option<i32>, _>("status_code")? {
                Some(value) => {
                    Some(
                        u16::try_from(value).map_err(|_| RegistryError::InvalidNumericValue {
                            field: "status_code",
                            value: i64::from(value),
                        })?,
                    )
                }
                None => None,
            },
            duration_ms: to_u64(row.try_get::<i64, _>("duration_ms")?, "duration_ms")?,
            error_kind: row.try_get("error_kind")?,
            request_preview: row.try_get::<Json<Value>, _>("request_preview_json")?.0,
            response_preview: row.try_get::<Json<Value>, _>("response_preview_json")?.0,
            created_at: row.try_get("created_at")?,
        },
        operation_name: row.try_get("operation_name")?,
        operation_display_name: row.try_get("operation_display_name")?,
        agent_slug: row.try_get("agent_slug")?,
        agent_display_name: row.try_get("agent_display_name")?,
    })
}

fn map_agent_summary(row: &PgRow) -> Result<AgentSummary, RegistryError> {
    Ok(AgentSummary {
        id: AgentId::new(row.try_get::<String, _>("id")?),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        slug: row.try_get("slug")?,
        display_name: row.try_get("display_name")?,
        description: row.try_get("description")?,
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        current_draft_version: from_db_version(
            row.try_get("current_draft_version")?,
            "current_draft_version",
        )?,
        latest_published_version: row
            .try_get::<Option<i32>, _>("latest_published_version")?
            .map(|value| from_db_version(value, "latest_published_version"))
            .transpose()?,
        created_at: row.try_get("created_at")?,
        updated_at: row.try_get("updated_at")?,
        published_at: row.try_get("published_at")?,
    })
}

fn map_operation_summary(row: &PgRow) -> Result<OperationSummary, RegistryError> {
    let target: Target = deserialize_json_value(row.try_get::<Json<Value>, _>("target_json")?.0)?;
    let (target_url, target_action) = target_summary(&target);

    Ok(OperationSummary {
        id: OperationId::new(row.try_get::<String, _>("id")?),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        name: row.try_get("name")?,
        display_name: row.try_get("display_name")?,
        category: row.try_get("category")?,
        protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
        target_url,
        target_action,
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        current_draft_version: from_db_version(
            row.try_get("current_draft_version")?,
            "current_draft_version",
        )?,
        latest_published_version: row
            .try_get::<Option<i32>, _>("latest_published_version")?
            .map(|value| from_db_version(value, "latest_published_version"))
            .transpose()?,
        created_at: row.try_get("created_at")?,
        updated_at: row.try_get("updated_at")?,
        published_at: row.try_get("published_at")?,
    })
}

fn map_operation_usage_summary(row: &PgRow) -> Result<OperationUsageSummary, RegistryError> {
    Ok(OperationUsageSummary {
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        calls_today: to_u64(row.try_get::<i64, _>("calls_today")?, "calls_today")?,
        error_rate_pct: row.try_get("error_rate_pct")?,
        avg_latency_ms: to_u64(row.try_get::<i64, _>("avg_latency_ms")?, "avg_latency_ms")?,
    })
}

fn target_summary(target: &Target) -> (String, String) {
    match target {
        Target::Rest(rest) => (
            join_target_url(&rest.base_url, &rest.path_template),
            match rest.method {
                HttpMethod::Get => "GET",
                HttpMethod::Post => "POST",
                HttpMethod::Put => "PUT",
                HttpMethod::Patch => "PATCH",
                HttpMethod::Delete => "DELETE",
            }
            .to_owned(),
        ),
        Target::Graphql(graphql) => (
            graphql.endpoint.clone(),
            match graphql.operation_type {
                GraphqlOperationType::Query => "QUERY",
                GraphqlOperationType::Mutation => "MUTATION",
            }
            .to_owned(),
        ),
        Target::Grpc(grpc) => (grpc.server_addr.clone(), grpc.method.clone()),
        Target::Websocket(websocket) => (websocket.url.clone(), "SUBSCRIBE".to_owned()),
        Target::Soap(soap) => (
            soap.endpoint_override
                .clone()
                .unwrap_or_else(|| format!("wsdl://{}", soap.service_name)),
            soap.operation_name.clone(),
        ),
    }
}

fn join_target_url(base: &str, path: &str) -> String {
    if path.is_empty() {
        return base.to_owned();
    }

    if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("grpc://") {
        return path.to_owned();
    }

    let trimmed_base = base.trim_end_matches('/');
    let trimmed_path = path.trim_start_matches('/');

    if trimmed_base.is_empty() {
        format!("/{trimmed_path}")
    } else if trimmed_path.is_empty() {
        trimmed_base.to_owned()
    } else {
        format!("{trimmed_base}/{trimmed_path}")
    }
}

fn map_operation_agent_ref(row: &PgRow) -> Result<OperationAgentRef, RegistryError> {
    Ok(OperationAgentRef {
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        agent_id: AgentId::new(row.try_get::<String, _>("agent_id")?),
        agent_slug: row.try_get("agent_slug")?,
        display_name: row.try_get("display_name")?,
    })
}

fn map_operation_version_record(row: &PgRow) -> Result<OperationVersionRecord, RegistryError> {
    let operation_id = OperationId::new(row.try_get::<String, _>("id")?);
    let version = from_db_version(row.try_get("version")?, "version")?;
    let status = deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?;

    Ok(OperationVersionRecord {
        operation_id: operation_id.clone(),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        version,
        status,
        change_note: row.try_get("change_note")?,
        created_at: row.try_get("created_at")?,
        created_by: row.try_get("created_by")?,
        snapshot: RegistryOperation {
            id: operation_id,
            name: row.try_get("name")?,
            display_name: row.try_get("display_name")?,
            category: row.try_get("category")?,
            protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
            status,
            version,
            target: deserialize_json_value(row.try_get::<Json<Value>, _>("target_json")?.0)?,
            input_schema: deserialize_json_value(
                row.try_get::<Json<Value>, _>("input_schema_json")?.0,
            )?,
            output_schema: deserialize_json_value(
                row.try_get::<Json<Value>, _>("output_schema_json")?.0,
            )?,
            input_mapping: deserialize_json_value(
                row.try_get::<Json<Value>, _>("input_mapping_json")?.0,
            )?,
            output_mapping: deserialize_json_value(
                row.try_get::<Json<Value>, _>("output_mapping_json")?.0,
            )?,
            execution_config: deserialize_json_value(
                row.try_get::<Json<Value>, _>("execution_config_json")?.0,
            )?,
            tool_description: deserialize_json_value(
                row.try_get::<Json<Value>, _>("tool_description_json")?.0,
            )?,
            samples: row
                .try_get::<Option<Json<Value>>, _>("samples_json")?
                .map(|value| deserialize_json_value(value.0))
                .transpose()?,
            generated_draft: row
                .try_get::<Option<Json<Value>>, _>("generated_draft_json")?
                .map(|value| deserialize_json_value(value.0))
                .transpose()?,
            config_export: row
                .try_get::<Option<Json<Value>>, _>("config_export_json")?
                .map(|value| deserialize_json_value(value.0))
                .transpose()?,
            created_at: row.try_get("operation_created_at")?,
            updated_at: row.try_get("operation_updated_at")?,
            published_at: row.try_get("operation_published_at")?,
        },
    })
}

fn map_auth_profile(row: &PgRow) -> Result<AuthProfile, RegistryError> {
    Ok(AuthProfile {
        id: crank_core::AuthProfileId::new(row.try_get::<String, _>("id")?),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        name: row.try_get("name")?,
        kind: deserialize_enum_text(&row.try_get::<String, _>("kind")?, "kind")?,
        config: deserialize_json_value(row.try_get::<Json<Value>, _>("config_json")?.0)?,
        created_at: row.try_get("created_at")?,
        updated_at: row.try_get("updated_at")?,
    })
}

fn map_agent_binding(row: &PgRow) -> Result<AgentOperationBinding, RegistryError> {
    Ok(AgentOperationBinding {
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        operation_version: from_db_version(row.try_get("operation_version")?, "operation_version")?,
        tool_name: row.try_get("tool_name")?,
        tool_title: row.try_get("tool_title")?,
        tool_description_override: row.try_get("tool_description_override")?,
        enabled: row.try_get("enabled")?,
    })
}

fn map_agent_version_record(
    summary: &AgentSummary,
    row: &PgRow,
    bindings: Vec<AgentOperationBinding>,
) -> Result<AgentVersionRecord, RegistryError> {
    let version = from_db_version(row.try_get("version")?, "version")?;
    let status = deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?;

    Ok(AgentVersionRecord {
        agent_id: summary.id.clone(),
        workspace_id: summary.workspace_id.clone(),
        version,
        status,
        created_at: row.try_get("created_at")?,
        bindings,
        snapshot: AgentVersion {
            agent_id: summary.id.clone(),
            version,
            status,
            instructions: row.try_get::<Json<Value>, _>("instructions_json")?.0,
            tool_selection_policy: row
                .try_get::<Json<Value>, _>("tool_selection_policy_json")?
                .0,
            created_at: row.try_get("created_at")?,
        },
    })
}

fn map_published_agent_tool(row: &PgRow) -> Result<PublishedAgentTool, RegistryError> {
    let record = map_operation_version_record(row)?;

    Ok(PublishedAgentTool {
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        workspace_slug: row.try_get("workspace_slug")?,
        agent_id: AgentId::new(row.try_get::<String, _>("agent_id")?),
        agent_slug: row.try_get("agent_slug")?,
        tool_name: row.try_get("tool_name")?,
        tool_title: row.try_get("tool_title")?,
        tool_description: row.try_get("tool_description")?,
        operation: record.snapshot,
    })
}

fn map_sample_metadata(row: &PgRow) -> Result<OperationSampleMetadata, RegistryError> {
    Ok(OperationSampleMetadata {
        id: crank_core::SampleId::new(row.try_get::<String, _>("id")?),
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        version: from_db_version(row.try_get("version")?, "version")?,
        sample_kind: deserialize_enum_text(
            &row.try_get::<String, _>("sample_kind")?,
            "sample_kind",
        )?,
        storage_ref: row.try_get("storage_ref")?,
        content_type: row.try_get("content_type")?,
        file_name: row.try_get("file_name")?,
        created_at: row.try_get("created_at")?,
    })
}

fn map_descriptor_metadata(row: &PgRow) -> Result<DescriptorMetadata, RegistryError> {
    Ok(DescriptorMetadata {
        id: crank_core::DescriptorId::new(row.try_get::<String, _>("id")?),
        operation_id: row
            .try_get::<Option<String>, _>("operation_id")?
            .map(OperationId::new),
        version: row
            .try_get::<Option<i32>, _>("version")?
            .map(|value| from_db_version(value, "version"))
            .transpose()?,
        descriptor_kind: deserialize_enum_text(
            &row.try_get::<String, _>("descriptor_kind")?,
            "descriptor_kind",
        )?,
        storage_ref: row.try_get("storage_ref")?,
        source_name: row.try_get("source_name")?,
        package_index: row
            .try_get::<Option<Json<Value>>, _>("package_index_json")?
            .map(|value| value.0),
        created_at: row.try_get("created_at")?,
    })
}

fn map_yaml_import_job(row: &PgRow) -> Result<YamlImportJob, RegistryError> {
    Ok(YamlImportJob {
        id: YamlImportJobId::new(row.try_get::<String, _>("id")?),
        source_sample_id: row
            .try_get::<Option<String>, _>("source_sample_id")?
            .map(crank_core::SampleId::new),
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        format_version: row.try_get("format_version")?,
        mode: deserialize_enum_text(&row.try_get::<String, _>("mode")?, "mode")?,
        result_operation_id: row
            .try_get::<Option<String>, _>("result_operation_id")?
            .map(OperationId::new),
        result_version: row
            .try_get::<Option<i32>, _>("result_version")?
            .map(|value| from_db_version(value, "result_version"))
            .transpose()?,
        error_text: row.try_get("error_text")?,
        created_at: row.try_get("created_at")?,
        finished_at: row.try_get("finished_at")?,
    })
}

fn map_usage_operation_breakdown(row: &PgRow) -> Result<UsageOperationBreakdown, RegistryError> {
    Ok(UsageOperationBreakdown {
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        operation_name: row.try_get("operation_name")?,
        operation_display_name: row.try_get("operation_display_name")?,
        protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
        calls_total: to_u64(row.try_get::<i64, _>("calls_total")?, "calls_total")?,
        calls_error: to_u64(row.try_get::<i64, _>("calls_error")?, "calls_error")?,
        p50_ms: to_u64(row.try_get::<i64, _>("p50_ms")?, "p50_ms")?,
        p95_ms: to_u64(row.try_get::<i64, _>("p95_ms")?, "p95_ms")?,
        p99_ms: to_u64(row.try_get::<i64, _>("p99_ms")?, "p99_ms")?,
    })
}

fn map_usage_agent_breakdown(row: &PgRow) -> Result<UsageAgentBreakdown, RegistryError> {
    Ok(UsageAgentBreakdown {
        agent_id: AgentId::new(row.try_get::<String, _>("agent_id")?),
        agent_slug: row.try_get("agent_slug")?,
        agent_display_name: row.try_get("agent_display_name")?,
        calls_total: to_u64(row.try_get::<i64, _>("calls_total")?, "calls_total")?,
        calls_error: to_u64(row.try_get::<i64, _>("calls_error")?, "calls_error")?,
        p50_ms: to_u64(row.try_get::<i64, _>("p50_ms")?, "p50_ms")?,
        p95_ms: to_u64(row.try_get::<i64, _>("p95_ms")?, "p95_ms")?,
        p99_ms: to_u64(row.try_get::<i64, _>("p99_ms")?, "p99_ms")?,
    })
}

fn serialize_json_value<T: Serialize>(value: &T) -> Result<Value, RegistryError> {
    Ok(serde_json::to_value(value)?)
}

fn serialize_option_json_value<T: Serialize>(
    value: &Option<T>,
) -> Result<Option<Value>, RegistryError> {
    value.as_ref().map(serialize_json_value).transpose()
}

fn deserialize_json_value<T: DeserializeOwned>(value: Value) -> Result<T, RegistryError> {
    Ok(serde_json::from_value(value)?)
}

fn serialize_enum_text<T: Serialize>(
    value: &T,
    field: &'static str,
) -> Result<String, RegistryError> {
    serde_json::to_value(value)?
        .as_str()
        .map(ToOwned::to_owned)
        .ok_or(RegistryError::InvalidEnumRepresentation { field })
}

fn deserialize_enum_text<T: DeserializeOwned>(
    value: &str,
    field: &'static str,
) -> Result<T, RegistryError> {
    serde_json::from_value(Value::String(value.to_owned()))
        .map_err(|_| RegistryError::InvalidEnumRepresentation { field })
}

fn to_db_version(value: u32) -> i32 {
    value as i32
}

fn from_db_version(value: i32, field: &'static str) -> Result<u32, RegistryError> {
    u32::try_from(value).map_err(|_| RegistryError::InvalidNumericValue {
        field,
        value: i64::from(value),
    })
}

fn validate_stream_session_transition(
    session_id: &StreamSessionId,
    from: StreamStatus,
    to: StreamStatus,
) -> Result<(), RegistryError> {
    let allowed = matches!(
        (from, to),
        (StreamStatus::Created, StreamStatus::Running)
            | (StreamStatus::Running, StreamStatus::Running)
            | (StreamStatus::Running, StreamStatus::Stopped)
            | (StreamStatus::Running, StreamStatus::Expired)
            | (StreamStatus::Running, StreamStatus::Failed)
    );

    if allowed {
        Ok(())
    } else {
        Err(RegistryError::InvalidStreamSessionTransition {
            session_id: session_id.as_str().to_owned(),
            from: serialize_enum_text(&from, "status").unwrap_or_else(|_| "unknown".to_owned()),
            to: serialize_enum_text(&to, "status").unwrap_or_else(|_| "unknown".to_owned()),
        })
    }
}

fn validate_async_job_transition(
    job_id: &AsyncJobId,
    from: JobStatus,
    to: JobStatus,
) -> Result<(), RegistryError> {
    let allowed = matches!(
        (from, to),
        (JobStatus::Created, JobStatus::Running)
            | (JobStatus::Running, JobStatus::Running)
            | (JobStatus::Running, JobStatus::Completed)
            | (JobStatus::Running, JobStatus::Failed)
            | (JobStatus::Running, JobStatus::Cancelled)
            | (JobStatus::Completed, JobStatus::Expired)
            | (JobStatus::Failed, JobStatus::Expired)
            | (JobStatus::Cancelled, JobStatus::Expired)
    );

    if allowed {
        Ok(())
    } else {
        Err(RegistryError::InvalidAsyncJobTransition {
            job_id: job_id.as_str().to_owned(),
            from: serialize_enum_text(&from, "status").unwrap_or_else(|_| "unknown".to_owned()),
            to: serialize_enum_text(&to, "status").unwrap_or_else(|_| "unknown".to_owned()),
        })
    }
}

fn to_u64(value: i64, field: &'static str) -> Result<u64, RegistryError> {
    u64::try_from(value).map_err(|_| RegistryError::InvalidNumericValue { field, value })
}

fn usage_bucket_sql(bucket: crate::model::UsageBucket) -> &'static str {
    match bucket {
        crate::model::UsageBucket::Hour => "hour",
        crate::model::UsageBucket::Day => "day",
        crate::model::UsageBucket::Week => "week",
        crate::model::UsageBucket::Month => "month",
    }
}

#[cfg(test)]
mod tests {
    use std::{
        collections::BTreeMap,
        env,
        time::{SystemTime, UNIX_EPOCH},
    };

    use crank_core::{
        ApiKeyHeaderAuthConfig, AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, ConfigExport,
        ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, JobStatus,
        OperationId, OperationStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId,
        StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription, ToolExample,
        WorkspaceId,
    };
    use crank_mapping::{MappingRule, MappingSet};
    use crank_schema::{Schema, SchemaKind};
    use serde_json::json;
    use sqlx::{Executor, PgPool, postgres::PgPoolOptions};

    use crate::{
        PostgresRegistry, RegistryError,
        model::{
            AsyncJobFilter, CreateAsyncJobRequest, CreateStreamSessionRequest,
            CreateVersionRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
            OperationSampleMetadata, PublishRequest, RegistryOperation, SampleKind,
            SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
            StreamSessionFilter, UpdateAsyncJobStatusRequest, UpdateStreamSessionStateRequest,
            YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
        },
    };

    fn test_workspace_id() -> WorkspaceId {
        WorkspaceId::new("ws_default")
    }

    #[tokio::test]
    async fn stores_versions_and_published_operations() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation_v1 = test_operation("op_rest_01", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation_v1, Some("alice"))
            .await
            .unwrap();

        let operation_v2 = test_operation("op_rest_01", 2, OperationStatus::Draft);

        registry
            .create_version(CreateVersionRequest {
                workspace_id: &test_workspace_id(),
                snapshot: &operation_v2,
                change_note: Some("add output mapping"),
                created_by: Some("alice"),
            })
            .await
            .unwrap();

        registry
            .publish_operation(PublishRequest {
                workspace_id: &test_workspace_id(),
                operation_id: &operation_v2.id,
                version: operation_v2.version,
                published_at: "2026-03-25T12:10:00Z",
                published_by: Some("alice"),
            })
            .await
            .unwrap();

        let summary = registry
            .get_operation_summary(&test_workspace_id(), &operation_v2.id)
            .await
            .unwrap()
            .unwrap();
        let versions = registry
            .list_operation_versions(&test_workspace_id(), &operation_v2.id)
            .await
            .unwrap();
        let published = registry
            .get_published_operation(&operation_v2.id)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(summary.current_draft_version, 2);
        assert_eq!(summary.latest_published_version, Some(2));
        assert_eq!(summary.status, OperationStatus::Published);
        assert_eq!(versions.len(), 2);
        assert_eq!(
            versions[1].change_note.as_deref(),
            Some("add output mapping")
        );
        assert_eq!(published.version, 2);
        assert!(published.is_published());

        database.cleanup().await;
    }

    #[tokio::test]
    async fn rejects_out_of_order_versions() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_rest_02", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        let invalid = test_operation("op_rest_02", 3, OperationStatus::Draft);
        let error = registry
            .create_version(CreateVersionRequest {
                workspace_id: &test_workspace_id(),
                snapshot: &invalid,
                change_note: None,
                created_by: None,
            })
            .await
            .unwrap_err();

        assert!(matches!(
            error,
            RegistryError::InvalidVersionSequence {
                expected: 2,
                actual: 3,
                ..
            }
        ));

        database.cleanup().await;
    }

    #[tokio::test]
    async fn update_operation_draft_persists_optional_json_columns_as_sql_null() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let mut operation = test_operation("op_rest_02b", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        operation.generated_draft = None;
        operation.samples = None;
        operation.config_export = None;
        operation.updated_at = "2026-03-25T12:34:00Z".to_owned();

        registry
            .update_operation_draft(&test_workspace_id(), &operation)
            .await
            .unwrap();

        let stored = registry
            .get_operation_version(&test_workspace_id(), &operation.id, operation.version)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(stored.snapshot.generated_draft, None);
        assert_eq!(stored.snapshot.samples, None);
        assert_eq!(stored.snapshot.config_export, None);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn stores_auth_profiles_and_artifact_metadata() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_rest_03", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        let auth_profile = AuthProfile {
            id: "auth_crank".into(),
            workspace_id: test_workspace_id(),
            name: "Crank API key".to_owned(),
            kind: AuthKind::ApiKeyHeader,
            config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig {
                header_name: "X-Api-Key".to_owned(),
                secret_id: SecretId::new("secret_crank_api_key"),
            }),
            created_at: "2026-03-25T12:00:00Z".to_owned(),
            updated_at: "2026-03-25T12:00:00Z".to_owned(),
        };

        registry
            .save_auth_profile(SaveAuthProfileRequest {
                workspace_id: &test_workspace_id(),
                profile: &auth_profile,
            })
            .await
            .unwrap();

        let input_sample = OperationSampleMetadata {
            id: "sample_input".into(),
            operation_id: operation.id.clone(),
            version: 1,
            sample_kind: SampleKind::InputJson,
            storage_ref: "file:///tmp/input.json".to_owned(),
            content_type: "application/json".to_owned(),
            file_name: Some("input.json".to_owned()),
            created_at: "2026-03-25T12:01:00Z".to_owned(),
        };
        let descriptor = DescriptorMetadata {
            id: "descriptor_01".into(),
            operation_id: Some(operation.id.clone()),
            version: Some(1),
            descriptor_kind: DescriptorKind::DescriptorSet,
            storage_ref: "file:///tmp/schema.desc".to_owned(),
            source_name: Some("schema.desc".to_owned()),
            package_index: Some(json!({ "crm.v1": ["LeadService"] })),
            created_at: "2026-03-25T12:02:00Z".to_owned(),
        };

        registry
            .save_sample_metadata(SaveSampleMetadataRequest {
                sample: &input_sample,
            })
            .await
            .unwrap();
        registry
            .save_descriptor_metadata(SaveDescriptorMetadataRequest {
                descriptor: &descriptor,
            })
            .await
            .unwrap();

        let auth_profiles = registry
            .list_auth_profiles(&test_workspace_id())
            .await
            .unwrap();
        let samples = registry
            .list_sample_metadata(&operation.id, 1)
            .await
            .unwrap();
        let descriptors = registry
            .list_descriptor_metadata(&operation.id, 1)
            .await
            .unwrap();

        assert_eq!(auth_profiles, vec![auth_profile]);
        assert_eq!(samples, vec![input_sample]);
        assert_eq!(descriptors, vec![descriptor]);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn lists_auth_profiles_referencing_secret() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let primary_secret_id = SecretId::new("secret_primary");
        let secondary_secret_id = SecretId::new("secret_secondary");
        let profile = AuthProfile {
            id: "auth_crank".into(),
            workspace_id: test_workspace_id(),
            name: "Crank basic auth".to_owned(),
            kind: AuthKind::Basic,
            config: AuthConfig::Basic(crank_core::BasicAuthConfig {
                username_secret_id: primary_secret_id.clone(),
                password_secret_id: secondary_secret_id.clone(),
            }),
            created_at: "2026-03-25T12:00:00Z".to_owned(),
            updated_at: "2026-03-25T12:00:00Z".to_owned(),
        };

        registry
            .save_auth_profile(SaveAuthProfileRequest {
                workspace_id: &test_workspace_id(),
                profile: &profile,
            })
            .await
            .unwrap();

        let profiles = registry
            .list_auth_profiles_referencing_secret(&test_workspace_id(), &primary_secret_id)
            .await
            .unwrap();

        assert_eq!(profiles, vec![profile]);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn stores_and_finishes_yaml_import_jobs() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let job_id = YamlImportJobId::new("job_yaml_01");
        let operation = test_operation("op_rest_04", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        registry
            .create_yaml_import_job(CreateYamlImportJobRequest {
                id: &job_id,
                source_sample_id: None,
                format_version: "v1",
                mode: ExportMode::Portable,
                created_at: "2026-03-25T12:00:00Z",
            })
            .await
            .unwrap();

        registry
            .finish_yaml_import_job(
                &job_id,
                &YamlImportJobCompletion {
                    status: YamlImportJobStatus::Completed,
                    result_operation_id: Some(operation.id.clone()),
                    result_version: Some(2),
                    error_text: None,
                    finished_at: "2026-03-25T12:05:00Z".to_owned(),
                },
            )
            .await
            .unwrap();

        let job = registry
            .get_yaml_import_job(&job_id)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(job.status, YamlImportJobStatus::Completed);
        assert_eq!(job.result_version, Some(2));
        assert_eq!(job.mode, ExportMode::Portable);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_stream_sessions_with_transitions_and_cleanup() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_stream_01", 1, OperationStatus::Draft);
        let session = test_stream_session("stream_01", "op_stream_01", StreamStatus::Running);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        registry
            .create_stream_session(CreateStreamSessionRequest { session: &session })
            .await
            .unwrap();

        let loaded = registry
            .get_stream_session(&session.id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(loaded.status, StreamStatus::Running);

        let closable_session =
            test_stream_session("stream_closable", "op_stream_01", StreamStatus::Running);
        registry
            .create_stream_session(CreateStreamSessionRequest {
                session: &closable_session,
            })
            .await
            .unwrap();
        registry
            .close_stream_session(&closable_session.id, "2026-04-06T12:00:30Z")
            .await
            .unwrap();

        let closed = registry
            .get_stream_session(&closable_session.id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(closed.status, StreamStatus::Stopped);

        let updated = registry
            .update_stream_session_state(UpdateStreamSessionStateRequest {
                session_id: &session.id,
                current_status: StreamStatus::Running,
                next_status: StreamStatus::Failed,
                cursor: Some(&json!({"cursor":"next"})),
                state: &json!({"phase":"failed"}),
                expires_at: Some("2026-04-06T12:10:00Z"),
                last_poll_at: Some("2026-04-06T12:01:00Z"),
                closed_at: Some("2026-04-06T12:01:00Z"),
            })
            .await
            .unwrap();
        assert_eq!(updated.status, StreamStatus::Failed);
        assert_eq!(updated.closed_at.as_deref(), Some("2026-04-06T12:01:00Z"));

        let page = registry
            .list_stream_sessions(StreamSessionFilter {
                workspace_id: &test_workspace_id(),
                agent_id: None,
                operation_id: None,
                status: Some(StreamStatus::Failed),
                mode: Some(crank_core::ExecutionMode::Session),
                limit: 10,
            })
            .await
            .unwrap();
        assert_eq!(page.total, 1);
        assert_eq!(page.items[0].id, session.id);

        let expired_session =
            test_stream_session("stream_expired", "op_stream_01", StreamStatus::Running);
        registry
            .create_stream_session(CreateStreamSessionRequest {
                session: &StreamSession {
                    id: expired_session.id.clone(),
                    expires_at: "2026-04-06T11:00:00Z".to_owned(),
                    ..expired_session
                },
            })
            .await
            .unwrap();

        let deleted = registry
            .delete_expired_stream_sessions("2026-04-06T12:00:00Z")
            .await
            .unwrap();
        assert_eq!(deleted, 1);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn rejects_invalid_stream_session_transition() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_stream_02", 1, OperationStatus::Draft);
        let session = test_stream_session("stream_invalid", "op_stream_02", StreamStatus::Stopped);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_stream_session(CreateStreamSessionRequest { session: &session })
            .await
            .unwrap();

        let error = registry
            .update_stream_session_state(UpdateStreamSessionStateRequest {
                session_id: &session.id,
                current_status: StreamStatus::Stopped,
                next_status: StreamStatus::Running,
                cursor: None,
                state: &json!({"phase":"resume"}),
                expires_at: None,
                last_poll_at: None,
                closed_at: None,
            })
            .await
            .unwrap_err();

        assert!(matches!(
            error,
            RegistryError::InvalidStreamSessionTransition { .. }
        ));

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_async_jobs_with_transitions_and_cleanup() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_job_01", 1, OperationStatus::Draft);
        let job = test_async_job("job_01", "op_job_01", JobStatus::Running);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_async_job(CreateAsyncJobRequest { job: &job })
            .await
            .unwrap();

        let cancellable_job = test_async_job("job_cancel", "op_job_01", JobStatus::Running);
        registry
            .create_async_job(CreateAsyncJobRequest {
                job: &cancellable_job,
            })
            .await
            .unwrap();
        registry
            .cancel_async_job(&cancellable_job.id, "2026-04-06T12:00:30Z")
            .await
            .unwrap();

        let cancelled = registry
            .get_async_job(&cancellable_job.id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(cancelled.status, JobStatus::Cancelled);

        let updated = registry
            .update_async_job_status(UpdateAsyncJobStatusRequest {
                job_id: &job.id,
                current_status: JobStatus::Running,
                next_status: JobStatus::Completed,
                progress: &json!({"percent":100}),
                result: Some(&json!({"ok":true})),
                error: None,
                expires_at: Some("2026-04-06T12:15:00Z"),
                updated_at: "2026-04-06T12:01:00Z",
                finished_at: Some("2026-04-06T12:01:00Z"),
            })
            .await
            .unwrap();
        assert_eq!(updated.status, JobStatus::Completed);
        assert_eq!(updated.result, Some(json!({"ok":true})));

        let loaded = registry.get_async_job(&job.id).await.unwrap().unwrap();
        assert_eq!(loaded.status, JobStatus::Completed);

        let page = registry
            .list_async_jobs(AsyncJobFilter {
                workspace_id: &test_workspace_id(),
                agent_id: None,
                operation_id: None,
                status: Some(JobStatus::Completed),
                limit: 10,
            })
            .await
            .unwrap();
        assert_eq!(page.total, 1);

        let expired_job = AsyncJobHandle {
            id: crank_core::AsyncJobId::new("job_expired"),
            expires_at: Some("2026-04-06T11:00:00Z".to_owned()),
            ..test_async_job("job_expired", "op_job_01", JobStatus::Cancelled)
        };
        registry
            .create_async_job(CreateAsyncJobRequest { job: &expired_job })
            .await
            .unwrap();

        let deleted = registry
            .delete_expired_async_jobs("2026-04-06T12:00:00Z")
            .await
            .unwrap();
        assert_eq!(deleted, 1);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn rejects_invalid_async_job_transition() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_job_02", 1, OperationStatus::Draft);
        let job = test_async_job("job_invalid", "op_job_02", JobStatus::Completed);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_async_job(CreateAsyncJobRequest { job: &job })
            .await
            .unwrap();

        let error = registry
            .update_async_job_status(UpdateAsyncJobStatusRequest {
                job_id: &job.id,
                current_status: JobStatus::Completed,
                next_status: JobStatus::Running,
                progress: &json!({"percent":50}),
                result: None,
                error: None,
                expires_at: None,
                updated_at: "2026-04-06T12:01:00Z",
                finished_at: None,
            })
            .await
            .unwrap_err();

        assert!(matches!(
            error,
            RegistryError::InvalidAsyncJobTransition { .. }
        ));

        database.cleanup().await;
    }

    struct TestDatabase {
        admin_pool: PgPool,
        database_url: String,
        schema: String,
    }

    impl TestDatabase {
        async fn new() -> Self {
            let database_url = env::var("TEST_DATABASE_URL")
                .expect("TEST_DATABASE_URL must point to a reachable PostgreSQL database");
            let admin_pool = PgPoolOptions::new()
                .max_connections(1)
                .connect(&database_url)
                .await
                .unwrap();
            let schema = format!(
                "test_registry_{}_{}",
                std::process::id(),
                SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .unwrap()
                    .as_nanos()
            );

            admin_pool
                .execute(sqlx::query(&format!("create schema {schema}")))
                .await
                .unwrap();

            Self {
                admin_pool,
                database_url,
                schema,
            }
        }

        async fn registry(&self) -> PostgresRegistry {
            PostgresRegistry::connect_in_schema(&self.database_url, Some(&self.schema))
                .await
                .unwrap()
        }

        async fn cleanup(&self) {
            self.admin_pool
                .execute(sqlx::query(&format!(
                    "drop schema if exists {} cascade",
                    self.schema
                )))
                .await
                .unwrap();
        }
    }

    fn test_operation(id: &str, version: u32, status: OperationStatus) -> RegistryOperation {
        RegistryOperation {
            id: OperationId::new(id),
            name: format!("{id}_tool"),
            display_name: format!("Display {id}"),
            category: "general".to_owned(),
            protocol: Protocol::Rest,
            status,
            version,
            target: Target::Rest(RestTarget {
                base_url: "https://api.example.com".to_owned(),
                method: HttpMethod::Post,
                path_template: "/v1/leads".to_owned(),
                static_headers: BTreeMap::from([("X-Static".to_owned(), "true".to_owned())]),
            }),
            input_schema: Schema {
                kind: SchemaKind::Object,
                description: Some("input".to_owned()),
                required: true,
                nullable: false,
                default_value: None,
                fields: BTreeMap::from([(
                    "email".to_owned(),
                    Schema {
                        kind: SchemaKind::String,
                        description: None,
                        required: true,
                        nullable: false,
                        default_value: None,
                        fields: BTreeMap::new(),
                        items: None,
                        enum_values: Vec::new(),
                        variants: Vec::new(),
                    },
                )]),
                items: None,
                enum_values: Vec::new(),
                variants: Vec::new(),
            },
            output_schema: Schema {
                kind: SchemaKind::Object,
                description: Some("output".to_owned()),
                required: true,
                nullable: false,
                default_value: None,
                fields: BTreeMap::from([(
                    "id".to_owned(),
                    Schema {
                        kind: SchemaKind::String,
                        description: None,
                        required: true,
                        nullable: false,
                        default_value: None,
                        fields: BTreeMap::new(),
                        items: None,
                        enum_values: Vec::new(),
                        variants: Vec::new(),
                    },
                )]),
                items: None,
                enum_values: Vec::new(),
                variants: Vec::new(),
            },
            input_mapping: MappingSet {
                rules: vec![MappingRule {
                    source: "$.mcp.email".to_owned(),
                    target: "$.request.body.email".to_owned(),
                    required: true,
                    default_value: None,
                    transform: None,
                    condition: None,
                    notes: None,
                }],
            },
            output_mapping: MappingSet {
                rules: vec![MappingRule {
                    source: "$.response.body.id".to_owned(),
                    target: "$.output.id".to_owned(),
                    required: true,
                    default_value: None,
                    transform: None,
                    condition: None,
                    notes: None,
                }],
            },
            execution_config: ExecutionConfig {
                timeout_ms: 10_000,
                retry_policy: Some(RetryPolicy { max_attempts: 3 }),
                auth_profile_ref: Some("auth_crank".into()),
                headers: BTreeMap::from([("X-Request-Id".to_owned(), "static".to_owned())]),
                protocol_options: None,
                streaming: None,
            },
            tool_description: ToolDescription {
                title: "Create lead".to_owned(),
                description: "Creates CRM lead".to_owned(),
                tags: vec!["crm".to_owned()],
                examples: vec![ToolExample {
                    input: json!({ "email": "a@example.com" }),
                }],
            },
            samples: Some(Samples {
                input_json_sample_ref: Some("sample_input".into()),
                output_json_sample_ref: Some("sample_output".into()),
                proto_file_ref: None,
                descriptor_ref: None,
            }),
            generated_draft: Some(GeneratedDraft {
                status: GeneratedDraftStatus::Available,
                source_types: vec!["input_json".to_owned(), "output_json".to_owned()],
                generated_at: Some("2026-03-25T11:59:00Z".to_owned()),
                input_schema_generated: true,
                output_schema_generated: true,
                input_mapping_generated: true,
                output_mapping_generated: true,
                warnings: Vec::new(),
            }),
            config_export: Some(ConfigExport {
                format_version: "v1".to_owned(),
                export_mode: ExportMode::Portable,
            }),
            created_at: "2026-03-25T11:58:00Z".to_owned(),
            updated_at: format!("2026-03-25T12:{version:02}:00Z"),
            published_at: None,
        }
    }

    fn test_stream_session(id: &str, operation_id: &str, status: StreamStatus) -> StreamSession {
        StreamSession {
            id: StreamSessionId::new(id),
            workspace_id: test_workspace_id(),
            agent_id: None,
            operation_id: OperationId::new(operation_id),
            protocol: Protocol::Rest,
            mode: crank_core::ExecutionMode::Session,
            status,
            cursor: Some(json!({"cursor":"initial"})),
            state: json!({"phase":"running"}),
            expires_at: "2026-04-06T12:05:00Z".to_owned(),
            last_poll_at: None,
            created_at: "2026-04-06T12:00:00Z".to_owned(),
            closed_at: None,
        }
    }

    fn test_async_job(id: &str, operation_id: &str, status: JobStatus) -> AsyncJobHandle {
        AsyncJobHandle {
            id: crank_core::AsyncJobId::new(id),
            workspace_id: test_workspace_id(),
            agent_id: None,
            operation_id: OperationId::new(operation_id),
            status,
            progress: json!({"percent": 25}),
            result: None,
            error: None,
            expires_at: Some("2026-04-06T12:05:00Z".to_owned()),
            created_at: "2026-04-06T12:00:00Z".to_owned(),
            updated_at: "2026-04-06T12:00:00Z".to_owned(),
            finished_at: None,
        }
    }
}
