import { and, count, eq, gte, inArray, lt, sql } from "drizzle-orm";
import { companies, companyLogos, assets, agents, agentApiKeys, agentRuntimeState, agentTaskSessions, agentWakeupRequests, issues, issueComments, projects, goals, heartbeatRuns, heartbeatRunEvents, costEvents, financeEvents, approvalComments, approvals, activityLog, companySecrets, joinRequests, invites, principalPermissionGrants, companyMemberships, } from "@zmeel/db";
import { notFound, unprocessable } from "../errors.js";
export function companyService(db) {
    const ISSUE_PREFIX_FALLBACK = "CMP";
    const companySelection = {
        id: companies.id,
        name: companies.name,
        description: companies.description,
        status: companies.status,
        issuePrefix: companies.issuePrefix,
        issueCounter: companies.issueCounter,
        budgetMonthlyCents: companies.budgetMonthlyCents,
        spentMonthlyCents: companies.spentMonthlyCents,
        requireBoardApprovalForNewAgents: companies.requireBoardApprovalForNewAgents,
        feedbackDataSharingEnabled: companies.feedbackDataSharingEnabled,
        feedbackDataSharingConsentAt: companies.feedbackDataSharingConsentAt,
        feedbackDataSharingConsentByUserId: companies.feedbackDataSharingConsentByUserId,
        feedbackDataSharingTermsVersion: companies.feedbackDataSharingTermsVersion,
        brandColor: companies.brandColor,
        logoAssetId: companyLogos.assetId,
        createdAt: companies.createdAt,
        updatedAt: companies.updatedAt,
    };
    function enrichCompany(company) {
        return {
            ...company,
            logoUrl: company.logoAssetId ? `/api/assets/${company.logoAssetId}/content` : null,
        };
    }
    function currentUtcMonthWindow(now = new Date()) {
        const year = now.getUTCFullYear();
        const month = now.getUTCMonth();
        return {
            start: new Date(Date.UTC(year, month, 1, 0, 0, 0, 0)),
            end: new Date(Date.UTC(year, month + 1, 1, 0, 0, 0, 0)),
        };
    }
    async function getMonthlySpendByCompanyIds(companyIds, database = db) {
        if (companyIds.length === 0)
            return new Map();
        const { start, end } = currentUtcMonthWindow();
        const rows = await database
            .select({
            companyId: costEvents.companyId,
            spentMonthlyCents: sql `coalesce(sum(${costEvents.costCents}), 0)::int`,
        })
            .from(costEvents)
            .where(and(inArray(costEvents.companyId, companyIds), gte(costEvents.occurredAt, start), lt(costEvents.occurredAt, end)))
            .groupBy(costEvents.companyId);
        return new Map(rows.map((row) => [row.companyId, Number(row.spentMonthlyCents ?? 0)]));
    }
    async function hydrateCompanySpend(rows, database = db) {
        const spendByCompanyId = await getMonthlySpendByCompanyIds(rows.map((row) => row.id), database);
        return rows.map((row) => ({
            ...row,
            spentMonthlyCents: spendByCompanyId.get(row.id) ?? 0,
        }));
    }
    function getCompanyQuery(database) {
        return database
            .select(companySelection)
            .from(companies)
            .leftJoin(companyLogos, eq(companyLogos.companyId, companies.id));
    }
    function deriveIssuePrefixBase(name) {
        const normalized = name.toUpperCase().replace(/[^A-Z]/g, "");
        return normalized.slice(0, 3) || ISSUE_PREFIX_FALLBACK;
    }
    function suffixForAttempt(attempt) {
        if (attempt <= 1)
            return "";
        return "A".repeat(attempt - 1);
    }
    function isIssuePrefixConflict(error) {
        const constraint = typeof error === "object" && error !== null && "constraint" in error
            ? error.constraint
            : typeof error === "object" && error !== null && "constraint_name" in error
                ? error.constraint_name
                : undefined;
        return typeof error === "object"
            && error !== null
            && "code" in error
            && error.code === "23505"
            && constraint === "companies_issue_prefix_idx";
    }
    async function createCompanyWithUniquePrefix(data) {
        const base = deriveIssuePrefixBase(data.name);
        let suffix = 1;
        while (suffix < 10000) {
            const candidate = `${base}${suffixForAttempt(suffix)}`;
            try {
                const rows = await db
                    .insert(companies)
                    .values({ ...data, issuePrefix: candidate })
                    .returning();
                return rows[0];
            }
            catch (error) {
                if (!isIssuePrefixConflict(error))
                    throw error;
            }
            suffix += 1;
        }
        throw new Error("Unable to allocate unique issue prefix");
    }
    return {
        list: async () => {
            const rows = await getCompanyQuery(db);
            const hydrated = await hydrateCompanySpend(rows);
            return hydrated.map((row) => enrichCompany(row));
        },
        getById: async (id) => {
            const row = await getCompanyQuery(db)
                .where(eq(companies.id, id))
                .then((rows) => rows[0] ?? null);
            if (!row)
                return null;
            const [hydrated] = await hydrateCompanySpend([row], db);
            return enrichCompany(hydrated);
        },
        create: async (data) => {
            const created = await createCompanyWithUniquePrefix(data);
            const row = await getCompanyQuery(db)
                .where(eq(companies.id, created.id))
                .then((rows) => rows[0] ?? null);
            if (!row)
                throw notFound("Company not found after creation");
            const [hydrated] = await hydrateCompanySpend([row], db);
            return enrichCompany(hydrated);
        },
        update: (id, data) => db.transaction(async (tx) => {
            const existing = await getCompanyQuery(tx)
                .where(eq(companies.id, id))
                .then((rows) => rows[0] ?? null);
            if (!existing)
                return null;
            const { logoAssetId, ...companyPatch } = data;
            if (logoAssetId !== undefined && logoAssetId !== null) {
                const nextLogoAsset = await tx
                    .select({ id: assets.id, companyId: assets.companyId })
                    .from(assets)
                    .where(eq(assets.id, logoAssetId))
                    .then((rows) => rows[0] ?? null);
                if (!nextLogoAsset)
                    throw notFound("Logo asset not found");
                if (nextLogoAsset.companyId !== existing.id) {
                    throw unprocessable("Logo asset must belong to the same company");
                }
            }
            const updated = await tx
                .update(companies)
                .set({ ...companyPatch, updatedAt: new Date() })
                .where(eq(companies.id, id))
                .returning()
                .then((rows) => rows[0] ?? null);
            if (!updated)
                return null;
            if (logoAssetId === null) {
                await tx.delete(companyLogos).where(eq(companyLogos.companyId, id));
            }
            else if (logoAssetId !== undefined) {
                await tx
                    .insert(companyLogos)
                    .values({
                    companyId: id,
                    assetId: logoAssetId,
                })
                    .onConflictDoUpdate({
                    target: companyLogos.companyId,
                    set: {
                        assetId: logoAssetId,
                        updatedAt: new Date(),
                    },
                });
            }
            if (logoAssetId !== undefined && existing.logoAssetId && existing.logoAssetId !== logoAssetId) {
                await tx.delete(assets).where(eq(assets.id, existing.logoAssetId));
            }
            const [hydrated] = await hydrateCompanySpend([{
                    ...updated,
                    logoAssetId: logoAssetId === undefined ? existing.logoAssetId : logoAssetId,
                }], tx);
            return enrichCompany(hydrated);
        }),
        archive: (id) => db.transaction(async (tx) => {
            const updated = await tx
                .update(companies)
                .set({ status: "archived", updatedAt: new Date() })
                .where(eq(companies.id, id))
                .returning()
                .then((rows) => rows[0] ?? null);
            if (!updated)
                return null;
            const row = await getCompanyQuery(tx)
                .where(eq(companies.id, id))
                .then((rows) => rows[0] ?? null);
            if (!row)
                return null;
            const [hydrated] = await hydrateCompanySpend([row], tx);
            return enrichCompany(hydrated);
        }),
        remove: (id) => db.transaction(async (tx) => {
            // Delete from child tables in dependency order
            await tx.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.companyId, id));
            await tx.delete(agentTaskSessions).where(eq(agentTaskSessions.companyId, id));
            await tx.delete(heartbeatRuns).where(eq(heartbeatRuns.companyId, id));
            await tx.delete(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, id));
            await tx.delete(agentApiKeys).where(eq(agentApiKeys.companyId, id));
            await tx.delete(agentRuntimeState).where(eq(agentRuntimeState.companyId, id));
            await tx.delete(issueComments).where(eq(issueComments.companyId, id));
            await tx.delete(costEvents).where(eq(costEvents.companyId, id));
            await tx.delete(financeEvents).where(eq(financeEvents.companyId, id));
            await tx.delete(approvalComments).where(eq(approvalComments.companyId, id));
            await tx.delete(approvals).where(eq(approvals.companyId, id));
            await tx.delete(companySecrets).where(eq(companySecrets.companyId, id));
            await tx.delete(joinRequests).where(eq(joinRequests.companyId, id));
            await tx.delete(invites).where(eq(invites.companyId, id));
            await tx.delete(principalPermissionGrants).where(eq(principalPermissionGrants.companyId, id));
            await tx.delete(companyMemberships).where(eq(companyMemberships.companyId, id));
            await tx.delete(issues).where(eq(issues.companyId, id));
            await tx.delete(companyLogos).where(eq(companyLogos.companyId, id));
            await tx.delete(assets).where(eq(assets.companyId, id));
            await tx.delete(goals).where(eq(goals.companyId, id));
            await tx.delete(projects).where(eq(projects.companyId, id));
            await tx.delete(agents).where(eq(agents.companyId, id));
            await tx.delete(activityLog).where(eq(activityLog.companyId, id));
            const rows = await tx
                .delete(companies)
                .where(eq(companies.id, id))
                .returning();
            return rows[0] ?? null;
        }),
        stats: () => Promise.all([
            db
                .select({ companyId: agents.companyId, count: count() })
                .from(agents)
                .groupBy(agents.companyId),
            db
                .select({ companyId: issues.companyId, count: count() })
                .from(issues)
                .groupBy(issues.companyId),
        ]).then(([agentRows, issueRows]) => {
            const result = {};
            for (const row of agentRows) {
                result[row.companyId] = { agentCount: row.count, issueCount: 0 };
            }
            for (const row of issueRows) {
                if (result[row.companyId]) {
                    result[row.companyId].issueCount = row.count;
                }
                else {
                    result[row.companyId] = { agentCount: 0, issueCount: row.count };
                }
            }
            return result;
        }),
    };
}
//# sourceMappingURL=companies.js.map