import { skillFiles, skills, subAgentSkills } from "../../db/manage/manage-schema.js";
import { SKILL_ENTRY_FILE_PATH, parseSkillFromMarkdown } from "../../utils/skill-files.js";
import { SkillFrontmatterSchema } from "../../validation/schemas/skills.js";
import { getLogger } from "../../utils/logger.js";
import { deriveRelationId, generateId } from "../../utils/conversations.js";
import { agentScopedWhere, projectScopedWhere, subAgentScopedWhere } from "./scope-helpers.js";
import { and, asc, count, desc, eq, inArray } from "drizzle-orm";

//#region src/data-access/manage/skills.ts
const logger = getLogger("skills-dal");
async function replaceSkillFiles(db, params) {
	const existingFiles = await db.select().from(skillFiles).where(and(eq(skillFiles.tenantId, params.scopes.tenantId), eq(skillFiles.projectId, params.scopes.projectId), eq(skillFiles.skillId, params.skillId)));
	const nextFilePaths = new Set(params.files.map((file) => file.filePath));
	const existingByPath = new Map(existingFiles.map((file) => [file.filePath, file]));
	const fileIdsToDelete = existingFiles.filter((file) => !nextFilePaths.has(file.filePath)).map((file) => file.id);
	if (fileIdsToDelete.length) await db.delete(skillFiles).where(and(projectScopedWhere(skillFiles, params.scopes), eq(skillFiles.skillId, params.skillId), inArray(skillFiles.id, fileIdsToDelete)));
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const filesToInsert = [];
	for (const file of params.files) {
		const existingFile = existingByPath.get(file.filePath);
		if (!existingFile) {
			filesToInsert.push({
				tenantId: params.scopes.tenantId,
				projectId: params.scopes.projectId,
				skillId: params.skillId,
				id: generateId(),
				filePath: file.filePath,
				content: file.content,
				createdAt: now,
				updatedAt: now
			});
			continue;
		}
		await db.update(skillFiles).set({
			content: file.content,
			updatedAt: now
		}).where(and(projectScopedWhere(skillFiles, params.scopes), eq(skillFiles.skillId, params.skillId), eq(skillFiles.id, existingFile.id)));
	}
	if (filesToInsert.length) await db.insert(skillFiles).values(filesToInsert);
	return (await getSkillFilesBySkillIds(db)({
		scopes: params.scopes,
		skillIds: [params.skillId]
	}))[params.skillId] ?? [];
}
const getSkillFilesBySkillIds = (db) => async (params) => {
	if (!params.skillIds.length) return {};
	return (await db.select().from(skillFiles).where(and(eq(skillFiles.tenantId, params.scopes.tenantId), eq(skillFiles.projectId, params.scopes.projectId), inArray(skillFiles.skillId, params.skillIds))).orderBy(asc(skillFiles.filePath))).reduce((acc, file) => {
		acc[file.skillId] ??= [];
		acc[file.skillId].push(file);
		return acc;
	}, {});
};
const getSkillById = (db) => async (params) => {
	return await db.query.skills.findFirst({ where: and(projectScopedWhere(skills, params.scopes), eq(skills.id, params.skillId)) }) ?? null;
};
const getSkillByIdWithFiles = (db) => async (params) => {
	const skill = await getSkillById(db)(params);
	if (!skill) return null;
	const files = await getSkillFilesBySkillIds(db)({
		scopes: params.scopes,
		skillIds: [params.skillId]
	});
	return {
		...skill,
		files: files[params.skillId] ?? []
	};
};
const getSkillFileById = (db) => async (params) => {
	return await db.query.skillFiles.findFirst({ where: and(projectScopedWhere(skillFiles, params.scopes), eq(skillFiles.skillId, params.skillId), eq(skillFiles.id, params.fileId)) }) ?? null;
};
const createSkillFileById = (db) => async (params) => {
	const skill = await getSkillByIdWithFiles(db)({
		scopes: params.scopes,
		skillId: params.skillId
	});
	if (!skill) return null;
	if (params.data.filePath === SKILL_ENTRY_FILE_PATH) throw new Error(`Use the skill update flow to manage ${SKILL_ENTRY_FILE_PATH}`);
	if (skill.files.some((file) => file.filePath === params.data.filePath)) throw new Error(`Skill file already exists at path "${params.data.filePath}"`);
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const [createdFile] = await db.insert(skillFiles).values({
		tenantId: params.scopes.tenantId,
		projectId: params.scopes.projectId,
		skillId: params.skillId,
		id: generateId(),
		filePath: params.data.filePath,
		content: params.data.content,
		createdAt: now,
		updatedAt: now
	}).returning();
	return createdFile ?? null;
};
function buildEntryFileUpdateData(params) {
	const parsed = parseSkillFromMarkdown(params.content);
	const frontmatterResult = SkillFrontmatterSchema.safeParse(parsed.frontmatter);
	if (!frontmatterResult.success) throw new Error(frontmatterResult.error.issues[0]?.message ?? "Invalid SKILL.md frontmatter");
	if (frontmatterResult.data.name !== params.skillId) throw new Error("SKILL.md name must match the skill id");
	return {
		description: frontmatterResult.data.description,
		metadata: frontmatterResult.data.metadata ?? null,
		content: parsed.content,
		files: params.files
	};
}
const updateSkillFileById = (db) => async (params) => {
	const skill = await getSkillByIdWithFiles(db)({
		scopes: params.scopes,
		skillId: params.skillId
	});
	if (!skill) return null;
	const existingFile = skill.files.find((file) => file.id === params.fileId);
	if (!existingFile) return null;
	const files = skill.files.map((file) => ({
		filePath: file.filePath,
		content: file.id === params.fileId ? params.content : file.content
	}));
	const data = existingFile.filePath === SKILL_ENTRY_FILE_PATH ? buildEntryFileUpdateData({
		skillId: params.skillId,
		files,
		content: params.content
	}) : { files };
	if (!await updateSkill(db)({
		scopes: params.scopes,
		skillId: params.skillId,
		data
	})) return null;
	return await getSkillFileById(db)({
		scopes: params.scopes,
		skillId: params.skillId,
		fileId: params.fileId
	});
};
const deleteSkillFileById = (db) => async (params) => {
	const skill = await getSkillByIdWithFiles(db)({
		scopes: params.scopes,
		skillId: params.skillId
	});
	if (!skill) return null;
	const existingFile = skill.files.find((file) => file.id === params.fileId);
	if (!existingFile) return null;
	if (existingFile.filePath === SKILL_ENTRY_FILE_PATH) throw new Error("Use the skill delete flow to remove SKILL.md");
	return await updateSkill(db)({
		scopes: params.scopes,
		skillId: params.skillId,
		data: { files: skill.files.filter((file) => file.id !== params.fileId).map((file) => ({
			filePath: file.filePath,
			content: file.content
		})) }
	}) !== null;
};
const listSkills = (db) => async (params) => {
	const page = params.pagination?.page || 1;
	const limit = Math.min(params.pagination?.limit || 10, 100);
	const offset = (page - 1) * limit;
	const whereClause = projectScopedWhere(skills, params.scopes);
	const [data, totalResult] = await Promise.all([db.select().from(skills).where(whereClause).limit(limit).offset(offset).orderBy(desc(skills.createdAt)), db.select({ count: count() }).from(skills).where(whereClause)]);
	const total = totalResult[0]?.count || 0;
	return {
		data,
		pagination: {
			page,
			limit,
			total,
			pages: Math.ceil(total / limit)
		}
	};
};
const listSkillsWithFiles = (db) => async (params) => {
	const result = await listSkills(db)(params);
	const skillIds = result.data.map((skill) => skill.id);
	const filesBySkillId = await getSkillFilesBySkillIds(db)({
		scopes: params.scopes,
		skillIds
	});
	return {
		...result,
		data: result.data.map((skill) => ({
			...skill,
			files: filesBySkillId[skill.id] ?? []
		}))
	};
};
const createSkill = (db) => async (data) => {
	return await db.transaction(async (tx) => {
		const now = (/* @__PURE__ */ new Date()).toISOString();
		const insertData = {
			tenantId: data.tenantId,
			projectId: data.projectId,
			id: data.name,
			name: data.name,
			description: data.description,
			content: data.content,
			metadata: data.metadata ?? null,
			createdAt: now,
			updatedAt: now
		};
		const [result] = await tx.insert(skills).values(insertData).returning();
		await replaceSkillFiles(tx, {
			scopes: {
				tenantId: data.tenantId,
				projectId: data.projectId
			},
			skillId: result.id,
			files: data.files
		});
		const createdSkill = await getSkillByIdWithFiles(tx)({
			scopes: {
				tenantId: data.tenantId,
				projectId: data.projectId
			},
			skillId: result.id
		});
		if (!createdSkill) throw new Error(`Failed to load created skill "${result.id}"`);
		return createdSkill;
	});
};
const upsertSkill = (db) => async (data) => {
	return await db.transaction(async (tx) => {
		const now = (/* @__PURE__ */ new Date()).toISOString();
		const baseData = {
			tenantId: data.tenantId,
			projectId: data.projectId,
			id: data.name,
			name: data.name,
			description: data.description,
			content: data.content,
			metadata: data.metadata ?? null
		};
		const scopes = {
			tenantId: baseData.tenantId,
			projectId: baseData.projectId
		};
		const existing = await tx.query.skills.findFirst({ where: and(projectScopedWhere(skills, scopes), eq(skills.id, baseData.id)) });
		const files = data.files;
		if (existing) {
			const [result$1] = await tx.update(skills).set({
				name: baseData.name,
				description: baseData.description,
				content: baseData.content,
				metadata: baseData.metadata,
				updatedAt: now
			}).where(and(projectScopedWhere(skills, scopes), eq(skills.id, baseData.id))).returning();
			await replaceSkillFiles(tx, {
				scopes: {
					tenantId: baseData.tenantId,
					projectId: baseData.projectId
				},
				skillId: baseData.id,
				files
			});
			logger.info({ skillId: baseData.id }, "Updated skill");
			return result$1;
		}
		const insertData = {
			...baseData,
			createdAt: now,
			updatedAt: now
		};
		const [result] = await tx.insert(skills).values(insertData).returning();
		await replaceSkillFiles(tx, {
			scopes: {
				tenantId: baseData.tenantId,
				projectId: baseData.projectId
			},
			skillId: baseData.id,
			files
		});
		logger.info({ skillId: baseData.id }, "Created skill");
		return result;
	});
};
const updateSkill = (db) => async (params) => {
	return await db.transaction(async (tx) => {
		if (!await tx.query.skills.findFirst({ where: and(eq(skills.tenantId, params.scopes.tenantId), eq(skills.projectId, params.scopes.projectId), eq(skills.id, params.skillId)) })) return null;
		const { description, metadata, content } = params.data;
		const updateData = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
		if (description !== void 0) updateData.description = description;
		if (metadata !== void 0) updateData.metadata = metadata;
		if (content !== void 0) updateData.content = content;
		if (!params.data.files) throw new Error("Skill updates must include files");
		const [result] = await tx.update(skills).set(updateData).where(and(projectScopedWhere(skills, params.scopes), eq(skills.id, params.skillId))).returning();
		await replaceSkillFiles(tx, {
			scopes: params.scopes,
			skillId: params.skillId,
			files: params.data.files
		});
		return result ? await getSkillByIdWithFiles(tx)({
			scopes: params.scopes,
			skillId: params.skillId
		}) : null;
	});
};
const deleteSkill = (db) => async (params) => {
	return (await db.delete(skills).where(and(projectScopedWhere(skills, params.scopes), eq(skills.id, params.skillId))).returning()).length > 0;
};
const getSkillsForSubAgents = (db) => async (params) => {
	if (!params.subAgentIds.length) return [];
	return await db.select({
		subAgentSkillId: subAgentSkills.id,
		subAgentId: subAgentSkills.subAgentId,
		index: subAgentSkills.index,
		alwaysLoaded: subAgentSkills.alwaysLoaded,
		id: skills.id,
		name: skills.name,
		description: skills.description,
		content: skills.content,
		metadata: skills.metadata,
		createdAt: skills.createdAt,
		updatedAt: skills.updatedAt
	}).from(subAgentSkills).innerJoin(skills, and(eq(subAgentSkills.skillId, skills.id), eq(subAgentSkills.tenantId, skills.tenantId), eq(subAgentSkills.projectId, skills.projectId))).where(and(agentScopedWhere(subAgentSkills, params.scopes), inArray(subAgentSkills.subAgentId, params.subAgentIds))).orderBy(asc(subAgentSkills.index), asc(subAgentSkills.createdAt));
};
const upsertSubAgentSkill = (db) => async (params) => {
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const existing = await db.query.subAgentSkills.findFirst({ where: and(subAgentScopedWhere(subAgentSkills, params.scopes), eq(subAgentSkills.skillId, params.skillId)) });
	if (existing) {
		const [result$1] = await db.update(subAgentSkills).set({
			index: params.index,
			alwaysLoaded: params.alwaysLoaded ?? existing.alwaysLoaded,
			updatedAt: now
		}).where(eq(subAgentSkills.id, existing.id)).returning();
		return result$1;
	}
	const insertData = {
		...params.scopes,
		id: deriveRelationId(params.scopes.tenantId, params.scopes.projectId, params.scopes.agentId, params.scopes.subAgentId, params.skillId),
		skillId: params.skillId,
		index: params.index,
		alwaysLoaded: params.alwaysLoaded ?? false,
		createdAt: now,
		updatedAt: now
	};
	const [result] = await db.insert(subAgentSkills).values(insertData).returning();
	return result;
};
const deleteSubAgentSkill = (db) => async (params) => {
	return (await db.delete(subAgentSkills).where(and(agentScopedWhere(subAgentSkills, params.scopes), eq(subAgentSkills.id, params.subAgentSkillId))).returning()).length > 0;
};

//#endregion
export { createSkill, createSkillFileById, deleteSkill, deleteSkillFileById, deleteSubAgentSkill, getSkillById, getSkillByIdWithFiles, getSkillFileById, getSkillFilesBySkillIds, getSkillsForSubAgents, listSkills, listSkillsWithFiles, updateSkill, updateSkillFileById, upsertSkill, upsertSubAgentSkill };