import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { router, adminProcedure, withAIRateLimit } from '../../trpc'
import {
  generateAIAssignments,
  type AssignmentProgressCallback,
} from '../../services/ai-assignment'
import { isOpenAIConfigured } from '@/lib/openai'
import { prisma } from '@/lib/prisma'
import { notifyAdmins, NotificationTypes } from '../../services/in-app-notification'
import { logAudit } from '@/server/utils/audit'
import { buildBatchNotifications } from './shared'

async function runAIAssignmentJob(jobId: string, roundId: string, userId: string) {
  try {
    await prisma.assignmentJob.update({
      where: { id: jobId },
      data: { status: 'RUNNING', startedAt: new Date() },
    })

    const round = await prisma.round.findUniqueOrThrow({
      where: { id: roundId },
      select: {
        name: true,
        configJson: true,
        competitionId: true,
        juryGroupId: true,
      },
    })

    const config = (round.configJson ?? {}) as Record<string, unknown>
    const requiredReviews = (config.requiredReviewsPerProject as number) ?? 3
    const minAssignmentsPerJuror =
      (config.minLoadPerJuror as number) ??
      (config.minAssignmentsPerJuror as number) ??
      1
    const maxAssignmentsPerJuror =
      (config.maxLoadPerJuror as number) ??
      (config.maxAssignmentsPerJuror as number) ??
      20

    // Scope jurors to jury group if the round has one assigned
    let scopedJurorIds: string[] | undefined
    if (round.juryGroupId) {
      const groupMembers = await prisma.juryGroupMember.findMany({
        where: { juryGroupId: round.juryGroupId },
        select: { userId: true },
      })
      scopedJurorIds = groupMembers.map((m) => m.userId)
    }

    const jurors = await prisma.user.findMany({
      where: {
        roles: { has: 'JURY_MEMBER' },
        status: 'ACTIVE',
        ...(scopedJurorIds ? { id: { in: scopedJurorIds } } : {}),
      },
      select: {
        id: true,
        name: true,
        email: true,
        expertiseTags: true,
        maxAssignments: true,
        _count: {
          select: {
            assignments: { where: { roundId } },
          },
        },
      },
    })

    const projectRoundStates = await prisma.projectRoundState.findMany({
      where: { roundId },
      select: { projectId: true },
    })
    const projectIds = projectRoundStates.map((prs) => prs.projectId)

    const projects = await prisma.project.findMany({
      where: { id: { in: projectIds } },
      select: {
        id: true,
        title: true,
        description: true,
        tags: true,
        teamName: true,
        projectTags: {
          select: { tag: { select: { name: true } }, confidence: true },
        },
        _count: { select: { assignments: { where: { roundId } } } },
      },
    })

    // Enrich projects with tag confidence data for AI matching
    const projectsWithConfidence = projects.map((p) => ({
      ...p,
      tagConfidences: p.projectTags.map((pt) => ({
        name: pt.tag.name,
        confidence: pt.confidence,
      })),
    }))

    const existingAssignments = await prisma.assignment.findMany({
      where: { roundId },
      select: { userId: true, projectId: true },
    })

    // Query COI records for this round to exclude conflicted juror-project pairs
    const coiRecords = await prisma.conflictOfInterest.findMany({
      where: {
        assignment: { roundId },
        hasConflict: true,
      },
      select: { userId: true, projectId: true },
    })
    const coiExclusions = new Set(
      coiRecords.map((c) => `${c.userId}:${c.projectId}`)
    )

    // Calculate batch info
    const BATCH_SIZE = 15
    const totalBatches = Math.ceil(projects.length / BATCH_SIZE)

    await prisma.assignmentJob.update({
      where: { id: jobId },
      data: { totalProjects: projects.length, totalBatches },
    })

    // Progress callback
    const onProgress: AssignmentProgressCallback = async (progress) => {
      await prisma.assignmentJob.update({
        where: { id: jobId },
        data: {
          currentBatch: progress.currentBatch,
          processedCount: progress.processedCount,
        },
      })
    }

    // Build per-juror limits map for jurors with personal maxAssignments
    const jurorLimits: Record<string, number> = {}
    for (const juror of jurors) {
      if (juror.maxAssignments !== null && juror.maxAssignments !== undefined) {
        jurorLimits[juror.id] = juror.maxAssignments
      }
    }

    const constraints = {
      requiredReviewsPerProject: requiredReviews,
      minAssignmentsPerJuror,
      maxAssignmentsPerJuror,
      jurorLimits: Object.keys(jurorLimits).length > 0 ? jurorLimits : undefined,
      existingAssignments: existingAssignments.map((a) => ({
        jurorId: a.userId,
        projectId: a.projectId,
      })),
    }

    const result = await generateAIAssignments(
      jurors,
      projectsWithConfidence,
      constraints,
      userId,
      roundId,
      onProgress
    )

    // Filter out suggestions that conflict with COI declarations
    const filteredSuggestions = coiExclusions.size > 0
      ? result.suggestions.filter((s) => !coiExclusions.has(`${s.jurorId}:${s.projectId}`))
      : result.suggestions

    // Enrich suggestions with names for storage
    const enrichedSuggestions = filteredSuggestions.map((s) => {
      const juror = jurors.find((j) => j.id === s.jurorId)
      const project = projects.find((p) => p.id === s.projectId)
      return {
        ...s,
        jurorName: juror?.name || juror?.email || 'Unknown',
        projectTitle: project?.title || 'Unknown',
      }
    })

    // Mark job as completed and store suggestions
    await prisma.assignmentJob.update({
      where: { id: jobId },
      data: {
        status: 'COMPLETED',
        completedAt: new Date(),
        processedCount: projects.length,
        suggestionsCount: filteredSuggestions.length,
        suggestionsJson: enrichedSuggestions,
        fallbackUsed: result.fallbackUsed ?? false,
      },
    })

    await notifyAdmins({
      type: NotificationTypes.AI_SUGGESTIONS_READY,
      title: 'AI Assignment Suggestions Ready',
      message: `AI generated ${filteredSuggestions.length} assignment suggestions for ${round.name || 'round'}${result.fallbackUsed ? ' (using fallback algorithm)' : ''}.`,
      linkUrl: `/admin/rounds/${roundId}`,
      linkLabel: 'View Suggestions',
      priority: 'high',
      metadata: {
        roundId,
        jobId,
        projectCount: projects.length,
        suggestionsCount: filteredSuggestions.length,
        fallbackUsed: result.fallbackUsed,
      },
    })

  } catch (error) {
    console.error('[AI Assignment Job] Error:', error)

    // Mark job as failed
    await prisma.assignmentJob.update({
      where: { id: jobId },
      data: {
        status: 'FAILED',
        errorMessage: error instanceof Error ? error.message : 'Unknown error',
        completedAt: new Date(),
      },
    })
  }
}

export const assignmentSuggestionsRouter = router({
  /**
   * Get smart assignment suggestions using algorithm
   */
  getSuggestions: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      const stage = await ctx.prisma.round.findUniqueOrThrow({
        where: { id: input.roundId },
        select: { configJson: true, juryGroupId: true },
      })
      const config = (stage.configJson ?? {}) as Record<string, unknown>
      const requiredReviews = (config.requiredReviewsPerProject as number) ?? 3
      const minAssignmentsPerJuror =
        (config.minLoadPerJuror as number) ??
        (config.minAssignmentsPerJuror as number) ??
        1
      const maxAssignmentsPerJuror =
        (config.maxLoadPerJuror as number) ??
        (config.maxAssignmentsPerJuror as number) ??
        20

      // Extract category quotas if enabled
      const categoryQuotasEnabled = config.categoryQuotasEnabled === true
      const categoryQuotas = categoryQuotasEnabled
        ? (config.categoryQuotas as Record<string, { min: number; max: number }> | undefined)
        : undefined

      // Scope jurors to jury group if the round has one assigned
      let scopedJurorIds: string[] | undefined
      if (stage.juryGroupId) {
        const groupMembers = await ctx.prisma.juryGroupMember.findMany({
          where: { juryGroupId: stage.juryGroupId },
          select: { userId: true },
        })
        scopedJurorIds = groupMembers.map((m) => m.userId)
      }

      const jurors = await ctx.prisma.user.findMany({
        where: {
          roles: { has: 'JURY_MEMBER' },
          status: 'ACTIVE',
          ...(scopedJurorIds ? { id: { in: scopedJurorIds } } : {}),
        },
        select: {
          id: true,
          name: true,
          email: true,
          expertiseTags: true,
          maxAssignments: true,
          _count: {
            select: {
              assignments: { where: { roundId: input.roundId } },
            },
          },
        },
      })

      const projectRoundStates = await ctx.prisma.projectRoundState.findMany({
        where: { roundId: input.roundId },
        select: { projectId: true },
      })
      const projectIds = projectRoundStates.map((pss) => pss.projectId)

      const projects = await ctx.prisma.project.findMany({
        where: { id: { in: projectIds } },
        select: {
          id: true,
          title: true,
          tags: true,
          competitionCategory: true,
          projectTags: {
            include: { tag: { select: { name: true } } },
          },
          _count: { select: { assignments: { where: { roundId: input.roundId } } } },
        },
      })

      const existingAssignments = await ctx.prisma.assignment.findMany({
        where: { roundId: input.roundId },
        select: { userId: true, projectId: true },
      })
      const assignmentSet = new Set(
        existingAssignments.map((a) => `${a.userId}-${a.projectId}`)
      )

      // Build per-juror category distribution for quota scoring
      const jurorCategoryDistribution = new Map<string, Record<string, number>>()
      if (categoryQuotas) {
        const assignmentsWithCategory = await ctx.prisma.assignment.findMany({
          where: { roundId: input.roundId },
          select: {
            userId: true,
            project: { select: { competitionCategory: true } },
          },
        })
        for (const a of assignmentsWithCategory) {
          const cat = a.project.competitionCategory?.toLowerCase().trim()
          if (!cat) continue
          let catMap = jurorCategoryDistribution.get(a.userId)
          if (!catMap) {
            catMap = {}
            jurorCategoryDistribution.set(a.userId, catMap)
          }
          catMap[cat] = (catMap[cat] || 0) + 1
        }
      }

      const suggestions: Array<{
        userId: string
        jurorName: string
        projectId: string
        projectTitle: string
        score: number
        reasoning: string[]
      }> = []

      for (const project of projects) {
        if (project._count.assignments >= requiredReviews) continue

        const neededAssignments = requiredReviews - project._count.assignments

        const jurorScores = jurors
          .filter((j) => {
            if (assignmentSet.has(`${j.id}-${project.id}`)) return false
            const effectiveMax = j.maxAssignments ?? maxAssignmentsPerJuror
            if (j._count.assignments >= effectiveMax) return false
            return true
          })
          .map((juror) => {
            const reasoning: string[] = []
            let score = 0

            const projectTagNames = project.projectTags.map((pt) => pt.tag.name.toLowerCase())

            const matchingTags = projectTagNames.length > 0
              ? juror.expertiseTags.filter((tag) =>
                  projectTagNames.includes(tag.toLowerCase())
                )
              : juror.expertiseTags.filter((tag) =>
                  project.tags.map((t) => t.toLowerCase()).includes(tag.toLowerCase())
                )

            const totalTags = projectTagNames.length > 0 ? projectTagNames.length : project.tags.length
            const expertiseScore =
              matchingTags.length > 0
                ? matchingTags.length / Math.max(totalTags, 1)
                : 0
            score += expertiseScore * 35
            if (matchingTags.length > 0) {
              reasoning.push(`Expertise match: ${matchingTags.join(', ')}`)
            }

            const effectiveMax = juror.maxAssignments ?? maxAssignmentsPerJuror
            const loadScore = 1 - juror._count.assignments / effectiveMax
            score += loadScore * 20

            const underMinBonus =
              juror._count.assignments < minAssignmentsPerJuror
                ? (minAssignmentsPerJuror - juror._count.assignments) * 3
                : 0
            score += Math.min(15, underMinBonus)

            if (juror._count.assignments < minAssignmentsPerJuror) {
              reasoning.push(
                `Under target: ${juror._count.assignments}/${minAssignmentsPerJuror} min`
              )
            }
            reasoning.push(
              `Capacity: ${juror._count.assignments}/${effectiveMax} max`
            )

            // Category quota scoring
            if (categoryQuotas) {
              const jurorCategoryCounts = jurorCategoryDistribution.get(juror.id) || {}
              const normalizedCat = project.competitionCategory?.toLowerCase().trim()
              if (normalizedCat) {
                const quota = Object.entries(categoryQuotas).find(
                  ([key]) => key.toLowerCase().trim() === normalizedCat
                )
                if (quota) {
                  const [, { min, max }] = quota
                  const currentCount = jurorCategoryCounts[normalizedCat] || 0
                  if (currentCount >= max) {
                    score -= 25
                    reasoning.push(`Category quota exceeded (-25)`)
                  } else if (currentCount < min) {
                    const otherAboveMin = Object.entries(categoryQuotas).some(([key, q]) => {
                      if (key.toLowerCase().trim() === normalizedCat) return false
                      return (jurorCategoryCounts[key.toLowerCase().trim()] || 0) >= q.min
                    })
                    if (otherAboveMin) {
                      score += 10
                      reasoning.push(`Category quota bonus (+10)`)
                    }
                  }
                }
              }
            }

            return {
              userId: juror.id,
              jurorName: juror.name || juror.email || 'Unknown',
              projectId: project.id,
              projectTitle: project.title || 'Unknown',
              score,
              reasoning,
            }
          })
          .sort((a, b) => b.score - a.score)
          .slice(0, neededAssignments)

        suggestions.push(...jurorScores)
      }

      return suggestions.sort((a, b) => b.score - a.score)
    }),

  /**
   * Check if AI assignment is available
   */
  isAIAvailable: adminProcedure.query(async () => {
    return isOpenAIConfigured()
  }),

  /**
   * Get AI-powered assignment suggestions (retrieves from completed job)
   */
  getAISuggestions: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        useAI: z.boolean().default(true),
      })
    )
    .query(async ({ ctx, input }) => {
      const completedJob = await ctx.prisma.assignmentJob.findFirst({
        where: {
          roundId: input.roundId,
          status: 'COMPLETED',
        },
        orderBy: { completedAt: 'desc' },
        select: {
          suggestionsJson: true,
          fallbackUsed: true,
          completedAt: true,
        },
      })

      if (completedJob?.suggestionsJson) {
        const suggestions = completedJob.suggestionsJson as Array<{
          jurorId: string
          jurorName: string
          projectId: string
          projectTitle: string
          confidenceScore: number
          expertiseMatchScore: number
          reasoning: string
        }>

        const existingAssignments = await ctx.prisma.assignment.findMany({
          where: { roundId: input.roundId },
          select: { userId: true, projectId: true },
        })
        const assignmentSet = new Set(
          existingAssignments.map((a) => `${a.userId}-${a.projectId}`)
        )

        const filteredSuggestions = suggestions.filter(
          (s) => !assignmentSet.has(`${s.jurorId}-${s.projectId}`)
        )

        return {
          success: true,
          suggestions: filteredSuggestions,
          fallbackUsed: completedJob.fallbackUsed,
          error: null,
          generatedAt: completedJob.completedAt,
        }
      }

      return {
        success: true,
        suggestions: [],
        fallbackUsed: false,
        error: null,
        generatedAt: null,
      }
    }),

  /**
   * Apply AI-suggested assignments
   */
  applyAISuggestions: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        assignments: z.array(
          z.object({
            userId: z.string(),
            projectId: z.string(),
            confidenceScore: z.number().optional(),
            expertiseMatchScore: z.number().optional(),
            reasoning: z.string().optional(),
          })
        ),
        usedAI: z.boolean().default(false),
        forceOverride: z.boolean().default(false),
      })
    )
    .mutation(async ({ ctx, input }) => {
      let assignmentsToCreate = input.assignments
      let skippedDueToCapacity = 0

      // Capacity check (unless forceOverride)
      if (!input.forceOverride) {
        const uniqueUserIds = [...new Set(input.assignments.map((a) => a.userId))]
        const users = await ctx.prisma.user.findMany({
          where: { id: { in: uniqueUserIds } },
          select: {
            id: true,
            maxAssignments: true,
            _count: {
              select: {
                assignments: { where: { roundId: input.roundId } },
              },
            },
          },
        })
        const userMap = new Map(users.map((u) => [u.id, u]))

        const stageData = await ctx.prisma.round.findUniqueOrThrow({
          where: { id: input.roundId },
          select: { configJson: true },
        })
        const config = (stageData.configJson ?? {}) as Record<string, unknown>
        const stageMaxPerJuror =
          (config.maxLoadPerJuror as number) ??
          (config.maxAssignmentsPerJuror as number) ??
          20

        const runningCounts = new Map<string, number>()
        for (const u of users) {
          runningCounts.set(u.id, u._count.assignments)
        }

        assignmentsToCreate = input.assignments.filter((a) => {
          const user = userMap.get(a.userId)
          if (!user) return true

          const effectiveMax = user.maxAssignments ?? stageMaxPerJuror
          const currentCount = runningCounts.get(a.userId) ?? 0

          if (currentCount >= effectiveMax) {
            skippedDueToCapacity++
            return false
          }

          runningCounts.set(a.userId, currentCount + 1)
          return true
        })
      }

      const created = await ctx.prisma.assignment.createMany({
        data: assignmentsToCreate.map((a) => ({
          userId: a.userId,
          projectId: a.projectId,
          roundId: input.roundId,
          method: input.usedAI ? 'AI_SUGGESTED' : 'ALGORITHM',
          aiConfidenceScore: a.confidenceScore,
          expertiseMatchScore: a.expertiseMatchScore,
          aiReasoning: a.reasoning,
          createdBy: ctx.user.id,
        })),
        skipDuplicates: true,
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: input.usedAI ? 'APPLY_AI_SUGGESTIONS' : 'APPLY_SUGGESTIONS',
        entityType: 'Assignment',
        detailsJson: {
          roundId: input.roundId,
          count: created.count,
          usedAI: input.usedAI,
          forceOverride: input.forceOverride,
          skippedDueToCapacity,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      if (created.count > 0) {
        const userAssignmentCounts = assignmentsToCreate.reduce(
          (acc, a) => {
            acc[a.userId] = (acc[a.userId] || 0) + 1
            return acc
          },
          {} as Record<string, number>
        )

        const stage = await ctx.prisma.round.findUnique({
          where: { id: input.roundId },
          select: { name: true, windowCloseAt: true },
        })

        const deadline = stage?.windowCloseAt
          ? new Date(stage.windowCloseAt).toLocaleDateString('en-US', {
              weekday: 'long',
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })
          : undefined

        await buildBatchNotifications(userAssignmentCounts, stage?.name, deadline)
      }

      return {
        created: created.count,
        requested: input.assignments.length,
        skippedDueToCapacity,
      }
    }),

  /**
   * Apply suggested assignments
   */
  applySuggestions: adminProcedure
    .input(
      z.object({
        roundId: z.string(),
        assignments: z.array(
          z.object({
            userId: z.string(),
            projectId: z.string(),
            reasoning: z.string().optional(),
          })
        ),
        forceOverride: z.boolean().default(false),
      })
    )
    .mutation(async ({ ctx, input }) => {
      let assignmentsToCreate = input.assignments
      let skippedDueToCapacity = 0

      // Capacity check (unless forceOverride)
      if (!input.forceOverride) {
        const uniqueUserIds = [...new Set(input.assignments.map((a) => a.userId))]
        const users = await ctx.prisma.user.findMany({
          where: { id: { in: uniqueUserIds } },
          select: {
            id: true,
            maxAssignments: true,
            _count: {
              select: {
                assignments: { where: { roundId: input.roundId } },
              },
            },
          },
        })
        const userMap = new Map(users.map((u) => [u.id, u]))

        const stageData = await ctx.prisma.round.findUniqueOrThrow({
          where: { id: input.roundId },
          select: { configJson: true },
        })
        const config = (stageData.configJson ?? {}) as Record<string, unknown>
        const stageMaxPerJuror =
          (config.maxLoadPerJuror as number) ??
          (config.maxAssignmentsPerJuror as number) ??
          20

        const runningCounts = new Map<string, number>()
        for (const u of users) {
          runningCounts.set(u.id, u._count.assignments)
        }

        assignmentsToCreate = input.assignments.filter((a) => {
          const user = userMap.get(a.userId)
          if (!user) return true

          const effectiveMax = user.maxAssignments ?? stageMaxPerJuror
          const currentCount = runningCounts.get(a.userId) ?? 0

          if (currentCount >= effectiveMax) {
            skippedDueToCapacity++
            return false
          }

          runningCounts.set(a.userId, currentCount + 1)
          return true
        })
      }

      const created = await ctx.prisma.assignment.createMany({
        data: assignmentsToCreate.map((a) => ({
          userId: a.userId,
          projectId: a.projectId,
          roundId: input.roundId,
          method: 'ALGORITHM',
          aiReasoning: a.reasoning,
          createdBy: ctx.user.id,
        })),
        skipDuplicates: true,
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'APPLY_SUGGESTIONS',
        entityType: 'Assignment',
        detailsJson: {
          roundId: input.roundId,
          count: created.count,
          forceOverride: input.forceOverride,
          skippedDueToCapacity,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      if (created.count > 0) {
        const userAssignmentCounts = assignmentsToCreate.reduce(
          (acc, a) => {
            acc[a.userId] = (acc[a.userId] || 0) + 1
            return acc
          },
          {} as Record<string, number>
        )

        const stage = await ctx.prisma.round.findUnique({
          where: { id: input.roundId },
          select: { name: true, windowCloseAt: true },
        })

        const deadline = stage?.windowCloseAt
          ? new Date(stage.windowCloseAt).toLocaleDateString('en-US', {
              weekday: 'long',
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })
          : undefined

        await buildBatchNotifications(userAssignmentCounts, stage?.name, deadline)
      }

      return {
        created: created.count,
        requested: input.assignments.length,
        skippedDueToCapacity,
      }
    }),

  /**
   * Start an AI assignment job (background processing)
   */
  startAIAssignmentJob: adminProcedure
    .use(withAIRateLimit)
    .input(z.object({ roundId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const existingJob = await ctx.prisma.assignmentJob.findFirst({
        where: {
          roundId: input.roundId,
          status: { in: ['PENDING', 'RUNNING'] },
        },
      })

      if (existingJob) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'An AI assignment job is already running for this stage',
        })
      }

      if (!isOpenAIConfigured()) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'OpenAI API is not configured',
        })
      }

      const job = await ctx.prisma.assignmentJob.create({
        data: {
          roundId: input.roundId,
          status: 'PENDING',
        },
      })

      runAIAssignmentJob(job.id, input.roundId, ctx.user.id).catch(console.error)

      return { jobId: job.id }
    }),

  /**
   * Get AI assignment job status (for polling)
   */
  getAIAssignmentJobStatus: adminProcedure
    .input(z.object({ jobId: z.string() }))
    .query(async ({ ctx, input }) => {
      const job = await ctx.prisma.assignmentJob.findUniqueOrThrow({
        where: { id: input.jobId },
      })

      return {
        id: job.id,
        status: job.status,
        totalProjects: job.totalProjects,
        totalBatches: job.totalBatches,
        currentBatch: job.currentBatch,
        processedCount: job.processedCount,
        suggestionsCount: job.suggestionsCount,
        fallbackUsed: job.fallbackUsed,
        errorMessage: job.errorMessage,
        startedAt: job.startedAt,
        completedAt: job.completedAt,
      }
    }),

  /**
   * Get the latest AI assignment job for a round
   */
  getLatestAIAssignmentJob: adminProcedure
    .input(z.object({ roundId: z.string() }))
    .query(async ({ ctx, input }) => {
      const job = await ctx.prisma.assignmentJob.findFirst({
        where: { roundId: input.roundId },
        orderBy: { createdAt: 'desc' },
      })

      if (!job) return null

      return {
        id: job.id,
        status: job.status,
        totalProjects: job.totalProjects,
        totalBatches: job.totalBatches,
        currentBatch: job.currentBatch,
        processedCount: job.processedCount,
        suggestionsCount: job.suggestionsCount,
        fallbackUsed: job.fallbackUsed,
        errorMessage: job.errorMessage,
        startedAt: job.startedAt,
        completedAt: job.completedAt,
        createdAt: job.createdAt,
      }
    }),
})
