import { updateDocument } from '../../actions.js';
import { logEvent } from '../../security/audit.js';
import { schedulingCronHandler } from '../../scheduling/index.js';
import { isAuthorizedCronRequest, processCleanup, processSeoScan } from '../../cron/index.js';
import { processContentHealthScan } from '../../content-health/cron.js';
import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { json, errorResponse, internalError, clampPageSize, requireAuth, buildActionContext, WRITE_ROLES, ADMIN_ROLES, requireRole, } from '../route-helpers.js';
export function registerSchedulingRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Workflow routes
    // ---------------------------------------------------------------------------
    router.post('/collections/:slug/:id/workflow', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const body = (await request.json());
            if (!body.stage)
                return errorResponse('Stage is required', 400);
            const { transitionDocument } = await import('../../workflow/index.js');
            const result = await transitionDocument(params.id, body.stage, auth.session.userId, auth.session.role, body.note);
            if (!result.success)
                return errorResponse(result.error ?? 'Transition failed', 400);
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.get('/collections/:slug/:id/workflow', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const { getAvailableTransitions } = await import('../../workflow/index.js');
            const doc = await db().document.findFirst({
                where: { id: params.id, deletedAt: null },
                select: { workflowStage: true, reviewerId: true, reviewNote: true },
            });
            if (!doc)
                return errorResponse('Document not found', 404);
            const stage = (doc.workflowStage ?? 'DRAFT');
            const transitions = getAvailableTransitions(stage, auth.session.role);
            return json({
                data: { stage, transitions, reviewerId: doc.reviewerId, reviewNote: doc.reviewNote },
            });
        }
        catch (err) {
            return internalError(err);
        }
    });
    // ---------------------------------------------------------------------------
    // Version history routes
    // ---------------------------------------------------------------------------
    router.get('/collections/:slug/:id/versions', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const url = new URL(request.url);
            const page = Number(url.searchParams.get('page')) || 1;
            const pageSize = clampPageSize(url.searchParams.get('pageSize'), 50, 20);
            const skip = (page - 1) * pageSize;
            const doc = await db().document.findFirst({
                where: { id: params.id, collection: params.slug },
            });
            if (!doc)
                return errorResponse('Document not found', 404);
            const [versions, total] = await Promise.all([
                db().version.findMany({
                    // See dashboard `recent docs` query for the rationale: `'join'`
                    // folds the User include into one round-trip, halving the cost
                    // of the versions list on a cross-region Function -> Postgres
                    // path.
                    relationLoadStrategy: 'join',
                    where: { documentId: params.id },
                    orderBy: { createdAt: 'desc' },
                    skip,
                    take: pageSize,
                    include: {
                        changedBy: { select: { id: true, name: true, email: true } },
                    },
                }),
                db().version.count({ where: { documentId: params.id } }),
            ]);
            return json({
                data: { versions, total, page, pageSize, totalPages: Math.ceil(total / pageSize) },
            });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.post('/collections/:slug/:id/versions/:versionId/restore', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            const version = await db().version.findFirst({
                where: { id: params.versionId, documentId: params.id },
            });
            if (!version)
                return errorResponse('Version not found', 404);
            const ctx = buildActionContext(auth.session, db());
            const doc = await updateDocument(params.slug, params.id, version.data, ctx);
            await logEvent({
                event: 'version_restored',
                userId: auth.session.userId,
                details: { collection: params.slug, documentId: params.id, versionId: params.versionId },
            });
            return json({ data: doc });
        }
        catch (err) {
            return internalError(err, 'restore version');
        }
    });
    // ---------------------------------------------------------------------------
    // Per-document scheduling — sets `scheduledAt` (when to publish) and/or
    // `scheduledUnpublishAt` (when to unpublish). The actual transitions are
    // performed by the scheduling cron (`/cron/publish`). Decoupling the
    // schedule from the publish action means a missed cron tick just delays
    // the publish — it can never accidentally publish twice or skip a doc.
    // ---------------------------------------------------------------------------
    router.post('/collections/:slug/:id/schedule', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            const body = (await request.json());
            const parseFuture = (v, label) => {
                if (v === null || v === undefined)
                    return null;
                const d = new Date(v);
                if (Number.isNaN(d.getTime())) {
                    throw new Error(`${label} is not a valid ISO date`);
                }
                if (d.getTime() <= Date.now()) {
                    throw new Error(`${label} must be in the future`);
                }
                return d;
            };
            let publishAt;
            let unpublishAt;
            try {
                publishAt = parseFuture(body.scheduledAt, 'scheduledAt');
                unpublishAt = parseFuture(body.scheduledUnpublishAt, 'scheduledUnpublishAt');
            }
            catch (err) {
                return errorResponse(err instanceof Error ? err.message : 'Invalid schedule', 400);
            }
            if (!publishAt && !unpublishAt) {
                return errorResponse('At least one of scheduledAt or scheduledUnpublishAt is required', 400);
            }
            if (publishAt && unpublishAt && unpublishAt <= publishAt) {
                return errorResponse('scheduledUnpublishAt must be after scheduledAt', 400);
            }
            const doc = await db().document.findFirst({
                where: { id: params.id, collection: params.slug, deletedAt: null },
            });
            if (!doc)
                return errorResponse('Document not found', 404);
            // If we're scheduling a future publish, force status → SCHEDULED so the
            // cron picks it up. If we're only scheduling an unpublish on an already
            // published doc we leave status as PUBLISHED.
            const nextStatus = publishAt ? 'SCHEDULED' : doc.status;
            const updateData = {
                updatedById: auth.session.userId,
            };
            if (publishAt) {
                updateData.scheduledAt = publishAt;
                updateData.status = nextStatus;
            }
            if (body.scheduledUnpublishAt !== undefined) {
                updateData.scheduledUnpublishAt = unpublishAt;
            }
            const updated = await db().document.update({
                where: { id: doc.id },
                data: updateData,
            });
            await logEvent({
                event: 'document_scheduled',
                userId: auth.session.userId,
                details: {
                    collection: params.slug,
                    documentId: doc.id,
                    scheduledAt: publishAt?.toISOString() ?? null,
                    scheduledUnpublishAt: unpublishAt?.toISOString() ?? null,
                },
            });
            return json({
                data: {
                    id: updated.id,
                    status: updated.status,
                    scheduledAt: updated.scheduledAt,
                    scheduledUnpublishAt: updated.scheduledUnpublishAt,
                },
            });
        }
        catch (err) {
            return internalError(err, 'schedule document');
        }
    });
    router.delete('/collections/:slug/:id/schedule', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            const doc = await db().document.findFirst({
                where: { id: params.id, collection: params.slug, deletedAt: null },
            });
            if (!doc)
                return errorResponse('Document not found', 404);
            // If the doc was SCHEDULED (not yet published) cancelling drops it
            // back to DRAFT. PUBLISHED docs with a future unpublish stay live.
            const nextStatus = doc.status === 'SCHEDULED' ? 'DRAFT' : doc.status;
            const updated = await db().document.update({
                where: { id: doc.id },
                data: {
                    scheduledAt: null,
                    scheduledUnpublishAt: null,
                    status: nextStatus,
                    updatedById: auth.session.userId,
                },
            });
            await logEvent({
                event: 'document_schedule_cancelled',
                userId: auth.session.userId,
                details: { collection: params.slug, documentId: doc.id },
            });
            return json({
                data: {
                    id: updated.id,
                    status: updated.status,
                    scheduledAt: null,
                    scheduledUnpublishAt: null,
                },
            });
        }
        catch (err) {
            return internalError(err, 'cancel schedule');
        }
    });
    // ---------------------------------------------------------------------------
    // Scheduling routes
    // ---------------------------------------------------------------------------
    router.post('/scheduling/run', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
            if (roleErr)
                return roleErr;
            const result = await schedulingCronHandler(db());
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'scheduling run');
        }
    });
    // ---------------------------------------------------------------------------
    // Cron endpoints — auth via `Authorization: Bearer ${CRON_SECRET}`.
    //
    // **HTTP method:** registered as GET because Vercel Cron sends GET requests
    // (https://vercel.com/docs/cron-jobs). We also register POST aliases so
    // self-hosted schedulers that POST (k8s CronJob, GH Actions step running
    // `curl -X POST`, EventBridge HTTP target) keep working without changes.
    //
    // Vercel Cron sets the `Authorization: Bearer <CRON_SECRET>` header
    // automatically when `CRON_SECRET` is defined in the project environment.
    // When CRON_SECRET is unset, requests are rejected — fail-closed so a
    // misconfigured deploy never exposes these to the public.
    //
    // CSRF is NOT required: these endpoints have no session and the body carries
    // no admin intent — auth is exclusively via the Bearer token. They are also
    // listed in `CSRF_EXEMPT_PATHS` in handler-factory.ts so the global CSRF
    // gate doesn't block them.
    // ---------------------------------------------------------------------------
    const cronPublish = async (request) => {
        try {
            if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
                return errorResponse('Unauthorized', 401);
            }
            const result = await schedulingCronHandler(db());
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'cron publish');
        }
    };
    router.get('/cron/publish', cronPublish);
    router.post('/cron/publish', cronPublish);
    const cronCleanup = async (request) => {
        try {
            if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
                return errorResponse('Unauthorized', 401);
            }
            const retentionDays = getActuateConfig()?.forms?.retentionDays;
            const formSubmissionRetentionMs = typeof retentionDays === 'number' && retentionDays > 0
                ? retentionDays * 24 * 60 * 60 * 1000
                : 0;
            const result = await processCleanup(db(), { formSubmissionRetentionMs });
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'cron cleanup');
        }
    };
    router.get('/cron/cleanup', cronCleanup);
    router.post('/cron/cleanup', cronCleanup);
    const cronSeoScan = async (request) => {
        try {
            if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
                return errorResponse('Unauthorized', 401);
            }
            const { frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
            const allowedCollections = Object.keys(frontEndContentCollectionTypes(getActuateConfig()));
            const result = await processSeoScan(db(), { allowedCollections });
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'cron seo-scan');
        }
    };
    router.get('/cron/seo-scan', cronSeoScan);
    router.post('/cron/seo-scan', cronSeoScan);
    const cronContentHealth = async (request) => {
        try {
            if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
                return errorResponse('Unauthorized', 401);
            }
            const config = getActuateConfig();
            const siteOrigin = config?.site?.url ?? config?.seo?.siteUrl ?? process.env.NEXT_PUBLIC_SITE_URL ?? null;
            const result = await processContentHealthScan(db(), { siteOrigin });
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'cron content-health');
        }
    };
    router.get('/cron/content-health', cronContentHealth);
    router.post('/cron/content-health', cronContentHealth);
    router.get('/scheduling/calendar', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const url = new URL(request.url);
            const fromStr = url.searchParams.get('from');
            const toStr = url.searchParams.get('to');
            const from = fromStr ? new Date(fromStr) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
            const to = toStr ? new Date(toStr) : new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
            const { getScheduleCalendar } = await import('../../scheduling/index.js');
            const entries = await getScheduleCalendar(from, to, db());
            return json({ data: entries });
        }
        catch (err) {
            return internalError(err);
        }
    });
}
//# sourceMappingURL=scheduling.js.map