import { GraphQLError, buildSchema, graphql, parse, printSchema } from 'graphql';
import { KNOWLEDGE_GRAPHQL_SDL, clampInt, clampOffset, installJsonScalar, mapJob, mapJobRun, mapPacket, mapProjectionBundle, mapProjectionTarget, pickOperation, toJobMode, toPacketDetail, toProjectionKind, } from './graphql-schema.js';
export function inspectKnowledgeGraphqlAccess(source, operationName) {
    const document = parse(source);
    const operation = pickOperation(document, operationName);
    return operation === 'mutation'
        ? { operation, requiredScopes: ['write:knowledge'], adminRequired: true }
        : { operation, requiredScopes: ['read:knowledge'], adminRequired: false };
}
function assertWriteAccess(context) {
    if (!context.admin) {
        throw new GraphQLError('Knowledge GraphQL mutation requires admin access.');
    }
    if (!context.scopes.includes('write:knowledge')) {
        throw new GraphQLError('Knowledge GraphQL mutation requires write:knowledge.');
    }
}
export class KnowledgeGraphqlService {
    service;
    static schema = (() => {
        const schema = buildSchema(KNOWLEDGE_GRAPHQL_SDL);
        installJsonScalar(schema);
        return schema;
    })();
    static schemaSdl = printSchema(KnowledgeGraphqlService.schema);
    constructor(service) {
        this.service = service;
    }
    get schemaText() {
        return KnowledgeGraphqlService.schemaSdl;
    }
    async execute(input) {
        const rootValue = this.createRootValue();
        const context = {
            service: this.service,
            admin: input.admin,
            scopes: [...input.scopes],
        };
        const result = await graphql({
            schema: KnowledgeGraphqlService.schema,
            source: input.query,
            rootValue,
            contextValue: context,
            variableValues: input.variables,
            operationName: input.operationName,
        });
        return result;
    }
    createRootValue() {
        return {
            status: async (args) => this.service.getStatus(graphqlScope(args)),
            sources: (args) => (this.service.querySources({
                limit: clampInt(args.limit, 100),
                ...graphqlScope(args),
            }).items),
            nodes: (args) => (this.service.queryNodes({
                limit: clampInt(args.limit, 100),
                ...graphqlScope(args),
            }).items),
            issues: (args) => (this.service.queryIssues({
                limit: clampInt(args.limit, 100),
                ...graphqlScope(args),
            }).items),
            source: ({ id }) => this.service.listSources(Number.MAX_SAFE_INTEGER).find((source) => source.id === id) ?? null,
            node: ({ id }) => this.service.listNodes(Number.MAX_SAFE_INTEGER).find((node) => node.id === id) ?? null,
            issue: ({ id }) => this.service.listIssues(Number.MAX_SAFE_INTEGER).find((issue) => issue.id === id) ?? null,
            item: ({ id }) => this.service.getItem(id),
            items: ({ ids }) => this.service.getItems(ids),
            sourcesConnection: (args) => this.service.querySources({
                limit: clampInt(args.limit, 100),
                offset: clampOffset(args.offset),
                ...graphqlScope(args),
                status: args.status,
                connectorId: args.connectorId,
                sourceType: args.sourceType,
                tag: args.tag,
                query: args.query,
            }),
            nodesConnection: (args) => this.service.queryNodes({
                limit: clampInt(args.limit, 100),
                offset: clampOffset(args.offset),
                ...graphqlScope(args),
                kind: args.kind,
                status: args.status,
                query: args.query,
            }),
            issuesConnection: (args) => this.service.queryIssues({
                limit: clampInt(args.limit, 100),
                offset: clampOffset(args.offset),
                ...graphqlScope(args),
                severity: args.severity,
                status: args.status,
                code: args.code,
                query: args.query,
            }),
            extractions: (args) => (this.service.listExtractions(clampInt(args.limit, 100), args.sourceId, graphqlScope(args))
                .slice(0, clampInt(args.limit, 100))),
            sourceExtraction: ({ sourceId }) => this.service.getSourceExtraction(sourceId),
            neighbors: ({ kind, id, relation, limit }) => {
                if (kind !== 'source' && kind !== 'node') {
                    throw new GraphQLError(`Unsupported knowledge neighbor kind: ${kind}`);
                }
                return this.service.getNeighbors(kind, id, { relation, limit: clampInt(limit, 20) });
            },
            search: (args) => (this.service.searchScoped({
                query: args.query,
                limit: clampInt(args.limit, 10),
                ...graphqlScope(args),
            })),
            packet: async (args) => mapPacket(await this.service.buildPacket(args.task, args.writeScope ?? [], clampInt(args.limit, 6), {
                detail: toPacketDetail(args.detail),
                ...(typeof args.budgetLimit === 'number' ? { budgetLimit: args.budgetLimit } : {}),
                ...graphqlScope(args),
            })),
            connectors: () => this.service.listConnectors(),
            connector: ({ id }) => this.service.getConnector(id),
            connectorDoctor: ({ id }) => this.service.doctorConnector(id),
            projectionTargets: async (args) => (await this.service.listProjectionTargets(clampInt(args.limit, 25), graphqlScope(args))).map((target) => mapProjectionTarget(target)),
            projection: async (args) => mapProjectionBundle(await this.service.renderProjection({
                kind: toProjectionKind(args.kind),
                id: args.id,
                limit: clampInt(args.limit, 12),
                ...graphqlScope(args),
            })),
            jobs: () => this.service.listJobs().map((job) => mapJob(job)),
            job: ({ id }) => {
                const job = this.service.getJob(id);
                return job ? mapJob(job) : null;
            },
            jobRuns: ({ limit, jobId }) => this.service.listJobRuns(clampInt(limit, 25), jobId).map((run) => mapJobRun(run)),
            usage: ({ limit, targetKind, targetId, usageKind }) => (this.service.listUsageRecords(clampInt(limit, 100), {
                ...(targetKind ? { targetKind } : {}),
                ...(targetId ? { targetId } : {}),
                ...(usageKind ? { usageKind: usageKind } : {}),
            })),
            consolidationCandidates: ({ limit, status, subjectKind, subjectId }) => (this.service.listConsolidationCandidates(clampInt(limit, 100), {
                ...(status ? { status: status } : {}),
                ...(subjectKind ? { subjectKind } : {}),
                ...(subjectId ? { subjectId } : {}),
            })),
            consolidationCandidate: ({ id }) => this.service.getConsolidationCandidate(id),
            consolidationReports: ({ limit }) => this.service.listConsolidationReports(clampInt(limit, 100)),
            consolidationReport: ({ id }) => this.service.getConsolidationReport(id),
            schedules: ({ limit }) => this.service.listSchedules(clampInt(limit, 100)),
            schedule: ({ id }) => this.service.getSchedule(id),
            ingestUrl: async (args, context) => {
                assertWriteAccess(context);
                const result = await this.service.ingestUrl({
                    url: args.url,
                    title: args.title,
                    tags: args.tags,
                    folderPath: args.folderPath,
                    sessionId: args.sessionId,
                    sourceType: args.sourceType,
                    connectorId: args.connectorId,
                    allowPrivateHosts: args.allowPrivateHosts,
                    metadata: args.metadata,
                });
                return result.source;
            },
            ingestArtifact: async (args, context) => {
                assertWriteAccess(context);
                const result = await this.service.ingestArtifact({
                    artifactId: args.artifactId,
                    path: args.path,
                    uri: args.uri,
                    title: args.title,
                    tags: args.tags,
                    folderPath: args.folderPath,
                    sessionId: args.sessionId,
                    sourceType: args.sourceType,
                    connectorId: args.connectorId,
                    allowPrivateHosts: args.allowPrivateHosts,
                    metadata: args.metadata,
                });
                return result.source;
            },
            importBookmarks: async (args, context) => {
                assertWriteAccess(context);
                return this.service.importBookmarksFromFile(args);
            },
            importUrls: async (args, context) => {
                assertWriteAccess(context);
                return this.service.importUrlsFromFile(args);
            },
            ingestConnector: async (args, context) => {
                assertWriteAccess(context);
                return this.service.ingestConnectorInput(args);
            },
            lint: async (_args, context) => {
                assertWriteAccess(context);
                return this.service.lint();
            },
            reindex: async (_args, context) => {
                assertWriteAccess(context);
                return this.service.reindex();
            },
            runJob: async (args, context) => {
                assertWriteAccess(context);
                return mapJobRun(await this.service.runJob(args.id, {
                    ...(args.mode ? { mode: toJobMode(args.mode) } : {}),
                    ...(args.sourceIds ? { sourceIds: args.sourceIds } : {}),
                    ...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
                }));
            },
            decideCandidate: async (args, context) => {
                assertWriteAccess(context);
                return this.service.decideConsolidationCandidate(args.id, args.decision, {
                    decidedBy: args.decidedBy,
                    memoryClass: args.memoryClass,
                    scope: args.scope,
                    detail: args.detail,
                });
            },
            saveSchedule: async (args, context) => {
                assertWriteAccess(context);
                return this.service.saveSchedule({
                    id: args.id,
                    jobId: args.jobId,
                    label: args.label,
                    enabled: args.enabled,
                    schedule: args.schedule,
                });
            },
            deleteSchedule: async (args, context) => {
                assertWriteAccess(context);
                return this.service.deleteSchedule(args.id);
            },
            setScheduleEnabled: async (args, context) => {
                assertWriteAccess(context);
                return this.service.setScheduleEnabled(args.id, args.enabled);
            },
            renderProjection: async (args, context) => {
                assertWriteAccess(context);
                return mapProjectionBundle(await this.service.renderProjection({
                    kind: toProjectionKind(args.kind),
                    id: args.id,
                    limit: clampInt(args.limit, 12),
                    ...graphqlScope(args),
                }));
            },
            materializeProjection: async (args, context) => {
                assertWriteAccess(context);
                const materialized = await this.service.materializeProjection({
                    kind: toProjectionKind(args.kind),
                    id: args.id,
                    limit: clampInt(args.limit, 12),
                    filename: args.filename,
                    ...graphqlScope(args),
                });
                return {
                    ...materialized,
                    bundle: mapProjectionBundle(materialized.bundle),
                };
            },
        };
    }
}
function graphqlScope(input = {}) {
    return {
        ...(typeof input.knowledgeSpaceId === 'string' && input.knowledgeSpaceId.trim()
            ? { knowledgeSpaceId: input.knowledgeSpaceId }
            : {}),
        ...(input.includeAllSpaces === true ? { includeAllSpaces: true } : {}),
    };
}
export function getKnowledgeGraphqlSchemaText() {
    return KnowledgeGraphqlService.schemaSdl;
}
