import crypto from 'node:crypto';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { buildTimelineItem } from '@bytelyst/events';
import { requireJwtOrApiKey } from '../../lib/api-key-auth.js';
import { BadRequestError, ForbiddenError } from '../../lib/errors.js';
import * as repo from './repository.js';
import {
  IngestTimelineEventsSchema,
  ListTimelineQuerySchema,
  type TimelineItemDoc,
} from './types.js';

function isAdmin(req: FastifyRequest): boolean {
  const role = req.jwtPayload?.role;
  return role === 'admin' || role === 'super_admin';
}

function resolveTimelineUserId(
  req: FastifyRequest,
  source: 'jwt' | 'api_key',
  explicitUserId: string | null | undefined,
  fallbackUserId: string
): string | undefined {
  if (source === 'api_key') {
    return explicitUserId ?? undefined;
  }

  if (isAdmin(req)) {
    return explicitUserId ?? undefined;
  }

  if (explicitUserId && explicitUserId !== fallbackUserId) {
    throw new ForbiddenError('Non-admin callers can only access their own timeline');
  }

  return fallbackUserId;
}

export async function timelineRoutes(app: FastifyInstance) {
  app.post('/timeline/ingest', async req => {
    const access = await requireJwtOrApiKey(req, {
      allowJwt: true,
      apiKeyScopes: ['timeline:write'],
      apiKeyTokenTypes: ['product_api', 'service_api'],
    });

    const parsed = IngestTimelineEventsSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
    }

    const requestedUserId = parsed.data.userId ?? undefined;
    const resolvedUserId = resolveTimelineUserId(
      req,
      access.source,
      requestedUserId,
      access.actorId
    );
    const visibilityOverride = parsed.data.visibility;

    const docs: TimelineItemDoc[] = parsed.data.events.map(event => {
      if (event.productId !== access.productId) {
        throw new BadRequestError(
          `Event '${event.eventId}' productId must match authenticated product '${access.productId}'`
        );
      }

      const item = buildTimelineItem(event);
      const normalizedItem =
        visibilityOverride === undefined ? item : { ...item, visibility: visibilityOverride };
      const now = new Date().toISOString();

      return {
        id: normalizedItem.itemId,
        productId: access.productId,
        userId: event.userId ?? resolvedUserId ?? null,
        orgId: event.orgId ?? null,
        eventId: event.eventId,
        eventName: event.eventName,
        artifactId: event.artifactId ?? null,
        correlationId: event.trace.correlationId ?? null,
        actorType: event.actor.actorType,
        visibility: normalizedItem.visibility,
        occurredAt: event.occurredAt,
        item: normalizedItem,
        createdAt: now,
        updatedAt: now,
      };
    });

    const persisted = await repo.upsertTimelineItems(docs);

    return {
      ingestedCount: persisted.length,
      items: persisted.map(doc => doc.item),
      eventIds: persisted.map(doc => doc.eventId),
      batchId: `timeline_batch_${crypto.randomUUID()}`,
    };
  });

  app.get('/timeline', async req => {
    const access = await requireJwtOrApiKey(req, {
      allowJwt: true,
      apiKeyScopes: ['timeline:read'],
      apiKeyTokenTypes: ['product_api', 'service_api'],
    });

    const parsed = ListTimelineQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(issue => issue.message).join('; '));
    }

    const effectiveUserId = resolveTimelineUserId(
      req,
      access.source,
      parsed.data.userId,
      access.actorId
    );
    const docs = await repo.listTimelineItems(access.productId, {
      ...parsed.data,
      userId: effectiveUserId,
    });

    return {
      items: docs.map(doc => doc.item),
      count: docs.length,
    };
  });
}
