import { and, asc, desc, eq } from "drizzle-orm";
import { documentRevisions, documents, issueDocuments, issues } from "@zmeel/db";
import { issueDocumentKeySchema } from "@zmeel/shared";
import { conflict, notFound, unprocessable } from "../errors.js";
function normalizeDocumentKey(key) {
    const normalized = key.trim().toLowerCase();
    const parsed = issueDocumentKeySchema.safeParse(normalized);
    if (!parsed.success) {
        throw unprocessable("Invalid document key", parsed.error.issues);
    }
    return parsed.data;
}
function isUniqueViolation(error) {
    return !!error && typeof error === "object" && "code" in error && error.code === "23505";
}
export function extractLegacyPlanBody(description) {
    if (!description)
        return null;
    const match = /<plan>\s*([\s\S]*?)\s*<\/plan>/i.exec(description);
    if (!match)
        return null;
    const body = match[1]?.trim();
    return body ? body : null;
}
function mapIssueDocumentRow(row, includeBody) {
    return {
        id: row.id,
        companyId: row.companyId,
        issueId: row.issueId,
        key: row.key,
        title: row.title,
        format: row.format,
        ...(includeBody ? { body: row.latestBody } : {}),
        latestRevisionId: row.latestRevisionId ?? null,
        latestRevisionNumber: row.latestRevisionNumber,
        createdByAgentId: row.createdByAgentId,
        createdByUserId: row.createdByUserId,
        updatedByAgentId: row.updatedByAgentId,
        updatedByUserId: row.updatedByUserId,
        createdAt: row.createdAt,
        updatedAt: row.updatedAt,
    };
}
const issueDocumentSelect = {
    id: documents.id,
    companyId: documents.companyId,
    issueId: issueDocuments.issueId,
    key: issueDocuments.key,
    title: documents.title,
    format: documents.format,
    latestBody: documents.latestBody,
    latestRevisionId: documents.latestRevisionId,
    latestRevisionNumber: documents.latestRevisionNumber,
    createdByAgentId: documents.createdByAgentId,
    createdByUserId: documents.createdByUserId,
    updatedByAgentId: documents.updatedByAgentId,
    updatedByUserId: documents.updatedByUserId,
    createdAt: documents.createdAt,
    updatedAt: documents.updatedAt,
};
export function documentService(db) {
    return {
        getIssueDocumentPayload: async (issue) => {
            const [planDocument, documentSummaries] = await Promise.all([
                db
                    .select(issueDocumentSelect)
                    .from(issueDocuments)
                    .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                    .where(and(eq(issueDocuments.issueId, issue.id), eq(issueDocuments.key, "plan")))
                    .then((rows) => rows[0] ?? null),
                db
                    .select(issueDocumentSelect)
                    .from(issueDocuments)
                    .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                    .where(eq(issueDocuments.issueId, issue.id))
                    .orderBy(asc(issueDocuments.key), desc(documents.updatedAt)),
            ]);
            const legacyPlanBody = planDocument ? null : extractLegacyPlanBody(issue.description);
            return {
                planDocument: planDocument ? mapIssueDocumentRow(planDocument, true) : null,
                documentSummaries: documentSummaries.map((row) => mapIssueDocumentRow(row, false)),
                legacyPlanDocument: legacyPlanBody
                    ? {
                        key: "plan",
                        body: legacyPlanBody,
                        source: "issue_description",
                    }
                    : null,
            };
        },
        listIssueDocuments: async (issueId) => {
            const rows = await db
                .select(issueDocumentSelect)
                .from(issueDocuments)
                .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                .where(eq(issueDocuments.issueId, issueId))
                .orderBy(asc(issueDocuments.key), desc(documents.updatedAt));
            return rows.map((row) => mapIssueDocumentRow(row, true));
        },
        getIssueDocumentByKey: async (issueId, rawKey) => {
            const key = normalizeDocumentKey(rawKey);
            const row = await db
                .select(issueDocumentSelect)
                .from(issueDocuments)
                .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                .where(and(eq(issueDocuments.issueId, issueId), eq(issueDocuments.key, key)))
                .then((rows) => rows[0] ?? null);
            return row ? mapIssueDocumentRow(row, true) : null;
        },
        listIssueDocumentRevisions: async (issueId, rawKey) => {
            const key = normalizeDocumentKey(rawKey);
            return db
                .select({
                id: documentRevisions.id,
                companyId: documentRevisions.companyId,
                documentId: documentRevisions.documentId,
                issueId: issueDocuments.issueId,
                key: issueDocuments.key,
                revisionNumber: documentRevisions.revisionNumber,
                title: documentRevisions.title,
                format: documentRevisions.format,
                body: documentRevisions.body,
                changeSummary: documentRevisions.changeSummary,
                createdByAgentId: documentRevisions.createdByAgentId,
                createdByUserId: documentRevisions.createdByUserId,
                createdAt: documentRevisions.createdAt,
            })
                .from(issueDocuments)
                .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                .innerJoin(documentRevisions, eq(documentRevisions.documentId, documents.id))
                .where(and(eq(issueDocuments.issueId, issueId), eq(issueDocuments.key, key)))
                .orderBy(desc(documentRevisions.revisionNumber));
        },
        upsertIssueDocument: async (input) => {
            const key = normalizeDocumentKey(input.key);
            const issue = await db
                .select({ id: issues.id, companyId: issues.companyId })
                .from(issues)
                .where(eq(issues.id, input.issueId))
                .then((rows) => rows[0] ?? null);
            if (!issue)
                throw notFound("Issue not found");
            try {
                return await db.transaction(async (tx) => {
                    const now = new Date();
                    const existing = await tx
                        .select({
                        id: documents.id,
                        companyId: documents.companyId,
                        issueId: issueDocuments.issueId,
                        key: issueDocuments.key,
                        title: documents.title,
                        format: documents.format,
                        latestBody: documents.latestBody,
                        latestRevisionId: documents.latestRevisionId,
                        latestRevisionNumber: documents.latestRevisionNumber,
                        createdByAgentId: documents.createdByAgentId,
                        createdByUserId: documents.createdByUserId,
                        updatedByAgentId: documents.updatedByAgentId,
                        updatedByUserId: documents.updatedByUserId,
                        createdAt: documents.createdAt,
                        updatedAt: documents.updatedAt,
                    })
                        .from(issueDocuments)
                        .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                        .where(and(eq(issueDocuments.issueId, issue.id), eq(issueDocuments.key, key)))
                        .then((rows) => rows[0] ?? null);
                    if (existing) {
                        if (!input.baseRevisionId) {
                            throw conflict("Document update requires baseRevisionId", {
                                currentRevisionId: existing.latestRevisionId,
                            });
                        }
                        if (input.baseRevisionId !== existing.latestRevisionId) {
                            throw conflict("Document was updated by someone else", {
                                currentRevisionId: existing.latestRevisionId,
                            });
                        }
                        const nextRevisionNumber = existing.latestRevisionNumber + 1;
                        const [revision] = await tx
                            .insert(documentRevisions)
                            .values({
                            companyId: issue.companyId,
                            documentId: existing.id,
                            revisionNumber: nextRevisionNumber,
                            title: input.title ?? null,
                            format: input.format,
                            body: input.body,
                            changeSummary: input.changeSummary ?? null,
                            createdByAgentId: input.createdByAgentId ?? null,
                            createdByUserId: input.createdByUserId ?? null,
                            createdByRunId: input.createdByRunId ?? null,
                            createdAt: now,
                        })
                            .returning();
                        await tx
                            .update(documents)
                            .set({
                            title: input.title ?? null,
                            format: input.format,
                            latestBody: input.body,
                            latestRevisionId: revision.id,
                            latestRevisionNumber: nextRevisionNumber,
                            updatedByAgentId: input.createdByAgentId ?? null,
                            updatedByUserId: input.createdByUserId ?? null,
                            updatedAt: now,
                        })
                            .where(eq(documents.id, existing.id));
                        await tx
                            .update(issueDocuments)
                            .set({ updatedAt: now })
                            .where(eq(issueDocuments.documentId, existing.id));
                        return {
                            created: false,
                            document: {
                                ...existing,
                                title: input.title ?? null,
                                format: input.format,
                                body: input.body,
                                latestRevisionId: revision.id,
                                latestRevisionNumber: nextRevisionNumber,
                                updatedByAgentId: input.createdByAgentId ?? null,
                                updatedByUserId: input.createdByUserId ?? null,
                                updatedAt: now,
                            },
                        };
                    }
                    if (input.baseRevisionId) {
                        throw conflict("Document does not exist yet", { key });
                    }
                    const [document] = await tx
                        .insert(documents)
                        .values({
                        companyId: issue.companyId,
                        title: input.title ?? null,
                        format: input.format,
                        latestBody: input.body,
                        latestRevisionId: null,
                        latestRevisionNumber: 1,
                        createdByAgentId: input.createdByAgentId ?? null,
                        createdByUserId: input.createdByUserId ?? null,
                        updatedByAgentId: input.createdByAgentId ?? null,
                        updatedByUserId: input.createdByUserId ?? null,
                        createdAt: now,
                        updatedAt: now,
                    })
                        .returning();
                    const [revision] = await tx
                        .insert(documentRevisions)
                        .values({
                        companyId: issue.companyId,
                        documentId: document.id,
                        revisionNumber: 1,
                        title: input.title ?? null,
                        format: input.format,
                        body: input.body,
                        changeSummary: input.changeSummary ?? null,
                        createdByAgentId: input.createdByAgentId ?? null,
                        createdByUserId: input.createdByUserId ?? null,
                        createdByRunId: input.createdByRunId ?? null,
                        createdAt: now,
                    })
                        .returning();
                    await tx
                        .update(documents)
                        .set({ latestRevisionId: revision.id })
                        .where(eq(documents.id, document.id));
                    await tx.insert(issueDocuments).values({
                        companyId: issue.companyId,
                        issueId: issue.id,
                        documentId: document.id,
                        key,
                        createdAt: now,
                        updatedAt: now,
                    });
                    return {
                        created: true,
                        document: {
                            id: document.id,
                            companyId: issue.companyId,
                            issueId: issue.id,
                            key,
                            title: document.title,
                            format: document.format,
                            body: document.latestBody,
                            latestRevisionId: revision.id,
                            latestRevisionNumber: 1,
                            createdByAgentId: document.createdByAgentId,
                            createdByUserId: document.createdByUserId,
                            updatedByAgentId: document.updatedByAgentId,
                            updatedByUserId: document.updatedByUserId,
                            createdAt: document.createdAt,
                            updatedAt: document.updatedAt,
                        },
                    };
                });
            }
            catch (error) {
                if (isUniqueViolation(error)) {
                    throw conflict("Document key already exists on this issue", { key });
                }
                throw error;
            }
        },
        restoreIssueDocumentRevision: async (input) => {
            const key = normalizeDocumentKey(input.key);
            return db.transaction(async (tx) => {
                const existing = await tx
                    .select(issueDocumentSelect)
                    .from(issueDocuments)
                    .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                    .where(and(eq(issueDocuments.issueId, input.issueId), eq(issueDocuments.key, key)))
                    .then((rows) => rows[0] ?? null);
                if (!existing)
                    throw notFound("Document not found");
                const revision = await tx
                    .select({
                    id: documentRevisions.id,
                    companyId: documentRevisions.companyId,
                    documentId: documentRevisions.documentId,
                    revisionNumber: documentRevisions.revisionNumber,
                    title: documentRevisions.title,
                    format: documentRevisions.format,
                    body: documentRevisions.body,
                })
                    .from(documentRevisions)
                    .where(and(eq(documentRevisions.id, input.revisionId), eq(documentRevisions.documentId, existing.id)))
                    .then((rows) => rows[0] ?? null);
                if (!revision)
                    throw notFound("Document revision not found");
                if (existing.latestRevisionId === revision.id) {
                    throw conflict("Selected revision is already the latest revision", {
                        currentRevisionId: existing.latestRevisionId,
                    });
                }
                const now = new Date();
                const nextRevisionNumber = existing.latestRevisionNumber + 1;
                const [restoredRevision] = await tx
                    .insert(documentRevisions)
                    .values({
                    companyId: existing.companyId,
                    documentId: existing.id,
                    revisionNumber: nextRevisionNumber,
                    title: revision.title ?? null,
                    format: revision.format,
                    body: revision.body,
                    changeSummary: `Restored from revision ${revision.revisionNumber}`,
                    createdByAgentId: input.createdByAgentId ?? null,
                    createdByUserId: input.createdByUserId ?? null,
                    createdAt: now,
                })
                    .returning();
                await tx
                    .update(documents)
                    .set({
                    title: revision.title ?? null,
                    format: revision.format,
                    latestBody: revision.body,
                    latestRevisionId: restoredRevision.id,
                    latestRevisionNumber: nextRevisionNumber,
                    updatedByAgentId: input.createdByAgentId ?? null,
                    updatedByUserId: input.createdByUserId ?? null,
                    updatedAt: now,
                })
                    .where(eq(documents.id, existing.id));
                await tx
                    .update(issueDocuments)
                    .set({ updatedAt: now })
                    .where(eq(issueDocuments.documentId, existing.id));
                return {
                    restoredFromRevisionId: revision.id,
                    restoredFromRevisionNumber: revision.revisionNumber,
                    document: {
                        ...existing,
                        title: revision.title ?? null,
                        format: revision.format,
                        body: revision.body,
                        latestRevisionId: restoredRevision.id,
                        latestRevisionNumber: nextRevisionNumber,
                        updatedByAgentId: input.createdByAgentId ?? null,
                        updatedByUserId: input.createdByUserId ?? null,
                        updatedAt: now,
                    },
                };
            });
        },
        deleteIssueDocument: async (issueId, rawKey) => {
            const key = normalizeDocumentKey(rawKey);
            return db.transaction(async (tx) => {
                const existing = await tx
                    .select(issueDocumentSelect)
                    .from(issueDocuments)
                    .innerJoin(documents, eq(issueDocuments.documentId, documents.id))
                    .where(and(eq(issueDocuments.issueId, issueId), eq(issueDocuments.key, key)))
                    .then((rows) => rows[0] ?? null);
                if (!existing)
                    return null;
                await tx.delete(issueDocuments).where(eq(issueDocuments.documentId, existing.id));
                await tx.delete(documents).where(eq(documents.id, existing.id));
                return {
                    ...existing,
                    body: existing.latestBody,
                    latestRevisionId: existing.latestRevisionId ?? null,
                };
            });
        },
    };
}
//# sourceMappingURL=documents.js.map