import {} from "kysely";
import { createCode, getAccessStatus, getRedeemedKeyCiphertext, isCodeUsable, listCodes, listRedemptions, redeemCode, revokeAccess, setActiveCode, setCodeAnthropicKey, } from "./db/codes.js";
/**
 * OpenBao transit pseudo-tenant under which attached invite-code keys are
 * encrypted at rest. Distinct from any real tenant id, so the code keys get
 * their own transit key (`vetra-tenant-access-codes`).
 */
export const ACCESS_CODES_TRANSIT_TENANT = "access-codes";
/** Canonical did:pkh for the authenticated caller, or null if unauthenticated. */
function callerDid(ctx) {
    const u = ctx.user;
    if (!u)
        return null;
    return `did:pkh:${u.networkId}:${u.chainId}:${u.address.toLowerCase()}`;
}
function requireAdmin(ctx) {
    const address = ctx.user?.address?.toLowerCase();
    if (!address || !(ctx.isAdmin?.(address) ?? false)) {
        throw new Error("FORBIDDEN");
    }
}
export function createResolvers(db, deps) {
    const { transit, secretsService } = deps;
    /** Encrypt a plaintext key for at-rest storage under the access-codes key. */
    async function encryptAttachedKey(plaintext) {
        await transit.ensureTenantKey(ACCESS_CODES_TRANSIT_TENANT);
        return transit.encrypt(ACCESS_CODES_TRANSIT_TENANT, plaintext);
    }
    return {
        Query: {
            VetraAccessCodes: () => ({}),
        },
        Mutation: {
            VetraAccessCodes: () => ({}),
        },
        VetraAccessCodesQueries: {
            inviteCodeValid: (_p, { code }) => isCodeUsable(db, code),
            myAccessStatus: (_p, _a, ctx) => {
                const did = callerDid(ctx);
                if (!did)
                    throw new Error("UNAUTHENTICATED");
                return getAccessStatus(db, did);
            },
            inviteCodes: (_p, _a, ctx) => {
                requireAdmin(ctx);
                return listCodes(db);
            },
            redemptions: (_p, { code, address }, ctx) => {
                requireAdmin(ctx);
                return listRedemptions(db, { code, address });
            },
        },
        VetraAccessCodesMutations: {
            redeemInviteCode: async (_p, { code }, ctx) => {
                const did = callerDid(ctx);
                if (!did)
                    throw new Error("UNAUTHENTICATED");
                const result = await redeemCode(db, code, did);
                if (!result.ok)
                    throw new Error("INVALID_CODE");
                return getAccessStatus(db, did);
            },
            applyInviteCodeSecret: async (_p, { tenantId, secretNames }, ctx) => {
                const did = callerDid(ctx);
                if (!did)
                    throw new Error("UNAUTHENTICATED");
                const ciphertext = await getRedeemedKeyCiphertext(db, did);
                if (ciphertext === null)
                    return { injected: false, secretNames: [] };
                const apiKey = await transit.decrypt(ACCESS_CODES_TRANSIT_TENANT, ciphertext);
                // Sequential: setSecret pg_notifies the reconciler per write; a handful
                // of studio secret names, so no need to parallelize.
                for (const name of secretNames) {
                    await secretsService.setSecret(tenantId, name, apiKey);
                }
                return { injected: true, secretNames };
            },
            createInviteCode: async (_p, args, ctx) => {
                requireAdmin(ctx);
                const anthropicKeyCiphertext = args.anthropicApiKey
                    ? await encryptAttachedKey(args.anthropicApiKey)
                    : null;
                return createCode(db, {
                    code: args.code,
                    label: args.label ?? null,
                    expiresAt: args.expiresAt ?? null,
                    maxUses: args.maxUses ?? null,
                    anthropicKeyCiphertext,
                });
            },
            setInviteCodeAnthropicKey: async (_p, { code, anthropicApiKey }, ctx) => {
                requireAdmin(ctx);
                const ciphertext = anthropicApiKey
                    ? await encryptAttachedKey(anthropicApiKey)
                    : null;
                const view = await setCodeAnthropicKey(db, code, ciphertext);
                if (!view)
                    throw new Error("CODE_NOT_FOUND");
                return view;
            },
            setInviteCodeActive: async (_p, { code, active }, ctx) => {
                requireAdmin(ctx);
                const view = await setActiveCode(db, code, active);
                if (!view)
                    throw new Error("CODE_NOT_FOUND");
                return view;
            },
            revokeAccess: (_p, { address }, ctx) => {
                requireAdmin(ctx);
                return revokeAccess(db, address);
            },
        },
    };
}
