import { and, asc, desc, eq, inArray } from "drizzle-orm";
import { projects, projectGoals, goals, projectWorkspaces } from "@zmeel/db";
import { PROJECT_COLORS, deriveProjectUrlKey, hasNonAsciiContent, isUuidLike, normalizeProjectUrlKey, } from "@zmeel/shared";
import { listWorkspaceRuntimeServicesForProjectWorkspaces } from "./workspace-runtime.js";
import { parseProjectExecutionWorkspacePolicy } from "./execution-workspace-policy.js";
import { mergeProjectWorkspaceRuntimeConfig, readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
import { resolveManagedProjectWorkspaceDir } from "../home-paths.js";
const REPO_ONLY_CWD_SENTINEL = "/__zmeel_repo_only__";
/** Batch-load goal refs for a set of projects. */
async function attachGoals(db, rows) {
    if (rows.length === 0)
        return [];
    const projectIds = rows.map((r) => r.id);
    // Fetch join rows + goal titles in one query
    const links = await db
        .select({
        projectId: projectGoals.projectId,
        goalId: projectGoals.goalId,
        goalTitle: goals.title,
    })
        .from(projectGoals)
        .innerJoin(goals, eq(projectGoals.goalId, goals.id))
        .where(inArray(projectGoals.projectId, projectIds));
    const map = new Map();
    for (const link of links) {
        let arr = map.get(link.projectId);
        if (!arr) {
            arr = [];
            map.set(link.projectId, arr);
        }
        arr.push({ id: link.goalId, title: link.goalTitle });
    }
    return rows.map((r) => {
        const g = map.get(r.id) ?? [];
        return {
            ...r,
            urlKey: deriveProjectUrlKey(r.name, r.id),
            goalIds: g.map((x) => x.id),
            goals: g,
            executionWorkspacePolicy: parseProjectExecutionWorkspacePolicy(r.executionWorkspacePolicy),
        };
    });
}
function toRuntimeService(row) {
    return {
        id: row.id,
        companyId: row.companyId,
        projectId: row.projectId ?? null,
        projectWorkspaceId: row.projectWorkspaceId ?? null,
        executionWorkspaceId: row.executionWorkspaceId ?? null,
        issueId: row.issueId ?? null,
        scopeType: row.scopeType,
        scopeId: row.scopeId ?? null,
        serviceName: row.serviceName,
        status: row.status,
        lifecycle: row.lifecycle,
        reuseKey: row.reuseKey ?? null,
        command: row.command ?? null,
        cwd: row.cwd ?? null,
        port: row.port ?? null,
        url: row.url ?? null,
        provider: row.provider,
        providerRef: row.providerRef ?? null,
        ownerAgentId: row.ownerAgentId ?? null,
        startedByRunId: row.startedByRunId ?? null,
        lastUsedAt: row.lastUsedAt,
        startedAt: row.startedAt,
        stoppedAt: row.stoppedAt ?? null,
        stopPolicy: row.stopPolicy ?? null,
        healthStatus: row.healthStatus,
        createdAt: row.createdAt,
        updatedAt: row.updatedAt,
    };
}
function toWorkspace(row, runtimeServices = []) {
    return {
        id: row.id,
        companyId: row.companyId,
        projectId: row.projectId,
        name: row.name,
        sourceType: row.sourceType,
        cwd: normalizeWorkspaceCwd(row.cwd),
        repoUrl: row.repoUrl ?? null,
        repoRef: row.repoRef ?? null,
        defaultRef: row.defaultRef ?? row.repoRef ?? null,
        visibility: row.visibility,
        setupCommand: row.setupCommand ?? null,
        cleanupCommand: row.cleanupCommand ?? null,
        remoteProvider: row.remoteProvider ?? null,
        remoteWorkspaceRef: row.remoteWorkspaceRef ?? null,
        sharedWorkspaceKey: row.sharedWorkspaceKey ?? null,
        metadata: row.metadata ?? null,
        runtimeConfig: readProjectWorkspaceRuntimeConfig(row.metadata ?? null),
        isPrimary: row.isPrimary,
        runtimeServices,
        createdAt: row.createdAt,
        updatedAt: row.updatedAt,
    };
}
function deriveRepoNameFromRepoUrl(repoUrl) {
    const raw = readNonEmptyString(repoUrl);
    if (!raw)
        return null;
    try {
        const parsed = new URL(raw);
        const cleanedPath = parsed.pathname.replace(/\/+$/, "");
        const repoName = cleanedPath.split("/").filter(Boolean).pop()?.replace(/\.git$/i, "") ?? "";
        return repoName || null;
    }
    catch {
        return null;
    }
}
function deriveProjectCodebase(input) {
    const primaryWorkspace = input.primaryWorkspace ?? input.fallbackWorkspaces[0] ?? null;
    const repoUrl = primaryWorkspace?.repoUrl ?? null;
    const repoName = deriveRepoNameFromRepoUrl(repoUrl);
    const localFolder = primaryWorkspace?.cwd ?? null;
    const managedFolder = resolveManagedProjectWorkspaceDir({
        companyId: input.companyId,
        projectId: input.projectId,
        repoName,
    });
    return {
        workspaceId: primaryWorkspace?.id ?? null,
        repoUrl,
        repoRef: primaryWorkspace?.repoRef ?? null,
        defaultRef: primaryWorkspace?.defaultRef ?? null,
        repoName,
        localFolder,
        managedFolder,
        effectiveLocalFolder: localFolder ?? managedFolder,
        origin: localFolder ? "local_folder" : "managed_checkout",
    };
}
function pickPrimaryWorkspace(rows, runtimeServicesByWorkspaceId) {
    if (rows.length === 0)
        return null;
    const explicitPrimary = rows.find((row) => row.isPrimary);
    const primary = explicitPrimary ?? rows[0];
    return toWorkspace(primary, runtimeServicesByWorkspaceId?.get(primary.id) ?? []);
}
/** Batch-load workspace refs for a set of projects. */
async function attachWorkspaces(db, rows) {
    if (rows.length === 0)
        return [];
    const projectIds = rows.map((r) => r.id);
    const workspaceRows = await db
        .select()
        .from(projectWorkspaces)
        .where(inArray(projectWorkspaces.projectId, projectIds))
        .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id));
    const runtimeServicesByWorkspaceId = await listWorkspaceRuntimeServicesForProjectWorkspaces(db, rows[0].companyId, workspaceRows.map((workspace) => workspace.id));
    const sharedRuntimeServicesByWorkspaceId = new Map(Array.from(runtimeServicesByWorkspaceId.entries()).map(([workspaceId, services]) => [
        workspaceId,
        services.map(toRuntimeService),
    ]));
    const map = new Map();
    for (const row of workspaceRows) {
        let arr = map.get(row.projectId);
        if (!arr) {
            arr = [];
            map.set(row.projectId, arr);
        }
        arr.push(row);
    }
    return rows.map((row) => {
        const projectWorkspaceRows = map.get(row.id) ?? [];
        const workspaces = projectWorkspaceRows.map((workspace) => toWorkspace(workspace, sharedRuntimeServicesByWorkspaceId.get(workspace.id) ?? []));
        const primaryWorkspace = pickPrimaryWorkspace(projectWorkspaceRows, sharedRuntimeServicesByWorkspaceId);
        return {
            ...row,
            codebase: deriveProjectCodebase({
                companyId: row.companyId,
                projectId: row.id,
                primaryWorkspace,
                fallbackWorkspaces: workspaces,
            }),
            workspaces,
            primaryWorkspace,
        };
    });
}
/** Sync the project_goals join table for a single project. */
async function syncGoalLinks(db, projectId, companyId, goalIds) {
    // Delete existing links
    await db.delete(projectGoals).where(eq(projectGoals.projectId, projectId));
    // Insert new links
    if (goalIds.length > 0) {
        await db.insert(projectGoals).values(goalIds.map((goalId) => ({ projectId, goalId, companyId })));
    }
}
/** Resolve goalIds from input, handling the legacy goalId field. */
function resolveGoalIds(data) {
    if (data.goalIds !== undefined)
        return data.goalIds;
    if (data.goalId !== undefined) {
        return data.goalId ? [data.goalId] : [];
    }
    return undefined;
}
function readNonEmptyString(value) {
    if (typeof value !== "string")
        return null;
    const trimmed = value.trim();
    return trimmed.length > 0 ? trimmed : null;
}
function normalizeWorkspaceCwd(value) {
    const cwd = readNonEmptyString(value);
    if (!cwd)
        return null;
    return cwd === REPO_ONLY_CWD_SENTINEL ? null : cwd;
}
function deriveNameFromCwd(cwd) {
    const normalized = cwd.replace(/[\\/]+$/, "");
    const segments = normalized.split(/[\\/]/).filter(Boolean);
    return segments[segments.length - 1] ?? "Local folder";
}
function deriveNameFromRepoUrl(repoUrl) {
    try {
        const url = new URL(repoUrl);
        const cleanedPath = url.pathname.replace(/\/+$/, "");
        const lastSegment = cleanedPath.split("/").filter(Boolean).pop() ?? "";
        const noGitSuffix = lastSegment.replace(/\.git$/i, "");
        return noGitSuffix || repoUrl;
    }
    catch {
        return repoUrl;
    }
}
function deriveWorkspaceName(input) {
    const explicit = readNonEmptyString(input.name);
    if (explicit)
        return explicit;
    const cwd = readNonEmptyString(input.cwd);
    if (cwd)
        return deriveNameFromCwd(cwd);
    const repoUrl = readNonEmptyString(input.repoUrl);
    if (repoUrl)
        return deriveNameFromRepoUrl(repoUrl);
    return "Workspace";
}
export function resolveProjectNameForUniqueShortname(requestedName, existingProjects, options) {
    const requestedShortname = normalizeProjectUrlKey(requestedName);
    if (!requestedShortname)
        return requestedName;
    // Non-ASCII names get a UUID suffix in deriveProjectUrlKey, making slugs inherently unique.
    if (hasNonAsciiContent(requestedName))
        return requestedName;
    const usedShortnames = new Set(existingProjects
        .filter((project) => !(options?.excludeProjectId && project.id === options.excludeProjectId))
        .map((project) => normalizeProjectUrlKey(project.name))
        .filter((value) => value !== null));
    if (!usedShortnames.has(requestedShortname))
        return requestedName;
    for (let suffix = 2; suffix < 10_000; suffix += 1) {
        const candidateName = `${requestedName} ${suffix}`;
        const candidateShortname = normalizeProjectUrlKey(candidateName);
        if (candidateShortname && !usedShortnames.has(candidateShortname)) {
            return candidateName;
        }
    }
    // Fallback guard for pathological naming collisions.
    return `${requestedName} ${Date.now()}`;
}
async function ensureSinglePrimaryWorkspace(dbOrTx, input) {
    await dbOrTx
        .update(projectWorkspaces)
        .set({ isPrimary: false, updatedAt: new Date() })
        .where(and(eq(projectWorkspaces.companyId, input.companyId), eq(projectWorkspaces.projectId, input.projectId)));
    await dbOrTx
        .update(projectWorkspaces)
        .set({ isPrimary: true, updatedAt: new Date() })
        .where(and(eq(projectWorkspaces.companyId, input.companyId), eq(projectWorkspaces.projectId, input.projectId), eq(projectWorkspaces.id, input.keepWorkspaceId)));
}
export function projectService(db) {
    return {
        list: async (companyId) => {
            const rows = await db.select().from(projects).where(eq(projects.companyId, companyId));
            const withGoals = await attachGoals(db, rows);
            return attachWorkspaces(db, withGoals);
        },
        listByIds: async (companyId, ids) => {
            const dedupedIds = [...new Set(ids)];
            if (dedupedIds.length === 0)
                return [];
            const rows = await db
                .select()
                .from(projects)
                .where(and(eq(projects.companyId, companyId), inArray(projects.id, dedupedIds)));
            const withGoals = await attachGoals(db, rows);
            const withWorkspaces = await attachWorkspaces(db, withGoals);
            const byId = new Map(withWorkspaces.map((project) => [project.id, project]));
            return dedupedIds.map((id) => byId.get(id)).filter((project) => Boolean(project));
        },
        getById: async (id) => {
            const row = await db
                .select()
                .from(projects)
                .where(eq(projects.id, id))
                .then((rows) => rows[0] ?? null);
            if (!row)
                return null;
            const [withGoals] = await attachGoals(db, [row]);
            if (!withGoals)
                return null;
            const [enriched] = await attachWorkspaces(db, [withGoals]);
            return enriched ?? null;
        },
        create: async (companyId, data) => {
            const { goalIds: inputGoalIds, ...projectData } = data;
            const ids = resolveGoalIds({ goalIds: inputGoalIds, goalId: projectData.goalId });
            // Auto-assign a color from the palette if none provided
            if (!projectData.color) {
                const existing = await db.select({ color: projects.color }).from(projects).where(eq(projects.companyId, companyId));
                const usedColors = new Set(existing.map((r) => r.color).filter(Boolean));
                const nextColor = PROJECT_COLORS.find((c) => !usedColors.has(c)) ?? PROJECT_COLORS[existing.length % PROJECT_COLORS.length];
                projectData.color = nextColor;
            }
            const existingProjects = await db
                .select({ id: projects.id, name: projects.name })
                .from(projects)
                .where(eq(projects.companyId, companyId));
            projectData.name = resolveProjectNameForUniqueShortname(projectData.name, existingProjects);
            // Also write goalId to the legacy column (first goal or null)
            const legacyGoalId = ids && ids.length > 0 ? ids[0] : projectData.goalId ?? null;
            const row = await db
                .insert(projects)
                .values({ ...projectData, goalId: legacyGoalId, companyId })
                .returning()
                .then((rows) => rows[0]);
            if (ids && ids.length > 0) {
                await syncGoalLinks(db, row.id, companyId, ids);
            }
            const [withGoals] = await attachGoals(db, [row]);
            const [enriched] = withGoals ? await attachWorkspaces(db, [withGoals]) : [];
            return enriched;
        },
        update: async (id, data) => {
            const { goalIds: inputGoalIds, ...projectData } = data;
            const ids = resolveGoalIds({ goalIds: inputGoalIds, goalId: projectData.goalId });
            const existingProject = await db
                .select({ id: projects.id, companyId: projects.companyId, name: projects.name })
                .from(projects)
                .where(eq(projects.id, id))
                .then((rows) => rows[0] ?? null);
            if (!existingProject)
                return null;
            if (projectData.name !== undefined) {
                const existingShortname = normalizeProjectUrlKey(existingProject.name);
                const nextShortname = normalizeProjectUrlKey(projectData.name);
                if (existingShortname !== nextShortname) {
                    const existingProjects = await db
                        .select({ id: projects.id, name: projects.name })
                        .from(projects)
                        .where(eq(projects.companyId, existingProject.companyId));
                    projectData.name = resolveProjectNameForUniqueShortname(projectData.name, existingProjects, {
                        excludeProjectId: id,
                    });
                }
            }
            // Keep legacy goalId column in sync
            const updates = {
                ...projectData,
                updatedAt: new Date(),
            };
            if (ids !== undefined) {
                updates.goalId = ids.length > 0 ? ids[0] : null;
            }
            const row = await db
                .update(projects)
                .set(updates)
                .where(eq(projects.id, id))
                .returning()
                .then((rows) => rows[0] ?? null);
            if (!row)
                return null;
            if (ids !== undefined) {
                await syncGoalLinks(db, id, row.companyId, ids);
            }
            const [withGoals] = await attachGoals(db, [row]);
            const [enriched] = withGoals ? await attachWorkspaces(db, [withGoals]) : [];
            return enriched ?? null;
        },
        remove: (id) => db
            .delete(projects)
            .where(eq(projects.id, id))
            .returning()
            .then((rows) => {
            const row = rows[0] ?? null;
            if (!row)
                return null;
            return { ...row, urlKey: deriveProjectUrlKey(row.name, row.id) };
        }),
        listWorkspaces: async (projectId) => {
            const rows = await db
                .select()
                .from(projectWorkspaces)
                .where(eq(projectWorkspaces.projectId, projectId))
                .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id));
            if (rows.length === 0)
                return [];
            const runtimeServicesByWorkspaceId = await listWorkspaceRuntimeServicesForProjectWorkspaces(db, rows[0].companyId, rows.map((workspace) => workspace.id));
            return rows.map((row) => toWorkspace(row, (runtimeServicesByWorkspaceId.get(row.id) ?? []).map(toRuntimeService)));
        },
        createWorkspace: async (projectId, data) => {
            const project = await db
                .select()
                .from(projects)
                .where(eq(projects.id, projectId))
                .then((rows) => rows[0] ?? null);
            if (!project)
                return null;
            const cwd = normalizeWorkspaceCwd(data.cwd);
            const repoUrl = readNonEmptyString(data.repoUrl);
            const sourceType = readNonEmptyString(data.sourceType) ?? (repoUrl ? "git_repo" : cwd ? "local_path" : "remote_managed");
            const remoteWorkspaceRef = readNonEmptyString(data.remoteWorkspaceRef);
            if (sourceType === "remote_managed") {
                if (!remoteWorkspaceRef && !repoUrl)
                    return null;
            }
            else if (!cwd && !repoUrl) {
                return null;
            }
            const name = deriveWorkspaceName({
                name: data.name,
                cwd,
                repoUrl,
            });
            const existing = await db
                .select()
                .from(projectWorkspaces)
                .where(eq(projectWorkspaces.projectId, projectId))
                .orderBy(asc(projectWorkspaces.createdAt))
                .then((rows) => rows);
            const shouldBePrimary = data.isPrimary === true || existing.length === 0;
            const created = await db.transaction(async (tx) => {
                if (shouldBePrimary) {
                    await tx
                        .update(projectWorkspaces)
                        .set({ isPrimary: false, updatedAt: new Date() })
                        .where(and(eq(projectWorkspaces.companyId, project.companyId), eq(projectWorkspaces.projectId, projectId)));
                }
                const row = await tx
                    .insert(projectWorkspaces)
                    .values({
                    companyId: project.companyId,
                    projectId,
                    name,
                    sourceType,
                    cwd: cwd ?? null,
                    repoUrl: repoUrl ?? null,
                    repoRef: readNonEmptyString(data.repoRef),
                    defaultRef: readNonEmptyString(data.defaultRef) ?? readNonEmptyString(data.repoRef),
                    visibility: readNonEmptyString(data.visibility) ?? "default",
                    setupCommand: readNonEmptyString(data.setupCommand),
                    cleanupCommand: readNonEmptyString(data.cleanupCommand),
                    remoteProvider: readNonEmptyString(data.remoteProvider),
                    remoteWorkspaceRef,
                    sharedWorkspaceKey: readNonEmptyString(data.sharedWorkspaceKey),
                    metadata: data.runtimeConfig !== undefined
                        ? mergeProjectWorkspaceRuntimeConfig(data.metadata ?? null, data.runtimeConfig ?? null)
                        : data.metadata ?? null,
                    isPrimary: shouldBePrimary,
                })
                    .returning()
                    .then((rows) => rows[0] ?? null);
                return row;
            });
            return created ? toWorkspace(created) : null;
        },
        updateWorkspace: async (projectId, workspaceId, data) => {
            const existing = await db
                .select()
                .from(projectWorkspaces)
                .where(and(eq(projectWorkspaces.id, workspaceId), eq(projectWorkspaces.projectId, projectId)))
                .then((rows) => rows[0] ?? null);
            if (!existing)
                return null;
            const nextCwd = data.cwd !== undefined
                ? normalizeWorkspaceCwd(data.cwd)
                : normalizeWorkspaceCwd(existing.cwd);
            const nextRepoUrl = data.repoUrl !== undefined
                ? readNonEmptyString(data.repoUrl)
                : readNonEmptyString(existing.repoUrl);
            const nextSourceType = data.sourceType !== undefined
                ? readNonEmptyString(data.sourceType)
                : readNonEmptyString(existing.sourceType);
            const nextRemoteWorkspaceRef = data.remoteWorkspaceRef !== undefined
                ? readNonEmptyString(data.remoteWorkspaceRef)
                : readNonEmptyString(existing.remoteWorkspaceRef);
            if (nextSourceType === "remote_managed") {
                if (!nextRemoteWorkspaceRef && !nextRepoUrl)
                    return null;
            }
            else if (!nextCwd && !nextRepoUrl) {
                return null;
            }
            const patch = {
                updatedAt: new Date(),
            };
            if (data.name !== undefined)
                patch.name = deriveWorkspaceName({ name: data.name, cwd: nextCwd, repoUrl: nextRepoUrl });
            if (data.name === undefined && (data.cwd !== undefined || data.repoUrl !== undefined)) {
                patch.name = deriveWorkspaceName({ cwd: nextCwd, repoUrl: nextRepoUrl });
            }
            if (data.cwd !== undefined)
                patch.cwd = nextCwd ?? null;
            if (data.repoUrl !== undefined)
                patch.repoUrl = nextRepoUrl ?? null;
            if (data.repoRef !== undefined)
                patch.repoRef = readNonEmptyString(data.repoRef);
            if (data.sourceType !== undefined && nextSourceType)
                patch.sourceType = nextSourceType;
            if (data.defaultRef !== undefined)
                patch.defaultRef = readNonEmptyString(data.defaultRef);
            if (data.visibility !== undefined && readNonEmptyString(data.visibility)) {
                patch.visibility = readNonEmptyString(data.visibility);
            }
            if (data.setupCommand !== undefined)
                patch.setupCommand = readNonEmptyString(data.setupCommand);
            if (data.cleanupCommand !== undefined)
                patch.cleanupCommand = readNonEmptyString(data.cleanupCommand);
            if (data.remoteProvider !== undefined)
                patch.remoteProvider = readNonEmptyString(data.remoteProvider);
            if (data.remoteWorkspaceRef !== undefined)
                patch.remoteWorkspaceRef = nextRemoteWorkspaceRef;
            if (data.sharedWorkspaceKey !== undefined)
                patch.sharedWorkspaceKey = readNonEmptyString(data.sharedWorkspaceKey);
            if (data.metadata !== undefined || data.runtimeConfig !== undefined) {
                patch.metadata =
                    data.runtimeConfig !== undefined
                        ? mergeProjectWorkspaceRuntimeConfig(data.metadata !== undefined
                            ? data.metadata
                            : (existing.metadata ?? null), data.runtimeConfig ?? null)
                        : data.metadata;
            }
            const updated = await db.transaction(async (tx) => {
                if (data.isPrimary === true) {
                    await tx
                        .update(projectWorkspaces)
                        .set({ isPrimary: false, updatedAt: new Date() })
                        .where(and(eq(projectWorkspaces.companyId, existing.companyId), eq(projectWorkspaces.projectId, projectId)));
                    patch.isPrimary = true;
                }
                else if (data.isPrimary === false) {
                    patch.isPrimary = false;
                }
                const row = await tx
                    .update(projectWorkspaces)
                    .set(patch)
                    .where(eq(projectWorkspaces.id, workspaceId))
                    .returning()
                    .then((rows) => rows[0] ?? null);
                if (!row)
                    return null;
                if (row.isPrimary)
                    return row;
                const hasPrimary = await tx
                    .select({ id: projectWorkspaces.id })
                    .from(projectWorkspaces)
                    .where(and(eq(projectWorkspaces.companyId, row.companyId), eq(projectWorkspaces.projectId, row.projectId), eq(projectWorkspaces.isPrimary, true)))
                    .then((rows) => rows[0] ?? null);
                if (!hasPrimary) {
                    const nextPrimaryCandidate = await tx
                        .select({ id: projectWorkspaces.id })
                        .from(projectWorkspaces)
                        .where(and(eq(projectWorkspaces.companyId, row.companyId), eq(projectWorkspaces.projectId, row.projectId), eq(projectWorkspaces.id, row.id)))
                        .then((rows) => rows[0] ?? null);
                    const alternateCandidate = await tx
                        .select({ id: projectWorkspaces.id })
                        .from(projectWorkspaces)
                        .where(and(eq(projectWorkspaces.companyId, row.companyId), eq(projectWorkspaces.projectId, row.projectId)))
                        .orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
                        .then((rows) => rows.find((candidate) => candidate.id !== row.id) ?? null);
                    await ensureSinglePrimaryWorkspace(tx, {
                        companyId: row.companyId,
                        projectId: row.projectId,
                        keepWorkspaceId: alternateCandidate?.id ?? nextPrimaryCandidate?.id ?? row.id,
                    });
                    const refreshed = await tx
                        .select()
                        .from(projectWorkspaces)
                        .where(eq(projectWorkspaces.id, row.id))
                        .then((rows) => rows[0] ?? row);
                    return refreshed;
                }
                return row;
            });
            return updated ? toWorkspace(updated) : null;
        },
        removeWorkspace: async (projectId, workspaceId) => {
            const existing = await db
                .select()
                .from(projectWorkspaces)
                .where(and(eq(projectWorkspaces.id, workspaceId), eq(projectWorkspaces.projectId, projectId)))
                .then((rows) => rows[0] ?? null);
            if (!existing)
                return null;
            const removed = await db.transaction(async (tx) => {
                const row = await tx
                    .delete(projectWorkspaces)
                    .where(eq(projectWorkspaces.id, workspaceId))
                    .returning()
                    .then((rows) => rows[0] ?? null);
                if (!row)
                    return null;
                if (!row.isPrimary)
                    return row;
                const next = await tx
                    .select()
                    .from(projectWorkspaces)
                    .where(and(eq(projectWorkspaces.companyId, row.companyId), eq(projectWorkspaces.projectId, row.projectId)))
                    .orderBy(asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
                    .limit(1)
                    .then((rows) => rows[0] ?? null);
                if (next) {
                    await ensureSinglePrimaryWorkspace(tx, {
                        companyId: row.companyId,
                        projectId: row.projectId,
                        keepWorkspaceId: next.id,
                    });
                }
                return row;
            });
            return removed ? toWorkspace(removed) : null;
        },
        resolveByReference: async (companyId, reference) => {
            const raw = reference.trim();
            if (raw.length === 0) {
                return { project: null, ambiguous: false };
            }
            if (isUuidLike(raw)) {
                const row = await db
                    .select({ id: projects.id, companyId: projects.companyId, name: projects.name })
                    .from(projects)
                    .where(and(eq(projects.id, raw), eq(projects.companyId, companyId)))
                    .then((rows) => rows[0] ?? null);
                if (!row)
                    return { project: null, ambiguous: false };
                return {
                    project: { id: row.id, companyId: row.companyId, urlKey: deriveProjectUrlKey(row.name, row.id) },
                    ambiguous: false,
                };
            }
            const urlKey = normalizeProjectUrlKey(raw);
            if (!urlKey) {
                return { project: null, ambiguous: false };
            }
            const rows = await db
                .select({ id: projects.id, companyId: projects.companyId, name: projects.name })
                .from(projects)
                .where(eq(projects.companyId, companyId));
            const matches = rows.filter((row) => deriveProjectUrlKey(row.name, row.id) === urlKey);
            if (matches.length === 1) {
                const match = matches[0];
                return {
                    project: { id: match.id, companyId: match.companyId, urlKey: deriveProjectUrlKey(match.name, match.id) },
                    ambiguous: false,
                };
            }
            if (matches.length > 1) {
                return { project: null, ambiguous: true };
            }
            return { project: null, ambiguous: false };
        },
    };
}
//# sourceMappingURL=projects.js.map